├── compile.sh ├── README.md └── flow_video.cpp /compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CODEFILE=${1:-'flow_video.cpp'} 4 | BINARYFILE=${2:-'flow_video'} 5 | 6 | OPENCV_INSTALL="/PATH/TO/OpenCV/install/" 7 | 8 | g++ -I${OPENCV_INSTALL}include -L${OPENCV_INSTALL}lib -g -o $BINARYFILE $CODEFILE -lopencv_calib3d -lopencv_core -lopencv_cudaarithm -lopencv_cudabgsegm -lopencv_cudacodec -lopencv_cudafeatures2d -lopencv_cudafilters -lopencv_cudaimgproc -lopencv_cudalegacy -lopencv_objdetect -lopencv_cudaoptflow -lopencv_cudastereo -lopencv_cudawarping -lopencv_cudev -lopencv_features2d -lopencv_flann -lopencv_highgui -lopencv_imgcodecs -lopencv_imgproc -lopencv_ml -lopencv_objdetect -lopencv_photo -lopencv_shape -lopencv_stitching -lopencv_superres -lopencv_videoio -lopencv_video -lopencv_videostab 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flow toolbox 2 | Optical flow extraction tool using OpenCV. The code is simple and mostly from documentation of OpenCV, but it makes it easy to use for pre-processing of videos with several options. It is easy to customize and change the algorithms used. Currently Brox algorithm is used for GPU and Farneback is used for CPU implementations. 3 | 4 | ## Compilation 5 | TO-DO 6 | 7 | ## Help 8 | Type `./flow_video -h` to see the help message below. 9 | 10 | 11 | ```shell 12 | USAGE: 13 | [-h] [-p ] [-o ] [-b ] [-e ] [-v ] [-m ] 14 | 15 | INPUT: 16 | : Path to video file or image directory (e.g. img_%04d.jpg) 17 | OPTIONS: 18 | -h : Display this help message 19 | -p [gpu] : Processor type (gpu or cpu) 20 | -o [./] : Output folder containing flow images and minmax.txt 21 | -b [1] : Frame index to start (one-based indexing) 22 | -e [last] : Frame index to stop 23 | -v [0] : Boolean for visualization of the optical flow 24 | -m [/_minmax.txt] : Name of the minmax file. 25 | 26 | Notes: 27 | *GPU method: Brox, CPU method: Farneback. 28 | *Only _%0xd.jpg (x any digit) is supported for image sequence input. 29 | ``` 30 | 31 | ## Example usage 32 | Extract flow from the frame interval 5-10. 33 | 34 | ```shell 35 | ./flow_video -b 5 -e 10 -o samples/out samples/video/video.avi 36 | ./flow_video -b 5 -e 10 -o samples/out samples/images/img1/img1_%05d.jpg 37 | ``` 38 | -------------------------------------------------------------------------------- /flow_video.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "opencv2/core.hpp" 4 | #include "opencv2/core/utility.hpp" 5 | #include "opencv2/highgui.hpp" 6 | #include "opencv2/cudaoptflow.hpp" 7 | #include "opencv2/cudaarithm.hpp" 8 | #include "opencv2/imgcodecs.hpp" 9 | #include "opencv2/imgproc/imgproc.hpp" 10 | #include "opencv2/cudacodec.hpp" 11 | #include 12 | #include 13 | #include "opencv2/video/tracking.hpp" 14 | #include 15 | 16 | using namespace cv; 17 | using namespace cv::cuda; 18 | using namespace std; 19 | 20 | 21 | //Mostly from https://github.com/opencv/opencv/blob/master/samples/gpu/optical_flow.cpp 22 | 23 | 24 | //http://stackoverflow.com/questions/24221605/find-all-files-in-a-directory-and-its-subdirectory 25 | int getdir(string dir, vector &files) 26 | { 27 | DIR *dp; 28 | struct dirent *dirp; 29 | if((dp = opendir(dir.c_str())) == NULL) 30 | { 31 | cout << "Error opening " << dir << endl; 32 | return -1; 33 | } 34 | 35 | while ((dirp = readdir(dp)) != NULL) 36 | { 37 | string s = string(dirp->d_name); 38 | if(strcmp(s.c_str(), ".") && strcmp(s.c_str(), "..")) 39 | files.push_back(s); 40 | } 41 | closedir(dp); 42 | return 1; 43 | } 44 | 45 | inline bool isFlowCorrect(Point2f u) 46 | { 47 | return !cvIsNaN(u.x) && !cvIsNaN(u.y) && fabs(u.x) < 1e9 && fabs(u.y) < 1e9; 48 | } 49 | 50 | static Vec3b computeColor(float fx, float fy) 51 | { 52 | static bool first = true; 53 | 54 | // relative lengths of color transitions: 55 | // these are chosen based on perceptual similarity 56 | // (e.g. one can distinguish more shades between red and yellow 57 | // than between yellow and green) 58 | const int RY = 15; 59 | const int YG = 6; 60 | const int GC = 4; 61 | const int CB = 11; 62 | const int BM = 13; 63 | const int MR = 6; 64 | const int NCOLS = RY + YG + GC + CB + BM + MR; 65 | static Vec3i colorWheel[NCOLS]; 66 | 67 | if (first) 68 | { 69 | int k = 0; 70 | 71 | for (int i = 0; i < RY; ++i, ++k) 72 | colorWheel[k] = Vec3i(255, 255 * i / RY, 0); 73 | 74 | for (int i = 0; i < YG; ++i, ++k) 75 | colorWheel[k] = Vec3i(255 - 255 * i / YG, 255, 0); 76 | 77 | for (int i = 0; i < GC; ++i, ++k) 78 | colorWheel[k] = Vec3i(0, 255, 255 * i / GC); 79 | 80 | for (int i = 0; i < CB; ++i, ++k) 81 | colorWheel[k] = Vec3i(0, 255 - 255 * i / CB, 255); 82 | 83 | for (int i = 0; i < BM; ++i, ++k) 84 | colorWheel[k] = Vec3i(255 * i / BM, 0, 255); 85 | 86 | for (int i = 0; i < MR; ++i, ++k) 87 | colorWheel[k] = Vec3i(255, 0, 255 - 255 * i / MR); 88 | 89 | first = false; 90 | } 91 | 92 | const float rad = sqrt(fx * fx + fy * fy); 93 | const float a = atan2(-fy, -fx) / (float) CV_PI; 94 | 95 | const float fk = (a + 1.0f) / 2.0f * (NCOLS - 1); 96 | const int k0 = static_cast(fk); 97 | const int k1 = (k0 + 1) % NCOLS; 98 | const float f = fk - k0; 99 | 100 | Vec3b pix; 101 | 102 | for (int b = 0; b < 3; b++) 103 | { 104 | const float col0 = colorWheel[k0][b] / 255.0f; 105 | const float col1 = colorWheel[k1][b] / 255.0f; 106 | 107 | float col = (1 - f) * col0 + f * col1; 108 | 109 | if (rad <= 1) 110 | col = 1 - rad * (1 - col); // increase saturation with radius 111 | else 112 | col *= .75; // out of range 113 | 114 | pix[2 - b] = static_cast(255.0 * col); 115 | } 116 | 117 | return pix; 118 | } 119 | 120 | static void drawOpticalFlow(const Mat_& flowx, const Mat_& flowy, Mat& dst, float maxmotion = -1) 121 | { 122 | dst.create(flowx.size(), CV_8UC3); 123 | dst.setTo(Scalar::all(0)); 124 | 125 | // determine motion range: 126 | float maxrad = maxmotion; 127 | 128 | if (maxmotion <= 0) 129 | { 130 | maxrad = 1; 131 | for (int y = 0; y < flowx.rows; ++y) 132 | { 133 | for (int x = 0; x < flowx.cols; ++x) 134 | { 135 | Point2f u(flowx(y, x), flowy(y, x)); 136 | 137 | if (!isFlowCorrect(u)) 138 | continue; 139 | 140 | maxrad = max(maxrad, sqrt(u.x * u.x + u.y * u.y)); 141 | } 142 | } 143 | } 144 | 145 | for (int y = 0; y < flowx.rows; ++y) 146 | { 147 | for (int x = 0; x < flowx.cols; ++x) 148 | { 149 | Point2f u(flowx(y, x), flowy(y, x)); 150 | 151 | if (isFlowCorrect(u)) 152 | dst.at(y, x) = computeColor(u.x / maxrad, u.y / maxrad); 153 | } 154 | } 155 | } 156 | 157 | static void showFlow(const char* name, const GpuMat& d_flow) 158 | { 159 | GpuMat planes[2]; 160 | cuda::split(d_flow, planes); 161 | 162 | Mat flowx(planes[0]); 163 | Mat flowy(planes[1]); 164 | 165 | Mat out; 166 | drawOpticalFlow(flowx, flowy, out, 10); 167 | 168 | //imwrite("deneme.jpg", out); 169 | imshow(name, out); 170 | } 171 | 172 | static void showFlow(const char* name, const Mat& d_flow) 173 | { 174 | vector planes; 175 | split(d_flow, planes); 176 | Mat flowx(planes[0]); 177 | Mat flowy(planes[1]); 178 | 179 | Mat out; 180 | drawOpticalFlow(flowx, flowy, out, 10); 181 | 182 | //imwrite("deneme.jpg", out); 183 | imshow(name, out); 184 | } 185 | 186 | /* Compute the magnitude of flow given x and y components */ 187 | static void computeFlowMagnitude(const Mat_& flowx, const Mat_& flowy, Mat& dst) 188 | { 189 | dst.create(flowx.size(), CV_32FC1); 190 | for (int y = 0; y < flowx.rows; ++y) 191 | { 192 | for (int x = 0; x < flowx.cols; ++x) 193 | { 194 | Point2f u(flowx(y, x), flowy(y, x)); 195 | 196 | if (!isFlowCorrect(u)) 197 | continue; 198 | 199 | dst.at(y, x) = sqrt(u.x * u.x + u.y * u.y); 200 | } 201 | } 202 | } 203 | 204 | /* Write raw optical flow values into txt file 205 | Example usage: 206 | writeFlowRaw(name+"_x_raw.txt", flowx); 207 | writeFlowRaw(name+"_x_raw_n.txt", flowx_n); 208 | */ 209 | template 210 | static void writeFlowRaw(string name, const Mat& flow) 211 | { 212 | ofstream file; 213 | file.open(name.c_str()); 214 | for(int y=0; y(y, x) << " "; 219 | } 220 | file << endl; 221 | } 222 | file.close(); 223 | } 224 | 225 | //min_x max_x min_y max_y 226 | static void writeMM(string name, vector mm) 227 | { 228 | ofstream file; 229 | file.open(name.c_str()); 230 | for(int i=0; i > mm) 239 | { 240 | ofstream file; 241 | file.open(name.c_str()); 242 | for(int i=0; i getMM(const Mat& flow) 254 | { 255 | double min, max; 256 | cv::minMaxLoc(flow, &min, &max); 257 | vector mm; 258 | mm.push_back(min); 259 | mm.push_back(max); 260 | return mm; 261 | } 262 | 263 | /* Write a 3-channel jpg image (flow_x, flow_y, flow_magnitude) in 0-255 range */ 264 | static void writeFlowMergedJpg(string name, const GpuMat& d_flow) 265 | { 266 | GpuMat planes[2]; 267 | cuda::split(d_flow, planes); 268 | 269 | Mat flowx(planes[0]); 270 | Mat flowy(planes[1]); 271 | 272 | Mat flowmag; 273 | computeFlowMagnitude(flowx, flowy, flowmag); 274 | 275 | Mat flowx_n, flowy_n, flowmag_n; 276 | cv::normalize(flowx, flowx_n, 0, 255, NORM_MINMAX, CV_8UC1); 277 | cv::normalize(flowy, flowy_n, 0, 255, NORM_MINMAX, CV_8UC1); 278 | cv::normalize(flowmag, flowmag_n, 0, 255, NORM_MINMAX, CV_8UC1); 279 | 280 | vector compression_params; 281 | compression_params.push_back(CV_IMWRITE_JPEG_QUALITY); 282 | compression_params.push_back(95); 283 | 284 | Mat flow; 285 | vector array_to_merge; 286 | array_to_merge.push_back(flowx_n); 287 | array_to_merge.push_back(flowy_n); 288 | array_to_merge.push_back(flowmag_n); 289 | cv::merge(array_to_merge, flow); 290 | 291 | imwrite(name+".jpg", flow, compression_params); 292 | } 293 | 294 | /* Write two 1-channel jpg images (flow_x and flow_y) in 0-255 range (input flow is gpumat)*/ 295 | static vector writeFlowJpg(string name, const GpuMat& d_flow) 296 | { 297 | // Split flow into x and y components in CPU 298 | GpuMat planes[2]; 299 | cuda::split(d_flow, planes); 300 | Mat flowx(planes[0]); 301 | Mat flowy(planes[1]); 302 | 303 | // Normalize optical flows in range [0, 255] 304 | Mat flowx_n, flowy_n; 305 | cv::normalize(flowx, flowx_n, 0, 255, NORM_MINMAX, CV_8UC1); 306 | cv::normalize(flowy, flowy_n, 0, 255, NORM_MINMAX, CV_8UC1); 307 | 308 | // Save optical flows (x, y) as jpg images 309 | vector compression_params; 310 | compression_params.push_back(CV_IMWRITE_JPEG_QUALITY); 311 | compression_params.push_back(95); 312 | 313 | imwrite(name+"_x.jpg", flowx_n, compression_params); 314 | imwrite(name+"_y.jpg", flowy_n, compression_params); 315 | 316 | // Return normalization elements 317 | vector mm_frame; 318 | vector temp = getMM(flowx); 319 | mm_frame.insert(mm_frame.end(), temp.begin(), temp.end()); 320 | temp = getMM(flowy); 321 | mm_frame.insert(mm_frame.end(), temp.begin(), temp.end()); 322 | 323 | return mm_frame; 324 | } 325 | 326 | /* Write two 1-channel jpg images (flow_x and flow_y) in 0-255 range (input flow is cpu mat)*/ 327 | static vector writeFlowJpg(string name, const Mat& d_flow) 328 | { 329 | vector planes; 330 | split(d_flow, planes); 331 | Mat flowx(planes[0]); 332 | Mat flowy(planes[1]); 333 | // Normalize optical flows in range [0, 255] 334 | Mat flowx_n, flowy_n; 335 | cv::normalize(flowx, flowx_n, 0, 255, NORM_MINMAX, CV_8UC1); //TO-DO 336 | cv::normalize(flowy, flowy_n, 0, 255, NORM_MINMAX, CV_8UC1); //TO-DO 337 | 338 | // Save optical flows (x, y) as jpg images 339 | vector compression_params; 340 | compression_params.push_back(CV_IMWRITE_JPEG_QUALITY); 341 | compression_params.push_back(95); 342 | 343 | imwrite(name+"_x.jpg", flowx_n, compression_params); 344 | imwrite(name+"_y.jpg", flowy_n, compression_params); 345 | 346 | // Return normalization elements 347 | vector mm_frame; 348 | vector temp = getMM(flowx); //TO-DO 349 | mm_frame.insert(mm_frame.end(), temp.begin(), temp.end()); 350 | temp = getMM(flowy); //TO-DO 351 | mm_frame.insert(mm_frame.end(), temp.begin(), temp.end()); 352 | 353 | return mm_frame; 354 | } 355 | 356 | int main(int argc, const char* argv[]) 357 | { 358 | // Parse parameters and options 359 | string input_name; // name of the video file or directory of the image sequence 360 | string proc_type = "gpu"; // "gpu" or "cpu" 361 | string out_dir = "./"; // directory for the output files 362 | int interval_beg = 1; // 1 for the beginning 363 | int interval_end = -1; // End (-1 for uninitialized, default set below) 364 | bool visualize = 0; // boolean for flow visualization 365 | string output_mm = ""; // name of the minmax.txt file (default set below) 366 | 367 | const char* usage = "[-h] [-p ] [-o ] [-b ] [-e ] [-v ] [-m ] "; 368 | string help = "\n\n\nUSAGE:\n\t"+ string(usage) +"\n\n" 369 | "INPUT:\n" 370 | "\t\t \t: Path to video file or image directory (e.g. img_%04d.jpg)\n" 371 | "OPTIONS:\n" 372 | "-h \t \t \t \t: Display this help message\n" 373 | "-p \t \t[gpu] \t: Processor type (gpu or cpu)\n" 374 | "-o \t \t[./] \t: Output folder containing flow images and minmax.txt\n" 375 | "-b \t \t[1] \t: Frame index to start (one-based indexing)\n" 376 | "-e \t \t[last] \t: Frame index to stop\n" 377 | "-v \t \t[0] \t: Boolean for visualization of the optical flow\n" 378 | "-m \t \t[/_minmax.txt] \t: Name of the minmax file.\n" 379 | "\n" 380 | "Notes:\n*GPU method: Brox, CPU method: Farneback.\n" 381 | "*Only _%0xd.jpg (x any digit) is supported for image sequence input.\n\n\n"; 382 | // brox cpu 383 | //fourcc check for image sequence detection, but not sure 384 | int option_char; 385 | while ((option_char = getopt(argc, (char **)argv, "hp:o:b:e:m:v:?")) != EOF) 386 | { 387 | switch (option_char) 388 | { 389 | case 'p': proc_type = optarg; break; 390 | case 'o': out_dir = optarg; break; 391 | case 'b': interval_beg = atoi(optarg); break; 392 | case 'e': interval_end = atoi(optarg); break; 393 | case 'v': visualize = atoi(optarg); break; 394 | case 'm': output_mm = optarg; break; 395 | case 'h': cout << help; return 0; break; 396 | case '?': fprintf(stderr, "Unknown option.\nUSAGE: %s %s\n", argv[0], usage); return -1; break; 397 | } 398 | } 399 | 400 | // Retrieve the (non-option) argument 401 | if ( (argc <= 1) || (argv[argc-1] == NULL) || (argv[argc-1][0] == '-') ) 402 | { 403 | fprintf(stderr, "No input name provided.\nUSAGE: %s %s\n", argv[0], usage); 404 | return -1; 405 | } 406 | else 407 | { 408 | input_name = argv[argc-1]; 409 | } 410 | 411 | if(out_dir.compare("") != 0) 412 | { 413 | if(out_dir[out_dir.length()-1]!= '/') { out_dir = out_dir + "/"; } //and if last char not / 414 | char cmd[200]; 415 | sprintf(cmd, "mkdir -p %s", out_dir.c_str()); 416 | system(cmd); 417 | } 418 | if(output_mm.compare("") == 0) 419 | { 420 | output_mm = out_dir+basename(input_name.c_str())+"_minmax.txt"; 421 | } 422 | 423 | // Declare useful variables 424 | Mat frame0, frame1; 425 | char name[200]; 426 | vector > mm; 427 | 428 | // VIDEO INPUT 429 | if(proc_type.compare("gpu") == 0) 430 | { 431 | cout << "Extracting flow from [" << input_name << "] using GPU." << endl; 432 | // Solve the bug of allocating gpu memory after cap.read 433 | cout << "Initialization (this may take awhile)..." << endl; 434 | GpuMat temp = GpuMat(3, 3, CV_32FC1); 435 | // Declare gpu mats 436 | GpuMat g_frame0, g_frame1; 437 | GpuMat gf_frame0, gf_frame1; 438 | GpuMat g_flow; 439 | // Create optical flow object 440 | Ptr brox = cuda::BroxOpticalFlow::create(0.197f, 50.0f, 0.8f, 10, 77, 10); 441 | 442 | // Open video file 443 | VideoCapture cap(input_name); 444 | double cap_fourcc = cap.get(CV_CAP_PROP_FOURCC); 445 | if(cap.isOpened()) 446 | { 447 | int noFrames = cap.get(CV_CAP_PROP_FRAME_COUNT); //get the frame count 448 | if(interval_end == -1) 449 | { 450 | interval_end = noFrames; 451 | } 452 | string outname = basename(input_name.c_str()); 453 | if(cap_fourcc == 0) // image sequence 454 | { 455 | outname = string(outname).substr(0, outname.length()-9); 456 | output_mm = out_dir+outname+"_minmax.txt"; 457 | } 458 | cout << "Total number of frames: " << noFrames << endl; 459 | cout << "Extracting interval [" << interval_beg << "-" << interval_end << "]" << endl; 460 | cap.set(CV_CAP_PROP_POS_FRAMES, interval_beg-1); // causes problem for image sequence! 461 | 462 | // Read first frame 463 | if(noFrames>0) 464 | { 465 | bool bSuccess = cap.read(frame0); 466 | if(!bSuccess) { cout << "Cannot read frame!" << endl; } 467 | else { cvtColor(frame0, frame0, CV_BGR2GRAY); } 468 | } 469 | // For each frame in video (starting from the 2nd) 470 | for(int k=1; kcalc(gf_frame0, gf_frame1, g_flow); 496 | vector mm_frame = writeFlowJpg(name, g_flow); 497 | if(visualize) 498 | { 499 | showFlow("Flow", g_flow); 500 | waitKey(30); 501 | } 502 | 503 | mm.push_back(mm_frame); 504 | frame1.copyTo(frame0); 505 | } 506 | } 507 | cout << "Outputting " << output_mm << endl; 508 | writeMM(output_mm, mm); 509 | cap.release(); 510 | } 511 | else 512 | { 513 | cout << "Video " << input_name << " cannot be opened." << endl; 514 | } 515 | } 516 | else if(proc_type.compare("cpu") == 0) 517 | { 518 | cout << "Extracting flow from [" << input_name << "] using CPU." << endl; 519 | 520 | Ptr tvl1 = createOptFlow_DualTVL1(); 521 | VideoCapture cap(input_name); 522 | if(cap.isOpened()) 523 | { 524 | int noFrames = cap.get(CV_CAP_PROP_FRAME_COUNT); 525 | if(interval_end == -1) 526 | { 527 | interval_end = noFrames; 528 | } 529 | cout << "Total number of frames: " << noFrames << endl; 530 | cout << "Extracting interval [" << interval_beg << "-" << interval_end << "]" << endl; 531 | cap.set(CV_CAP_PROP_POS_FRAMES, interval_beg-1); 532 | // Read first frame 533 | if(noFrames>0) 534 | { 535 | bool bSuccess = cap.read(frame0); 536 | if(!bSuccess) { cout << "Cannot read frame!" << endl; } 537 | else { cvtColor(frame0, frame0, CV_BGR2GRAY); } 538 | } 539 | // For each frame in video (starting from the 2nd) 540 | for(int k=1; kcalc(frame0, frame1, flow); 559 | 560 | calcOpticalFlowFarneback(frame0, frame1, flow, 0.5, 3, 3, 3, 5, 1.1, 0); 561 | vector mm_frame = writeFlowJpg(name, flow); 562 | 563 | if(visualize) 564 | { 565 | showFlow("Flow", flow); 566 | waitKey(30); 567 | } 568 | 569 | mm.push_back(mm_frame); 570 | frame1.copyTo(frame0); 571 | 572 | } 573 | } 574 | cout << "Outputting " << output_mm << endl; 575 | writeMM(output_mm, mm); 576 | cap.release(); 577 | } 578 | else 579 | { 580 | cout << "Video " << input_name << " cannot be opened." << endl; 581 | } 582 | } 583 | return 0; 584 | } 585 | --------------------------------------------------------------------------------