View Javadoc
1   /*
2    * Copyright (C) 2017 Alberto Irurueta Carro (alberto@irurueta.com)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.irurueta.ar.sfm;
17  
18  import com.irurueta.ar.slam.BaseCalibrationData;
19  import com.irurueta.ar.slam.BaseSlamEstimator;
20  import com.irurueta.geometry.GeometryException;
21  import com.irurueta.geometry.InhomogeneousPoint3D;
22  import com.irurueta.geometry.MetricTransformation3D;
23  import com.irurueta.geometry.PinholeCamera;
24  import com.irurueta.geometry.PinholeCameraIntrinsicParameters;
25  import com.irurueta.geometry.Point3D;
26  import com.irurueta.geometry.Quaternion;
27  
28  import java.util.ArrayList;
29  
30  /**
31   * Base class in charge of estimating cameras and 3D reconstructed points from sparse
32   * image point correspondences in multiple views and also in charge of estimating overall
33   * scene scale by means of SLAM (Simultaneous Location And Mapping) using data obtained
34   * from sensors like accelerometers or gyroscopes.
35   *
36   * @param <D> type defining calibration data.
37   * @param <C> type of configuration.
38   * @param <R> type of re-constructor.
39   * @param <L> type of listener.
40   * @param <S> type of SLAM estimator.
41   */
42  @SuppressWarnings("DuplicatedCode")
43  public abstract class BaseSlamSparseReconstructor<
44          D extends BaseCalibrationData,
45          C extends BaseSlamSparseReconstructorConfiguration<D, C>,
46          R extends BaseSlamSparseReconstructor<D, C, R, L, S>,
47          L extends BaseSlamSparseReconstructorListener<R>,
48          S extends BaseSlamEstimator<D>> extends BaseSparseReconstructor<C, R, L> {
49  
50      /**
51       * Slam estimator to estimate position, speed, orientation using
52       * accelerometer and gyroscope data.
53       */
54      protected S slamEstimator;
55  
56      /**
57       * Position estimated by means of SLAM. It is reused each time it is notified.
58       */
59      protected final InhomogeneousPoint3D slamPosition = new InhomogeneousPoint3D();
60  
61      /**
62       * Camera estimated by means of SLAM. It is reused each time it is notified.
63       */
64      private final PinholeCamera slamCamera = new PinholeCamera();
65  
66      /**
67       * Camera rotation estimated by means of SLAM. It is reused each time it is notified.
68       */
69      private final Quaternion slamRotation = new Quaternion();
70  
71      /**
72       * Constructor.
73       *
74       * @param configuration configuration for this re-constructor.
75       * @param listener      listener in charge of handling events.
76       * @throws NullPointerException if listener or configuration is not
77       *                              provided.
78       */
79      protected BaseSlamSparseReconstructor(final C configuration, final L listener) {
80          super(configuration, listener);
81      }
82  
83      /**
84       * Provides a new accelerometer sample to update SLAM estimation.
85       * This method must be called whenever the accelerometer sensor receives new
86       * data.
87       * If re-constructor is not running, calling this method has no effect.
88       *
89       * @param timestamp     timestamp of accelerometer sample since epoch time and
90       *                      expressed in nanoseconds.
91       * @param accelerationX linear acceleration along x-axis expressed in meters
92       *                      per squared second (m/s^2).
93       * @param accelerationY linear acceleration along y-axis expressed in meters
94       *                      per squared second (m/s^2).
95       * @param accelerationZ linear acceleration along z-axis expressed in meters
96       *                      per squared second (m/s^2).
97       */
98      public void updateAccelerometerSample(final long timestamp, final float accelerationX, final float accelerationY,
99                                            final float accelerationZ) {
100         if (slamEstimator != null) {
101             slamEstimator.updateAccelerometerSample(timestamp, accelerationX, accelerationY, accelerationZ);
102         }
103     }
104 
105     /**
106      * Provides a new accelerometer sample to update SLAM estimation.
107      * This method must be called whenever the accelerometer sensor receives new
108      * data.
109      * If re-constructor is not running, calling this method has no effect.
110      *
111      * @param timestamp timestamp of accelerometer sample since epoch time and
112      *                  expressed in nanoseconds.
113      * @param data      array containing x,y,z components of linear acceleration
114      *                  expressed in meters per squared second (m/s^2).
115      * @throws IllegalArgumentException if provided array does not have length
116      *                                  3.
117      */
118     public void updateAccelerometerSample(final long timestamp, final float[] data) {
119         if (slamEstimator != null) {
120             slamEstimator.updateAccelerometerSample(timestamp, data);
121         }
122     }
123 
124     /**
125      * Provides a new gyroscope sample to update SLAM estimation.
126      * If re-constructor is not running, calling this method has no effect.
127      *
128      * @param timestamp     timestamp of gyroscope sample since epoch time and
129      *                      expressed in nanoseconds.
130      * @param angularSpeedX angular speed of rotation along x-axis expressed in
131      *                      radians per second (rad/s).
132      * @param angularSpeedY angular speed of rotation along y-axis expressed in
133      *                      radians per second (rad/s).
134      * @param angularSpeedZ angular speed of rotation along z-axis expressed in
135      *                      radians per second (rad/s).
136      */
137     public void updateGyroscopeSample(final long timestamp, final float angularSpeedX,
138                                       final float angularSpeedY, final float angularSpeedZ) {
139         if (slamEstimator != null) {
140             slamEstimator.updateGyroscopeSample(timestamp, angularSpeedX, angularSpeedY, angularSpeedZ);
141         }
142     }
143 
144     /**
145      * Provides a new gyroscope sample to update SLAM estimation.
146      * If re-constructor is not running, calling this method has no effect.
147      *
148      * @param timestamp timestamp of gyroscope sample since epoch time and
149      *                  expressed in nanoseconds.
150      * @param data      angular speed of rotation along x,y,z axes expressed in
151      *                  radians per second (rad/s).
152      * @throws IllegalArgumentException if provided array does not have length
153      *                                  3.
154      */
155     public void updateGyroscopeSample(final long timestamp, final float[] data) {
156         if (slamEstimator != null) {
157             slamEstimator.updateGyroscopeSample(timestamp, data);
158         }
159     }
160 
161     /**
162      * Configures calibration data on SLAM estimator if available.
163      */
164     protected void setUpCalibrationData() {
165         final var calibrationData = configuration.getCalibrationData();
166         if (calibrationData != null) {
167             slamEstimator.setCalibrationData(calibrationData);
168         }
169     }
170 
171     /**
172      * Configures listener of SLAM estimator
173      */
174     protected void setUpSlamEstimatorListener() {
175         slamEstimator.setListener(new BaseSlamEstimator.BaseSlamEstimatorListener<>() {
176             @Override
177             public void onFullSampleReceived(final BaseSlamEstimator<D> estimator) {
178                 // not used
179             }
180 
181             @Override
182             public void onFullSampleProcessed(final BaseSlamEstimator<D> estimator) {
183                 notifySlamStateAndCamera();
184             }
185 
186             @Override
187             public void onCorrectWithPositionMeasure(final BaseSlamEstimator<D> estimator) {
188                 // not used
189             }
190 
191             @Override
192             public void onCorrectedWithPositionMeasure(final BaseSlamEstimator<D> estimator) {
193                 notifySlamStateAndCamera();
194             }
195 
196             private void notifySlamStateAndCamera() {
197                 notifySlamStateIfNeeded();
198                 notifySlamCameraIfNeeded();
199             }
200         });
201     }
202 
203     /**
204      * Update scene scale using SLAM data.
205      *
206      * @param isInitialPairOfViews true if initial pair of views is being processed, false otherwise.
207      * @return true if scale was successfully updated, false otherwise.
208      */
209     protected boolean updateScale(final boolean isInitialPairOfViews) {
210         try {
211             var metricCamera1 = previousMetricEstimatedCamera.getCamera();
212             var metricCamera2 = currentMetricEstimatedCamera.getCamera();
213 
214             double slamPosX;
215             double slamPosY;
216             double slamPosZ;
217             double scale;
218             if (isInitialPairOfViews) {
219                 // obtain baseline (camera separation from slam estimator data
220                 slamPosX = slamEstimator.getStatePositionX();
221                 slamPosY = slamEstimator.getStatePositionY();
222                 slamPosZ = slamEstimator.getStatePositionZ();
223 
224                 slamPosition.setInhomogeneousCoordinates(slamPosX, slamPosY, slamPosZ);
225 
226                 if (!metricCamera1.isCameraCenterAvailable()) {
227                     metricCamera1.decompose(false, true);
228                 }
229                 if (!metricCamera2.isCameraCenterAvailable()) {
230                     metricCamera2.decompose(false, true);
231                 }
232 
233                 final var center1 = metricCamera1.getCameraCenter();
234                 final var center2 = metricCamera2.getCameraCenter();
235 
236                 final var baseline = center1.distanceTo(slamPosition);
237                 final var estimatedBaseline = center1.distanceTo(center2);
238 
239                 scale = currentScale = baseline / estimatedBaseline;
240             } else {
241                 scale = currentScale;
242             }
243 
244             final var scaleTransformation = new MetricTransformation3D(scale);
245 
246             // update scale of cameras
247             final var euclideanCamera1 = scaleTransformation.transformAndReturnNew(metricCamera1);
248             final var euclideanCamera2 = scaleTransformation.transformAndReturnNew(metricCamera2);
249 
250             if (!euclideanCamera2.isCameraCenterAvailable()) {
251                 euclideanCamera2.decompose(false, true);
252             }
253             slamEstimator.correctWithPositionMeasure(euclideanCamera2.getCameraCenter(),
254                     configuration.getCameraPositionCovariance());
255 
256             if (!isInitialPairOfViews) {
257 
258                 slamPosX = slamEstimator.getStatePositionX();
259                 slamPosY = slamEstimator.getStatePositionY();
260                 slamPosZ = slamEstimator.getStatePositionZ();
261                 slamPosition.setInhomogeneousCoordinates(slamPosX, slamPosY, slamPosZ);
262 
263                 // adjust scale of current camera
264                 final var euclideanCenter2 = euclideanCamera2.getCameraCenter();
265 
266                 final var euclideanPosX = euclideanCenter2.getInhomX();
267                 final var euclideanPosY = euclideanCenter2.getInhomY();
268                 final var euclideanPosZ = euclideanCenter2.getInhomZ();
269 
270                 final var scaleVariationX = euclideanPosX / slamPosX;
271                 final var scaleVariationY = euclideanPosY / slamPosY;
272                 final var scaleVariationZ = euclideanPosZ / slamPosZ;
273 
274                 final var scaleVariation = (scaleVariationX + scaleVariationY + scaleVariationZ) / 3.0;
275                 scale *= scaleVariation;
276                 currentScale = scale;
277                 scaleTransformation.setScale(currentScale);
278 
279                 // update camera
280                 scaleTransformation.transform(metricCamera2, euclideanCamera2);
281             }
282             final var sqrScale = scale * scale;
283 
284             previousEuclideanEstimatedCamera = new EstimatedCamera();
285             previousEuclideanEstimatedCamera.setCamera(euclideanCamera1);
286             previousEuclideanEstimatedCamera.setViewId(previousMetricEstimatedCamera.getViewId());
287             previousEuclideanEstimatedCamera.setQualityScore(previousMetricEstimatedCamera.getQualityScore());
288             if (previousMetricEstimatedCamera.getCovariance() != null) {
289                 previousEuclideanEstimatedCamera.setCovariance(
290                         previousMetricEstimatedCamera.getCovariance().multiplyByScalarAndReturnNew(sqrScale));
291             }
292 
293             currentEuclideanEstimatedCamera = new EstimatedCamera();
294             currentEuclideanEstimatedCamera.setCamera(euclideanCamera2);
295             currentEuclideanEstimatedCamera.setViewId(currentMetricEstimatedCamera.getViewId());
296             currentEuclideanEstimatedCamera.setQualityScore(currentMetricEstimatedCamera.getQualityScore());
297             if (currentMetricEstimatedCamera.getCovariance() != null) {
298                 currentEuclideanEstimatedCamera.setCovariance(
299                         currentMetricEstimatedCamera.getCovariance().multiplyByScalarAndReturnNew(sqrScale));
300             }
301 
302             // update scale of reconstructed points
303             final var numPoints = activeMetricReconstructedPoints.size();
304             final var metricReconstructedPoints3D = new ArrayList<Point3D>();
305             for (final var reconstructedPoint : activeMetricReconstructedPoints) {
306                 metricReconstructedPoints3D.add(reconstructedPoint.getPoint());
307             }
308 
309             final var euclideanReconstructedPoints3D = scaleTransformation.transformPointsAndReturnNew(
310                     metricReconstructedPoints3D);
311 
312             // set scaled points into result
313             activeEuclideanReconstructedPoints = new ArrayList<>();
314             ReconstructedPoint3D euclideanPoint;
315             ReconstructedPoint3D metricPoint;
316             for (var i = 0; i < numPoints; i++) {
317                 metricPoint = activeMetricReconstructedPoints.get(i);
318 
319                 euclideanPoint = new ReconstructedPoint3D();
320                 euclideanPoint.setId(metricPoint.getId());
321                 euclideanPoint.setPoint(euclideanReconstructedPoints3D.get(i));
322                 euclideanPoint.setInlier(metricPoint.isInlier());
323                 euclideanPoint.setQualityScore(metricPoint.getQualityScore());
324                 if (metricPoint.getCovariance() != null) {
325                     euclideanPoint.setCovariance(metricPoint.getCovariance().multiplyByScalarAndReturnNew(sqrScale));
326                 }
327                 euclideanPoint.setColorData(metricPoint.getColorData());
328 
329                 activeEuclideanReconstructedPoints.add(euclideanPoint);
330             }
331 
332             return true;
333         } catch (final Exception e) {
334             failed = true;
335             //noinspection unchecked
336             listener.onFail((R) this);
337 
338             return false;
339         }
340     }
341 
342     /**
343      * Notifies SLAM state if notification is enabled at configuration time.
344      */
345     private void notifySlamStateIfNeeded() {
346         if (!configuration.isNotifyAvailableSlamDataEnabled()) {
347             return;
348         }
349 
350         final var positionX = slamEstimator.getStatePositionX();
351         final var positionY = slamEstimator.getStatePositionY();
352         final var positionZ = slamEstimator.getStatePositionZ();
353 
354         final var velocityX = slamEstimator.getStateVelocityX();
355         final var velocityY = slamEstimator.getStateVelocityY();
356         final var velocityZ = slamEstimator.getStateVelocityZ();
357 
358         final var accelerationX = slamEstimator.getStateAccelerationX();
359         final var accelerationY = slamEstimator.getStateAccelerationY();
360         final var accelerationZ = slamEstimator.getStateAccelerationZ();
361 
362         final var quaternionA = slamEstimator.getStateQuaternionA();
363         final var quaternionB = slamEstimator.getStateQuaternionB();
364         final var quaternionC = slamEstimator.getStateQuaternionC();
365         final var quaternionD = slamEstimator.getStateQuaternionD();
366 
367         final var angularSpeedX = slamEstimator.getStateAngularSpeedX();
368         final var angularSpeedY = slamEstimator.getStateAngularSpeedY();
369         final var angularSpeedZ = slamEstimator.getStateAngularSpeedZ();
370 
371         //noinspection unchecked
372         listener.onSlamDataAvailable((R) this, positionX, positionY, positionZ,
373                 velocityX, velocityY, velocityZ,
374                 accelerationX, accelerationY, accelerationZ,
375                 quaternionA, quaternionB, quaternionC, quaternionD,
376                 angularSpeedX, angularSpeedY, angularSpeedZ, slamEstimator.getStateCovariance());
377     }
378 
379     /**
380      * Notifies estimated camera by means of SLAM if notification is enabled at
381      * configuration time and intrinsics are already available.
382      */
383     private void notifySlamCameraIfNeeded() {
384         if (!configuration.isNotifyEstimatedSlamCameraEnabled()) {
385             return;
386         }
387 
388         // try with current camera
389         var camera = currentEuclideanEstimatedCamera != null ? currentEuclideanEstimatedCamera.getCamera() : null;
390         if (camera == null) {
391             // if not available try with previous camera
392             camera = previousEuclideanEstimatedCamera != null ? previousEuclideanEstimatedCamera.getCamera() : null;
393         }
394 
395         try {
396             PinholeCameraIntrinsicParameters intrinsicParameters = null;
397             if (camera != null) {
398                 if (!camera.areIntrinsicParametersAvailable()) {
399                     // decompose camera to obtain intrinsic parameters
400                     camera.decompose();
401                 }
402 
403                 intrinsicParameters = camera.getIntrinsicParameters();
404             } else if (configuration.getInitialIntrinsic1() != null) {
405                 intrinsicParameters = configuration.getInitialIntrinsic1();
406             } else if (configuration.getInitialIntrinsic2() != null) {
407                 intrinsicParameters = configuration.getInitialIntrinsic2();
408             } else if (configuration.getAdditionalCamerasIntrinsics() != null) {
409                 intrinsicParameters = configuration.getAdditionalCamerasIntrinsics();
410             }
411 
412             if (intrinsicParameters == null) {
413                 return;
414             }
415 
416             final var positionX = slamEstimator.getStatePositionX();
417             final var positionY = slamEstimator.getStatePositionY();
418             final var positionZ = slamEstimator.getStatePositionZ();
419             slamPosition.setInhomogeneousCoordinates(positionX, positionY, positionZ);
420 
421             final var quaternionA = slamEstimator.getStateQuaternionA();
422             final var quaternionB = slamEstimator.getStateQuaternionB();
423             final var quaternionC = slamEstimator.getStateQuaternionC();
424             final var quaternionD = slamEstimator.getStateQuaternionD();
425             slamRotation.setA(quaternionA);
426             slamRotation.setB(quaternionB);
427             slamRotation.setC(quaternionC);
428             slamRotation.setD(quaternionD);
429 
430             slamCamera.setIntrinsicAndExtrinsicParameters(intrinsicParameters, slamRotation, slamPosition);
431 
432             //noinspection unchecked
433             listener.onSlamCameraEstimated((R) this, slamCamera);
434 
435         } catch (final GeometryException ignore) {
436             // do nothing
437         }
438     }
439 }