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

boofcv.gui.calibration.DisplayCalibrationPanel 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.calibration;

import boofcv.abst.geo.calibration.ImageResults;
import boofcv.alg.geo.calibration.CalibrationObservation;
import boofcv.alg.geo.calibration.ScoreCalibrationFill.RegionInfo;
import boofcv.gui.BoofSwingUtil;
import boofcv.gui.feature.VisualizeFeatures;
import boofcv.gui.image.ImageZoomPanel;
import boofcv.struct.distort.DoNothing2Transform2_F32;
import boofcv.struct.distort.Point2Transform2_F32;
import boofcv.struct.geo.PointIndex2D_F64;
import georegression.struct.point.Point2D_F32;
import georegression.struct.point.Point2D_F64;
import lombok.Getter;
import org.ddogleg.struct.DogArray;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.List;

import static boofcv.gui.calibration.UtilCalibrationGui.drawNumbers;
import static boofcv.gui.calibration.UtilCalibrationGui.renderOrder;

/**
 * Panel for displaying results from camera calibration. Controls and renders visuals.
 *
 * @author Peter Abeles
 */
public abstract class DisplayCalibrationPanel extends ImageZoomPanel {

	// number of pixels away at zoom=1 a corner can be selected
	double canonicalClickDistance = 15;

	// configures what is displayed or not
	public boolean showPoints = true;
	public boolean showErrors = true;
	public boolean showUndistorted = false;
	public boolean showAll = false;
	public boolean showNumbers = true;
	public boolean showOrder = true;
	public boolean showResiduals = false;
	public boolean showImageUnoccupied = false;
	public double errorScale;

	// Which observation in the current image has the user selected
	@Getter protected int selectedObservation = -1;

	// observed feature locations
	@Nullable @Getter CalibrationObservation observation = null;
	// results of calibration
	@Nullable @Getter public ImageResults results = null;
	@Nullable List allObservations = null;

	// Used to transform point coordinate system
	protected Point2Transform2_F32 pixelTransform = new DoNothing2Transform2_F32();

	// Specified which regions in the image have been filled in
	public final DogArray unoccupied = new DogArray<>(RegionInfo::new, RegionInfo::reset);

	// workspace
	protected Point2D_F32 adj = new Point2D_F32();
	protected Point2D_F32 adj2 = new Point2D_F32();
	protected Ellipse2D.Double ellipse = new Ellipse2D.Double();
	protected Line2D.Double line = new Line2D.Double();
	protected Rectangle2D.Double rect = new Rectangle2D.Double();

	// Called after setScale has been called
	public SetScale setScale = ( s ) -> {};

	final Color lightRed = new Color(255, 150, 150);

	protected DisplayCalibrationPanel() {
		panel.addMouseWheelListener(e -> setScale(BoofSwingUtil.mouseWheelImageZoom(scale, e)));

		// navigate using left mouse clicks
		panel.addMouseListener(new MouseAdapter() {
			@Override
			public void mousePressed( MouseEvent e ) {
				panel.requestFocus();
				if (!SwingUtilities.isRightMouseButton(e))
					return;
				CalibrationObservation features = DisplayCalibrationPanel.this.observation;
				if (features == null)
					return;
				selectedObservation = findClickedPoint(e, features);
				repaint();
			}
		});
	}

	/**
	 * Finds the landmark which is the closest to the point clicked by the user. Maximum distance is determined
	 * in image pixels
	 */
	protected int findClickedPoint( MouseEvent e, CalibrationObservation features ) {
		// use square distance since it's faster
		double bestDistanceSq = canonicalClickDistance/scale;
		bestDistanceSq *= bestDistanceSq;

		int bestIndex = -1;
		Point2D_F64 p = pixelToPoint(e.getX(), e.getY());
		for (int i = 0; i < features.points.size(); i++) {
			Point2D_F64 f = features.points.get(i).p;
			pixelTransform.compute((float)f.x, (float)f.y, adj);
			double d = p.distance2(adj.x, adj.y);
			if (d <= bestDistanceSq) {
				bestDistanceSq = d;
				bestIndex = i;
			}
		}
		return bestIndex;
	}

	public void setResults( CalibrationObservation features, @Nullable ImageResults results,
							List allFeatures ) {
		BoofSwingUtil.checkGuiThread();

		this.observation = features;
		this.results = results;
		this.allObservations = allFeatures;
		this.selectedObservation = -1;
	}

	public void setUnoccupied( List unoccupied ) {
		BoofSwingUtil.checkGuiThread();

		this.unoccupied.reset().resize(unoccupied.size());
		for (int i = 0; i < unoccupied.size(); i++) {
			this.unoccupied.get(i).setTo(unoccupied.get(i));
		}
	}

	/** Clears visualization image that is specific to a single view */
	public void clearViewResults() {
		BoofSwingUtil.checkGuiThread();

		observation = null;
		results = null;
		selectedObservation = -1;
	}

	/** Clears all calibration information for all views */
	public void clearAllResults() {
		clearViewResults();
		allObservations = null;
		unoccupied.reset();
	}

	public void setDisplay( boolean showPoints, boolean showErrors,
							boolean showUndistorted, boolean showAll, boolean showNumbers,
							boolean showOrder,
							double errorScale ) {
		this.showPoints = showPoints;
		this.showErrors = showErrors;
		this.showUndistorted = showUndistorted;
		this.showAll = showAll;
		this.showNumbers = showNumbers;
		this.showOrder = showOrder;
		this.errorScale = errorScale;
	}

	@Override public synchronized void setScale( double scale ) {
		// Avoid endless loops by making sure it's changing
		if (this.scale == scale)
			return;
		super.setScale(scale);
		setScale.setScale(scale);
	}

	/**
	 * Forgets the previously passed in calibration
	 */
	public abstract void clearCalibration();

	public void deselectPoint() {
		selectedObservation = -1;
	}

	/**
	 * Visualizes calibration information, such as feature location and order.
	 */
	protected void drawFeatures( Graphics2D g2, double scale ) {
		BoofSwingUtil.antialiasing(g2);

		if (showImageUnoccupied)
			drawImageUnoccupied(g2, scale);

		if (showAll && allObservations != null) {
			for (CalibrationObservation l : allObservations) {
				for (PointIndex2D_F64 p : l.points) {
					pixelTransform.compute((float)p.p.x, (float)p.p.y, adj);
					VisualizeFeatures.drawPoint(g2, adj.x*scale, adj.y*scale, 3, Color.BLUE, Color.WHITE, ellipse);
				}
			}
		}

		// Everything else relies on observations so abort if it's null
		final CalibrationObservation set = observation;
		if (set == null) {
			return;
		}


		if (showOrder) {
			renderOrder(g2, pixelTransform, scale, set.points);
		}

		if (showResiduals && results != null) {
			// draw a line showing the difference between observed and reprojected points
			// Draw this before points so that the observed points are drawn on top and you can see the delta
			g2.setStroke(new BasicStroke(4));
			g2.setColor(Color.GREEN);
			for (int i = 0; i < set.size(); i++) {
				PointIndex2D_F64 p = set.get(i);
				float dx = (float)results.residuals[i*2];
				float dy = (float)results.residuals[i*2 + 1];

				pixelTransform.compute((float)p.p.x, (float)p.p.y, adj);
				pixelTransform.compute((float)p.p.x + dx, (float)p.p.y + dy, adj2);
				line.setLine(scale*adj.x, scale*adj.y, scale*adj2.x, scale*adj2.y);
				g2.draw(line);
			}
		}

		if (showPoints) {
			g2.setColor(Color.BLACK);
			g2.setStroke(new BasicStroke(5));
			for (PointIndex2D_F64 p : set.points) {
				pixelTransform.compute((float)p.p.x, (float)p.p.y, adj);
				VisualizeFeatures.drawCross(g2, adj.x*scale, adj.y*scale, 5);
			}
			g2.setStroke(new BasicStroke(2));
			g2.setColor(lightRed);
			for (PointIndex2D_F64 p : set.points) {
				pixelTransform.compute((float)p.p.x, (float)p.p.y, adj);
				VisualizeFeatures.drawCross(g2, adj.x*scale, adj.y*scale, 5);
			}
		}

		if (showNumbers) {
			drawNumbers(g2, set.points, pixelTransform, scale);
		}

		if (showErrors && results != null) {
			g2.setStroke(new BasicStroke(4));
			g2.setColor(Color.BLACK);
			for (int i = 0; i < set.size(); i++) {
				PointIndex2D_F64 p = set.get(i);
				pixelTransform.compute((float)p.p.x, (float)p.p.y, adj);

				double r = errorScale*scale*results.pointError[i];
				if (r < 1)
					continue;

				VisualizeFeatures.drawCircle(g2, adj.x*scale, adj.y*scale, r, ellipse);
			}

			g2.setStroke(new BasicStroke(2.5f));
			g2.setColor(Color.ORANGE);
			for (int i = 0; i < set.size(); i++) {
				PointIndex2D_F64 p = set.get(i);
				pixelTransform.compute((float)p.p.x, (float)p.p.y, adj);

				double r = errorScale*scale*results.pointError[i];
				if (r < 1)
					continue;

				VisualizeFeatures.drawCircle(g2, adj.x*scale, adj.y*scale, r, ellipse);
			}
		}

		// Draw the selected feature
		if (selectedObservation >= 0 && selectedObservation < set.size()) {
			PointIndex2D_F64 p = set.get(selectedObservation);
			pixelTransform.compute((float)p.p.x, (float)p.p.y, adj);
			VisualizeFeatures.drawPoint(g2, adj.x*scale, adj.y*scale, 10.0, Color.GREEN, true, ellipse);
		}
	}

	protected void drawImageUnoccupied( Graphics2D g2, double scale ) {
		if (unoccupied.isEmpty())
			return;

		// Make it translucent so you can see inside
		var colorBorder = new Color(255, 0, 0, 100);
		var colorInner = new Color(255, 200, 0, 100);

		for (int i = 0; i < unoccupied.size; i++) {
			// Convert region into a format Swing understands and compensate for the image being scaled
			RegionInfo r = unoccupied.get(i);
			rect.x = scale*r.region.x0;
			rect.y = scale*r.region.y0;
			rect.width = scale*r.region.getWidth();
			rect.height = scale*r.region.getHeight();

			// Fill with a solid color and draw a black outline so you can see individual regions
			if (r.inner)
				g2.setColor(colorInner);
			else
				g2.setColor(colorBorder);
			g2.fill(rect);
			g2.setColor(Color.BLACK);
			g2.draw(rect);
		}
	}

	@FunctionalInterface public interface SetScale {
		void setScale( double scale );
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy