From ee7c1cab2fd5ad85e3d65bba240c6c057c11b0ee Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 29 Sep 2026 03:53:27 +0200 Subject: [PATCH] Fix complex morphology --- src/main/java/app/AppModule.java | 19 +- src/main/java/app/INavigator.java | 1 + src/main/java/app/SwingNavigator.java | 13 +- ...gyState.java => BasicMorphologyState.java} | 12 +- .../app/state/ComplexMorphologyState.java | 64 +++++ .../morphology/ComplexMorphology.java | 43 ++-- src/main/java/presenters/NavPresenter.java | 2 + ...ener.java => BasicMorphologyListener.java} | 4 +- ...ter.java => BasicMorphologyPresenter.java} | 10 +- .../morphology/ComplexMorphologyListener.java | 10 + .../ComplexMorphologyPresenter.java | 126 ++++++++++ src/main/java/services/MorphologyService.java | 13 +- .../abstractions/IComplexMorphologyView.java | 10 + .../implementation/components/nav/NavBar.java | 3 +- .../views/morphology/BasicMorphologyView.java | 90 ++----- .../morphology/ComplexMorphologyView.java | 234 ++++++++++++++++++ src/main/resources/Thumbs.db | Bin 16384 -> 0 bytes 17 files changed, 543 insertions(+), 111 deletions(-) rename src/main/java/app/state/{MorphologyState.java => BasicMorphologyState.java} (77%) create mode 100644 src/main/java/app/state/ComplexMorphologyState.java rename src/main/java/presenters/morphology/{MorphologyListener.java => BasicMorphologyListener.java} (83%) rename src/main/java/presenters/morphology/{MorphologyPresenter.java => BasicMorphologyPresenter.java} (90%) create mode 100644 src/main/java/presenters/morphology/ComplexMorphologyListener.java create mode 100644 src/main/java/presenters/morphology/ComplexMorphologyPresenter.java create mode 100644 src/main/java/ui/abstractions/IComplexMorphologyView.java create mode 100644 src/main/java/ui/implementation/views/morphology/ComplexMorphologyView.java delete mode 100644 src/main/resources/Thumbs.db diff --git a/src/main/java/app/AppModule.java b/src/main/java/app/AppModule.java index 3272329..050fbf2 100644 --- a/src/main/java/app/AppModule.java +++ b/src/main/java/app/AppModule.java @@ -1,7 +1,6 @@ package app; -import app.state.AppState; -import app.state.DoubleMatrixState; +import app.state.*; import com.google.common.eventbus.EventBus; import com.google.inject.AbstractModule; import com.google.inject.Scopes; @@ -10,13 +9,18 @@ import presenters.filtering.global.GlobalFilteringPresenter; import presenters.MainPresenter; import presenters.NavPresenter; import presenters.filtering.local.LocalFilteringPresenter; +import presenters.morphology.BasicMorphologyPresenter; +import services.GlobalFilteringService; import services.ImageService; +import services.MorphologyService; import ui.abstractions.*; import ui.implementation.components.nav.NavBar; import ui.implementation.views.DoubleMatrix; import ui.implementation.views.fitering.global.IdealFilteringView; import ui.implementation.views.MainView; import ui.implementation.views.fitering.local.LocalFilteringView; +import ui.implementation.views.morphology.BasicMorphologyView; +import ui.implementation.views.morphology.ComplexMorphologyView; /** * Gathers some configurations in order to set the dependency injection container up @@ -30,6 +34,10 @@ public class AppModule extends AbstractModule { //--- States --- bind(AppState.class).in(Scopes.SINGLETON); bind(DoubleMatrixState.class); + bind(LocalFilteringState.class); + bind(GlobalFilteringState.class); + bind(BasicMorphologyState.class); + bind(ComplexMorphologyState.class); //--- Application layer --- bind(EventBus.class).in(Scopes.SINGLETON); @@ -41,6 +49,8 @@ public class AppModule extends AbstractModule { bind(IDoubleMatrix.class).to(DoubleMatrix.class); bind(IGlobalFilteringView.class).to(IdealFilteringView.class); bind(ILocalFilteringView.class).to(LocalFilteringView.class); + bind(IBasicMorphologyView.class).to(BasicMorphologyView.class); + bind(IComplexMorphologyView.class).to(ComplexMorphologyView.class); //--- Presentation layer --- bind(MainPresenter.class); @@ -48,9 +58,14 @@ public class AppModule extends AbstractModule { bind(DoubleMatrixPresenter.class); bind(GlobalFilteringPresenter.class); bind(LocalFilteringPresenter.class); + bind(BasicMorphologyPresenter.class); + bind(ComplexMorphologyView.class); //--- Service layer layer --- bind(ImageService.class); + bind(LocalFilteringView.class); + bind(GlobalFilteringService.class); + bind(MorphologyService.class); } } \ No newline at end of file diff --git a/src/main/java/app/INavigator.java b/src/main/java/app/INavigator.java index a95c857..8f6b447 100644 --- a/src/main/java/app/INavigator.java +++ b/src/main/java/app/INavigator.java @@ -14,4 +14,5 @@ public interface INavigator { void showButterworth(); void showConvolutionFiltering(); void showBasicMorphology(); + void showComplexMorphology(); } diff --git a/src/main/java/app/SwingNavigator.java b/src/main/java/app/SwingNavigator.java index 47f7065..fc51123 100644 --- a/src/main/java/app/SwingNavigator.java +++ b/src/main/java/app/SwingNavigator.java @@ -2,6 +2,7 @@ package app; import com.google.inject.Inject; import com.google.inject.Provider; +import domain.image.processing.complex.ComplexMatrix; import ui.abstractions.AbstractView; import ui.implementation.views.DoubleMatrix; import ui.implementation.views.fitering.global.IdealFilteringView; @@ -9,6 +10,7 @@ import ui.implementation.views.fitering.global.NonIdealFilteringView; import ui.implementation.views.ViewInitializer; import ui.implementation.views.fitering.local.LocalFilteringView; import ui.implementation.views.morphology.BasicMorphologyView; +import ui.implementation.views.morphology.ComplexMorphologyView; /** * Concrete implement of the INavigator interface. @@ -22,6 +24,7 @@ public class SwingNavigator implements INavigator { private final Provider nonIdealFilteringProvider; private final Provider convolutionFilteringProvider; private final Provider basicMorphologyProvider; + private final Provider complexMorphologyProvider; @Inject public SwingNavigator( @@ -29,13 +32,15 @@ public class SwingNavigator implements INavigator { Provider filteringProvider, Provider nonIdealFilteringProvider, Provider convolutionFilterProvider, - Provider basicMorphologyProvider + Provider basicMorphologyProvider, + Provider complexMorphologyProvider ) { this.doubleMatrixProvider = doubleMatrixProvider; this.filteringProvider = filteringProvider; this.nonIdealFilteringProvider = nonIdealFilteringProvider; this.convolutionFilteringProvider = convolutionFilterProvider; this.basicMorphologyProvider = basicMorphologyProvider; + this.complexMorphologyProvider = complexMorphologyProvider; } @Override @@ -68,6 +73,12 @@ public class SwingNavigator implements INavigator { runView(view); } + @Override + public void showComplexMorphology() { + ComplexMorphologyView view = complexMorphologyProvider.get(); + runView(view); + } + private void runView(AbstractView view) { ViewInitializer.Initialize(view); view.setVisible(true); diff --git a/src/main/java/app/state/MorphologyState.java b/src/main/java/app/state/BasicMorphologyState.java similarity index 77% rename from src/main/java/app/state/MorphologyState.java rename to src/main/java/app/state/BasicMorphologyState.java index 1aecd50..8785e9a 100644 --- a/src/main/java/app/state/MorphologyState.java +++ b/src/main/java/app/state/BasicMorphologyState.java @@ -2,28 +2,28 @@ package app.state; import com.google.inject.Inject; import domain.image.Image; -import presenters.morphology.MorphologyListener; +import presenters.morphology.BasicMorphologyListener; import java.util.ArrayList; import java.util.List; -public class MorphologyState extends State { +public class BasicMorphologyState extends State { private Image erosion; private Image dilation; private Image opening; private Image closing; - private final List listeners = new ArrayList<>(); + private final List listeners = new ArrayList<>(); @Inject - public MorphologyState() {} + public BasicMorphologyState() {} - public void subscribe(MorphologyListener listener) { + public void subscribe(BasicMorphologyListener listener) { listeners.add(listener); } - public void unsubscribe(MorphologyListener listener) { + public void unsubscribe(BasicMorphologyListener listener) { listeners.remove(listener); } diff --git a/src/main/java/app/state/ComplexMorphologyState.java b/src/main/java/app/state/ComplexMorphologyState.java new file mode 100644 index 0000000..93eb093 --- /dev/null +++ b/src/main/java/app/state/ComplexMorphologyState.java @@ -0,0 +1,64 @@ +package app.state; + +import com.google.inject.Inject; +import domain.image.Image; +import presenters.morphology.ComplexMorphologyListener; + +import java.util.ArrayList; +import java.util.List; + +public class ComplexMorphologyState extends State { + + private Image mask; + private Image dilation; + private Image reconstruction; + private Image mean; + + private final List listeners = new ArrayList<>(); + + @Inject + public ComplexMorphologyState() {} + + public void subscribe(ComplexMorphologyListener listener) { + listeners.add(listener); + } + + public void unsubscribe(ComplexMorphologyListener listener) { + listeners.remove(listener); + } + + public void setMask(Image mask) { + this.mask = mask; + listeners.forEach(listeners -> listeners.onMaskChanged(mask)); + } + + public void setDilation(Image image) { + dilation = image; + listeners.forEach(listener -> listener.onGeodesicDilationChanged(image)); + } + + public void setReconstruction(Image image) { + reconstruction = image; + listeners.forEach(listener -> listener.onGeodesicReconstructionChanged(image)); + } + + public void setMedian(Image image) { + mean = image; + listeners.forEach(listener -> listener.onMedianChanged(image)); + } + + public Image getMask() { return mask; } + + public Image getDilation() { + return dilation; + } + + public Image getReconstruction() { + return reconstruction; + } + + public Image getMean() { + return mean; + } + +} \ No newline at end of file diff --git a/src/main/java/domain/image/processing/morphology/ComplexMorphology.java b/src/main/java/domain/image/processing/morphology/ComplexMorphology.java index c5fcc7e..965b268 100644 --- a/src/main/java/domain/image/processing/morphology/ComplexMorphology.java +++ b/src/main/java/domain/image/processing/morphology/ComplexMorphology.java @@ -10,26 +10,30 @@ import org.opencv.imgproc.Imgproc; public class ComplexMorphology { - public static GrayScaleMatrix geodesicDilation(Image image, Image mask, int size) { - Mat input = toMat(image); + public static GrayScaleMatrix geodesicDilation(Image image, Image mask, int nbIter) { Mat maskMat = toMat(mask); - Mat kernel = createKernel(size); + Mat current = toMat(image); + Mat kernel = createKernel(3); - Mat dilated = new Mat(); - Imgproc.dilate(input, dilated, kernel); + for (int i = 0; i < nbIter; i++) { + Mat dilated = new Mat(); - Mat result = new Mat(); - Core.min(dilated, maskMat, result); + Imgproc.dilate(current, dilated, kernel); - return toGrayScaleMatrix(result); + Mat result = new Mat(); + + Core.min(dilated, maskMat, result); + + current = result; + } + + return toGrayScaleMatrix(current); } - public static GrayScaleMatrix geodesicReconstruction(Image marker, Image mask, int size) { - Mat markerMat = toMat(marker); + public static GrayScaleMatrix geodesicReconstruction(Image image, Image mask) { Mat maskMat = toMat(mask); - Mat kernel = createKernel(size); - - Mat current = markerMat.clone(); + Mat current = toMat(image); + Mat kernel = createKernel(3); while (true) { Mat dilated = new Mat(); @@ -38,7 +42,9 @@ public class ComplexMorphology { Mat next = new Mat(); Core.min(dilated, maskMat, next); - if (Core.norm(next, current, Core.NORM_INF) == 0) { + double difference = Core.norm(next, current, Core.NORM_INF); + + if (difference == 0) { return toGrayScaleMatrix(next); } @@ -46,11 +52,11 @@ public class ComplexMorphology { } } - public static GrayScaleMatrix meanFilter(Image image, int size) { + public static GrayScaleMatrix medianFilter(Image image, int tailleMasque) { Mat input = toMat(image); Mat result = new Mat(); - Imgproc.blur(input, result, new Size(size, size)); + Imgproc.medianBlur(input, result, tailleMasque); return toGrayScaleMatrix(result); } @@ -73,10 +79,7 @@ public class ComplexMorphology { } private static Mat createKernel(int size) { - return Imgproc.getStructuringElement( - Imgproc.MORPH_RECT, - new Size(size, size) - ); + return Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(size, size)); } private static GrayScaleMatrix toGrayScaleMatrix(Mat mat) { diff --git a/src/main/java/presenters/NavPresenter.java b/src/main/java/presenters/NavPresenter.java index eb02cb6..fe5bdd8 100644 --- a/src/main/java/presenters/NavPresenter.java +++ b/src/main/java/presenters/NavPresenter.java @@ -52,6 +52,8 @@ public class NavPresenter { public void goToBasicMorphology(){ navigator.showBasicMorphology(); } + public void goToComplexMorphology(){ navigator.showComplexMorphology(); } + public GrayScaleMatrix getImageGrayMatrix() { return appState.getImage().toGrayScale(); } public void setMode(Mode mode) { modeService.setMode(mode); } diff --git a/src/main/java/presenters/morphology/MorphologyListener.java b/src/main/java/presenters/morphology/BasicMorphologyListener.java similarity index 83% rename from src/main/java/presenters/morphology/MorphologyListener.java rename to src/main/java/presenters/morphology/BasicMorphologyListener.java index 9fc9db2..4538113 100644 --- a/src/main/java/presenters/morphology/MorphologyListener.java +++ b/src/main/java/presenters/morphology/BasicMorphologyListener.java @@ -2,7 +2,7 @@ package presenters.morphology; import domain.image.Image; -public interface MorphologyListener { +public interface BasicMorphologyListener { void onErosionChanged(Image image); void onDilationChanged(Image image); @@ -10,4 +10,6 @@ public interface MorphologyListener { void onOpeningChanged(Image image); void onClosingChanged(Image image); + + } diff --git a/src/main/java/presenters/morphology/MorphologyPresenter.java b/src/main/java/presenters/morphology/BasicMorphologyPresenter.java similarity index 90% rename from src/main/java/presenters/morphology/MorphologyPresenter.java rename to src/main/java/presenters/morphology/BasicMorphologyPresenter.java index c3a279c..123ca6f 100644 --- a/src/main/java/presenters/morphology/MorphologyPresenter.java +++ b/src/main/java/presenters/morphology/BasicMorphologyPresenter.java @@ -1,23 +1,23 @@ package presenters.morphology; import app.state.AppState; -import app.state.MorphologyState; +import app.state.BasicMorphologyState; import com.google.inject.Inject; import domain.image.Image; import services.MorphologyService; import ui.abstractions.IBasicMorphologyView; -public class MorphologyPresenter implements MorphologyListener { +public class BasicMorphologyPresenter implements BasicMorphologyListener { private final AppState appState; - private final MorphologyState morphologyState; + private final BasicMorphologyState morphologyState; private final MorphologyService morphologyService; private IBasicMorphologyView view; @Inject - public MorphologyPresenter( + public BasicMorphologyPresenter( AppState appState, - MorphologyState morphologyState, + BasicMorphologyState morphologyState, MorphologyService morphologyService ) { this.appState = appState; diff --git a/src/main/java/presenters/morphology/ComplexMorphologyListener.java b/src/main/java/presenters/morphology/ComplexMorphologyListener.java new file mode 100644 index 0000000..f6cd55c --- /dev/null +++ b/src/main/java/presenters/morphology/ComplexMorphologyListener.java @@ -0,0 +1,10 @@ +package presenters.morphology; + +import domain.image.Image; + +public interface ComplexMorphologyListener { + void onMaskChanged(Image mask); + void onGeodesicDilationChanged(Image image); + void onGeodesicReconstructionChanged(Image image); + void onMedianChanged(Image image); +} diff --git a/src/main/java/presenters/morphology/ComplexMorphologyPresenter.java b/src/main/java/presenters/morphology/ComplexMorphologyPresenter.java new file mode 100644 index 0000000..c1774d2 --- /dev/null +++ b/src/main/java/presenters/morphology/ComplexMorphologyPresenter.java @@ -0,0 +1,126 @@ +package presenters.morphology; + +import app.state.AppState; +import app.state.ComplexMorphologyState; +import com.google.inject.Inject; +import domain.image.Image; +import services.MorphologyService; +import ui.abstractions.IComplexMorphologyView; + +public class ComplexMorphologyPresenter implements ComplexMorphologyListener { + + private final AppState appState; + private final ComplexMorphologyState morphologyState; + private final MorphologyService morphologyService; + + private IComplexMorphologyView view; + + @Inject + public ComplexMorphologyPresenter( + AppState appState, + ComplexMorphologyState morphologyState, + MorphologyService morphologyService + ) { + this.appState = appState; + this.morphologyState = morphologyState; + this.morphologyService = morphologyService; + + morphologyState.subscribe(this); + } + + public void setView(IComplexMorphologyView view) { + this.view = view; + } + + public void setMask(Image mask) { + this.morphologyState.setMask(mask); + } + + public void computeGeodesicDilation(int iterations) { + Image image = appState.getImage(); + Image mask = morphologyState.getMask(); + + Image result = morphologyService.geodesicDilation(image, mask, iterations); + + morphologyState.setDilation(result); + } + + public void computeGeodesicReconstruction() { + Image image = appState.getImage(); + Image mask = morphologyState.getMask(); + + Image result = morphologyService.geodesicReconstruction(image, mask); + + + System.out.println("IMAGE: " + + image.getWidth() + "x" + image.getHeight()); + + System.out.println("MASK: " + + mask.getWidth() + "x" + mask.getHeight()); + + System.out.println( + "Same object? " + (image == mask) + ); + + System.out.println("Image : " + image.getPixel(0, 0).grayValue()); + + System.out.println("Mask : " + mask.getPixel(0, 0).grayValue()); + System.out.println("Result : " + result.getPixel(0, 0).grayValue()); + + double[][] imageData = image.toGrayScale().getRawData(); + double[][] maskData = mask.toGrayScale().getRawData(); + + int violations = 0; + + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + if (imageData[y][x] > maskData[y][x]) { + violations++; + } + } + } + + System.out.println("Marker > mask pixels: " + violations); + + morphologyState.setReconstruction(result); + } + + public void computeMedianFilter(int size) { + Image image = appState.getImage(); + + Image result = morphologyService.medianFilter(image, size); + + morphologyState.setMedian(result); + } + + public void dispose() { + view = null; + morphologyState.unsubscribe(this); + } + + @Override + public void onMaskChanged(Image mask) { + + } + + @Override + public void onGeodesicDilationChanged(Image image) { + if (view != null) { + view.updateDilation(image); + } + } + + @Override + public void onGeodesicReconstructionChanged(Image image) { + if (view != null) { + view.updateReconstruction(image); + } + } + + @Override + public void onMedianChanged(Image image) { + if (view != null) { + view.updateMedian(image); + } + } +} \ No newline at end of file diff --git a/src/main/java/services/MorphologyService.java b/src/main/java/services/MorphologyService.java index 0594767..aba9977 100644 --- a/src/main/java/services/MorphologyService.java +++ b/src/main/java/services/MorphologyService.java @@ -22,22 +22,21 @@ public class MorphologyService { return BasicMorphology.closing(image, size).toImage(); } - public Image geodesicDilation(Image image, Image mask, int size) { + public Image geodesicDilation(Image image, Image mask, int iterations) { return ComplexMorphology - .geodesicDilation(image, mask, size) + .geodesicDilation(image, mask, iterations) .toImage(); } - public Image geodesicReconstruction(Image marker, Image mask, int size) { + public Image geodesicReconstruction(Image image, Image mask) { return ComplexMorphology - .geodesicReconstruction(marker, mask, size) + .geodesicReconstruction(image, mask) .toImage(); } - public Image meanFilter(Image image, int size) { + public Image medianFilter(Image image, int size) { return ComplexMorphology - .meanFilter(image, size) + .medianFilter(image, size) .toImage(); } - } \ No newline at end of file diff --git a/src/main/java/ui/abstractions/IComplexMorphologyView.java b/src/main/java/ui/abstractions/IComplexMorphologyView.java new file mode 100644 index 0000000..628a91c --- /dev/null +++ b/src/main/java/ui/abstractions/IComplexMorphologyView.java @@ -0,0 +1,10 @@ +package ui.abstractions; + +import domain.image.Image; + +public interface IComplexMorphologyView { + void updateMask(Image mask); + void updateDilation(Image image); + void updateReconstruction(Image image); + void updateMedian(Image image); +} diff --git a/src/main/java/ui/implementation/components/nav/NavBar.java b/src/main/java/ui/implementation/components/nav/NavBar.java index 9816a79..9bfada7 100644 --- a/src/main/java/ui/implementation/components/nav/NavBar.java +++ b/src/main/java/ui/implementation/components/nav/NavBar.java @@ -254,6 +254,5 @@ public class NavBar extends JMenuBar implements INavBar { private void displayBasicMorphology(ActionEvent actionEvent) { presenter.goToBasicMorphology(); } - private void displayComplexeMorphology(ActionEvent actionEvent) { - } + private void displayComplexeMorphology(ActionEvent actionEvent) { presenter.goToComplexMorphology(); } } diff --git a/src/main/java/ui/implementation/views/morphology/BasicMorphologyView.java b/src/main/java/ui/implementation/views/morphology/BasicMorphologyView.java index cb96585..0842e99 100644 --- a/src/main/java/ui/implementation/views/morphology/BasicMorphologyView.java +++ b/src/main/java/ui/implementation/views/morphology/BasicMorphologyView.java @@ -2,17 +2,13 @@ package ui.implementation.views.morphology; import domain.image.Image; import jakarta.inject.Inject; -import presenters.morphology.MorphologyPresenter; +import presenters.morphology.BasicMorphologyPresenter; import ui.abstractions.AbstractView; import ui.abstractions.IBasicMorphologyView; import ui.implementation.components.controls.SaveButton; import ui.implementation.components.image.TabbedImagePanel; import javax.swing.*; -import javax.swing.text.AbstractDocument; -import javax.swing.text.AttributeSet; -import javax.swing.text.BadLocationException; -import javax.swing.text.DocumentFilter; import java.awt.*; public class BasicMorphologyView extends AbstractView implements IBasicMorphologyView { @@ -22,13 +18,13 @@ public class BasicMorphologyView extends AbstractView implements IBasicMorpholog private static final String OPENING_TAB_TITLE = "Ouverture"; private static final String CLOSING_TAB_TITLE = "Fermeture"; - private final MorphologyPresenter presenter; + private final BasicMorphologyPresenter presenter; private TabbedImagePanel modeTab; - private JTextField maskSizeField; + private JSpinner maskSizeSpinner; @Inject - public BasicMorphologyView(MorphologyPresenter presenter) { + public BasicMorphologyView(BasicMorphologyPresenter presenter) { this.presenter = presenter; } @@ -41,7 +37,9 @@ public class BasicMorphologyView extends AbstractView implements IBasicMorpholog modeTab.addPreviewTab(OPENING_TAB_TITLE); modeTab.addPreviewTab(CLOSING_TAB_TITLE); - maskSizeField = createPositiveIntegerField(); + maskSizeSpinner = new JSpinner( + new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 2) + ); JButton applyButton = new JButton("Appliquer"); applyButton.addActionListener(e -> applyMorphology()); @@ -49,7 +47,7 @@ public class BasicMorphologyView extends AbstractView implements IBasicMorpholog JButton saveButton = new SaveButton(); JPanel controlPanel = createControlPanel( - maskSizeField, + maskSizeSpinner, applyButton ); @@ -100,12 +98,17 @@ public class BasicMorphologyView extends AbstractView implements IBasicMorpholog @Override public void initValues() { - this.modeTab.addChangeListener(e -> applyMorphology()); + modeTab.addChangeListener(e -> applyMorphology()); + presenter.setView(this); + applyMorphology(); } - private JPanel createControlPanel(JTextField sizeField, JButton applyButton) { + private JPanel createControlPanel( + JSpinner sizeSpinner, + JButton applyButton + ) { JPanel panel = new JPanel(); GroupLayout layout = new GroupLayout(panel); @@ -119,9 +122,9 @@ public class BasicMorphologyView extends AbstractView implements IBasicMorpholog layout.createParallelGroup(GroupLayout.Alignment.CENTER) .addComponent(label) .addComponent( - sizeField, + sizeSpinner, GroupLayout.PREFERRED_SIZE, - 60, + 70, GroupLayout.PREFERRED_SIZE ) .addComponent(applyButton) @@ -131,7 +134,7 @@ public class BasicMorphologyView extends AbstractView implements IBasicMorpholog layout.createSequentialGroup() .addComponent(label) .addComponent( - sizeField, + sizeSpinner, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE @@ -142,18 +145,8 @@ public class BasicMorphologyView extends AbstractView implements IBasicMorpholog return panel; } - private JTextField createPositiveIntegerField() { - JTextField field = new JTextField("1", 5); - ((AbstractDocument) field.getDocument()).setDocumentFilter(new PositiveIntegerFilter()); - return field; - } - private void applyMorphology() { - Integer size = getMaskSize(); - - if (size == null || size % 2 == 0) { - return; - } + int size = (Integer) maskSizeSpinner.getValue(); switch (modeTab.getSelectedIndex()) { case 0 -> presenter.computeErosion(size); @@ -163,32 +156,22 @@ public class BasicMorphologyView extends AbstractView implements IBasicMorpholog } } - private Integer getMaskSize() { - try { - int size = Integer.parseInt(maskSizeField.getText()); - - if (size <= 0) { - return null; - } - - return size; - } catch (NumberFormatException e) { - return null; - } - } - + @Override public void updateErosion(Image image) { modeTab.loadImage(EROSION_TAB_TITLE, image); } + @Override public void updateDilation(Image image) { modeTab.loadImage(DILATION_TAB_TITLE, image); } + @Override public void updateOpening(Image image) { modeTab.loadImage(OPENING_TAB_TITLE, image); } + @Override public void updateClosing(Image image) { modeTab.loadImage(CLOSING_TAB_TITLE, image); } @@ -198,31 +181,4 @@ public class BasicMorphologyView extends AbstractView implements IBasicMorpholog presenter.setView(null); presenter.dispose(); } - - private static class PositiveIntegerFilter extends DocumentFilter { - - @Override - public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr - ) throws BadLocationException { - if (isValid(fb.getDocument().getText(0, fb.getDocument().getLength()), offset, 0, string)) { - super.insertString(fb, offset, string, attr); - } - } - - @Override - public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { - if (isValid(fb.getDocument().getText(0, fb.getDocument().getLength()), offset, length, text)) { - super.replace(fb, offset, length, text, attrs); - } - } - - private boolean isValid(String current, int offset, int length, String replacement) { - String result = current.substring(0, offset) + replacement + current.substring(offset + length); - if (result.isEmpty()) { - return true; - } - - return result.matches("[1-9][0-9]*"); - } - } } \ No newline at end of file diff --git a/src/main/java/ui/implementation/views/morphology/ComplexMorphologyView.java b/src/main/java/ui/implementation/views/morphology/ComplexMorphologyView.java new file mode 100644 index 0000000..0138351 --- /dev/null +++ b/src/main/java/ui/implementation/views/morphology/ComplexMorphologyView.java @@ -0,0 +1,234 @@ +package ui.implementation.views.morphology; + +import domain.image.Image; +import infrastructure.image.io.ImageLoader; +import infrastructure.ui.ImageMapper; +import jakarta.inject.Inject; +import presenters.morphology.ComplexMorphologyPresenter; +import ui.abstractions.AbstractView; +import ui.abstractions.IComplexMorphologyView; +import ui.implementation.components.controls.SaveButton; +import ui.implementation.components.image.TabbedImagePanel; +import ui.implementation.components.intent.FileChooser; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.IOException; + +public class ComplexMorphologyView extends AbstractView implements IComplexMorphologyView { + + private static final String DILATION_TAB_TITLE = "Dilatation géodésique"; + private static final String RECONSTRUCTION_TAB_TITLE = "Reconstruction géodésique"; + private static final String MEDIAN_TAB_TITLE = "Filtre médian"; + + private final ComplexMorphologyPresenter presenter; + + private TabbedImagePanel modeTab; + + private JButton maskButton; + private JLabel maskLabel; + private JSpinner iterationsSpinner; + + @Inject + public ComplexMorphologyView(ComplexMorphologyPresenter presenter) { + this.presenter = presenter; + } + + @Override + public void initComponents() { + modeTab = new TabbedImagePanel(); + + modeTab.addPreviewTab(DILATION_TAB_TITLE); + modeTab.addPreviewTab(RECONSTRUCTION_TAB_TITLE); + modeTab.addPreviewTab(MEDIAN_TAB_TITLE); + + iterationsSpinner = new JSpinner( + new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1) + ); + + maskButton = new JButton("Choisir..."); + maskLabel = new JLabel("Aucun masque"); + + maskButton.addActionListener(e -> chooseGeodesicMask()); + + JButton applyButton = new JButton("Appliquer"); + applyButton.addActionListener(e -> applyMorphology()); + + JButton saveButton = new SaveButton(); + + JPanel controlPanel = createControlPanel(applyButton); + + 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("Traitement morphologique complexe"); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + pack(); + } + + @Override + public void initValues() { + modeTab.addChangeListener(e -> updateControls()); + + presenter.setView(this); + + updateControls(); + } + + private JPanel createControlPanel(JButton applyButton) { + JPanel panel = new JPanel(); + + GroupLayout layout = new GroupLayout(panel); + panel.setLayout(layout); + layout.setAutoCreateGaps(true); + layout.setAutoCreateContainerGaps(true); + + JLabel maskLabelTitle = new JLabel("Masque géodésique :"); + JLabel iterationsLabel = new JLabel("Nombre d'itérations :"); + + layout.setHorizontalGroup( + layout.createParallelGroup(GroupLayout.Alignment.CENTER) + .addComponent(maskLabelTitle) + .addComponent(maskButton) + .addComponent(maskLabel) + .addComponent(iterationsLabel) + .addComponent( + iterationsSpinner, + GroupLayout.PREFERRED_SIZE, + 70, + GroupLayout.PREFERRED_SIZE + ) + .addComponent(applyButton) + ); + + layout.setVerticalGroup( + layout.createSequentialGroup() + .addComponent(maskLabelTitle) + .addComponent(maskButton) + .addComponent(maskLabel) + .addComponent(iterationsLabel) + .addComponent( + iterationsSpinner, + GroupLayout.PREFERRED_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE + ) + .addComponent(applyButton) + ); + + return panel; + } + + private void chooseGeodesicMask() { + FileChooser chooser = new FileChooser("./"); + + if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) { + return; + } + + File file = chooser.getSelectedFile(); + loadImage(file); + maskLabel.setText(file.getName()); + applyMorphology(); + } + + private void updateControls() { + boolean geodesicOperation = modeTab.getSelectedIndex() == 0 || modeTab.getSelectedIndex() == 1; + + maskButton.setEnabled(geodesicOperation); + maskLabel.setEnabled(geodesicOperation); + } + + private void applyMorphology() { + int operation = modeTab.getSelectedIndex(); + int iterations = (Integer) iterationsSpinner.getValue(); + + switch (operation) { + case 0 -> presenter.computeGeodesicDilation(iterations); + case 1 -> presenter.computeGeodesicReconstruction(); + case 2 -> presenter.computeMedianFilter(iterations); + } + } + + @Override + public void updateMask(Image mask) { + + } + + @Override + public void updateDilation(Image image) { + modeTab.loadImage(DILATION_TAB_TITLE, image); + } + + @Override + public void updateReconstruction(Image image) { + modeTab.loadImage(RECONSTRUCTION_TAB_TITLE, image); + } + + @Override + public void updateMedian(Image image) { + modeTab.loadImage(MEDIAN_TAB_TITLE, image); + } + + @Override + public void disposeCore() { + presenter.setView(null); + presenter.dispose(); + } + + private void loadImage(File file) { + try { + Image mask = ImageLoader.loadImage(file); + presenter.setMask(mask); + } catch (IOException ioe) { + displayErrorMessage("Oups !", "Impossible de charger le masque"); + } + } + + private void displayErrorMessage(String title, String message) { + JOptionPane.showMessageDialog( + this, + message, + title, + JOptionPane.ERROR_MESSAGE + ); + } +} \ No newline at end of file diff --git a/src/main/resources/Thumbs.db b/src/main/resources/Thumbs.db deleted file mode 100644 index 79d32e63d59d9ec6d6175d290ced5626c76d4427..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 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