1 /*
2 * Copyright (C) 2016 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.numerical.robust;
17
18 import com.irurueta.numerical.LockedException;
19 import com.irurueta.numerical.NotReadyException;
20 import com.irurueta.sorting.Sorter;
21
22 import java.util.ArrayList;
23 import java.util.BitSet;
24
25 /**
26 * This class implements LMedS (Least Median of Squares) algorithm to robustly
27 * estimate a data model.
28 * LMedS is based on the idea that a given proportion of outliers exists in the
29 * total amount of samples provided. This algorithm tries to iteratively find
30 * the beast subset of samples picking the ones with the least median of error.
31 * To determine whether a sample is an outlier or not, and the estimated error
32 * for each sample, provided listener must be used.
33 * Contrary to RANSAC, this algorithm does not require a fixed threshold to be
34 * set to determine whether samples are inliers or not. Instead, threshold is
35 * computed dynamically. Because of that LMedS typically produces results with
36 * larger error than RANSAC having a similar computational cost, because samples
37 * usually contain a large error. Hence, if threshold is known in advance for a
38 * given estimation, RANSAC should be preferred rather than LMedS.
39 * On the contrary, if it can be ensured that samples are very accurate except
40 * for some outliers, then LMedS becomes much more accurate than RANSAC because
41 * it typically converges to a solution with a very small threshold. However,
42 * typically inlier samples tend to have certain error, and in practice LMedS
43 * produces results with a similar accuracy and computational cost than RANSAC.
44 *
45 * @param <T> type of object to be estimated.
46 */
47 @SuppressWarnings("DuplicatedCode")
48 public class LMedSRobustEstimator<T> extends RobustEstimator<T> {
49
50 /**
51 * Constant defining default confidence of the estimated result, which is
52 * 99%. This means that with a probability of 99% estimation will be
53 * accurate because chosen sub-samples will be inliers.
54 */
55 public static final double DEFAULT_CONFIDENCE = 0.99;
56
57 /**
58 * Default maximum allowed number of iterations.
59 */
60 public static final int DEFAULT_MAX_ITERATIONS = 5000;
61
62 /**
63 * Minimum allowed confidence value.
64 */
65 public static final double MIN_CONFIDENCE = 0.0;
66
67 /**
68 * Maximum allowed confidence value.
69 */
70 public static final double MAX_CONFIDENCE = 1.0;
71
72 /**
73 * Minimum allowed number of iterations.
74 */
75 public static final int MIN_ITERATIONS = 1;
76
77 /**
78 * Default value to be used for stop threshold. Stop threshold can be used
79 * to keep the algorithm iterating in case that best threshold is not small
80 * enough. Once a better solution is found yielding a threshold smaller than
81 * this value, the algorithm will stop.
82 */
83 public static final double DEFAULT_STOP_THRESHOLD = 0.0;
84
85 /**
86 * Minimum allowed stop threshold value.
87 */
88 public static final double MIN_STOP_THRESHOLD = 0.0;
89
90 /**
91 * Default factor to normalize threshold to determine inliers. This factor
92 * can be used to increase or lower the dynamically computed threshold so
93 * that the algorithm becomes more or less accurate. The stricter the
94 * threshold (lower factor), the more time the algorithm will need to
95 * converge, if it can converge. By default, the factor is 1.0, which makes
96 * the threshold to be computed as the median of residuals.
97 */
98 public static final double DEFAULT_INLIER_FACTOR = 1.0; // 1.5 would also be reasonable
99
100 /**
101 * Minimum allowed value for inlier factor.
102 */
103 public static final double MIN_INLER_FACTOR = 0.0;
104
105 /**
106 * Constant to estimate standard deviation of residuals based on their
107 * median.
108 */
109 public static final double STD_CONSTANT = 1.4826;
110
111 /**
112 * Amount of confidence expressed as a value between 0 and 1.0 (which is
113 * equivalent to 100%). The amount of confidence indicates the probability
114 * that the estimated result is correct. Usually this value will be close
115 * to 1.0, but not exactly 1.0.
116 */
117 private double confidence;
118
119 /**
120 * Maximum allowed number of iterations. When the maximum number of
121 * iterations is exceeded, result will not be available, however an
122 * approximate result will be available for retrieval.
123 */
124 private int maxIterations;
125
126 /**
127 * Instance in charge of picking random subsets of samples.
128 */
129 private SubsetSelector subsetSelector;
130
131 /**
132 * Number of iterations to be done to obtain required confidence.
133 */
134 private int iters;
135
136 /**
137 * Best solution that has been found so far during an estimation.
138 */
139 private T bestResult;
140
141 /**
142 * Data related to inliers found for best result.
143 */
144 private LMedSInliersData bestInliersData;
145
146 /**
147 * Threshold to be used to keep the algorithm iterating in case that
148 * best threshold is not small enough. Once a better solution is found
149 * yielding a threshold smaller than this value, the algorithm will stop.
150 */
151 private double stopThreshold;
152
153 /**
154 * Factor to normalize threshold to determine inliers. This factor can be
155 * used to increase or lower the dynamically computed threshold so that the
156 * algorithm becomes more or less accurate. The stricter the threshold
157 * (lower factor), the more time the algorithm will need to converge, if
158 * it can converge. By default, the factor is 1.0, which makes the threshold
159 * to be computed as the median of residuals.
160 */
161 private double inlierFactor;
162
163
164 /**
165 * Constructor.
166 */
167 public LMedSRobustEstimator() {
168 super();
169 confidence = DEFAULT_CONFIDENCE;
170 maxIterations = DEFAULT_MAX_ITERATIONS;
171 iters = maxIterations;
172 bestResult = null;
173 bestInliersData = null;
174 stopThreshold = DEFAULT_STOP_THRESHOLD;
175 inlierFactor = DEFAULT_INLIER_FACTOR;
176 }
177
178 /**
179 * Constructor with listener.
180 *
181 * @param listener listener to be notified of events such as when estimation
182 * starts, ends or its progress significantly changes, as well as in charge
183 * of picking samples and doing per-iteration estimations.
184 */
185 public LMedSRobustEstimator(final LMedSRobustEstimatorListener<T> listener) {
186 super(listener);
187 confidence = DEFAULT_CONFIDENCE;
188 maxIterations = DEFAULT_MAX_ITERATIONS;
189 iters = maxIterations;
190 bestResult = null;
191 bestInliersData = null;
192 stopThreshold = DEFAULT_STOP_THRESHOLD;
193 inlierFactor = DEFAULT_INLIER_FACTOR;
194 }
195
196 /**
197 * Returns amount of confidence expressed as a value between 0 and 1.0
198 * (which is equivalent to 100%). The amount of confidence indicates the
199 * probability that the estimated result is correct. Usually this value will
200 * be close to 1.0, but not exactly 1.0.
201 *
202 * @return amount of confidence as a value between 0.0 and 1.0.
203 */
204 public double getConfidence() {
205 return confidence;
206 }
207
208 /**
209 * Sets amount of confidence expressed as a value between 0 and 1.0 (which
210 * is equivalent to 100%). The amount of confidence indicates the
211 * probability that the estimated result is correct. Usually this value will
212 * be close to 1.0, but not exactly 1.0.
213 *
214 * @param confidence confidence to be set as a value between 0.0 and 1.0.
215 * @throws IllegalArgumentException if provided value is not between 0.0 and
216 * 1.0.
217 * @throws LockedException if this estimator is locked because an estimation
218 * is being computed.
219 */
220 public void setConfidence(final double confidence) throws LockedException {
221 if (isLocked()) {
222 throw new LockedException();
223 }
224 if (confidence < MIN_CONFIDENCE || confidence > MAX_CONFIDENCE) {
225 throw new IllegalArgumentException();
226 }
227 this.confidence = confidence;
228 }
229
230 /**
231 * Maximum allowed number of iterations. When the maximum number of
232 * iterations is exceeded, result will not be available, however an
233 * approximate result will be available for retrieval.
234 *
235 * @return maximum allowed number of iterations.
236 */
237 public int getMaxIterations() {
238 return maxIterations;
239 }
240
241 /**
242 * Sets maximum allowed number of iterations. When the maximum number of
243 * iterations is exceeded, result will not be available, however an
244 * approximate result will be available for retrieval.
245 *
246 * @param maxIterations maximum allowed number of iterations to be set.
247 * @throws IllegalArgumentException if provided value is less than 1.
248 * @throws LockedException if this estimator is locked because an estimation
249 * is being computed.
250 */
251 public void setMaxIterations(final int maxIterations) throws LockedException {
252 if (isLocked()) {
253 throw new LockedException();
254 }
255 if (maxIterations < MIN_ITERATIONS) {
256 throw new IllegalArgumentException();
257 }
258 this.maxIterations = maxIterations;
259 }
260
261 /**
262 * Returns threshold to be used to keep the algorithm iterating in case that
263 * best threshold is not small enough. Once a better solution is found
264 * yielding a threshold smaller than this value, the algorithm will stop.
265 *
266 * @return threshold to be used to keep the algorithm iterating in case that
267 * best threshold is not small enough.
268 */
269 public double getStopThreshold() {
270 return stopThreshold;
271 }
272
273 /**
274 * Sets threshold to be used to keep the algorithm iterating in case that
275 * best threshold is not small enough. Once a better solution is found
276 * yielding a threshold smaller than this vlaue, the algorithm will stop.
277 *
278 * @param stopThreshold threshold to be used to keep the algorithm iterating
279 * in case that best threshold is not small enough.
280 * @throws IllegalArgumentException if provided value is less or equal than
281 * 0.0.
282 * @throws LockedException if this estimator is locked because an estimation
283 * is being computed.
284 */
285 public void setStopThreshold(final double stopThreshold) throws LockedException {
286 if (isLocked()) {
287 throw new LockedException();
288 }
289 if (stopThreshold < MIN_STOP_THRESHOLD) {
290 throw new IllegalArgumentException();
291 }
292
293 this.stopThreshold = stopThreshold;
294 }
295
296 /**
297 * Returns factor to normalize or adjust threshold to determine inliers.
298 * This factor can be used to increase or lower the dynamically computed
299 * threshold so that the algorithm becomes more or less accurate. The
300 * stricter the threshold (lower factor), the more time the algorithm will
301 * need to converge, if it can converge. By default, the factor is 1.0, which
302 * makes the threshold to be computed as the median of residuals.
303 *
304 * @return factor to normalize threshold to determine inliers.
305 */
306 public double getInlierFactor() {
307 return inlierFactor;
308 }
309
310 /**
311 * Sets factor to normalize or adjust threshold to determine inliers.
312 * This factor can be used to increase or lower the dynamically computed
313 * threshold so that the algorithm becomes more or less accurate. The
314 * stricter the threshold (lower factor), the more time the algorithm will
315 * need to converge, if it can converge. By default, the factor is 1.0, which
316 * makes the threshold to be computed as the median of residuals.
317 *
318 * @param inlierFactor inlier factor to be set.
319 * @throws IllegalArgumentException if provided value is less or equal than
320 * 0.0.
321 * @throws LockedException if this estimator is locked because an estimation
322 * is being computed.
323 */
324 public void setInlierFactor(final double inlierFactor) throws LockedException {
325 if (isLocked()) {
326 throw new LockedException();
327 }
328 if (inlierFactor <= MIN_INLER_FACTOR) {
329 throw new IllegalArgumentException();
330 }
331
332 this.inlierFactor = inlierFactor;
333 }
334
335 /**
336 * Returns number of iterations to be done to obtain required confidence.
337 *
338 * @return number of iterations to be done to obtain required confidence.
339 */
340 public int getNIters() {
341 return iters;
342 }
343
344 /**
345 * Returns best solution that has been found so far during an estimation.
346 *
347 * @return best solution that has been found so far during an estimation.
348 */
349 public T getBestResult() {
350 return bestResult;
351 }
352
353 /**
354 * Returns data related to inliers found for best result.
355 *
356 * @return data related to inliers found for best result.
357 */
358 public LMedSInliersData getBestInliersData() {
359 return bestInliersData;
360 }
361
362 /**
363 * Indicates if estimator is ready to start the estimation process.
364 *
365 * @return true if ready, false otherwise.
366 */
367 @Override
368 public boolean isReady() {
369 if (!super.isReady()) {
370 return false;
371 }
372 return (listener instanceof LMedSRobustEstimatorListener);
373 }
374
375 /**
376 * Robustly estimates an instance of T.
377 *
378 * @return estimated object.
379 * @throws LockedException if robust estimator is locked.
380 * @throws NotReadyException if provided input data is not enough to start
381 * the estimation.
382 * @throws RobustEstimatorException if estimation fails for any reason
383 * (i.e. numerical instability, no solution available, etc).
384 */
385 @Override
386 public T estimate() throws LockedException, NotReadyException, RobustEstimatorException {
387 if (isLocked()) {
388 throw new LockedException();
389 }
390 if (!isReady()) {
391 throw new NotReadyException();
392 }
393
394 try {
395 final var listener = (LMedSRobustEstimatorListener<T>) this.listener;
396
397 locked = true;
398
399 listener.onEstimateStart(this);
400
401 final var totalSamples = listener.getTotalSamples();
402 final var subsetSize = listener.getSubsetSize();
403 int bestNumInliers;
404 var threshold = Double.MAX_VALUE;
405 iters = Integer.MAX_VALUE;
406 int newNIters;
407 var currentIter = 0;
408 // reusable list that will contain preliminary solutions on each
409 // iteration
410 final var iterResults = new ArrayList<T>();
411 bestResult = null; // best result found so far
412 // progress and previous progress to determine when progress
413 // notification must occur
414 var previousProgress = 0.0f;
415 float progress;
416 // indices of subset picked in one iteration
417 final var subsetIndices = new int[subsetSize];
418 final var residualsTemp = new double[totalSamples];
419 // indicates if result improved
420 boolean improved;
421 // indicates whether algorithm must continue iterating
422 var continueIteration = true;
423
424 if (subsetSelector == null) {
425 // create new subset selector
426 subsetSelector = SubsetSelector.create(totalSamples);
427 } else {
428 // set number of samples to current subset selector
429 subsetSelector.setNumSamples(totalSamples);
430 }
431
432 // data related to inliers
433 var inliersData = new LMedSInliersData(totalSamples);
434 // sorter to compute medians
435 final var sorter = Sorter.<Double>create();
436
437 while (continueIteration) {
438 // generate a random subset of samples
439 subsetSelector.computeRandomSubsets(subsetSize, subsetIndices);
440
441 // clear list of preliminary solutions before calling listener
442 iterResults.clear();
443 // compute solution for current iteration
444 listener.estimatePreliminarSolutions(subsetIndices, iterResults);
445
446 // iterate over all solutions that have been found
447 improved = false;
448 for (final var iterResult : iterResults) {
449 // compute inliers
450 computeInliers(iterResult, subsetSize, inlierFactor, residualsTemp, listener, sorter, inliersData);
451
452 // save solution that produces the best residual
453 if (inliersData.isMedianResidualImproved()) {
454 improved = true;
455
456 // keep current solution
457 bestResult = iterResult;
458
459 // keep the best inliers data corresponding to best solution,
460 // in case it can be useful along with the result
461 bestInliersData = inliersData;
462
463 // recompute number of times the algorithm needs to be
464 // executed depending on current number of inliers to
465 // achieve with probability mConfidence that we have
466 // inliers and probability 1 - mConfidence that we have
467 // outliers
468 bestNumInliers = inliersData.getNumInliers();
469 final var probInlier = ((double) bestNumInliers) / ((double) totalSamples);
470
471 final var probSubsetAllInliers = Math.pow(probInlier, subsetSize);
472
473 if (Math.abs(probSubsetAllInliers) < Double.MIN_VALUE || Double.isNaN(probSubsetAllInliers)) {
474 newNIters = Integer.MAX_VALUE;
475 } else {
476 final var logProbSomeOutliers = Math.log(1.0 - probSubsetAllInliers);
477 if (Math.abs(logProbSomeOutliers) < Double.MIN_VALUE || Double.isNaN(logProbSomeOutliers)) {
478 newNIters = Integer.MAX_VALUE;
479 } else {
480 newNIters = (int) Math.ceil(Math.abs(Math.log(1.0 - confidence) / logProbSomeOutliers));
481 }
482 }
483
484 if (newNIters < iters) {
485 iters = newNIters;
486 }
487
488 threshold = inliersData.getEstimatedThreshold();
489
490 // create new inliers data instance until a new best
491 // solution is found
492 final var bestMedianResidual = inliersData.getBestMedianResidual();
493 inliersData = new LMedSInliersData(totalSamples);
494 // update the best median residual on new instance so
495 // that only better solutions that are found later
496 // can update inliers data
497 inliersData.update(bestMedianResidual, inliersData.getStandardDeviation(),
498 inliersData.getInliers(), inliersData.getResiduals(), inliersData.getNumInliers(),
499 inliersData.getEstimatedThreshold(), false);
500 }
501 }
502
503 if (iters > 0) {
504 progress = Math.min((float) currentIter / (float) iters, 1.0f);
505 } else {
506 progress = 1.0f;
507 }
508 if (progress - previousProgress > progressDelta) {
509 previousProgress = progress;
510 listener.onEstimateProgressChange(this, progress);
511 }
512 currentIter++;
513 continueIteration = (currentIter < maxIterations) && (threshold > stopThreshold);
514 if (!improved) {
515 continueIteration &= (currentIter < iters);
516 }
517
518 listener.onEstimateNextIteration(this, currentIter);
519 }
520
521 // no solution could be found after completing all iterations
522 if (bestResult == null) {
523 throw new RobustEstimatorException();
524 }
525
526 listener.onEstimateEnd(this);
527
528 return bestResult;
529 } catch (final SubsetSelectorException e) {
530 throw new RobustEstimatorException(e);
531 } finally {
532 locked = false;
533 }
534 }
535
536 /**
537 * Returns data about inliers once estimation has been done.
538 *
539 * @return data about inliers or null if estimation has not been done.
540 */
541 @Override
542 public InliersData getInliersData() {
543 return getBestInliersData();
544 }
545
546 /**
547 * Returns method being used for robust estimation.
548 *
549 * @return method being used for robust estimation.
550 */
551 @Override
552 public RobustEstimatorMethod getMethod() {
553 return RobustEstimatorMethod.LMEDS;
554 }
555
556 /**
557 * Computes inliers data for current iteration.
558 *
559 * @param <T> type of result to be estimated.
560 * @param iterResult result to be tested on current iteration.
561 * @param subsetSize subset sample size to be picked on each iteration.
562 * @param inlierFactor factor to adjust threshold to determine whether
563 * samples are inliers or not.
564 * @param residualsTemp temporal array to store residuals, since median
565 * computation requires modifying the original array.
566 * @param listener listener to obtain residuals for samples.
567 * @param sorter sorter instance to compute median of residuals.
568 * @param inliersData inliers data to be reused on each iteration.
569 */
570 private static <T> void computeInliers(
571 final T iterResult, final int subsetSize, final double inlierFactor, final double[] residualsTemp,
572 final LMedSRobustEstimatorListener<T> listener, final Sorter<Double> sorter, LMedSInliersData inliersData) {
573
574 final var residuals = inliersData.getResiduals();
575 final var inliers = inliersData.getInliers();
576 var bestMedianResidual = inliersData.getBestMedianResidual();
577 var medianResidualImproved = false;
578
579 final var totalSamples = residuals.length;
580
581 for (var i = 0; i < totalSamples; i++) {
582 residuals[i] = Math.abs(listener.computeResidual(iterResult, i));
583 }
584 System.arraycopy(residuals, 0, residualsTemp, 0, residuals.length);
585 final var medianResidual = sorter.median(residualsTemp);
586 if (medianResidual < bestMedianResidual) {
587 bestMedianResidual = medianResidual;
588 medianResidualImproved = true;
589 }
590
591 final var standardDeviation = STD_CONSTANT * (1.0 + 5.0 / (totalSamples - subsetSize))
592 * Math.sqrt(medianResidual);
593 final var normEstimatedThreshold = inlierFactor * medianResidual;
594
595 // determine which points are inliers
596 var numInliers = 0;
597 for (var i = 0; i < totalSamples; i++) {
598 if (residuals[i] <= normEstimatedThreshold) {
599 numInliers++;
600 inliers.set(i);
601 } else {
602 inliers.clear(i);
603 }
604 }
605
606 // store values in inliers data, only if residuals improve
607 if (medianResidualImproved) {
608 inliersData.update(bestMedianResidual, standardDeviation, inliers, residuals, numInliers,
609 normEstimatedThreshold, true);
610 }
611 }
612
613 /**
614 * Contains data related to inliers estimated in one iteration.
615 */
616 public static class LMedSInliersData extends InliersData {
617 /**
618 * Best median of error found so far taking into account all provided
619 * samples.
620 */
621 private double bestMedianResidual;
622
623 /**
624 * Standard deviation of error among all provided samples respect to
625 * currently estimated result.
626 */
627 private double standardDeviation;
628
629 /**
630 * Efficiently stores which samples are considered inliers and which
631 * ones aren't.
632 */
633 private BitSet inliers;
634
635 /**
636 * Estimated threshold to determine whether samples are inliers or not.
637 */
638 private double estimatedThreshold;
639
640 /**
641 * Indicates whether median residual computed in current iteration has
642 * improved respect to previous iterations.
643 */
644 private boolean medianResidualImproved;
645
646 /**
647 * Constructor.
648 *
649 * @param totalSamples total number of samples.
650 */
651 protected LMedSInliersData(final int totalSamples) {
652 bestMedianResidual = Double.MAX_VALUE;
653 standardDeviation = Double.MAX_VALUE;
654 estimatedThreshold = Double.MAX_VALUE;
655 inliers = new BitSet(totalSamples);
656 residuals = new double[totalSamples];
657 numInliers = 0;
658 medianResidualImproved = false;
659 }
660
661 /**
662 * Returns best median of error found so far taking into account all
663 * provided samples.
664 *
665 * @return best median of error found so far taking into account all
666 * provided samples.
667 */
668 public double getBestMedianResidual() {
669 return bestMedianResidual;
670 }
671
672 /**
673 * Returns standard deviation of error among all provided samples
674 * respect to currently estimated result.
675 *
676 * @return standard deviation of error among all provided samples
677 * respect to currently estimated result.
678 */
679 public double getStandardDeviation() {
680 return standardDeviation;
681 }
682
683 /**
684 * Returns efficient array indicating which samples are considered
685 * inliers and which ones aren't.
686 *
687 * @return array indicating which samples are considered inliers and
688 * which ones aren't.
689 */
690 @Override
691 public BitSet getInliers() {
692 return inliers;
693 }
694
695 /**
696 * Returns estimated threshold to determine whether samples are inliers
697 * or not.
698 *
699 * @return estimated threshold to determine whether samples are inliers
700 * or not.
701 */
702 public double getEstimatedThreshold() {
703 return estimatedThreshold;
704 }
705
706 /**
707 * Returns boolean indicating whether median residual computed in
708 * current iteration has improved respect to previous iterations.
709 *
710 * @return true if median residual improved, false otherwise.
711 */
712 public boolean isMedianResidualImproved() {
713 return medianResidualImproved;
714 }
715
716 /**
717 * Updates data contained in this instance.
718 *
719 * @param bestMedianResidual best median of error found so far taking
720 * into account all provided samples.
721 * @param standardDeviation standard deviation of error among all
722 * provided samples respect to currently estimated result.
723 * @param inliers efficiently stores which samples are considered
724 * inliers and which ones aren't.
725 * @param residuals residuals obtained for each sample of data.
726 * @param numInliers number of inliers found on current iteration.
727 * @param estimatedThreshold estimated threshold to determine whether
728 * samples are inliers or not.
729 * @param medianResidualImproved indicates whether median residual
730 * computed in current iteration has improved respect to previous
731 * iteration.
732 */
733 protected void update(final double bestMedianResidual, final double standardDeviation,
734 final BitSet inliers, final double[] residuals, final int numInliers,
735 final double estimatedThreshold, final boolean medianResidualImproved) {
736 this.bestMedianResidual = bestMedianResidual;
737 this.standardDeviation = standardDeviation;
738 this.inliers = inliers;
739 this.residuals = residuals;
740 this.numInliers = numInliers;
741 this.estimatedThreshold = estimatedThreshold;
742 this.medianResidualImproved = medianResidualImproved;
743 }
744 }
745 }