/*
* Copyright (c) 2011-2016, 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.demonstrations.feature.disparity;
import boofcv.abst.feature.disparity.StereoDisparity;
import boofcv.alg.distort.ImageDistort;
import boofcv.alg.geo.PerspectiveOps;
import boofcv.alg.geo.RectifyImageOps;
import boofcv.alg.geo.rectify.RectifyCalibrated;
import boofcv.core.image.GeneralizedImageOps;
import boofcv.core.image.border.BorderType;
import boofcv.factory.feature.disparity.DisparityAlgorithms;
import boofcv.factory.feature.disparity.FactoryStereoDisparity;
import boofcv.gui.SelectAlgorithmAndInputPanel;
import boofcv.gui.d3.PointCloudTiltPanel;
import boofcv.gui.image.ImagePanel;
import boofcv.gui.image.ShowImages;
import boofcv.gui.image.VisualizeImageData;
import boofcv.io.PathLabel;
import boofcv.io.ProgressMonitorThread;
import boofcv.io.UtilIO;
import boofcv.io.calibration.CalibrationIO;
import boofcv.io.image.ConvertBufferedImage;
import boofcv.struct.calib.StereoParameters;
import boofcv.struct.distort.Point2Transform2_F64;
import boofcv.struct.image.GrayU8;
import boofcv.struct.image.ImageGray;
import boofcv.struct.image.ImageType;
import georegression.struct.se.Se3_F64;
import org.ejml.data.DenseMatrix64F;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import static boofcv.alg.geo.RectifyImageOps.transformRectToPixel_F64;
/**
* Computes and displays disparity from still disparity images. The disparity can be viewed
* as a color surface plot or as a 3D point cloud. Different tuning parameters can be adjusted
* use a side control panel.
*
* @author Peter Abeles
*/
public class VisualizeStereoDisparity <T extends ImageGray, D extends ImageGray>
extends SelectAlgorithmAndInputPanel
implements DisparityDisplayPanel.Listener
{
// original input before rescaling
BufferedImage origLeft;
BufferedImage origRight;
StereoParameters origCalib;
// rectified color image from left and right camera for display
private BufferedImage colorLeft;
private BufferedImage colorRight;
// Output disparity color surface plot
private BufferedImage disparityOut;
// gray scale input image before rectification
private T inputLeft;
private T inputRight;
// gray scale input images after rectification
private T rectLeft;
private T rectRight;
// calibration parameters
private StereoParameters calib;
// rectification algorithm
private RectifyCalibrated rectifyAlg = RectifyImageOps.createCalibrated();
// GUI components
private DisparityDisplayPanel control = new DisparityDisplayPanel();
private JPanel panel = new JPanel();
private ImagePanel gui = new ImagePanel();
private PointCloudTiltPanel cloudGui = new PointCloudTiltPanel();
// if true the point cloud has already been computed and does not need to be recomputed
private boolean computedCloud;
// which algorithm has been selected
private int selectedAlg;
// instance of the selected algorithm
private StereoDisparity<T,D> activeAlg;
// camera calibration matrix of rectified images
private DenseMatrix64F rectK;
// makes sure process has been called before render disparity is done
// There was a threading issue where disparitySettingChange() created a new alg() but render was called before
// it could process an image.
private volatile boolean processCalled = false;
private boolean processedImage = false;
private boolean rectifiedImages = false;
// coordinate transform from left rectified image to its original pixels
Point2Transform2_F64 leftRectToPixel;
public VisualizeStereoDisparity() {
super(1);
selectedAlg = 0;
addAlgorithm(0,"Five Region",0);
addAlgorithm(0,"Region",1);
addAlgorithm(0,"Region Basic",2);
control.setListener(this);
panel.setLayout(new BorderLayout());
panel.add(control, BorderLayout.WEST);
panel.add(gui,BorderLayout.CENTER);
setMainGUI(panel);
}
public synchronized void process() {
if( !rectifiedImages )
return;
ProcessThread progress = new ProcessThread(this);
progress.start();
computedCloud = false;
activeAlg.process(rectLeft, rectRight);
processCalled = true;
progress.stopThread();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
disparityRender();
}
});
}
/**
* Changes which image is being displayed depending on GUI selection
*/
private synchronized void changeImageView() {
JComponent comp;
if( control.selectedView < 3 ) {
BufferedImage img;
switch (control.selectedView) {
case 0:
img = disparityOut;
break;
case 1:
img = colorLeft;
break;
case 2:
img = colorRight;
break;
default:
throw new RuntimeException("Unknown option");
}
gui.setBufferedImage(img);
gui.setPreferredSize(new Dimension(origLeft.getWidth(), origLeft.getHeight()));
comp = gui;
} else {
if( !computedCloud ) {
computedCloud = true;
double baseline = calib.getRightToLeft().getT().norm();
cloudGui.configure(baseline,rectK,leftRectToPixel,control.minDisparity,control.maxDisparity);
cloudGui.process(activeAlg.getDisparity(),colorLeft);
}
comp = cloudGui;
}
panel.remove(gui);
panel.remove(cloudGui);
panel.add(comp,BorderLayout.CENTER);
panel.validate();
comp.repaint();
processedImage = true;
}
@Override
public void refreshAll(Object[] cookies) {
process();
}
@Override
public synchronized void setActiveAlgorithm(int indexFamily, String name, Object cookie) {
int s = ((Number)cookie).intValue();
if( s != selectedAlg ) {
selectedAlg = s;
activeAlg = createAlg();
doRefreshAll();
}
}
@Override
public synchronized void changeInput(String name, int index) {
origCalib = CalibrationIO.load(media.openFile(inputRefs.get(index).getPath(0)));
origLeft = media.openImage(inputRefs.get(index).getPath(1) );
origRight = media.openImage(inputRefs.get(index).getPath(2) );
changeInputScale();
}
/**
* Removes distortion and rectifies images.
*/
private void rectifyInputImages() {
// get intrinsic camera calibration matrices
DenseMatrix64F K1 = PerspectiveOps.calibrationMatrix(calib.left, null);
DenseMatrix64F K2 = PerspectiveOps.calibrationMatrix(calib.right, null);
// compute rectification matrices
rectifyAlg.process(K1,new Se3_F64(),K2,calib.getRightToLeft().invert(null));
DenseMatrix64F rect1 = rectifyAlg.getRect1();
DenseMatrix64F rect2 = rectifyAlg.getRect2();
rectK = rectifyAlg.getCalibrationMatrix();
// adjust view to maximize viewing area while not including black regions
RectifyImageOps.allInsideLeft(calib.left, rect1, rect2, rectK);
// compute transforms to apply rectify the images
leftRectToPixel = transformRectToPixel_F64(calib.left, rect1);
ImageType<T> imageType = ImageType.single(activeAlg.getInputType());
ImageDistort<T,T> distortRect1 = RectifyImageOps.rectifyImage(
calib.left, rect1, BorderType.SKIP,imageType);
ImageDistort<T,T> distortRect2 = RectifyImageOps.rectifyImage(
calib.right, rect2, BorderType.SKIP, imageType);
// rectify and undo distortion
distortRect1.apply(inputLeft, rectLeft);
distortRect2.apply(inputRight,rectRight);
rectifiedImages = true;
}
@Override
public void loadConfigurationFile(String fileName) {
}
@Override
public boolean getHasProcessedImage() {
return processedImage;
}
@Override
public synchronized void disparitySettingChange() {
processCalled = false;
activeAlg = createAlg();
doRefreshAll();
}
@Override
public synchronized void disparityGuiChange() {
changeImageView();
}
@Override
public synchronized void disparityRender() {
if( !processCalled )
return;
int color = control.colorInvalid ? 0x02 << 16 | 0xB0 << 8 | 0x90 : 0;
D disparity = activeAlg.getDisparity();
disparityOut = VisualizeImageData.disparity(disparity,null,
activeAlg.getMinDisparity(),activeAlg.getMaxDisparity(),
color);
changeImageView();
}
@SuppressWarnings("unchecked")
public StereoDisparity<T,D> createAlg() {
processCalled = false;
int r = control.regionRadius;
// make sure the disparity is in a valid range
int maxDisparity = Math.min(colorLeft.getWidth()-2*r,control.maxDisparity);
int minDisparity = Math.min(maxDisparity,control.minDisparity);
if( control.useSubpixel ) {
switch( selectedAlg ) {
case 2:
changeGuiActive(false,false);
return (StereoDisparity)FactoryStereoDisparity.regionSubpixelWta(DisparityAlgorithms.RECT,minDisparity,
maxDisparity, r, r, -1, -1, -1, GrayU8.class);
case 1:
changeGuiActive(true,true);
return (StereoDisparity)FactoryStereoDisparity.regionSubpixelWta(DisparityAlgorithms.RECT,minDisparity,
maxDisparity, r, r, control.pixelError, control.reverseTol, control.texture,
GrayU8.class);
case 0:
changeGuiActive(true,true);
return (StereoDisparity)FactoryStereoDisparity.regionSubpixelWta(DisparityAlgorithms.RECT_FIVE,
minDisparity, maxDisparity, r, r,
control.pixelError, control.reverseTol, control.texture,
GrayU8.class);
default:
throw new RuntimeException("Unknown selection");
}
} else {
switch( selectedAlg ) {
case 2:
changeGuiActive(false,false);
return (StereoDisparity)FactoryStereoDisparity.regionWta(DisparityAlgorithms.RECT,minDisparity,
maxDisparity, r, r, -1, -1, -1, GrayU8.class);
case 1:
changeGuiActive(true,true);
return (StereoDisparity)FactoryStereoDisparity.regionWta(DisparityAlgorithms.RECT,minDisparity,
maxDisparity, r, r, control.pixelError, control.reverseTol, control.texture,
GrayU8.class);
case 0:
changeGuiActive(true,true);
return (StereoDisparity)FactoryStereoDisparity.regionWta(DisparityAlgorithms.RECT_FIVE,
minDisparity, maxDisparity, r, r,
control.pixelError, control.reverseTol, control.texture,
GrayU8.class);
default:
throw new RuntimeException("Unknown selection");
}
}
}
/**
* Active and deactivates different GUI configurations
*/
private void changeGuiActive( final boolean error , final boolean reverse ) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
control.setActiveGui(error,reverse);
}
});
}
@Override
public synchronized void changeInputScale() {
calib = new StereoParameters(origCalib);
double scale = control.inputScale;
PerspectiveOps.scaleIntrinsic(calib.left,scale);
PerspectiveOps.scaleIntrinsic(calib.right,scale);
int w = (int)(origLeft.getWidth()*scale);
int h = (int)(origLeft.getHeight()*scale);
colorLeft = new BufferedImage(w,h,BufferedImage.TYPE_INT_BGR);
colorRight = new BufferedImage(w,h,BufferedImage.TYPE_INT_BGR);
colorLeft.createGraphics().drawImage(origLeft, AffineTransform.getScaleInstance(scale,scale),null);
colorRight.createGraphics().drawImage(origRight, AffineTransform.getScaleInstance(scale,scale),null);
activeAlg = createAlg();
inputLeft = GeneralizedImageOps.createSingleBand(activeAlg.getInputType(),w,h);
inputRight = GeneralizedImageOps.createSingleBand(activeAlg.getInputType(),w,h);
rectLeft = GeneralizedImageOps.createSingleBand(activeAlg.getInputType(),w,h);
rectRight = GeneralizedImageOps.createSingleBand(activeAlg.getInputType(),w,h);
ConvertBufferedImage.convertFrom(colorLeft,inputLeft,true);
ConvertBufferedImage.convertFrom(colorRight,inputRight,true);
rectifyInputImages();
doRefreshAll();
}
/**
* Displays a progress monitor and updates its state periodically
*/
public class ProcessThread extends ProgressMonitorThread
{
int state = 0;
public ProcessThread( JComponent owner ) {
super(new ProgressMonitor(owner, "Computing Disparity", "", 0, 100));
}
@Override
public void doRun() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
monitor.setProgress(state);
state = (++state % 100);
}});
}
}
public static void main( String args[] ) {
VisualizeStereoDisparity app = new VisualizeStereoDisparity();
// app.setBaseDirectory(UtilIO.pathExample(""));
// app.loadInputData(UtilIO.pathExample("disparity.txt"));
String dirCalib = UtilIO.pathExample("calibration/stereo/Bumblebee2_Chess/");
String dirImgs = UtilIO.pathExample("stereo/");
List<PathLabel> inputs = new ArrayList<>();
inputs.add(new PathLabel("Chair 1", dirCalib+"stereo.yaml",dirImgs+"chair01_left.jpg",dirImgs+"chair01_right.jpg"));
// inputs.add(new PathLabel("Chair 2", dirCalib+"stereo.yaml",dirImgs+"chair02_left.jpg",dirImgs+"chair02_right.jpg"));
inputs.add(new PathLabel("Stones 1", dirCalib+"stereo.yaml",dirImgs+"stones01_left.jpg",dirImgs+"stones01_right.jpg"));
inputs.add(new PathLabel("Lantern 1",dirCalib+"stereo.yaml",dirImgs+"lantern01_left.jpg",dirImgs+"lantern01_right.jpg"));
inputs.add(new PathLabel("Wall 1", dirCalib+"stereo.yaml",dirImgs+"wall01_left.jpg",dirImgs+"wall01_right.jpg"));
// inputs.add(new PathLabel("Garden 1", dirCalib+"stereo.yaml",dirImgs+"garden01_left.jpg",dirImgs+"garden01_right.jpg"));
inputs.add(new PathLabel("Garden 2", dirCalib+"stereo.yaml",dirImgs+"garden02_left.jpg",dirImgs+"garden02_right.jpg"));
inputs.add(new PathLabel("Sundial 1", dirCalib+"stereo.yaml",dirImgs+"sundial01_left.jpg",dirImgs+"sundial01_right.jpg"));
app.setInputList(inputs);
// wait for it to process one image so that the size isn't all screwed up
while( !app.getHasProcessedImage() ) {
Thread.yield();
}
ShowImages.showWindow(app, "Stereo Disparity", true);
}
}