1 package com.android.cts.verifier.sensors; 2 3 /* 4 * Copyright (C) 2014 The Android Open Source Project 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 import android.media.MediaCodec; 19 import android.media.MediaExtractor; 20 import android.media.MediaFormat; 21 import android.os.Debug; 22 import android.os.Environment; 23 import android.os.PowerManager; 24 import android.util.JsonWriter; 25 import android.util.Log; 26 27 import org.opencv.core.Mat; 28 import org.opencv.core.CvType; 29 import org.opencv.core.MatOfDouble; 30 import org.opencv.core.MatOfFloat; 31 import org.opencv.core.MatOfPoint2f; 32 import org.opencv.core.MatOfPoint3f; 33 import org.opencv.core.Size; 34 import org.opencv.imgcodecs.Imgcodecs; 35 import org.opencv.imgproc.Imgproc; 36 import org.opencv.calib3d.Calib3d; 37 import org.opencv.core.Core; 38 39 import org.json.JSONObject; 40 import org.json.JSONException; 41 42 import java.io.BufferedReader; 43 import java.io.ByteArrayOutputStream; 44 import java.io.File; 45 import java.io.FileNotFoundException; 46 import java.io.FileOutputStream; 47 import java.io.FileReader; 48 import java.io.IOException; 49 import java.io.OutputStream; 50 import java.io.OutputStreamWriter; 51 import java.nio.ByteBuffer; 52 import java.util.ArrayList; 53 54 import android.opengl.GLES20; 55 import javax.microedition.khronos.opengles.GL10; 56 57 /** 58 * This class does analysis on the recorded RVCVCXCheck data sets. 59 */ 60 public class RVCVXCheckAnalyzer { 61 private static final String TAG = "RVCVXAnalysis"; 62 private static final boolean LOCAL_LOGV = false; 63 private static final boolean LOCAL_LOGD = true; 64 private final String mPath; 65 66 private static final boolean OUTPUT_DEBUG_IMAGE = false; 67 private static final double VALID_FRAME_THRESHOLD = 0.8; 68 private static final double REPROJECTION_THREASHOLD_RATIO = 0.008; 69 private static final boolean FORCE_CV_ANALYSIS = false; 70 private static final boolean TRACE_VIDEO_ANALYSIS = false; 71 private static final double DECIMATION_FPS_TARGET = 15.0; 72 private static final double MIN_VIDEO_LENGTH_SEC = 10; 73 74 RVCVXCheckAnalyzer(String path) 75 { 76 mPath = path; 77 } 78 79 /** 80 * A class that contains the analysis results 81 * 82 */ 83 class AnalyzeReport { 84 public boolean error=true; 85 public String reason = "incomplete"; 86 87 // roll pitch yaw RMS error ( \sqrt{\frac{1}{n} \sum e_i^2 }) 88 // unit in rad 89 public double roll_rms_error; 90 public double pitch_rms_error; 91 public double yaw_rms_error; 92 93 // roll pitch yaw max error 94 // unit in rad 95 public double roll_max_error; 96 public double pitch_max_error; 97 public double yaw_max_error; 98 99 // optimal t delta between sensor and camera data set to make best match 100 public double optimal_delta_t; 101 // the associate yaw offset based on initial values 102 public double yaw_offset; 103 104 public int n_of_frame; 105 public int n_of_valid_frame; 106 107 // both data below are in [sec] 108 public double sensor_period_avg; 109 public double sensor_period_stdev; 110 111 /** 112 * write Json format serialization to a file in case future processing need the data 113 */ 114 public void writeToFile(File file) { 115 try { 116 writeJSONToStream(new FileOutputStream(file)); 117 } catch (FileNotFoundException e) { 118 e.printStackTrace(); 119 Log.e(TAG, "Cannot create analyze report file."); 120 } 121 } 122 123 /** 124 * Get the JSON format serialization 125 *@return Json format serialization as String 126 */ 127 @Override 128 public String toString() { 129 ByteArrayOutputStream s = new ByteArrayOutputStream(); 130 writeJSONToStream(s); 131 return new String(s.toByteArray(), java.nio.charset.StandardCharsets.UTF_8); 132 } 133 134 private void writeJSONToStream(OutputStream s) { 135 try{ 136 JsonWriter writer = 137 new JsonWriter( 138 new OutputStreamWriter( s ) 139 ); 140 writer.beginObject(); 141 writer.setLenient(true); 142 143 writer.name("roll_rms_error").value(roll_rms_error); 144 writer.name("pitch_rms_error").value(pitch_rms_error); 145 writer.name("yaw_rms_error").value(yaw_rms_error); 146 writer.name("roll_max_error").value(roll_max_error); 147 writer.name("pitch_max_error").value(pitch_max_error); 148 writer.name("yaw_max_error").value(yaw_max_error); 149 writer.name("optimal_delta_t").value(optimal_delta_t); 150 writer.name("yaw_offset").value(yaw_offset); 151 writer.name("n_of_frame").value(n_of_frame); 152 writer.name("n_of_valid_frame").value(n_of_valid_frame); 153 writer.name("sensor_period_avg").value(sensor_period_avg); 154 writer.name("sensor_period_stdev").value(sensor_period_stdev); 155 156 writer.endObject(); 157 158 writer.close(); 159 } catch (IOException e) { 160 // do nothing 161 Log.e(TAG, "Error in serialize analyze report to JSON"); 162 } catch (IllegalArgumentException e) { 163 e.printStackTrace(); 164 Log.e(TAG, "Invalid parameter to write into JSON format"); 165 } 166 } 167 } 168 169 /** 170 * Process data set stored in the path specified in constructor 171 * and return an analyze report to caller 172 * 173 * @return An AnalyzeReport that contains detailed information about analysis 174 */ 175 public AnalyzeReport processDataSet() { 176 int nframe;// number of frames in video 177 int nslog; // number of sensor log 178 int nvlog; // number of video generated log 179 180 181 AnalyzeReport report = new AnalyzeReport(); 182 183 ArrayList<AttitudeRec> srecs = new ArrayList<>(); 184 ArrayList<AttitudeRec> vrecs = new ArrayList<>(); 185 ArrayList<AttitudeRec> srecs2 = new ArrayList<>(); 186 187 188 final boolean use_solved = new File(mPath, "vision_rpy.log").exists() && !FORCE_CV_ANALYSIS; 189 190 if (use_solved) { 191 nframe = nvlog = loadAttitudeRecs(new File(mPath, "vision_rpy.log"), vrecs); 192 nslog = loadAttitudeRecs(new File(mPath, "sensor_rpy.log"),srecs); 193 }else { 194 nframe = analyzeVideo(vrecs); 195 nvlog = vrecs.size(); 196 197 if (LOCAL_LOGV) { 198 Log.v(TAG, "Post video analysis nvlog = " + nvlog + " nframe=" + nframe); 199 } 200 if (nvlog <= 0 || nframe <= 0) { 201 // invalid results 202 report.reason = "Unable to to load recorded video."; 203 return report; 204 } 205 if (nframe < MIN_VIDEO_LENGTH_SEC*VALID_FRAME_THRESHOLD) { 206 // video is too short 207 Log.w(TAG, "Video record is to short, n frame = " + nframe); 208 report.reason = "Video too short."; 209 return report; 210 } 211 if ((double) nvlog / nframe < VALID_FRAME_THRESHOLD) { 212 // too many invalid frames 213 Log.w(TAG, "Too many invalid frames, n valid frame = " + nvlog + 214 ", n total frame = " + nframe); 215 report.reason = "Too many invalid frames."; 216 return report; 217 } 218 219 fixFlippedAxis(vrecs); 220 221 nslog = loadSensorLog(srecs); 222 } 223 224 // Gradient descent will have faster performance than this simple search, 225 // but the performance is dominated by the vision part, so it is not very necessary. 226 double delta_t; 227 double min_rms = Double.MAX_VALUE; 228 double min_delta_t =0.; 229 double min_yaw_offset =0.; 230 231 // pre-allocation 232 for (AttitudeRec i: vrecs) { 233 srecs2.add(new AttitudeRec(0,0,0,0)); 234 } 235 236 // find optimal offset 237 for (delta_t = -2.0; delta_t<2.0; delta_t +=0.01) { 238 double rms; 239 resampleSensorLog(srecs, vrecs, delta_t, 0.0, srecs2); 240 rms = Math.sqrt(calcSqrErr(vrecs, srecs2, 0)+ calcSqrErr(vrecs, srecs2, 1)); 241 if (rms < min_rms) { 242 min_rms = rms; 243 min_delta_t = delta_t; 244 min_yaw_offset = vrecs.get(0).yaw - srecs2.get(0).yaw; 245 } 246 } 247 // sample at optimal offset 248 resampleSensorLog(srecs, vrecs, min_delta_t, min_yaw_offset, srecs2); 249 250 if (!use_solved) { 251 dumpAttitudeRecs(new File(mPath, "vision_rpy.log"), vrecs); 252 dumpAttitudeRecs(new File(mPath, "sensor_rpy.log"), srecs); 253 } 254 dumpAttitudeRecs(new File(mPath, "sensor_rpy_resampled.log"), srecs2); 255 dumpAttitudeError(new File(mPath, "attitude_error.log"), vrecs, srecs2); 256 257 // fill report fields 258 report.roll_rms_error = Math.sqrt(calcSqrErr(vrecs, srecs2, 0)); 259 report.pitch_rms_error = Math.sqrt(calcSqrErr(vrecs, srecs2, 1)); 260 report.yaw_rms_error = Math.sqrt(calcSqrErr(vrecs, srecs2, 2)); 261 262 report.roll_max_error = calcMaxErr(vrecs, srecs2, 0); 263 report.pitch_max_error = calcMaxErr(vrecs, srecs2, 1); 264 report.yaw_max_error = calcMaxErr(vrecs, srecs2, 2); 265 266 report.optimal_delta_t = min_delta_t; 267 report.yaw_offset = (min_yaw_offset); 268 269 report.n_of_frame = nframe; 270 report.n_of_valid_frame = nvlog; 271 272 double [] sensor_period_stat = calcSensorPeriodStat(srecs); 273 report.sensor_period_avg = sensor_period_stat[0]; 274 report.sensor_period_stdev = sensor_period_stat[1]; 275 276 // output report to file and log in JSON format as well 277 report.writeToFile(new File(mPath, "report.json")); 278 if (LOCAL_LOGV) Log.v(TAG, "Report in JSON:" + report.toString()); 279 280 report.reason = "Completed"; 281 report.error = false; 282 return report; 283 } 284 285 /** 286 * Generate pattern geometry like this one 287 * http://docs.opencv.org/trunk/_downloads/acircles_pattern.png 288 * 289 * @return Array of 3D points 290 */ 291 private MatOfPoint3f asymmetricalCircleGrid(Size size) { 292 final int cn = 3; 293 294 int n = (int)(size.width * size.height); 295 float positions[] = new float[n * cn]; 296 float unit=0.02f; 297 MatOfPoint3f grid = new MatOfPoint3f(); 298 299 for (int i = 0; i < size.height; i++) { 300 for (int j = 0; j < size.width * cn; j += cn) { 301 positions[(int) (i * size.width * cn + j + 0)] = 302 (2 * (j / cn) + i % 2) * (float) unit; 303 positions[(int) (i * size.width * cn + j + 1)] = 304 i * unit; 305 positions[(int) (i * size.width * cn + j + 2)] = 0; 306 } 307 } 308 grid.create(n, 1, CvType.CV_32FC3); 309 grid.put(0, 0, positions); 310 return grid; 311 } 312 313 /** 314 * Create a camera intrinsic matrix using input parameters 315 * 316 * The camera intrinsic matrix will be like: 317 * 318 * +- -+ 319 * | f 0 center.width | 320 * A = | 0 f center.height | 321 * | 0 0 1 | 322 * +- -+ 323 * 324 * @return An approximated (not actually calibrated) camera matrix 325 */ 326 private static Mat cameraMatrix(float f, Size center) { 327 final double [] data = {f, 0, center.width, 0, f, center.height, 0, 0, 1f}; 328 Mat m = new Mat(3,3, CvType.CV_64F); 329 m.put(0, 0, data); 330 return m; 331 } 332 333 /** 334 * Attitude record in time roll pitch yaw format. 335 * 336 */ 337 private class AttitudeRec { 338 public double time; 339 public double roll; 340 public double pitch; 341 public double yaw; 342 343 // ctor 344 AttitudeRec(double atime, double aroll, double apitch, double ayaw) { 345 time = atime; 346 roll = aroll; 347 pitch = apitch; 348 yaw = ayaw; 349 } 350 351 // ctor 352 AttitudeRec(double atime, double [] rpy) { 353 time = atime; 354 roll = rpy[0]; 355 pitch = rpy[1]; 356 yaw = rpy[2]; 357 } 358 359 // copy value of another to this 360 void assign(AttitudeRec rec) { 361 time = rec.time; 362 roll = rec.time; 363 pitch = rec.pitch; 364 yaw = rec.yaw; 365 } 366 367 // copy roll-pitch-yaw value but leave the time specified by atime 368 void assign(AttitudeRec rec, double atime) { 369 time = atime; 370 roll = rec.time; 371 pitch = rec.pitch; 372 yaw = rec.yaw; 373 } 374 375 // set each field separately 376 void set(double atime, double aroll, double apitch, double ayaw) { 377 time = atime; 378 roll = aroll; 379 pitch = apitch; 380 yaw = ayaw; 381 } 382 } 383 384 385 /** 386 * Load the sensor log in (time Roll-pitch-yaw) format to a ArrayList<AttitudeRec> 387 * 388 * @return the number of sensor log items 389 */ 390 private int loadSensorLog(ArrayList<AttitudeRec> recs) { 391 //ArrayList<AttitudeRec> recs = new ArrayList<AttitudeRec>(); 392 File csvFile = new File(mPath, "sensor.log"); 393 BufferedReader br=null; 394 String line; 395 396 // preallocate and reuse 397 double [] quat = new double[4]; 398 double [] rpy = new double[3]; 399 400 double t0 = -1; 401 402 try { 403 br = new BufferedReader(new FileReader(csvFile)); 404 while ((line = br.readLine()) != null) { 405 //space separator 406 String[] items = line.split(" "); 407 408 if (items.length != 5) { 409 recs.clear(); 410 return -1; 411 } 412 413 quat[0] = Double.parseDouble(items[1]); 414 quat[1] = Double.parseDouble(items[2]); 415 quat[2] = Double.parseDouble(items[3]); 416 quat[3] = Double.parseDouble(items[4]); 417 418 // 419 quat2rpy(quat, rpy); 420 421 if (t0 < 0) { 422 t0 = Long.parseLong(items[0])/1e9; 423 } 424 recs.add(new AttitudeRec(Long.parseLong(items[0])/1e9-t0, rpy)); 425 } 426 427 } catch (FileNotFoundException e) { 428 e.printStackTrace(); 429 Log.e(TAG, "Cannot find sensor logging data"); 430 } catch (IOException e) { 431 e.printStackTrace(); 432 Log.e(TAG, "Cannot read sensor logging data"); 433 } finally { 434 if (br != null) { 435 try { 436 br.close(); 437 } catch (IOException e) { 438 e.printStackTrace(); 439 } 440 } 441 } 442 443 return recs.size(); 444 } 445 446 /** 447 * Read video meta info 448 */ 449 private class VideoMetaInfo { 450 public double fps; 451 public int frameWidth; 452 public int frameHeight; 453 public double fovWidth; 454 public double fovHeight; 455 public boolean valid = false; 456 457 VideoMetaInfo(File file) { 458 459 BufferedReader br=null; 460 String line; 461 String content=""; 462 try { 463 br = new BufferedReader(new FileReader(file)); 464 while ((line = br.readLine()) != null) { 465 content = content +line; 466 } 467 468 } catch (FileNotFoundException e) { 469 e.printStackTrace(); 470 Log.e(TAG, "Cannot find video meta info file"); 471 } catch (IOException e) { 472 e.printStackTrace(); 473 Log.e(TAG, "Cannot read video meta info file"); 474 } finally { 475 if (br != null) { 476 try { 477 br.close(); 478 } catch (IOException e) { 479 e.printStackTrace(); 480 } 481 } 482 } 483 484 if (content.isEmpty()) { 485 return; 486 } 487 488 try { 489 JSONObject json = new JSONObject(content); 490 frameWidth = json.getInt("width"); 491 frameHeight = json.getInt("height"); 492 fps = json.getDouble("frameRate"); 493 fovWidth = json.getDouble("fovW")*Math.PI/180.0; 494 fovHeight = json.getDouble("fovH")*Math.PI/180.0; 495 } catch (JSONException e) { 496 return; 497 } 498 499 valid = true; 500 501 } 502 } 503 504 505 506 /** 507 * Debugging helper function, load ArrayList<AttitudeRec> from a file dumped out by 508 * dumpAttitudeRecs 509 */ 510 private int loadAttitudeRecs(File file, ArrayList<AttitudeRec> recs) { 511 BufferedReader br=null; 512 String line; 513 double time; 514 double [] rpy = new double[3]; 515 516 try { 517 br = new BufferedReader(new FileReader(file)); 518 while ((line = br.readLine()) != null) { 519 //space separator 520 String[] items = line.split(" "); 521 522 if (items.length != 4) { 523 recs.clear(); 524 return -1; 525 } 526 527 time = Double.parseDouble(items[0]); 528 rpy[0] = Double.parseDouble(items[1]); 529 rpy[1] = Double.parseDouble(items[2]); 530 rpy[2] = Double.parseDouble(items[3]); 531 532 recs.add(new AttitudeRec(time, rpy)); 533 } 534 535 } catch (FileNotFoundException e) { 536 e.printStackTrace(); 537 Log.e(TAG, "Cannot find AttitudeRecs file specified."); 538 } catch (IOException e) { 539 e.printStackTrace(); 540 Log.e(TAG, "Read AttitudeRecs file failure"); 541 } finally { 542 if (br != null) { 543 try { 544 br.close(); 545 } catch (IOException e) { 546 e.printStackTrace(); 547 } 548 } 549 } 550 551 return recs.size(); 552 } 553 /** 554 * Debugging helper function, Dump an ArrayList<AttitudeRec> to a file 555 */ 556 private void dumpAttitudeRecs(File file, ArrayList<AttitudeRec> recs) { 557 OutputStreamWriter w=null; 558 try { 559 w = new OutputStreamWriter(new FileOutputStream(file)); 560 561 for (AttitudeRec r : recs) { 562 w.write(String.format("%f %f %f %f\r\n", r.time, r.roll, r.pitch, r.yaw)); 563 } 564 w.close(); 565 } catch(FileNotFoundException e) { 566 e.printStackTrace(); 567 Log.e(TAG, "Cannot create AttitudeRecs file."); 568 } catch (IOException e) { 569 Log.e(TAG, "Write AttitudeRecs file failure"); 570 } finally { 571 if (w!=null) { 572 try { 573 w.close(); 574 } catch (IOException e) { 575 e.printStackTrace(); 576 } 577 } 578 } 579 } 580 581 /** 582 * Read the sensor log in ArrayList<AttitudeRec> format and find out the sensor sample time 583 * statistics: mean and standard deviation. 584 * 585 * @return The returned value will be a double array with exact 2 items, first [0] will be 586 * mean and the second [1] will be the standard deviation. 587 * 588 */ 589 private double [] calcSensorPeriodStat(ArrayList<AttitudeRec> srec) { 590 double tp = srec.get(0).time; 591 int i; 592 double sum = 0.0; 593 double sumsq = 0.0; 594 for(i=1; i<srec.size(); ++i) { 595 double dt; 596 dt = srec.get(i).time - tp; 597 sum += dt; 598 sumsq += dt*dt; 599 tp += dt; 600 } 601 double [] ret = new double[2]; 602 ret[0] = sum/srec.size(); 603 ret[1] = Math.sqrt(sumsq/srec.size() - ret[0]*ret[0]); 604 return ret; 605 } 606 607 /** 608 * Flipping the axis as the image are flipped upside down in OpenGL frames 609 */ 610 private void fixFlippedAxis(ArrayList<AttitudeRec> vrecs) { 611 for (AttitudeRec i: vrecs) { 612 i.yaw = -i.yaw; 613 } 614 } 615 616 /** 617 * Calculate the maximum error on the specified axis between two time aligned (resampled) 618 * ArrayList<AttitudeRec>. Yaw axis needs special treatment as 0 and 2pi error are same thing 619 * 620 * @param ra one ArrayList of AttitudeRec 621 * @param rb the other ArrayList of AttitudeRec 622 * @param axis axis id for the comparison (0 = roll, 1 = pitch, 2 = yaw) 623 * @return Maximum error 624 */ 625 private double calcMaxErr(ArrayList<AttitudeRec> ra, ArrayList<AttitudeRec> rb, int axis) { 626 // check if they are valid and comparable data 627 if (ra.size() != rb.size()) { 628 throw new ArrayIndexOutOfBoundsException("Two array has to be the same"); 629 } 630 // check input parameter validity 631 if (axis<0 || axis > 2) { 632 throw new IllegalArgumentException("Invalid data axis."); 633 } 634 635 int i; 636 double max = 0.0; 637 double diff = 0.0; 638 for(i=0; i<ra.size(); ++i) { 639 // make sure they are aligned data 640 if (ra.get(i).time != rb.get(i).time) { 641 throw new IllegalArgumentException("Element "+i+ 642 " of two inputs has different time."); 643 } 644 switch(axis) { 645 case 0: 646 diff = ra.get(i).roll - rb.get(i).roll; // they always opposite of each other.. 647 break; 648 case 1: 649 diff = ra.get(i).pitch - rb.get(i).pitch; 650 break; 651 case 2: 652 diff = Math.abs(((4*Math.PI + ra.get(i).yaw - rb.get(i).yaw)%(2*Math.PI)) 653 -Math.PI)-Math.PI; 654 break; 655 } 656 diff = Math.abs(diff); 657 if (diff>max) { 658 max = diff; 659 } 660 } 661 return max; 662 } 663 664 /** 665 * Calculate the RMS error on the specified axis between two time aligned (resampled) 666 * ArrayList<AttitudeRec>. Yaw axis needs special treatment as 0 and 2pi error are same thing 667 * 668 * @param ra one ArrayList of AttitudeRec 669 * @param rb the other ArrayList of AttitudeRec 670 * @param axis axis id for the comparison (0 = roll, 1 = pitch, 2 = yaw) 671 * @return Mean square error 672 */ 673 private double calcSqrErr(ArrayList<AttitudeRec> ra, ArrayList<AttitudeRec> rb, int axis) { 674 // check if they are valid and comparable data 675 if (ra.size() != rb.size()) { 676 throw new ArrayIndexOutOfBoundsException("Two array has to be the same"); 677 } 678 // check input parameter validity 679 if (axis<0 || axis > 2) { 680 throw new IllegalArgumentException("Invalid data axis."); 681 } 682 683 int i; 684 double sum = 0.0; 685 double diff = 0.0; 686 for(i=0; i<ra.size(); ++i) { 687 // check input data validity 688 if (ra.get(i).time != rb.get(i).time) { 689 throw new IllegalArgumentException("Element "+i+ 690 " of two inputs has different time."); 691 } 692 693 switch(axis) { 694 case 0: 695 diff = ra.get(i).roll - rb.get(i).roll; 696 break; 697 case 1: 698 diff = ra.get(i).pitch - rb.get(i).pitch; 699 break; 700 case 2: 701 diff = Math.abs(((4*Math.PI + ra.get(i).yaw - rb.get(i).yaw)%(2*Math.PI))- 702 Math.PI)-Math.PI; 703 break; 704 } 705 706 sum += diff*diff; 707 } 708 return sum/ra.size(); 709 } 710 711 /** 712 * Debugging helper function. Dump the error between two time aligned ArrayList<AttitudeRec>'s 713 * 714 * @param file File to write to 715 * @param ra one ArrayList of AttitudeRec 716 * @param rb the other ArrayList of AttitudeRec 717 */ 718 private void dumpAttitudeError(File file, ArrayList<AttitudeRec> ra, ArrayList<AttitudeRec> rb){ 719 if (ra.size() != rb.size()) { 720 throw new ArrayIndexOutOfBoundsException("Two array has to be the same"); 721 } 722 723 int i; 724 725 ArrayList<AttitudeRec> rerr = new ArrayList<>(); 726 for(i=0; i<ra.size(); ++i) { 727 if (ra.get(i).time != rb.get(i).time) { 728 throw new IllegalArgumentException("Element "+ i 729 + " of two inputs has different time."); 730 } 731 732 rerr.add(new AttitudeRec(ra.get(i).time, ra.get(i).roll - rb.get(i).roll, 733 ra.get(i).pitch - rb.get(i).pitch, 734 (Math.abs(((4*Math.PI + ra.get(i).yaw - rb.get(i).yaw)%(2*Math.PI)) 735 -Math.PI)-Math.PI))); 736 737 } 738 dumpAttitudeRecs(file, rerr); 739 } 740 741 /** 742 * Resample one ArrayList<AttitudeRec> with respect to another ArrayList<AttitudeRec> 743 * 744 * @param rec the ArrayList of AttitudeRec to be sampled 745 * @param timebase the other ArrayList of AttitudeRec that serves as time base 746 * @param delta_t offset in time before resample 747 * @param yaw_offset offset in yaw axis 748 * @param resampled output ArrayList of AttitudeRec 749 */ 750 751 private void resampleSensorLog(ArrayList<AttitudeRec> rec, ArrayList<AttitudeRec> timebase, 752 double delta_t, double yaw_offset, ArrayList<AttitudeRec> resampled) { 753 int i; 754 int j = -1; 755 for(i=0; i<timebase.size(); i++) { 756 double time = timebase.get(i).time + delta_t; 757 758 while(j<rec.size()-1 && rec.get(j+1).time < time) j++; 759 760 if (j == -1) { 761 //use first 762 resampled.get(i).assign(rec.get(0), timebase.get(i).time); 763 } else if (j == rec.size()-1) { 764 // use last 765 resampled.get(i).assign(rec.get(j), timebase.get(i).time); 766 } else { 767 // do linear resample 768 double alpha = (time - rec.get(j).time)/((rec.get(j+1).time - rec.get(j).time)); 769 double roll = (1-alpha) * rec.get(j).roll + alpha * rec.get(j+1).roll; 770 double pitch = (1-alpha) * rec.get(j).pitch + alpha * rec.get(j+1).pitch; 771 double yaw = (1-alpha) * rec.get(j).yaw + alpha * rec.get(j+1).yaw + yaw_offset; 772 resampled.get(i).set(timebase.get(i).time, roll, pitch, yaw); 773 } 774 } 775 } 776 777 /** 778 * Analyze video frames using computer vision approach and generate a ArrayList<AttitudeRec> 779 * 780 * @param recs output ArrayList of AttitudeRec 781 * @return total number of frame of the video 782 */ 783 private int analyzeVideo(ArrayList<AttitudeRec> recs) { 784 VideoMetaInfo meta = new VideoMetaInfo(new File(mPath, "videometa.json")); 785 786 int decimation = 1; 787 boolean use_timestamp = true; 788 789 // roughly determine if decimation is necessary 790 if (meta.fps > DECIMATION_FPS_TARGET) { 791 decimation = (int)(meta.fps / DECIMATION_FPS_TARGET); 792 meta.fps /=decimation; 793 } 794 795 VideoDecoderForOpenCV videoDecoder = new VideoDecoderForOpenCV( 796 new File(mPath, "video.mp4"), decimation); 797 798 799 Mat frame; 800 Mat gray = new Mat(); 801 int i = -1; 802 803 Size frameSize = videoDecoder.getSize(); 804 805 if (frameSize.width != meta.frameWidth || frameSize.height != meta.frameHeight) { 806 // this is very unlikely 807 return -1; 808 } 809 810 if (TRACE_VIDEO_ANALYSIS) { 811 Debug.startMethodTracing("cvprocess"); 812 } 813 814 Size patternSize = new Size(4,11); 815 816 float fc = (float)(meta.frameWidth/2.0/Math.tan(meta.fovWidth/2.0)); 817 Mat camMat = cameraMatrix(fc, new Size(frameSize.width/2, frameSize.height/2)); 818 MatOfDouble coeff = new MatOfDouble(); // dummy 819 820 MatOfPoint2f centers = new MatOfPoint2f(); 821 MatOfPoint3f grid = asymmetricalCircleGrid(patternSize); 822 Mat rvec = new MatOfFloat(); 823 Mat tvec = new MatOfFloat(); 824 825 MatOfPoint2f reprojCenters = new MatOfPoint2f(); 826 827 if (LOCAL_LOGV) { 828 Log.v(TAG, "Camera Mat = \n" + camMat.dump()); 829 } 830 831 long startTime = System.nanoTime(); 832 long [] ts = new long[1]; 833 834 while ((frame = videoDecoder.getFrame(ts)) !=null) { 835 if (LOCAL_LOGV) { 836 Log.v(TAG, "got a frame " + i); 837 } 838 839 if (use_timestamp && ts[0] == -1) { 840 use_timestamp = false; 841 } 842 843 // has to be in front, as there are cases where execution 844 // will skip the later part of this while 845 i++; 846 847 // convert to gray manually as by default findCirclesGridDefault uses COLOR_BGR2GRAY 848 Imgproc.cvtColor(frame, gray, Imgproc.COLOR_RGB2GRAY); 849 850 boolean foundPattern = Calib3d.findCirclesGrid( 851 gray, patternSize, centers, Calib3d.CALIB_CB_ASYMMETRIC_GRID); 852 853 if (!foundPattern) { 854 // skip to next frame 855 continue; 856 } 857 858 if (OUTPUT_DEBUG_IMAGE) { 859 Calib3d.drawChessboardCorners(frame, patternSize, centers, true); 860 } 861 862 // figure out the extrinsic parameters using real ground truth 3D points and the pixel 863 // position of blobs found in findCircleGrid, an estimated camera matrix and 864 // no-distortion are assumed. 865 boolean foundSolution = 866 Calib3d.solvePnP(grid, centers, camMat, coeff, rvec, tvec, 867 false, Calib3d.CV_ITERATIVE); 868 869 if (!foundSolution) { 870 // skip to next frame 871 if (LOCAL_LOGV) { 872 Log.v(TAG, "cannot find pnp solution in frame " + i + ", skipped."); 873 } 874 continue; 875 } 876 877 // reproject points to for evaluation of result accuracy of solvePnP 878 Calib3d.projectPoints(grid, rvec, tvec, camMat, coeff, reprojCenters); 879 880 // error is evaluated in norm2, which is real error in pixel distance / sqrt(2) 881 double error = Core.norm(centers, reprojCenters, Core.NORM_L2); 882 883 if (LOCAL_LOGV) { 884 Log.v(TAG, "Found attitude, re-projection error = " + error); 885 } 886 887 // if error is reasonable, add it into the results. use ratio to frame height to avoid 888 // discriminating higher definition videos 889 if (error < REPROJECTION_THREASHOLD_RATIO * frameSize.height) { 890 double [] rv = new double[3]; 891 double timestamp; 892 893 rvec.get(0,0, rv); 894 if (use_timestamp) { 895 timestamp = (double)ts[0] / 1e6; 896 } else { 897 timestamp = (double) i / meta.fps; 898 } 899 if (LOCAL_LOGV) Log.v(TAG, String.format("Added frame %d ts = %f", i, timestamp)); 900 recs.add(new AttitudeRec(timestamp, rodr2rpy(rv))); 901 } 902 903 if (OUTPUT_DEBUG_IMAGE) { 904 Calib3d.drawChessboardCorners(frame, patternSize, reprojCenters, true); 905 Imgcodecs.imwrite(Environment.getExternalStorageDirectory().getPath() 906 + "/RVCVRecData/DebugCV/img" + i + ".png", frame); 907 } 908 } 909 910 if (LOCAL_LOGV) { 911 Log.v(TAG, "Finished decoding"); 912 } 913 914 if (TRACE_VIDEO_ANALYSIS) { 915 Debug.stopMethodTracing(); 916 } 917 918 if (LOCAL_LOGV) { 919 // time analysis 920 double totalTime = (System.nanoTime()-startTime)/1e9; 921 Log.i(TAG, "Total time: "+totalTime +"s, Per frame time: "+totalTime/i ); 922 } 923 return i; 924 } 925 926 /** 927 * OpenCV for Android have not support the VideoCapture from file 928 * This is a make shift solution before it is supported. 929 * One issue right now is that the glReadPixels is quite slow .. around 6.5ms for a 720p frame 930 */ 931 private class VideoDecoderForOpenCV implements Runnable { 932 static final String TAG = "VideoDecoderForOpenCV"; 933 934 private MediaExtractor extractor=null; 935 private MediaCodec decoder=null; 936 private CtsMediaOutputSurface surface=null; 937 938 private MatBuffer mMatBuffer; 939 940 private final File mVideoFile; 941 942 private boolean valid; 943 private Object setupSignal; 944 945 private Thread mThread; 946 private int mDecimation; 947 948 /** 949 * Constructor 950 * @param file video file 951 * @param decimation process every "decimation" number of frame 952 */ 953 VideoDecoderForOpenCV(File file, int decimation) { 954 mVideoFile = file; 955 mDecimation = decimation; 956 valid = false; 957 958 start(); 959 } 960 961 /** 962 * Constructor 963 * @param file video file 964 */ 965 VideoDecoderForOpenCV(File file) { 966 this(file, 1); 967 } 968 969 /** 970 * Test if video decoder is in valid states ready to output video. 971 * @return true of force. 972 */ 973 public boolean isValid() { 974 return valid; 975 } 976 977 private void start() { 978 setupSignal = new Object(); 979 mThread = new Thread(this); 980 mThread.start(); 981 982 synchronized (setupSignal) { 983 try { 984 setupSignal.wait(); 985 } catch (InterruptedException e) { 986 Log.e(TAG, "Interrupted when waiting for video decoder setup ready"); 987 } 988 } 989 } 990 private void stop() { 991 if (mThread != null) { 992 mThread.interrupt(); 993 try { 994 mThread.join(); 995 } catch (InterruptedException e) { 996 Log.e(TAG, "Interrupted when waiting for video decoder thread to stop"); 997 } 998 try { 999 decoder.stop(); 1000 }catch (IllegalStateException e) { 1001 Log.e(TAG, "Video decoder is not in a state that can be stopped"); 1002 } 1003 } 1004 mThread = null; 1005 } 1006 1007 void teardown() { 1008 if (decoder!=null) { 1009 decoder.release(); 1010 decoder = null; 1011 } 1012 if (surface!=null) { 1013 surface.release(); 1014 surface = null; 1015 } 1016 if (extractor!=null) { 1017 extractor.release(); 1018 extractor = null; 1019 } 1020 } 1021 1022 void setup() { 1023 int width=0, height=0; 1024 1025 extractor = new MediaExtractor(); 1026 1027 try { 1028 extractor.setDataSource(mVideoFile.getPath()); 1029 } catch (IOException e) { 1030 return; 1031 } 1032 1033 for (int i = 0; i < extractor.getTrackCount(); i++) { 1034 MediaFormat format = extractor.getTrackFormat(i); 1035 String mime = format.getString(MediaFormat.KEY_MIME); 1036 width = format.getInteger(MediaFormat.KEY_WIDTH); 1037 height = format.getInteger(MediaFormat.KEY_HEIGHT); 1038 1039 if (mime.startsWith("video/")) { 1040 extractor.selectTrack(i); 1041 try { 1042 decoder = MediaCodec.createDecoderByType(mime); 1043 }catch (IOException e) { 1044 continue; 1045 } 1046 // Decode to surface 1047 //decoder.configure(format, surface, null, 0); 1048 1049 // Decode to offscreen surface 1050 surface = new CtsMediaOutputSurface(width, height); 1051 mMatBuffer = new MatBuffer(width, height); 1052 1053 decoder.configure(format, surface.getSurface(), null, 0); 1054 break; 1055 } 1056 } 1057 1058 if (decoder == null) { 1059 Log.e(TAG, "Can't find video info!"); 1060 return; 1061 } 1062 valid = true; 1063 } 1064 1065 @Override 1066 public void run() { 1067 setup(); 1068 1069 synchronized (setupSignal) { 1070 setupSignal.notify(); 1071 } 1072 1073 if (!valid) { 1074 return; 1075 } 1076 1077 decoder.start(); 1078 1079 ByteBuffer[] inputBuffers = decoder.getInputBuffers(); 1080 ByteBuffer[] outputBuffers = decoder.getOutputBuffers(); 1081 MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 1082 1083 boolean isEOS = false; 1084 long startMs = System.currentTimeMillis(); 1085 long timeoutUs = 10000; 1086 1087 int iframe = 0; 1088 long frameTimestamp = 0; 1089 1090 while (!Thread.interrupted()) { 1091 if (!isEOS) { 1092 int inIndex = decoder.dequeueInputBuffer(10000); 1093 if (inIndex >= 0) { 1094 ByteBuffer buffer = inputBuffers[inIndex]; 1095 int sampleSize = extractor.readSampleData(buffer, 0); 1096 if (sampleSize < 0) { 1097 if (LOCAL_LOGD) { 1098 Log.d("VideoDecoderForOpenCV", 1099 "InputBuffer BUFFER_FLAG_END_OF_STREAM"); 1100 } 1101 decoder.queueInputBuffer(inIndex, 0, 0, 0, 1102 MediaCodec.BUFFER_FLAG_END_OF_STREAM); 1103 isEOS = true; 1104 } else { 1105 frameTimestamp = extractor.getSampleTime(); 1106 decoder.queueInputBuffer(inIndex, 0, sampleSize, frameTimestamp, 0); 1107 if (LOCAL_LOGD) { 1108 Log.d(TAG, String.format("Frame %d sample time %f s", 1109 iframe, (double)frameTimestamp/1e6)); 1110 } 1111 extractor.advance(); 1112 } 1113 } 1114 } 1115 1116 int outIndex = decoder.dequeueOutputBuffer(info, 10000); 1117 MediaFormat outFormat; 1118 switch (outIndex) { 1119 case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: 1120 if (LOCAL_LOGD) { 1121 Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED"); 1122 } 1123 outputBuffers = decoder.getOutputBuffers(); 1124 break; 1125 case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: 1126 outFormat = decoder.getOutputFormat(); 1127 if (LOCAL_LOGD) { 1128 Log.d(TAG, "New format " + outFormat); 1129 } 1130 break; 1131 case MediaCodec.INFO_TRY_AGAIN_LATER: 1132 if (LOCAL_LOGD) { 1133 Log.d(TAG, "dequeueOutputBuffer timed out!"); 1134 } 1135 break; 1136 default: 1137 1138 ByteBuffer buffer = outputBuffers[outIndex]; 1139 boolean doRender = (info.size != 0); 1140 1141 // As soon as we call releaseOutputBuffer, the buffer will be forwarded 1142 // to SurfaceTexture to convert to a texture. The API doesn't 1143 // guarantee that the texture will be available before the call 1144 // returns, so we need to wait for the onFrameAvailable callback to 1145 // fire. If we don't wait, we risk rendering from the previous frame. 1146 decoder.releaseOutputBuffer(outIndex, doRender); 1147 1148 if (doRender) { 1149 surface.awaitNewImage(); 1150 surface.drawImage(); 1151 if (LOCAL_LOGV) { 1152 Log.v(TAG, "Finish drawing a frame!"); 1153 } 1154 if ((iframe++ % mDecimation) == 0) { 1155 //Send the frame for processing 1156 mMatBuffer.put(frameTimestamp); 1157 } 1158 } 1159 break; 1160 } 1161 1162 if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { 1163 if (LOCAL_LOGD) { 1164 Log.d("VideoDecoderForOpenCV", "OutputBuffer BUFFER_FLAG_END_OF_STREAM"); 1165 } 1166 break; 1167 } 1168 } 1169 mMatBuffer.invalidate(); 1170 1171 decoder.stop(); 1172 1173 teardown(); 1174 mThread = null; 1175 } 1176 1177 1178 /** 1179 * Get next valid frame 1180 * @return Frame in OpenCV mat 1181 */ 1182 public Mat getFrame(long ts[]) { 1183 return mMatBuffer.get(ts); 1184 } 1185 1186 /** 1187 * Get the size of the frame 1188 * @return size of the frame 1189 */ 1190 Size getSize() { 1191 return mMatBuffer.getSize(); 1192 } 1193 1194 /** 1195 * A synchronized buffer 1196 */ 1197 class MatBuffer { 1198 private Mat mat; 1199 private byte[] bytes; 1200 private ByteBuffer buf; 1201 private long timestamp; 1202 private boolean full; 1203 1204 private int mWidth, mHeight; 1205 private boolean mValid = false; 1206 1207 MatBuffer(int width, int height) { 1208 mWidth = width; 1209 mHeight = height; 1210 1211 mat = new Mat(height, width, CvType.CV_8UC4); //RGBA 1212 buf = ByteBuffer.allocateDirect(width*height*4); 1213 bytes = new byte[width*height*4]; 1214 timestamp = -1; 1215 1216 mValid = true; 1217 full = false; 1218 } 1219 1220 public synchronized void invalidate() { 1221 mValid = false; 1222 notifyAll(); 1223 } 1224 1225 public synchronized Mat get(long ts[]) { 1226 1227 if (!mValid) return null; 1228 while (full == false) { 1229 try { 1230 wait(); 1231 if (!mValid) return null; 1232 } catch (InterruptedException e) { 1233 return null; 1234 } 1235 } 1236 mat.put(0,0, bytes); 1237 full = false; 1238 notifyAll(); 1239 ts[0] = timestamp; 1240 return mat; 1241 } 1242 1243 public synchronized void put(long ts) { 1244 while (full) { 1245 try { 1246 wait(); 1247 } catch (InterruptedException e) { 1248 Log.e(TAG, "Interrupted when waiting for space in buffer"); 1249 } 1250 } 1251 GLES20.glReadPixels(0, 0, mWidth, mHeight, GL10.GL_RGBA, 1252 GL10.GL_UNSIGNED_BYTE, buf); 1253 buf.get(bytes); 1254 buf.rewind(); 1255 1256 timestamp = ts; 1257 full = true; 1258 notifyAll(); 1259 } 1260 1261 public Size getSize() { 1262 if (valid) { 1263 return mat.size(); 1264 } 1265 return new Size(); 1266 } 1267 } 1268 } 1269 1270 1271 /* a small set of math functions */ 1272 private static double [] quat2rpy( double [] q) { 1273 double [] rpy = {Math.atan2(2*(q[0]*q[1]+q[2]*q[3]), 1-2*(q[1]*q[1]+q[2]*q[2])), 1274 Math.asin(2*(q[0]*q[2] - q[3]*q[1])), 1275 Math.atan2(2*(q[0]*q[3]+q[1]*q[2]), 1-2*(q[2]*q[2]+q[3]*q[3]))}; 1276 return rpy; 1277 } 1278 1279 private static void quat2rpy( double [] q, double[] rpy) { 1280 rpy[0] = Math.atan2(2*(q[0]*q[1]+q[2]*q[3]), 1-2*(q[1]*q[1]+q[2]*q[2])); 1281 rpy[1] = Math.asin(2*(q[0]*q[2] - q[3]*q[1])); 1282 rpy[2] = Math.atan2(2*(q[0]*q[3]+q[1]*q[2]), 1-2*(q[2]*q[2]+q[3]*q[3])); 1283 } 1284 1285 private static Mat quat2rpy(Mat quat) { 1286 double [] q = new double[4]; 1287 quat.get(0,0,q); 1288 1289 double [] rpy = {Math.atan2(2*(q[0]*q[1]+q[2]*q[3]), 1-2*(q[1]*q[1]+q[2]*q[2])), 1290 Math.asin(2*(q[0]*q[2] - q[3]*q[1])), 1291 Math.atan2(2*(q[0]*q[3]+q[1]*q[2]), 1-2*(q[2]*q[2]+q[3]*q[3]))}; 1292 1293 Mat rpym = new Mat(3,1, CvType.CV_64F); 1294 rpym.put(0,0, rpy); 1295 return rpym; 1296 } 1297 1298 private static double [] rodr2quat( double [] r) { 1299 double t = Math.sqrt(r[0]*r[0]+r[1]*r[1]+r[2]*r[2]); 1300 double [] quat = {Math.cos(t/2), Math.sin(t/2)*r[0]/t,Math.sin(t/2)*r[1]/t, 1301 Math.sin(t/2)*r[2]/t}; 1302 return quat; 1303 } 1304 1305 private static void rodr2quat( double [] r, double [] quat) { 1306 double t = Math.sqrt(r[0]*r[0]+r[1]*r[1]+r[2]*r[2]); 1307 quat[0] = Math.cos(t/2); 1308 quat[1] = Math.sin(t/2)*r[0]/t; 1309 quat[2] = Math.sin(t/2)*r[1]/t; 1310 quat[3] = Math.sin(t/2)*r[2]/t; 1311 } 1312 1313 private static Mat rodr2quat(Mat rodr) { 1314 double t = Core.norm(rodr); 1315 double [] r = new double[3]; 1316 rodr.get(0,0,r); 1317 1318 double [] quat = {Math.cos(t/2), Math.sin(t/2)*r[0]/t,Math.sin(t/2)*r[1]/t, 1319 Math.sin(t/2)*r[2]/t}; 1320 Mat quatm = new Mat(4,1, CvType.CV_64F); 1321 quatm.put(0, 0, quat); 1322 return quatm; 1323 } 1324 1325 private static double [] rodr2rpy( double [] r) { 1326 return quat2rpy(rodr2quat(r)); 1327 } 1328 ////////////////// 1329 1330 } 1331