diff --git a/src/main/java/app/INavigator.java b/src/main/java/app/INavigator.java index cb7e249..830adcd 100644 --- a/src/main/java/app/INavigator.java +++ b/src/main/java/app/INavigator.java @@ -17,4 +17,5 @@ public interface INavigator { void showComplexMorphology(); void showLinearOutline(); void showNonLinearOutline(); + void showThresholding(); } diff --git a/src/main/java/app/SwingNavigator.java b/src/main/java/app/SwingNavigator.java index 5a1bb26..2fcbcda 100644 --- a/src/main/java/app/SwingNavigator.java +++ b/src/main/java/app/SwingNavigator.java @@ -13,6 +13,7 @@ import ui.implementation.views.morphology.BasicMorphologyView; import ui.implementation.views.morphology.ComplexMorphologyView; import ui.implementation.views.outline.LinearOutlineView; import ui.implementation.views.outline.NonLinearOutlineView; +import ui.implementation.views.threshold.ThresholdingView; /** * Concrete implement of the INavigator interface. @@ -29,6 +30,7 @@ public class SwingNavigator implements INavigator { private final Provider complexMorphologyProvider; private final Provider linearOutlineProvider; private final Provider nonLinearOutlineProvider; + private final Provider thresholdingProvider; @Inject public SwingNavigator( @@ -39,7 +41,8 @@ public class SwingNavigator implements INavigator { Provider basicMorphologyProvider, Provider complexMorphologyProvider, Provider linearOutlineProvider, - Provider nonLinearOutlineProvider + Provider nonLinearOutlineProvider, + Provider thresholdingProvider ) { this.doubleMatrixProvider = doubleMatrixProvider; this.filteringProvider = filteringProvider; @@ -49,6 +52,7 @@ public class SwingNavigator implements INavigator { this.complexMorphologyProvider = complexMorphologyProvider; this.linearOutlineProvider = linearOutlineProvider; this.nonLinearOutlineProvider = nonLinearOutlineProvider; + this.thresholdingProvider = thresholdingProvider; } @Override @@ -99,6 +103,12 @@ public class SwingNavigator implements INavigator { runView(view); } + @Override + public void showThresholding() { + ThresholdingView view = thresholdingProvider.get(); + runView(view); + } + private void runView(AbstractView view) { ViewInitializer.Initialize(view); view.setVisible(true); diff --git a/src/main/java/app/state/ThresholdingState.java b/src/main/java/app/state/ThresholdingState.java new file mode 100644 index 0000000..3e80c2f --- /dev/null +++ b/src/main/java/app/state/ThresholdingState.java @@ -0,0 +1,65 @@ +package app.state; + +import com.google.inject.Inject; +import domain.image.Image; +import presenters.threshold.ThresholdingListener; + +import java.util.ArrayList; +import java.util.List; + +public class ThresholdingState extends State { + + private Image simpleThreshold; + private Image doubleThreshold; + private Image automaticThreshold; + + private final List listeners = new ArrayList<>(); + + @Inject + public ThresholdingState() { + } + + public void subscribe(ThresholdingListener listener) { + listeners.add(listener); + } + + public void unsubscribe(ThresholdingListener listener) { + listeners.remove(listener); + } + + public void setSimpleThreshold(Image image) { + simpleThreshold = image; + + listeners.forEach(listener -> + listener.onSimpleThresholdChanged(image) + ); + } + + public void setDoubleThreshold(Image image) { + doubleThreshold = image; + + listeners.forEach(listener -> + listener.onDoubleThresholdChanged(image) + ); + } + + public void setAutomaticThreshold(Image image) { + automaticThreshold = image; + + listeners.forEach(listener -> + listener.onAutomaticThresholdChanged(image) + ); + } + + public Image getSimpleThreshold() { + return simpleThreshold; + } + + public Image getDoubleThreshold() { + return doubleThreshold; + } + + public Image getAutomaticThreshold() { + return automaticThreshold; + } +} \ No newline at end of file diff --git a/src/main/java/domain/image/processing/threshold/Thresholding.java b/src/main/java/domain/image/processing/threshold/Thresholding.java new file mode 100644 index 0000000..1aab039 --- /dev/null +++ b/src/main/java/domain/image/processing/threshold/Thresholding.java @@ -0,0 +1,116 @@ +package domain.image.processing.threshold; + +import domain.image.GrayScaleMatrix; +import domain.image.Image; + +public class Thresholding { + + public static GrayScaleMatrix simpleThreshold( + Image image, + int threshold + ) { + GrayScaleMatrix gray = image.toGrayScale(); + + int height = gray.getHeight(); + int width = gray.getWidth(); + + double[][] data = gray.getRawData(); + double[][] result = new double[height][width]; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + result[y][x] = data[y][x] >= threshold + ? 255 + : 0; + } + } + + return new GrayScaleMatrix(result); + } + + public static GrayScaleMatrix doubleThreshold( + Image image, + int threshold1, + int threshold2 + ) { + if (threshold1 > threshold2) { + int temp = threshold1; + threshold1 = threshold2; + threshold2 = temp; + } + + GrayScaleMatrix gray = image.toGrayScale(); + + int height = gray.getHeight(); + int width = gray.getWidth(); + + double[][] data = gray.getRawData(); + double[][] result = new double[height][width]; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + + double value = data[y][x]; + + if (value < threshold1) { + result[y][x] = 0; + } else if (value < threshold2) { + result[y][x] = 127; + } else { + result[y][x] = 255; + } + } + } + + return new GrayScaleMatrix(result); + } + + public static GrayScaleMatrix automaticThreshold(Image image) { + GrayScaleMatrix gray = image.toGrayScale(); + + double[][] data = gray.getRawData(); + + double threshold = (gray.getMinValue() + gray.getMaxValue()) / 2.0; + + while (true) { + double lowerSum = 0; + double upperSum = 0; + + int lowerCount = 0; + int upperCount = 0; + + for (int y = 0; y < gray.getHeight(); y++) { + for (int x = 0; x < gray.getWidth(); x++) { + + double value = data[y][x]; + + if (value <= threshold) { + lowerSum += value; + lowerCount++; + } else { + upperSum += value; + upperCount++; + } + } + } + + if (lowerCount == 0 || upperCount == 0) { + break; + } + + double lowerMean = lowerSum / lowerCount; + double upperMean = upperSum / upperCount; + + double newThreshold = (lowerMean + upperMean) / 2.0; + + if (Math.abs(newThreshold - threshold) < 0.5) { + threshold = newThreshold; + break; + } + + threshold = newThreshold; + } + + return simpleThreshold(image, (int) threshold); + } +} \ No newline at end of file diff --git a/src/main/java/presenters/NavPresenter.java b/src/main/java/presenters/NavPresenter.java index a7d7897..8292978 100644 --- a/src/main/java/presenters/NavPresenter.java +++ b/src/main/java/presenters/NavPresenter.java @@ -58,6 +58,8 @@ public class NavPresenter { public void goToNonLinearOutline() { navigator.showNonLinearOutline(); } + public void goToThresholding() { navigator.showThresholding(); } + public GrayScaleMatrix getImageGrayMatrix() { return appState.getImage().toGrayScale(); } public void setMode(Mode mode) { modeService.setMode(mode); } diff --git a/src/main/java/presenters/threshold/ThresholdingListener.java b/src/main/java/presenters/threshold/ThresholdingListener.java new file mode 100644 index 0000000..e05d5b1 --- /dev/null +++ b/src/main/java/presenters/threshold/ThresholdingListener.java @@ -0,0 +1,12 @@ +package presenters.threshold; + +import domain.image.Image; + +public interface ThresholdingListener { + + void onSimpleThresholdChanged(Image image); + + void onDoubleThresholdChanged(Image image); + + void onAutomaticThresholdChanged(Image image); +} \ No newline at end of file diff --git a/src/main/java/presenters/threshold/ThresholdingPresenter.java b/src/main/java/presenters/threshold/ThresholdingPresenter.java new file mode 100644 index 0000000..d18fa31 --- /dev/null +++ b/src/main/java/presenters/threshold/ThresholdingPresenter.java @@ -0,0 +1,94 @@ +package presenters.threshold; + +import app.state.AppState; +import app.state.ThresholdingState; +import domain.image.Image; +import jakarta.inject.Inject; +import services.ThresholdingService; +import ui.abstractions.IThresholdingView; + +public class ThresholdingPresenter implements ThresholdingListener { + + private final AppState appState; + private final ThresholdingState thresholdingState; + private final ThresholdingService thresholdingService; + + private IThresholdingView view; + + @Inject + public ThresholdingPresenter( + AppState appState, + ThresholdingState thresholdingState, + ThresholdingService thresholdingService + ) { + this.appState = appState; + this.thresholdingState = thresholdingState; + this.thresholdingService = thresholdingService; + + thresholdingState.subscribe(this); + } + + public void setView(IThresholdingView view) { + this.view = view; + } + + public void dispose() { + view = null; + thresholdingState.unsubscribe(this); + } + + public void computeSimpleThreshold(int threshold) { + Image image = appState.getImage(); + + Image result = thresholdingService.simpleThreshold( + image, + threshold + ); + + thresholdingState.setSimpleThreshold(result); + } + + public void computeDoubleThreshold( + int threshold1, + int threshold2 + ) { + Image image = appState.getImage(); + + Image result = thresholdingService.doubleThreshold( + image, + threshold1, + threshold2 + ); + + thresholdingState.setDoubleThreshold(result); + } + + public void computeAutomaticThreshold() { + Image image = appState.getImage(); + + Image result = thresholdingService.automaticThreshold(image); + + thresholdingState.setAutomaticThreshold(result); + } + + @Override + public void onSimpleThresholdChanged(Image image) { + if (view != null) { + view.updateSimpleThreshold(image); + } + } + + @Override + public void onDoubleThresholdChanged(Image image) { + if (view != null) { + view.updateDoubleThreshold(image); + } + } + + @Override + public void onAutomaticThresholdChanged(Image image) { + if (view != null) { + view.updateAutomaticThreshold(image); + } + } +} \ No newline at end of file diff --git a/src/main/java/services/ThresholdingService.java b/src/main/java/services/ThresholdingService.java new file mode 100644 index 0000000..8c1f887 --- /dev/null +++ b/src/main/java/services/ThresholdingService.java @@ -0,0 +1,29 @@ +package services; + +import domain.image.Image; +import domain.image.processing.threshold.Thresholding; + +public class ThresholdingService { + + public Image simpleThreshold(Image image, int threshold) { + return Thresholding + .simpleThreshold(image, threshold) + .toImage(); + } + + public Image doubleThreshold( + Image image, + int threshold1, + int threshold2 + ) { + return Thresholding + .doubleThreshold(image, threshold1, threshold2) + .toImage(); + } + + public Image automaticThreshold(Image image) { + return Thresholding + .automaticThreshold(image) + .toImage(); + } +} \ No newline at end of file diff --git a/src/main/java/ui/abstractions/IThresholdingView.java b/src/main/java/ui/abstractions/IThresholdingView.java new file mode 100644 index 0000000..bd73039 --- /dev/null +++ b/src/main/java/ui/abstractions/IThresholdingView.java @@ -0,0 +1,12 @@ +package ui.abstractions; + +import domain.image.Image; + +public interface IThresholdingView { + + void updateSimpleThreshold(Image image); + + void updateDoubleThreshold(Image image); + + void updateAutomaticThreshold(Image image); +} \ No newline at end of file diff --git a/src/main/java/ui/implementation/components/nav/NavBar.java b/src/main/java/ui/implementation/components/nav/NavBar.java index 50027ed..645d20c 100644 --- a/src/main/java/ui/implementation/components/nav/NavBar.java +++ b/src/main/java/ui/implementation/components/nav/NavBar.java @@ -33,9 +33,10 @@ public class NavBar extends JMenuBar implements INavBar { private JMenu drawingMenu; private JMenu fourierMenu; private JMenu linearMenu; - private JMenu histogramMenu; private JMenu morphologyMenu; private Menu outlineMenu; + private Menu thresholdingMenu; + private JMenu histogramMenu; private NavPresenter presenter; @@ -103,6 +104,10 @@ public class NavBar extends JMenuBar implements INavBar { new MenuItem("Non-linéaire", this::displayNonLinearOutline) ); + thresholdingMenu = new Menu("Seuillage","/cp_51_p1.jpg", + new MenuItem("Afficher", this::displayThresholding) + ); + histogramMenu = new Menu("Histogramme","/report_48_hot.jpg", new MenuItem("Afficher", "/report_32_hot.jpg",this::displayHistogram) ); @@ -113,6 +118,7 @@ public class NavBar extends JMenuBar implements INavBar { this.add(linearMenu); this.add(morphologyMenu); this.add(outlineMenu); + this.add(thresholdingMenu); this.add(histogramMenu); drawingMenu.setEnabled(false); @@ -120,9 +126,12 @@ public class NavBar extends JMenuBar implements INavBar { linearMenu.setEnabled(false); morphologyMenu.setEnabled(false); outlineMenu.setEnabled(false); + thresholdingMenu.setEnabled(false); histogramMenu.setEnabled(false); } + + //#################################################### private File chooseFile(){ @@ -190,6 +199,7 @@ public class NavBar extends JMenuBar implements INavBar { linearMenu.setEnabled(true); morphologyMenu.setEnabled(true); outlineMenu.setEnabled(true); + thresholdingMenu.setEnabled(true); histogramMenu.setEnabled(true); } @@ -200,6 +210,7 @@ public class NavBar extends JMenuBar implements INavBar { linearMenu.setEnabled(false); morphologyMenu.setEnabled(false); outlineMenu.setEnabled(false); + thresholdingMenu.setEnabled(false); histogramMenu.setEnabled(false); } @@ -261,6 +272,7 @@ public class NavBar extends JMenuBar implements INavBar { private void displayNonLinearOutline(ActionEvent actionEvent) { presenter.goToNonLinearOutline();} + private void displayThresholding(ActionEvent actionEvent) { presenter.goToThresholding(); } private void displayErrorMessage(String title, String message) { JOptionPane.showMessageDialog( diff --git a/src/main/java/ui/implementation/views/threshold/ThresholdingView.java b/src/main/java/ui/implementation/views/threshold/ThresholdingView.java new file mode 100644 index 0000000..321d163 --- /dev/null +++ b/src/main/java/ui/implementation/views/threshold/ThresholdingView.java @@ -0,0 +1,292 @@ +package ui.implementation.views.threshold; + +import domain.image.Image; +import jakarta.inject.Inject; +import presenters.threshold.ThresholdingPresenter; +import ui.abstractions.AbstractView; +import ui.abstractions.IThresholdingView; +import ui.implementation.components.controls.SaveButton; +import ui.implementation.components.image.TabbedImagePanel; + +import javax.swing.*; + +public class ThresholdingView extends AbstractView implements IThresholdingView { + + private static final String SIMPLE_TAB_TITLE = "Simple"; + private static final String DOUBLE_TAB_TITLE = "Double"; + private static final String AUTOMATIC_TAB_TITLE = "Automatique"; + + private final ThresholdingPresenter presenter; + + private TabbedImagePanel modeTab; + + private JSlider thresholdSlider; + private JSlider threshold1Slider; + private JSlider threshold2Slider; + + private JLabel thresholdValueLabel; + private JLabel threshold1ValueLabel; + private JLabel threshold2ValueLabel; + + @Inject + public ThresholdingView(ThresholdingPresenter presenter) { + this.presenter = presenter; + } + + @Override + public void initComponents() { + modeTab = new TabbedImagePanel(); + + modeTab.addPreviewTab(SIMPLE_TAB_TITLE); + modeTab.addPreviewTab(DOUBLE_TAB_TITLE); + modeTab.addPreviewTab(AUTOMATIC_TAB_TITLE); + + thresholdSlider = createThresholdSlider(128); + threshold1Slider = createThresholdSlider(85); + threshold2Slider = createThresholdSlider(170); + + thresholdValueLabel = new JLabel(); + threshold1ValueLabel = new JLabel(); + threshold2ValueLabel = new JLabel(); + + updateValueLabels(); + + thresholdSlider.addChangeListener(e -> { + updateValueLabels(); + presenter.computeSimpleThreshold( + thresholdSlider.getValue() + ); + }); + + threshold1Slider.addChangeListener(e -> { + if (threshold1Slider.getValue() > threshold2Slider.getValue()) { + threshold2Slider.setValue(threshold1Slider.getValue()); + } + + updateValueLabels(); + + presenter.computeDoubleThreshold( + threshold1Slider.getValue(), + threshold2Slider.getValue() + ); + }); + + threshold2Slider.addChangeListener(e -> { + if (threshold2Slider.getValue() < threshold1Slider.getValue()) { + threshold1Slider.setValue(threshold2Slider.getValue()); + } + + updateValueLabels(); + + presenter.computeDoubleThreshold( + threshold1Slider.getValue(), + threshold2Slider.getValue() + ); + }); + + JButton saveButton = new SaveButton(); + + JPanel controlPanel = createControlPanel(); + + JPanel rightPanel = new JPanel(); + + GroupLayout rightLayout = new GroupLayout(rightPanel); + rightPanel.setLayout(rightLayout); + rightLayout.setAutoCreateGaps(true); + rightLayout.setAutoCreateContainerGaps(true); + + rightLayout.setHorizontalGroup( + rightLayout.createParallelGroup(GroupLayout.Alignment.CENTER) + .addComponent(controlPanel) + .addComponent(saveButton) + ); + + rightLayout.setVerticalGroup( + rightLayout.createSequentialGroup() + .addComponent(controlPanel) + .addComponent(saveButton) + ); + + JPanel mainPanel = new JPanel(); + + GroupLayout mainLayout = new GroupLayout(mainPanel); + mainPanel.setLayout(mainLayout); + mainLayout.setAutoCreateGaps(true); + mainLayout.setAutoCreateContainerGaps(true); + + mainLayout.setHorizontalGroup( + mainLayout.createSequentialGroup() + .addComponent(modeTab) + .addComponent(rightPanel) + ); + + mainLayout.setVerticalGroup( + mainLayout.createParallelGroup(GroupLayout.Alignment.CENTER) + .addComponent(modeTab) + .addComponent(rightPanel) + ); + + setContentPane(mainPanel); + + setTitle("Seuillage"); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + pack(); + } + + private JSlider createThresholdSlider(int value) { + JSlider slider = new JSlider( + SwingConstants.VERTICAL, + 0, + 255, + value + ); + + slider.setMajorTickSpacing(50); + slider.setMinorTickSpacing(10); + slider.setPaintTicks(true); + slider.setPaintLabels(true); + + slider.setLabelTable(slider.createStandardLabels(50)); + + return slider; + } + + private JPanel createControlPanel() { + JPanel panel = new JPanel(); + + GroupLayout layout = new GroupLayout(panel); + panel.setLayout(layout); + layout.setAutoCreateGaps(true); + layout.setAutoCreateContainerGaps(true); + + JPanel simplePanel = createSliderPanel( + thresholdSlider, + thresholdValueLabel + ); + + JPanel double1Panel = createSliderPanel( + threshold1Slider, + threshold1ValueLabel + ); + + JPanel double2Panel = createSliderPanel( + threshold2Slider, + threshold2ValueLabel + ); + + layout.setHorizontalGroup( + layout.createSequentialGroup() + .addComponent(simplePanel) + .addComponent(double1Panel) + .addComponent(double2Panel) + ); + + layout.setVerticalGroup( + layout.createParallelGroup(GroupLayout.Alignment.CENTER) + .addComponent(simplePanel) + .addComponent(double1Panel) + .addComponent(double2Panel) + ); + + return panel; + } + + private JPanel createSliderPanel(JSlider slider, JLabel valueLabel) { + JPanel panel = new JPanel(); + + GroupLayout layout = new GroupLayout(panel); + panel.setLayout(layout); + layout.setAutoCreateGaps(true); + + layout.setHorizontalGroup( + layout.createParallelGroup(GroupLayout.Alignment.CENTER) + .addComponent(slider) + .addComponent(valueLabel) + ); + + layout.setVerticalGroup( + layout.createSequentialGroup() + .addComponent(slider) + .addComponent(valueLabel) + ); + + return panel; + } + + private void updateValueLabels() { + thresholdValueLabel.setText( + String.valueOf(thresholdSlider.getValue()) + ); + + threshold1ValueLabel.setText( + String.valueOf(threshold1Slider.getValue()) + ); + + threshold2ValueLabel.setText( + String.valueOf(threshold2Slider.getValue()) + ); + } + + @Override + public void initValues() { + modeTab.addChangeListener(e -> { + updateControls(); + applyThreshold(); + }); + + presenter.setView(this); + + updateControls(); + applyThreshold(); + } + + private void updateControls() { + int operation = modeTab.getSelectedIndex(); + + thresholdSlider.setVisible(operation == 0); + + threshold1Slider.setVisible(operation == 1); + threshold2Slider.setVisible(operation == 1); + + thresholdValueLabel.setVisible(operation == 0); + + threshold1ValueLabel.setVisible(operation == 1); + threshold2ValueLabel.setVisible(operation == 1); + } + + private void applyThreshold() { + switch (modeTab.getSelectedIndex()) { + case 0 -> presenter.computeSimpleThreshold( + thresholdSlider.getValue() + ); + + case 1 -> presenter.computeDoubleThreshold( + threshold1Slider.getValue(), + threshold2Slider.getValue() + ); + + case 2 -> presenter.computeAutomaticThreshold(); + } + } + + @Override + public void updateSimpleThreshold(Image image) { + modeTab.loadImage(SIMPLE_TAB_TITLE, image); + } + + @Override + public void updateDoubleThreshold(Image image) { + modeTab.loadImage(DOUBLE_TAB_TITLE, image); + } + + @Override + public void updateAutomaticThreshold(Image image) { + modeTab.loadImage(AUTOMATIC_TAB_TITLE, image); + } + + @Override + public void disposeCore() { + presenter.setView(null); + presenter.dispose(); + } +} \ No newline at end of file