-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #170 from gerasidev/main
Otsu's Thresholding Augmentation with example
- Loading branch information
Showing
3 changed files
with
221 additions
and
0 deletions.
There are no files selected for viewing
48 changes: 48 additions & 0 deletions
48
example/src/main/java/de/example/augmentation/OtsuThresholdingAugmentationExample.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package de.example.augmentation; | ||
|
||
import de.edux.augmentation.effects.OtsuThresholdingAugmentation; | ||
|
||
import javax.imageio.ImageIO; | ||
import javax.swing.JFrame; | ||
import javax.swing.JLabel; | ||
import javax.swing.ImageIcon; | ||
import javax.swing.WindowConstants; | ||
import java.awt.BorderLayout; | ||
import java.awt.image.BufferedImage; | ||
import java.io.File; | ||
import java.io.IOException; | ||
|
||
public class OtsuThresholdingAugmentationExample { | ||
|
||
private static final String IMAGE_PATH = "images" + File.separator + "cyborg.png"; | ||
|
||
public static void main(String[] args) throws IOException { | ||
BufferedImage originalImage = loadTestImage(IMAGE_PATH); | ||
OtsuThresholdingAugmentation otsuThreshold = new OtsuThresholdingAugmentation(); | ||
BufferedImage otsuImage = otsuThreshold.apply(originalImage); | ||
display(originalImage, "Original Image"); | ||
display(otsuImage, "After Otsu Augmentation"); | ||
} | ||
|
||
public static BufferedImage loadTestImage(String path) throws IOException { | ||
var resourcePath = path; | ||
var imageStream = | ||
SingleImageAugmentationExample.class.getClassLoader().getResourceAsStream(resourcePath); | ||
if (imageStream == null) { | ||
throw new IOException("Cannot find resource: " + resourcePath); | ||
} | ||
return ImageIO.read(imageStream); | ||
} | ||
|
||
public static void display(BufferedImage img, String title) { | ||
JFrame frame = new JFrame(title); | ||
JLabel label = new JLabel(); | ||
frame.setSize(img.getWidth(), img.getHeight()); | ||
label.setIcon(new ImageIcon(img)); | ||
frame.getContentPane().add(label, BorderLayout.CENTER); | ||
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); | ||
frame.pack(); | ||
frame.setVisible(true); | ||
} | ||
|
||
} |
104 changes: 104 additions & 0 deletions
104
lib/src/main/java/de/edux/augmentation/effects/OtsuThresholdingAugmentation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package de.edux.augmentation.effects; | ||
|
||
import de.edux.augmentation.core.AbstractAugmentation; | ||
|
||
import java.awt.Graphics; | ||
import java.awt.image.BufferedImage; | ||
|
||
/** | ||
* Applies Otsu's thresholding method to the given image. | ||
* Converts the image to grayscale, calculates the histogram, finds the optimal threshold, | ||
* and applies the threshold to create a binary image. | ||
*/ | ||
public class OtsuThresholdingAugmentation extends AbstractAugmentation { | ||
private static final Integer HISTOGRAM_SIZE = 256; | ||
private static final int RED_INTENSITY_SHIFT = 16; | ||
|
||
/** | ||
* Applies Otsu's thresholding method to the given image. | ||
* @param image the original image to be thresholded | ||
* @return a binary image where pixels with intensity greater than the threshold are white, | ||
* and others are black | ||
*/ | ||
@Override | ||
public BufferedImage apply(BufferedImage image) { | ||
BufferedImage grayImage = toGrayScale((image)); | ||
int[] histogram = calculateHistogram(grayImage); | ||
int threshold = findOtsuThreshold(histogram, grayImage.getWidth() * grayImage.getHeight()); | ||
BufferedImage thresholdedImage = new BufferedImage(grayImage.getWidth(), grayImage.getHeight(), BufferedImage.TYPE_BYTE_BINARY); | ||
for (int i = 0; i < grayImage.getWidth(); i++) { | ||
for (int j = 0; j < grayImage.getHeight(); j++) { | ||
int pixel = grayImage.getRGB(i, j); | ||
int intensity = (pixel >> RED_INTENSITY_SHIFT) & 0xff; | ||
thresholdedImage.setRGB(i, j, intensity > threshold ? 0xFFFFFF : 0x000000); | ||
} | ||
} | ||
return thresholdedImage; | ||
} | ||
|
||
private BufferedImage toGrayScale(BufferedImage img) { | ||
System.out.println(" Converting to GrayScale."); | ||
BufferedImage grayImage = new BufferedImage( | ||
img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY); | ||
Graphics g = grayImage.getGraphics(); | ||
g.drawImage(img, 0, 0, null); | ||
g.dispose(); | ||
return grayImage; | ||
} | ||
|
||
private int[] calculateHistogram(BufferedImage image) { | ||
int[] histogram = new int[HISTOGRAM_SIZE]; | ||
for (int i = 0; i < image.getWidth(); i++) { | ||
for (int j = 0; j < image.getHeight(); j++) { | ||
int pixel = image.getRGB(i, j); | ||
int intensity = (pixel >> 16) & 0xff; | ||
histogram[intensity]++; | ||
} | ||
} | ||
return histogram; | ||
} | ||
|
||
private int findOtsuThreshold(int[] histogram, int totalPixels) { | ||
float totalPixelIntensity = 0; | ||
for (int intensity = 0; intensity < HISTOGRAM_SIZE; intensity++) { | ||
totalPixelIntensity += intensity * histogram[intensity]; | ||
} | ||
float backgroundPixelIntensity = 0; | ||
int backgroundWeight = 0; | ||
int foregroundWeight = 0; | ||
float maxVariance = 0; | ||
int optimalThreshold = 0; | ||
for (int threshold = 0; threshold < HISTOGRAM_SIZE; threshold++) { | ||
backgroundWeight += histogram[threshold]; | ||
|
||
if (backgroundWeight == 0) continue; | ||
foregroundWeight = totalPixels - backgroundWeight; | ||
if (foregroundWeight == 0) break; | ||
|
||
backgroundPixelIntensity += (float) (threshold * histogram[threshold]); | ||
|
||
float meanBackgroundIntensity = backgroundPixelIntensity / backgroundWeight; | ||
float meanForegroundIntensity = (totalPixelIntensity - backgroundPixelIntensity) / foregroundWeight; | ||
float betweenClassVariance = calculateBetweenClassVariance(backgroundWeight, foregroundWeight, meanBackgroundIntensity, meanForegroundIntensity); | ||
|
||
if (betweenClassVariance > maxVariance) { | ||
maxVariance = betweenClassVariance; | ||
optimalThreshold = threshold; | ||
} | ||
} | ||
return optimalThreshold; | ||
} | ||
|
||
private float calculateBetweenClassVariance(int backgroundWeight, int foregroundWeight, float meanBackgroundIntensity, float meanForegroundIntensity) { | ||
return (float) ((float) backgroundWeight * (float) foregroundWeight * Math.pow(meanBackgroundIntensity - meanForegroundIntensity, 2)); | ||
} | ||
|
||
} | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
69 changes: 69 additions & 0 deletions
69
lib/src/test/java/de/edux/augmentation/effects/OtsuThresholdingAugmentationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package de.edux.augmentation.effects; | ||
|
||
import de.edux.augmentation.core.AugmentationBuilder; | ||
import de.edux.augmentation.core.AugmentationSequence; | ||
import org.junit.jupiter.api.Test; | ||
import javax.imageio.ImageIO; | ||
import java.awt.image.BufferedImage; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.Arrays; | ||
|
||
import static de.edux.augmentation.AugmentationTestUtils.loadTestImage; | ||
import static de.edux.augmentation.AugmentationTestUtils.openImageInPreview; | ||
import static org.junit.jupiter.api.Assertions.*; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
public class OtsuThresholdingAugmentationTest { | ||
|
||
@Test | ||
void apply() throws IOException, InterruptedException { | ||
var image = loadTestImage("augmentation" + File.separator + "shaman-shadows.png"); | ||
int originalWidth = image.getWidth(); | ||
int originalHeight = image.getHeight(); | ||
|
||
AugmentationSequence augmentationSequence = | ||
new AugmentationBuilder().addAugmentation(new OtsuThresholdingAugmentation()).build(); | ||
BufferedImage augmentedImage = augmentationSequence.applyTo(image); | ||
|
||
int[] originalPixels = | ||
image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth()); | ||
int[] augmentedPixels = | ||
augmentedImage.getRGB( | ||
0, | ||
0, | ||
augmentedImage.getWidth(), | ||
augmentedImage.getHeight(), | ||
null, | ||
0, | ||
augmentedImage.getWidth()); | ||
|
||
assertNotNull(augmentedImage, "Augmented image should not be null."); | ||
|
||
assertEquals( | ||
originalWidth, | ||
augmentedImage.getWidth(), | ||
"Augmented image width should match the specified width."); | ||
assertEquals( | ||
originalHeight, | ||
augmentedImage.getHeight(), | ||
"Augmented image height should match the specified height."); | ||
assertFalse( | ||
Arrays.equals(originalPixels, augmentedPixels), | ||
"The augmented image should differ from the original."); | ||
|
||
Path outputPath = Paths.get("augmented.png"); | ||
ImageIO.write(augmentedImage, "png", outputPath.toFile()); | ||
|
||
assertTrue(Files.exists(outputPath), "Output image file should exist."); | ||
assertTrue(Files.size(outputPath) > 0, "Output image file should not be empty."); | ||
|
||
openImageInPreview(image); | ||
openImageInPreview(augmentedImage); | ||
} | ||
|
||
|
||
} |