All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.gui.controls.ControlPanelDisparityDense Maven / Gradle / Ivy

/*
 * Copyright (c) 2022, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.gui.controls;

import boofcv.abst.disparity.ConfigSpeckleFilter;
import boofcv.abst.disparity.DisparitySmoother;
import boofcv.abst.disparity.StereoDisparity;
import boofcv.alg.disparity.sgm.SgmDisparityCost;
import boofcv.factory.disparity.*;
import boofcv.factory.transform.census.CensusVariants;
import boofcv.gui.StandardAlgConfigPanel;
import boofcv.gui.image.ShowImages;
import boofcv.struct.image.GrayF32;
import boofcv.struct.image.GrayU8;

import javax.swing.*;
import java.awt.*;

/**
 * Controls for configuring disparity algorithms
 *
 * @author Peter Abeles
 */
@SuppressWarnings({"NullAway.Init"})
public class ControlPanelDisparityDense extends StandardAlgConfigPanel {

	private final static String[] ERRORS_BLOCK = new String[]{"SAD", "Census", "NCC"};
	private final static String[] ERRORS_SGM = new String[]{"Absolute Diff", "Census", "HMI"};

	// Disparity configuration. NOT publicly accessible because BM and BM5 are mirrored. use accessor
	private final ConfigDisparity configDisparity;
	// Configuration for speckle filtering
	public final ConfigSpeckleFilter configSpeckle;

	JComboBox comboMethod, comboError;

	JTabbedPane tabbedPane = new JTabbedPane();

	// Controls for families of algorithms
	ControlsBlockMatching controlBM;
	ControlsSemiGlobal controlSGM;

	// Controls for error types
	ControlsSAD controlSad;
	ControlsCensus controlCensus;
	ControlsNCC controlNCC;
	ControlsMutualInfo controlHMI;

	// Disparity smoothing
	ControlsSpeckleConnComp controlSpeckle;

	boolean ignoreChanges = false;

	Listener listener;
	Class imageType;

	public ControlPanelDisparityDense( ConfigDisparity configDisparity,
									   ConfigSpeckleFilter configSpeckle,
									   Class imageType ) {
		setBorder(BorderFactory.createEmptyBorder());
		this.configDisparity = configDisparity;
		this.configSpeckle = configSpeckle;
		this.imageType = imageType;

		comboMethod = combo(e -> handleMethod(), configDisparity.approach.ordinal(), "BlockMatch", "BlockMatch-5", "SGM");
		if (isBlockSelected())
			comboError = combo(e -> handleErrorSelected(false), configDisparity.approachBM.errorType.ordinal(), (Object[])ERRORS_BLOCK);
		else
			comboError = combo(e -> handleErrorSelected(false), configDisparity.approachSGM.errorType.ordinal(), (Object[])ERRORS_SGM);
		controlBM = new ControlsBlockMatching();
		controlSGM = new ControlsSemiGlobal();
		controlSad = new ControlsSAD();
		controlCensus = new ControlsCensus();
		controlNCC = new ControlsNCC();
		controlHMI = new ControlsMutualInfo();
		controlSpeckle = new ControlsSpeckleConnComp();

		tabbedPane.addTab("Method", getModelControl(isBlockSelected()));
		tabbedPane.addTab("Error", getErrorControl(comboError.getSelectedIndex()));
		tabbedPane.addTab("Smooth", controlSpeckle);

		// SGM is larger than BM, make the initial area larger
		controlBM.setPreferredSize(controlSGM.getPreferredSize());

		addLabeled(comboMethod, "Method");
		addLabeled(comboError, "Error");
		add(tabbedPane);

		// Make sure the GUI is updated with the latest selection
		handleErrorSelected(true);
	}

	public static ControlPanelDisparityDense createRange( int disparityMin, int disparityRange,
														  Class imageType ) {
		var c = new ConfigDisparity();
		c.approachBM.disparityMin = disparityMin;
		c.approachBM.disparityRange = disparityRange;
		c.approachSGM.disparityMin = disparityMin;
		c.approachSGM.disparityRange = disparityRange;
		c.approachBM.saveScore = true;
		c.approachBM5.saveScore = true;

		return new ControlPanelDisparityDense(c, new ConfigSpeckleFilter(), imageType);
	}

	public void broadcastChange() {
		Listener listener = this.listener;
		if (listener == null)
			return;
		if (ignoreChanges)
			return;

		listener.handleDisparityChange();
	}

	public void updateControlEnabled() {
		if (!isBlockSelected()) {
			controlSGM.updateControlsEnabled();
		}
	}

	/**
	 * Accessor function to copy over the disparity configuration. This is needed because BM and BM5 are mirrored
	 */
	public void getDisparityConfig( ConfigDisparity dst ) {
		configDisparity.approachBM5.setTo(configDisparity.approachBM);
		dst.setTo(configDisparity);
	}

	@SuppressWarnings("unchecked")
	public StereoDisparity createAlgorithm() {
//		BoofSwingUtil.checkGuiThread(); // TODO lock instead to make this safe?

		configDisparity.approachBM5.setTo(configDisparity.approachBM);
		return FactoryStereoDisparity.generic(configDisparity, imageType);
	}

	public DisparitySmoother createSmoother() {
		configDisparity.approachBM5.setTo(configDisparity.approachBM);
		Class dispType = configDisparity.isSubpixel() ? GrayF32.class : GrayU8.class;
		return FactoryStereoDisparity.removeSpeckle(configSpeckle, dispType);
	}

	public int getDisparityMin() {
		if (isBlockSelected())
			return configDisparity.approachBM.disparityMin;
		else
			return configDisparity.approachSGM.disparityMin;
	}

	public int getDisparityRange() {
		if (isBlockSelected())
			return configDisparity.approachBM.disparityRange;
		else
			return configDisparity.approachSGM.disparityRange;
	}

	/**
	 * The user changed which method is being used
	 */
	private void handleMethod() {
		if (configDisparity.approach.ordinal() == comboMethod.getSelectedIndex())
			return;
		boolean previousBlock = isBlockSelected();
		configDisparity.approach = ConfigDisparity.Approach.values()[comboMethod.getSelectedIndex()];
		boolean block = isBlockSelected();

		// All the code above can cause multiple calls to broadcastChange() as listeners are triggered
		ignoreChanges = true;

		// swap out the controls and stuff
		if (block != previousBlock) {
			int activeTab = tabbedPane.getSelectedIndex(); // don't switch out of the current tab
			if (block) {
				comboError.setModel(new DefaultComboBoxModel<>(ERRORS_BLOCK));
				comboError.setSelectedIndex(configDisparity.approachBM.errorType.ordinal());
			} else {
				comboError.setModel(new DefaultComboBoxModel<>(ERRORS_SGM));
				comboError.setSelectedIndex(configDisparity.approachSGM.errorType.ordinal());
			}
			Component c = getModelControl(block);
			if (!block)
				controlSGM.updateControlsEnabled();
			tabbedPane.removeTabAt(0);
			tabbedPane.insertTab("Method", null, c, null, 0);
			tabbedPane.setSelectedIndex(activeTab);
			handleErrorSelected(true);
		}

		// This will ignore all changes until after they have been processed
		SwingUtilities.invokeLater(() -> {
			ignoreChanges = false;
			broadcastChange();
		});
	}

	private Component getModelControl( boolean block ) {
		Component c;
		if (block) {
			c = controlBM;
		} else {
			c = controlSGM;
		}
		return c;
	}

	private boolean isBlockSelected() {
		return configDisparity.approach != ConfigDisparity.Approach.SGM;
	}

	private void handleErrorSelected( boolean force ) {
		boolean block = isBlockSelected();
		int previousIdx = block ? configDisparity.approachBM.errorType.ordinal() : configDisparity.approachSGM.errorType.ordinal();
		if (!force && previousIdx == comboError.getSelectedIndex())
			return;
		int selectedIdx = comboError.getSelectedIndex();

		// Avoid multiple calls to broadcastChange()
		if (!force)
			ignoreChanges = true;

		// If forced keep the previously active tab active
		int activeTab = tabbedPane.getSelectedIndex();
//		System.out.println("error for block="+block+" idx="+selectedIdx);

		if (block) {
			configDisparity.approachBM.errorType = DisparityError.values()[selectedIdx];
			controlCensus.update(configDisparity.approachBM.configCensus);
			controlNCC.update(configDisparity.approachBM.configNCC);
		} else {
			configDisparity.approachSGM.errorType = DisparitySgmError.values()[selectedIdx];
			controlCensus.update(configDisparity.approachSGM.configCensus);
			controlHMI.update(configDisparity.approachSGM.configHMI);
		}
		Component c = getErrorControl(selectedIdx);
		tabbedPane.removeTabAt(1);
		tabbedPane.insertTab("Error", null, c, null, 1);
		tabbedPane.setSelectedIndex(activeTab);

		if (!force)
			SwingUtilities.invokeLater(() -> {
				ignoreChanges = false;
				broadcastChange();
			});
	}

	private Component getErrorControl( int selectedIdx ) {
		Component c;
		if (isBlockSelected()) {
			c = switch (selectedIdx) {
				case 0 -> controlSad;
				case 1 -> controlCensus;
				case 2 -> controlNCC;
				default -> throw new IllegalArgumentException("Unknown");
			};
		} else {
			c = switch (selectedIdx) {
				case 0 -> controlSad;
				case 1 -> controlCensus;
				case 2 -> controlHMI;
				default -> throw new IllegalArgumentException("Unknown");
			};
		}
		return c;
	}

	public class ControlsBlockMatching extends StandardAlgConfigPanel {
		JSpinner spinnerDisparityMin = spinner(configDisparity.approachBM.disparityMin, 0, 1000, 5);
		JSpinner spinnerDisparityRange = spinner(configDisparity.approachBM.disparityRange, 1, 254, 5);
		JSpinner radiusXSpinner = spinner(configDisparity.approachBM.regionRadiusX, 0, 50, 1);
		JSpinner radiusYSpinner = spinner(configDisparity.approachBM.regionRadiusY, 0, 50, 1);
		JSpinner spinnerError = spinner(configDisparity.approachBM.maxPerPixelError, -1, 80, 5);
		JSpinner spinnerReverse = spinner(configDisparity.approachBM.validateRtoL, -1, 50, 1);
		JSpinner spinnerTexture = spinner(configDisparity.approachBM.texture, 0.0, 1.0, 0.05, 1, 3);
		JCheckBox subpixelToggle = checkbox("Subpixel", configDisparity.approachBM.subpixel, "Subpixel Disparity Estimate");

		ControlsBlockMatching() {
			setBorder(BorderFactory.createEmptyBorder());
			addLabeled(spinnerDisparityMin, "Min Disp.", "Minimum disparity value considered. (Pixels)");
			addLabeled(spinnerDisparityRange, "Range Disp.", "Range of disparity values searched. (Pixels)");
			addLabeled(radiusXSpinner, "Radius X", "Block Width. (Pixels)");
			addLabeled(radiusYSpinner, "Radius Y", "Block Height. (Pixels)");
			addLabeled(spinnerError, "Max Error", "Maximum allowed matching error");
			addLabeled(spinnerTexture, "Texture", "Texture validation. 0 = disabled. 1 = most strict.");
			addLabeled(spinnerReverse, "Reverse", "Reverse Validation Tolerance. -1 = disable. (Pixels)");
			addAlignLeft(subpixelToggle);
		}

		@Override
		public void controlChanged( final Object source ) {
			ConfigDisparityBM c = configDisparity.approachBM;

			if (source == spinnerReverse) {
				c.validateRtoL = ((Number)spinnerReverse.getValue()).intValue();
			} else if (source == spinnerDisparityMin) {
				c.disparityMin = ((Number)spinnerDisparityMin.getValue()).intValue();
			} else if (source == spinnerDisparityRange) {
				c.disparityRange = ((Number)spinnerDisparityRange.getValue()).intValue();
			} else if (source == spinnerError) {
				c.maxPerPixelError = ((Number)spinnerError.getValue()).intValue();
			} else if (source == radiusXSpinner) {
				c.regionRadiusX = ((Number)radiusXSpinner.getValue()).intValue();
			} else if (source == radiusYSpinner) {
				c.regionRadiusY = ((Number)radiusYSpinner.getValue()).intValue();
			} else if (source == spinnerTexture) {
				c.texture = ((Number)spinnerTexture.getValue()).doubleValue();
			} else if (source == subpixelToggle) {
				c.subpixel = subpixelToggle.isSelected();
			} else {
				throw new RuntimeException("Unknown");
			}
			broadcastChange();
		}
	}

	public class ControlsSemiGlobal extends StandardAlgConfigPanel {
		JComboBox comboPaths = combo(configDisparity.approachSGM.paths.ordinal(), "1", "2", "4", "8", "16");

		JSpinner spinnerPenaltySmall = spinner(configDisparity.approachSGM.penaltySmallChange, 0, SgmDisparityCost.MAX_COST, 10);
		JSpinner spinnerPenaltyLarge = spinner(configDisparity.approachSGM.penaltyLargeChange, 1, SgmDisparityCost.MAX_COST, 10);

		JSpinner spinnerDisparityMin = spinner(configDisparity.approachSGM.disparityMin, 0, 1000, 5);
		JSpinner spinnerDisparityRange = spinner(configDisparity.approachSGM.disparityRange, 1, 254, 5);
		JSpinner spinnerError = spinner(configDisparity.approachSGM.maxError, -1, Short.MAX_VALUE, 200);
		JSpinner spinnerReverse = spinner(configDisparity.approachSGM.validateRtoL, -1, 50, 1);
		JSpinner spinnerTexture = spinner(configDisparity.approachSGM.texture, 0.0, 1.0, 0.05, 1, 3);
		JCheckBox subpixelToggle = checkbox("Subpixel", configDisparity.approachSGM.subpixel);
		JCheckBox useBlocks = checkbox("Use Blocks", configDisparity.approachSGM.useBlocks);
		JComboBox comboBlockApproach = combo(configDisparity.approachSGM.configBlockMatch.approach.ordinal(), (Object[])BlockMatchingApproach.values());
		JSpinner radiusXSpinner = spinner(configDisparity.approachSGM.configBlockMatch.radiusX, 0, 50, 1); // TODO move to error
		JSpinner radiusYSpinner = spinner(configDisparity.approachSGM.configBlockMatch.radiusY, 0, 50, 1);

		ControlsSemiGlobal() {
			setBorder(BorderFactory.createEmptyBorder());
			addLabeled(spinnerDisparityMin, "Min Disp.");
			addLabeled(spinnerDisparityRange, "Range Disp.");
			addLabeled(spinnerError, "Max Error");
			addLabeled(spinnerTexture, "Texture");
			addLabeled(spinnerReverse, "Reverse");
			addLabeled(comboPaths, "Paths");
			addLabeled(spinnerPenaltySmall, "Penalty Small");
			addLabeled(spinnerPenaltyLarge, "Penalty Large");
			addAlignLeft(subpixelToggle);
			addAlignLeft(useBlocks);
			addLabeled(comboBlockApproach, "Approach");
			addLabeled(radiusXSpinner, "Radius X");
			addLabeled(radiusYSpinner, "Radius Y");
			updateControlsEnabled();
		}

		@Override
		public void controlChanged( final Object source ) {
			ConfigDisparitySGM c = configDisparity.approachSGM;

			if (source == spinnerReverse) {
				c.validateRtoL = ((Number)spinnerReverse.getValue()).intValue();
			} else if (source == spinnerDisparityMin) {
				c.disparityMin = ((Number)spinnerDisparityMin.getValue()).intValue();
			} else if (source == spinnerDisparityRange) {
				c.disparityRange = ((Number)spinnerDisparityRange.getValue()).intValue();
			} else if (source == spinnerError) {
				c.maxError = ((Number)spinnerError.getValue()).intValue();
			} else if (source == spinnerTexture) {
				c.texture = ((Number)spinnerTexture.getValue()).doubleValue();
			} else if (source == spinnerPenaltySmall) {
				c.penaltySmallChange = ((Number)spinnerPenaltySmall.getValue()).intValue();
			} else if (source == spinnerPenaltyLarge) {
				c.penaltyLargeChange = ((Number)spinnerPenaltyLarge.getValue()).intValue();
			} else if (source == radiusXSpinner) {
				c.configBlockMatch.radiusX = ((Number)radiusXSpinner.getValue()).intValue();
			} else if (source == radiusYSpinner) {
				c.configBlockMatch.radiusY = ((Number)radiusYSpinner.getValue()).intValue();
			} else if (source == comboPaths) {
				c.paths = ConfigDisparitySGM.Paths.values()[comboPaths.getSelectedIndex()];
			} else if (source == subpixelToggle) {
				c.subpixel = subpixelToggle.isSelected();
			} else if (source == useBlocks) {
				c.useBlocks = useBlocks.isSelected();
				updateControlsEnabled();
			} else if (source == comboBlockApproach) {
				c.configBlockMatch.approach = BlockMatchingApproach.values()[comboBlockApproach.getSelectedIndex()];
			} else {
				throw new RuntimeException("Unknown");
			}
			broadcastChange();
		}

		void updateControlsEnabled() {
			final boolean e = configDisparity.approachSGM.useBlocks;
			comboBlockApproach.setEnabled(e);
			radiusXSpinner.setEnabled(e);
			radiusYSpinner.setEnabled(e);
		}
	}

	static class ControlsSAD extends StandardAlgConfigPanel {}

	@SuppressWarnings({"NullAway.Init"})
	class ControlsCensus extends StandardAlgConfigPanel {
		JComboBox comboVariant = combo(0, (Object[])CensusVariants.values());
		ConfigDisparityError.Census settings;

		public ControlsCensus() {
			setBorder(BorderFactory.createEmptyBorder());
			addLabeled(comboVariant, "Variant");
		}

		public void update( ConfigDisparityError.Census settings ) {
			this.settings = settings;
			comboVariant.setSelectedIndex(settings.variant.ordinal());
		}

		@Override
		public void controlChanged( final Object source ) {
			if (source == comboVariant) {
				settings.variant = CensusVariants.values()[comboVariant.getSelectedIndex()];
			} else {
				throw new RuntimeException("Unknown");
			}
			broadcastChange();
		}
	}

	@SuppressWarnings({"NullAway.Init"})
	class ControlsNCC extends StandardAlgConfigPanel {
		JSpinner spinnerEps = spinner(0.0, 0, 1.0, 0.001, "0.0E0", 10);
		ConfigDisparityError.NCC settings;

		ControlsNCC() {
			setBorder(BorderFactory.createEmptyBorder());
			addLabeled(spinnerEps, "EPS");
		}

		public void update( ConfigDisparityError.NCC settings ) {
			this.settings = settings;
			spinnerEps.setValue(settings.eps);
		}

		@Override
		public void controlChanged( final Object source ) {
			if (source == spinnerEps) {
				settings.eps = ((Number)spinnerEps.getValue()).doubleValue();
			} else {
				throw new RuntimeException("Unknown");
			}
			broadcastChange();
		}
	}

	@SuppressWarnings({"NullAway.Init"})
	class ControlsMutualInfo extends StandardAlgConfigPanel {
		JSpinner spinnerBlur = spinner(1, 0, 10, 1);
		JSpinner spinnerPyramidWidth = spinner(20, 20, 10000, 50);
		JSpinner spinnerExtra = spinner(0, 0, 5, 1);

		ConfigDisparityError.HMI settings;

		ControlsMutualInfo() {
			setBorder(BorderFactory.createEmptyBorder());
			addLabeled(spinnerBlur, "Blur Radius");
			addLabeled(spinnerPyramidWidth, "Pyr Min W");
			addLabeled(spinnerExtra, "Extra Iter.");
		}

		public void update( ConfigDisparityError.HMI settings ) {
			this.settings = settings;
			spinnerBlur.setValue(settings.smoothingRadius);
			spinnerPyramidWidth.setValue(settings.pyramidLayers.minWidth);
			spinnerExtra.setValue(settings.extraIterations);
		}

		@Override
		public void controlChanged( final Object source ) {
			if (source == spinnerBlur) {
				settings.smoothingRadius = ((Number)spinnerBlur.getValue()).intValue();
			} else if (source == spinnerPyramidWidth) {
				settings.pyramidLayers.minWidth = ((Number)spinnerPyramidWidth.getValue()).intValue();
				settings.pyramidLayers.minHeight = ((Number)spinnerPyramidWidth.getValue()).intValue();
			} else if (source == spinnerExtra) {
				settings.extraIterations = ((Number)spinnerExtra.getValue()).intValue();
			} else {
				throw new RuntimeException("Unknown");
			}
			broadcastChange();
		}
	}

	class ControlsSpeckleConnComp extends StandardAlgConfigPanel {
		JSpinner spinnerSimilar = spinner(configSpeckle.similarTol, 0.0, 100.0, 0.5);
		JConfigLength lengthRegion = configLength(configSpeckle.maximumArea, 0.0, 1e6);

		public ControlsSpeckleConnComp() {
			setBorder(BorderFactory.createEmptyBorder());
			addLabeled(spinnerSimilar, "Simularity",
					"How similar two pixel values need to be considered connected.");
			addLabeled(lengthRegion, "Region", "Maximum region size for removal");
		}

		@Override
		public void controlChanged( final Object source ) {
			if (source == spinnerSimilar) {
				configSpeckle.similarTol = ((Number)spinnerSimilar.getValue()).floatValue();
			} else if (source == lengthRegion) {
				configSpeckle.maximumArea.setTo(lengthRegion.getValue());
			} else {
				throw new RuntimeException("Unknown");
			}
			broadcastChange();
		}
	}

	public Listener getListener() {
		return listener;
	}

	public void setListener( Listener listener ) {
		this.listener = listener;
	}

	public interface Listener {
		void handleDisparityChange();
	}

	public static void main( String[] args ) {
		ControlPanelDisparityDense controls = ControlPanelDisparityDense.createRange(0, 150, GrayU8.class);
		ShowImages.showWindow(controls, "Controls");
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy