├── .gitignore ├── README.md ├── crosshatched.cpp ├── examples ├── emily.gif ├── goldengate.png ├── matrix.gif ├── obama.png └── thailand.jpg ├── makefile └── sources ├── goldengate.jpg ├── obama.jpg ├── pipe.jpg ├── sagrada.JPG └── thailand.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.mp4 3 | *.m4a 4 | *.so 5 | *.mov 6 | /*.png 7 | a.out 8 | out\.* 9 | crosshatched 10 | historical/* 11 | www/* 12 | examples/ignore/* 13 | sources/ignore/* 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### Use OpenCV to create crosshatch-style drawings and videos. 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #### Try out a canvas-based implementation at https://www.sketchify.me 10 | 11 | #### To use: 12 | ``` 13 | $ make 14 | $ ./crosshatched sources/goldengate.jpg 15 | $ ./crosshatched --laplacian sources/thailand.jpg 16 | $ ./crosshatched path/to/a/video.mp4 17 | ``` 18 | 19 | This script is optimized for images and videos >= 1080 pixels wide. You can also 20 | get decent results out of smaller images by tweaking the constants although 21 | bigger seems to be better in most cases. 22 | 23 | There are a bunch more flags too worth playing around with 24 | 25 | #### The algorithm: 26 | - Calculate the gradient of the image 27 | - Draw short, connected bezier lines parallel and perpendicular to that gradient 28 | - Generate the "edge gradient" from either the Laplacian or Canny (default) algorithms 29 | - Using slightly tweaked rules, draw more bezier lines parallel to the edge gradient 30 | 31 | #### Dependencies: 32 | - g++ (Part of the GNU Compiler Collection) 33 | - OpenCV 34 | - Tons on other stuff (install everything one error at a time) 35 | 36 | #### TODO: 37 | I'd love to pipe this through a neural-network style-transfer first to get something really artistic 38 | 39 | #### A note: 40 | I'm generally a Python programmer so please forgive my shitty C++ code. I'm also not at work so I'm not keeping my code clean 41 | 42 | -------------------------------------------------------------------------------- /crosshatched.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | This will take an input image and stylize it to make it look like a crosshatch 3 | drawing. 4 | 5 | Opencv is the big requirement here and you will need to install that. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include "opencv2/highgui/highgui.hpp" 18 | #include "opencv2/imgproc/imgproc.hpp" 19 | #include "opencv2/core/core.hpp" 20 | #include "opencv2/video/video.hpp" 21 | 22 | using namespace std; 23 | using namespace cv; 24 | using namespace std::chrono; 25 | 26 | 27 | #define BLACK 0 28 | #define WHITE 255 29 | #define PI 3.1415926 30 | #define OUT_VIDEO_FILENAME "out.mp4" 31 | #define WINDOW_NAME "window" 32 | #define DEBUG_WINDOW_WIDTH 700 33 | 34 | int SOBEL_KERNEL = 3; 35 | int BLUR_SIZE = 20; 36 | int BLOCK_SIZE_THRESHOLD = 35; 37 | int LINE_LEN = 5; 38 | int CANNY_LOWER = 80; 39 | int CANNY_UPPER = 250; 40 | int LAPLACE = 7; 41 | int CANNY_GAUSS_BLUR = 1; 42 | int PERPENDICULAR_TOLERANCE = 15; 43 | int GRADIENT_TOLERANCE = 30; 44 | int CANNY_TOLERANCE = 50; 45 | 46 | int GRAD_LIMIT = 4; 47 | int EDGE_LIMIT = 8; 48 | int CANNY_DIV = 2; 49 | int WHITE_THRESHOLD = 180; 50 | int NORMALIZE_BRIGHTNESS_TO = 140; 51 | int PERP_LESS_THRESHOLD = 50; 52 | 53 | int num_strokes = 0; 54 | 55 | mutex g_pages_mutex; 56 | 57 | struct Flags { 58 | bool laplacian = false; 59 | bool noequalize = false; 60 | bool debug = false; 61 | bool invert = false; 62 | bool noedges = false; 63 | bool nofill = false; 64 | bool strongcorners = false; 65 | bool crayolaize = false; 66 | bool saturate = false; 67 | bool threshold = false; 68 | bool bw = false; 69 | bool strokes = false; 70 | int resize_width = 2000; 71 | int line_density = 5; 72 | int brighten = 0; 73 | string out_filename = "out.png"; 74 | string edges_filename = ""; 75 | string edges_mask_filename = ""; 76 | string fill_filename = ""; 77 | string color_filename = ""; 78 | }; 79 | 80 | int get_int(Mat img, Point p) { 81 | return img.at(p.y, p.x); 82 | } 83 | 84 | float get_fl(Mat img, Point p) { 85 | return img.at(p.y, p.x); 86 | } 87 | 88 | Vec3b get_color(Mat img, Point p) { 89 | return img.at(p.y, p.x); 90 | } 91 | 92 | void set_color(Mat img, Point p, Vec3b color) { 93 | img.at(p.y, p.x) = color; 94 | } 95 | 96 | void set_int(Mat img, Point p, int val) { 97 | img.at(p.y, p.x) = val; 98 | } 99 | 100 | float vect_len(Point pnt) { 101 | return sqrt((float)(pnt.x*pnt.x + pnt.y*pnt.y)); 102 | } 103 | 104 | float dot(Point a, Point b) { 105 | // dot product 106 | return a.x*b.x + a.y*b.y; 107 | } 108 | 109 | float rads_btwn_vectors(Point a, Point b) { 110 | // shortest distance angle between two vectors (so max == PI) 111 | return acos(dot(a, b)/(vect_len(a)*vect_len(b))); 112 | } 113 | 114 | Mat zeroes_int(Mat img) { 115 | return img.zeros(img.rows, img.cols, CV_8UC1); 116 | } 117 | 118 | Mat white_int(Mat img) { 119 | Mat ret = zeroes_int(img); 120 | ret.setTo(255); 121 | return ret; 122 | } 123 | 124 | Mat zeroes_fl(Mat img) { 125 | return img.zeros(img.rows, img.cols, CV_32FC1); 126 | } 127 | 128 | Mat white_fl(Mat img) { 129 | Mat ret = zeroes_fl(img); 130 | ret.setTo(Scalar(255,255,255)); 131 | return ret; 132 | } 133 | 134 | float rand_float() { 135 | // returns a float between 0 and 1 136 | return (float)rand()/RAND_MAX; 137 | } 138 | 139 | int tim() { 140 | milliseconds ms = duration_cast< milliseconds >(system_clock::now().time_since_epoch()); 141 | return ms.count(); 142 | } 143 | 144 | void wait() { 145 | while (waitKey(30) < 0); 146 | } 147 | 148 | void show_and_wait(Mat img) { 149 | // show image and wait for keyboard input 150 | imshow(WINDOW_NAME, img); 151 | wait(); 152 | } 153 | 154 | 155 | vector crayola(24); 156 | 157 | void setup_colors() { 158 | crayola.push_back(Vec3b(237, 10, 63)); // "#ED0A3F" //Red 159 | crayola.push_back(Vec3b(255, 134, 31)); // #FF3F34 // Red Orange (255, 63, 52) 160 | crayola.push_back(Vec3b(255, 134, 31)); // #FF861F // Orange 161 | crayola.push_back(Vec3b(251, 232, 112)); // #FBE870 // Yellow 162 | crayola.push_back(Vec3b(197, 225, 122)); // #C5E17A // Yellow Green 163 | crayola.push_back(Vec3b(1, 163, 104)); // #01A368 // Green 164 | crayola.push_back(Vec3b(118, 215, 234)); // #76D7EA // Sky Blue 165 | crayola.push_back(Vec3b(0, 102, 255)); //0066FF Blue 166 | crayola.push_back(Vec3b(131, 89, 163)); // #8359A3 // Violet (Purple) 167 | crayola.push_back(Vec3b(175, 89, 62)); // #AF593E // Brown 168 | crayola.push_back(Vec3b(0, 0, 0)); // #000000 //Black 169 | crayola.push_back(Vec3b(255, 255, 255)); // #FFFFFF // White 170 | crayola.push_back(Vec3b(3, 187, 133)); // Aqua Green #03BB85 171 | crayola.push_back(Vec3b(255, 223, 0)); // Golden Yellow #FFDF00 172 | crayola.push_back(Vec3b(139, 134, 128)); // Gray #8B8680 173 | crayola.push_back(Vec3b(10, 107, 13)); // Jade Green #0A6B0D 174 | crayola.push_back(Vec3b(143, 216, 216)); // Light Blue #8FD8D8 175 | crayola.push_back(Vec3b(246, 83, 166)); // Magenta #F653A6 176 | crayola.push_back(Vec3b(202, 52, 53)); // Mahogany #CA3435 177 | crayola.push_back(Vec3b(255, 203, 164)); // Peach #FFCBA4 178 | crayola.push_back(Vec3b(205, 145, 158)); // Pink #CD919E 179 | crayola.push_back(Vec3b(250, 157, 90)); // Tan #FA9D5A 180 | crayola.push_back(Vec3b(163, 111, 64)); // Light Brown #A36F40 181 | crayola.push_back(Vec3b(255, 174, 66)); // Yellow Orange #FFAE42 182 | 183 | for (int i=0; i 128) 196 | val = 256-val; 197 | if (val < best_val) { 198 | best_idx = i; 199 | best_val = val; 200 | } 201 | 202 | } 203 | 204 | return crayola[best_idx]; 205 | } 206 | 207 | void crayolaize(Mat color_img, Flags f) { 208 | cvtColor(color_img, color_img, CV_RGB2HSV); 209 | vector channels(3); 210 | split(color_img, channels); 211 | 212 | Vec3b red = Vec3b(251, 232, 112); 213 | 214 | for (int j=0; j 255) 219 | color[0] = 255; 220 | color[2] = (color[2]/50)*50; // 5 shades 221 | if (f.saturate) { 222 | if (color[1] > 128 && color[2] < 128) { 223 | int diff = 255 - color[1]; 224 | color[1] = 255; 225 | color[2] += diff; 226 | 227 | } 228 | } 229 | set_color(color_img, Point(i,j), color); 230 | } 231 | } 232 | cvtColor(color_img, color_img, CV_HSV2RGB); 233 | } 234 | 235 | Mat apply_threshold(Mat bw) { 236 | Mat mask = zeroes_int(bw); 237 | Mat ret = white_int(bw); 238 | adaptiveThreshold(bw, mask, 255, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, bw.cols/10+1, 0); 239 | bw.copyTo(ret, mask); 240 | return ret; 241 | } 242 | 243 | Mat compute_gradient_mask(Mat img, bool is_edge) { 244 | // Returns an image of gradient angles. 245 | // Edges have smaller convolution kernels because they need to be more detailed 246 | // Non-edges use integer rather than float matricies to reduce distracting swirling 247 | // in the image and, as a side effect, speed up the program. 248 | 249 | int sobel_kernel_1; 250 | Size blur_size_1; 251 | 252 | Mat mask = zeroes_fl(img); 253 | Mat sobelx, sobely; 254 | int datatype; 255 | 256 | medianBlur(img, img, 3); 257 | 258 | if (is_edge) { 259 | sobel_kernel_1 = 7; 260 | blur_size_1 = Size(3, 3); 261 | sobelx = zeroes_fl(img); 262 | sobely = zeroes_fl(img); 263 | datatype = CV_32FC1; 264 | } 265 | else { 266 | sobel_kernel_1 = SOBEL_KERNEL*2+1; 267 | blur_size_1 = Size(BLUR_SIZE*2+1, BLUR_SIZE*2+1); 268 | sobelx = zeroes_int(img); 269 | sobely = zeroes_int(img); 270 | datatype = CV_8UC1; 271 | } 272 | 273 | Sobel(img, sobelx, datatype, 1, 0, sobel_kernel_1); 274 | Sobel(img, sobely, datatype, 0, 1, sobel_kernel_1); 275 | 276 | boxFilter(sobelx, sobelx, datatype, blur_size_1); 277 | boxFilter(sobely, sobely, datatype, blur_size_1); 278 | 279 | for (int j=0; j(j,i) = atan2(sobely.at(j,i), sobelx.at(j,i)); 283 | else 284 | mask.at(j,i) = atan2(sobely.at(j,i), sobelx.at(j,i)); 285 | 286 | if (mask.at(j,i) == 0) 287 | mask.at(j,i) = PI/2 + (rand_float()-1)/2; 288 | } 289 | } 290 | return mask; 291 | } 292 | 293 | Point gradient_vector(float grad_val) { 294 | int dx = (int)(round(cos(grad_val)*LINE_LEN)); 295 | int dy = (int)(round(sin(grad_val)*LINE_LEN)); 296 | return Point(dx, dy); 297 | } 298 | 299 | Point perpendicular_vector(float grad_val) { 300 | int dx = (int)(round(sin(grad_val)*-1*LINE_LEN)); 301 | int dy = (int)(round(cos(grad_val)*LINE_LEN)); 302 | return Point(dx, dy); 303 | 304 | } 305 | 306 | Point next_point(Mat grad, Point prev, bool is_edge, bool is_perpendicular) { 307 | // depending on whether we want a point perpendicular or parallel to this 308 | // one, find it, check the range, and return it 309 | Point nxt; 310 | float grad_val = get_fl(grad, prev); 311 | if (is_edge || is_perpendicular) 312 | nxt = perpendicular_vector(grad_val); 313 | else 314 | nxt = gradient_vector(grad_val); 315 | nxt += prev; 316 | if (nxt.x<0 || nxt.x>=grad.cols || nxt.y<0 || nxt.y>=grad.rows) 317 | throw exception(); 318 | return nxt; 319 | } 320 | 321 | void draw_stroke_on_img(vector stroke, Mat color, Mat ret, Flags f) { 322 | Point cur, nxt; 323 | cur = stroke[0]; 324 | Scalar the_color = Scalar(0,0,0); 325 | for (int j=1; j> all_strokes; 346 | bool is_edge; 347 | int line_density; 348 | Flags f; 349 | // vector< vector > &strokes; 350 | 351 | public: 352 | Parallel_process(Mat _img, Mat _grad, Mat _color, Mat &_ret, bool _is_edge, int _line_density, vector> _all_strokes, Flags _f) 353 | : img(_img), grad(_grad), color(_color), ret(_ret), is_edge(_is_edge), line_density(_line_density), all_strokes(_all_strokes), f(_f){} 354 | 355 | virtual void operator()(const Range& range) const { 356 | for(int _y = range.start; _y < range.end; _y++) { 357 | for(int x = 0; x < img.cols; x+=line_density) { 358 | int y = _y*line_density; 359 | // printf("%d,%d\n", x, y); 360 | Point p0, p1, p2, dxdy, o_dxdy; 361 | bool is_perpendicular = rand()%2 == 0; 362 | 363 | int original_val = get_int(img, Point(x, y)); 364 | if (original_val > WHITE_THRESHOLD+(is_perpendicular-1)*PERP_LESS_THRESHOLD || 365 | rand_float() < original_val/255.0) 366 | continue; 367 | 368 | if (f.strongcorners) { 369 | int middle_x = img.cols/2; 370 | int middle_y = img.rows/2; 371 | float norm = (float)(middle_x*middle_y); 372 | if (pow((middle_y-y)*(middle_x-x)/norm,2) > pow(rand_float(), 2)) 373 | continue; 374 | } 375 | 376 | int tolerance; 377 | if (is_edge) 378 | tolerance = CANNY_TOLERANCE; 379 | else if (is_perpendicular) 380 | tolerance = PERPENDICULAR_TOLERANCE; 381 | else 382 | tolerance = GRADIENT_TOLERANCE; 383 | 384 | // float grad_val = get_fl(grad,Point(x,y); 385 | // if (is_edge || is_perpendicular) 386 | // o_dxdy = perpendicular_vector(grad_val); 387 | // else 388 | // o_dxdy = gradient_vector(grad_val); 389 | 390 | p0 = Point(x, y); 391 | // int limit = is_edge ? 10-line : GRAD_LIMIT; 392 | int limit = 10-line_density; 393 | for (int i=0; i<5; i++) { 394 | 395 | try { 396 | p1 = next_point(grad, p0, is_edge, is_perpendicular); 397 | p2 = next_point(grad, p1, is_edge, is_perpendicular); 398 | } 399 | catch (exception& e) { 400 | break; 401 | } 402 | 403 | // if (i>0 && rads_btwn_vectors(o_dxdy, dxdy) > PI/8.0) 404 | // break; 405 | 406 | vector stroke; 407 | vector> all_strokes_2; 408 | 409 | stroke.push_back(p0); 410 | for (float t=.2; t<=1.0; t+=.2) { 411 | float bez_x = p0.x*(1.-t)*(1.-t) + 2*p1.x*(1.-t)*t + p2.x*t*t; 412 | float bez_y = p0.y*(1.-t)*(1.-t) + 2*p1.y*(1.-t)*t + p2.y*t*t; 413 | stroke.push_back(Point((int)bez_x, (int)bez_y)); 414 | } 415 | 416 | // if (f.strokes) { 417 | // #pragma omp critical 418 | // all_strokes.insert(all_strokes.end(), all_strokes_2.begin(), all_strokes_2.end()) 419 | // // all_strokes.push_back(stroke); 420 | // } 421 | // else { 422 | num_strokes += 1; 423 | draw_stroke_on_img(stroke, color, ret, f); 424 | // } 425 | 426 | int val = get_int(img, p2); 427 | if (val == WHITE || original_val + tolerance < val) 428 | break; 429 | p0 = p2; 430 | } 431 | } 432 | } 433 | } 434 | }; 435 | 436 | 437 | // Apply our gradient algorithm to draw multiple lines on the image 438 | // This is the meat of this program 439 | // Given an image: 440 | // A: Calculate the gradient 441 | // B. For every "line_density" pixels 442 | // a. Randomly decide whether this will be a perpendicular or gradient line 443 | // b. If the image is white at that point or less than our random function, break 444 | // c. Extend two connected lines from that point (either along or perpendicular 445 | // to the gradient) 446 | // d. Create a bezier line from these two lines 447 | // e. If we have reached a spot that is too white (by a random function), break 448 | // f. Otherwise, continue this line 449 | 450 | Mat get_l_free(Mat img) { 451 | Mat ret = img.zeros(img.rows, img.cols, CV_8UC1); 452 | Mat lab = img.clone(); 453 | cvtColor(lab, lab, CV_BGR2HSV); 454 | Mat planes[3]; 455 | split(lab, planes); 456 | for(int y = 0; y < lab.rows; y++) { 457 | for(int x = 0; x < lab.cols; x++) { 458 | int l = get_int(planes[2], Point(x, y)); 459 | int a = get_int(planes[1], Point(x, y)); 460 | int b = get_int(planes[0], Point(x, y)); 461 | set_int(ret, Point(x, y), (a+b+l/5)/3); 462 | } 463 | } 464 | equalizeHist(ret, ret); 465 | GaussianBlur(ret, ret, Size(CANNY_GAUSS_BLUR*2+1,CANNY_GAUSS_BLUR*2+1), 5); 466 | medianBlur(ret, ret, 5); 467 | return ret; 468 | } 469 | 470 | Mat get_canny(Mat img) { 471 | // The canny edges are crisper and look more like a kid's pencil drawing 472 | // which is the goal. 473 | 474 | Mat canny = img.clone(); 475 | int height = img.rows; 476 | int width = img.cols; 477 | 478 | resize(canny, canny, Size(width/CANNY_DIV, height/CANNY_DIV)); 479 | canny = get_l_free(canny); 480 | // imwrite("lfree.png", canny); 481 | Canny(canny, canny, CANNY_LOWER, CANNY_UPPER); 482 | GaussianBlur(canny, canny, Size(CANNY_GAUSS_BLUR*2+1,CANNY_GAUSS_BLUR*2+1), 0); 483 | resize(canny, canny, Size(width, height)); 484 | // imwrite("canny.png", WHITE - canny); 485 | return WHITE - canny; 486 | } 487 | 488 | Mat get_laplacian(Mat img) { 489 | // Laplacian edges look much more realistic but a little less like a drawing 490 | 491 | Mat lap = zeroes_fl(img); 492 | Laplacian(img, lap, CV_8UC1, LAPLACE*2+1); 493 | GaussianBlur(lap, lap, Size(5,5), 0); 494 | return WHITE - lap; 495 | } 496 | 497 | // Mat draw_strokes_on_img(vector< vector > strokes, Mat color, Mat ret) { 498 | // // Strokes -> lines on image. 499 | // // Using the color of the original image is kind of cheating but the effect 500 | // // is still there 501 | 502 | // for (int i=0; i ac) ac = 0; 525 | if (ac > 255) ac = 255; 526 | if (0 > bc) bc = 0; 527 | if (bc > 255) bc = 255; 528 | if (0 > cc) cc = 0; 529 | if (cc > 255) cc = 255; 530 | set_color(img, Point(x, y), Vec3b(ac, bc, cc)); 531 | } 532 | } 533 | } 534 | 535 | 536 | void apply_contrast_equalization(Mat color) { 537 | cvtColor(color, color, CV_RGB2HSV); 538 | vector planes(3); 539 | split(color, planes); 540 | Ptr clahe = createCLAHE(); 541 | clahe->setClipLimit(4); 542 | clahe->apply(planes[2], planes[2]); 543 | 544 | int mean_brightness = (int)mean(planes[2])[0]; 545 | cout << mean_brightness << endl; 546 | int add = NORMALIZE_BRIGHTNESS_TO - mean_brightness; 547 | 548 | // for(int y = 0; y < color.rows; y++) { 549 | // for(int x = 0; x < color.cols; x++) { 550 | // int ac = get_int(planes[2], Point(x, y)) + add; 551 | // if (0 > ac) ac = 0; 552 | // if (ac > 255) ac = 255; 553 | // set_int(planes[2], Point(x, y), ac); 554 | // } 555 | // } 556 | planes[2].convertTo(planes[2], -1, 1.0, add); 557 | 558 | // TODO this is might bea a hue-shift 559 | // for(int y = 0; y < dst.rows; y++) { 560 | // for(int x = 0; x < dst.cols; x++) { 561 | // set_int(planes[2], Point(x, y), abs(get_int(planes[2], Point(x, y))-30)); 562 | // } 563 | // } 564 | 565 | merge(planes, color); 566 | cvtColor(color, color, CV_HSV2RGB); 567 | } 568 | 569 | Mat resize_to_width(Mat img, int width) { 570 | float rows_per_col = ((float)img.rows) / ((float)img.cols); 571 | resize(img, img, Size(width, (int)(width*rows_per_col))); 572 | return img; 573 | } 574 | 575 | 576 | void parallel_it(Mat bw, Mat grad, Mat color, Mat ret, bool is_edge, int line_density, vector> all_strokes, Flags f) { 577 | if (f.bw) { 578 | line_density*=3/4; 579 | } 580 | parallel_for_(Range(0, bw.rows/line_density), Parallel_process(bw, grad, color, ret, is_edge, line_density, all_strokes, f)); 581 | } 582 | 583 | 584 | Mat apply_fast_gradient(Mat color, Flags f) { 585 | // We apply the algorithm in separate ways for the edges and for the rest of 586 | // the image. This gives us thicker edges that look more like a crosshatch 587 | // drawing 588 | 589 | if (!f.noequalize) 590 | apply_contrast_equalization(color); 591 | 592 | if (f.crayolaize) { 593 | crayolaize(color, f); 594 | // imwrite("crayola.png", color); 595 | } 596 | 597 | if (f.brighten) { 598 | brighten(color, f.brighten); 599 | } 600 | 601 | color = resize_to_width(color, f.resize_width); 602 | 603 | if (f.color_filename.length()) { 604 | imwrite(f.color_filename, color); 605 | } 606 | 607 | Mat bw = color.zeros(color.rows, color.cols, CV_8UC1); 608 | cvtColor(color, bw, CV_BGR2GRAY); 609 | 610 | if (f.threshold) 611 | bw = apply_threshold(bw); 612 | 613 | int a, b; 614 | Mat grad, grad2; 615 | 616 | Mat ret = bw.zeros(color.rows, color.cols, CV_8UC3); 617 | ret.setTo(WHITE); 618 | 619 | if (f.invert) 620 | bitwise_not(bw, bw); 621 | 622 | vector> all_strokes; 623 | 624 | if (!f.noedges) { 625 | a = tim(); 626 | Mat edges = f.laplacian ? get_laplacian(bw) : get_canny(color); 627 | if (f.edges_mask_filename.length()) 628 | imwrite(f.edges_mask_filename, edges); 629 | grad = compute_gradient_mask(edges, true); 630 | if (f.edges_filename.length()){ 631 | grad.convertTo(grad2, CV_8UC1, (int)(255/6.28)); 632 | imwrite(f.edges_filename, grad2); 633 | } 634 | else{ 635 | int line_density = f.line_density/2; 636 | parallel_it(edges, grad, color, ret, true, line_density, all_strokes, f); 637 | } 638 | b = tim(); 639 | } 640 | 641 | if (!f.nofill) { 642 | a = tim(); 643 | grad = compute_gradient_mask(bw, false); 644 | if (f.fill_filename.length()){ 645 | grad.convertTo(grad2, CV_8UC1, (int)(255/6.28)); 646 | imwrite(f.fill_filename, grad2); 647 | } 648 | else{ 649 | int line_density = f.line_density+1; 650 | parallel_it(bw, grad, color, ret, false, line_density, all_strokes, f); 651 | } 652 | b = tim(); 653 | } 654 | 655 | cout << num_strokes << endl; 656 | 657 | if (f.strokes) { 658 | cout << all_strokes.size() << endl; 659 | exit(0); 660 | } 661 | 662 | return ret; 663 | } 664 | 665 | // void white_edges_corner() { 666 | // GaussianBlur(edges, edges, Size(CANNY_GAUSS_BLUR*5+1,CANNY_GAUSS_BLUR*5+1), 0); 667 | // threshold(edges, edges, 240, 255,THRESH_BINARY); 668 | // Mat colors2 = img.zeros(color.rows, color.cols, CV_8UC3); 669 | // colors2.setTo(WHITE); 670 | // imwrite("edges.png", edges); 671 | // // bitwise_and(color,color,color,edges); 672 | // color.copyTo(colors2, edges); 673 | // imwrite("mask.png", colors2); 674 | 675 | // } 676 | 677 | bool endswith(string const &fullString, string const &ending) { 678 | // string endswith 679 | if (fullString.length() >= ending.length()) 680 | return (0 == fullString.compare(fullString.length() - ending.length(), 681 | ending.length(), ending)); 682 | else 683 | return false; 684 | } 685 | 686 | void make_debug_window() { 687 | // if we want to change some constants 688 | namedWindow(WINDOW_NAME,1); 689 | createTrackbar( "BLUR_SIZE", WINDOW_NAME, &BLUR_SIZE, 30); 690 | createTrackbar( "SOBEL_KERNEL", WINDOW_NAME, &SOBEL_KERNEL, 15); 691 | createTrackbar( "BLOCK_SIZE_THRESHOLD", WINDOW_NAME, &BLOCK_SIZE_THRESHOLD, 50); 692 | createTrackbar( "LINE_LEN", WINDOW_NAME, &LINE_LEN, 15); 693 | createTrackbar( "CANNY_UPPER", WINDOW_NAME, &CANNY_UPPER, 256); 694 | createTrackbar( "CANNY_LOWER", WINDOW_NAME, &CANNY_LOWER, 256); 695 | createTrackbar( "LAPLACE", WINDOW_NAME, &LAPLACE, 30); 696 | createTrackbar( "CANNY_TOLERANCE", WINDOW_NAME, &CANNY_TOLERANCE, 256); 697 | createTrackbar( "CANNY_GAUSS_BLUR", WINDOW_NAME, &CANNY_GAUSS_BLUR, 20); 698 | createTrackbar( "CANNY_DIV", WINDOW_NAME, &CANNY_DIV, 10); 699 | } 700 | 701 | 702 | bool handle_video(VideoCapture cap, Flags f, bool is_live) { 703 | 704 | if(!cap.isOpened()) 705 | return -1; 706 | 707 | int num_frames = cap.get(CV_CAP_PROP_FRAME_COUNT); 708 | int frame_width = cap.get(CV_CAP_PROP_FRAME_WIDTH); 709 | int frame_height = cap.get(CV_CAP_PROP_FRAME_HEIGHT); 710 | VideoWriter outputVideo(OUT_VIDEO_FILENAME, CV_FOURCC('8', 'B', 'P', 'S'), 711 | 10, Size(frame_width,frame_height), true); 712 | 713 | if (!outputVideo.isOpened()) { 714 | cout << "Could not open the output video for write" << endl; 715 | return -1; 716 | } 717 | 718 | if (f.debug) { 719 | make_debug_window(); 720 | is_live = true; 721 | } 722 | 723 | Mat color, grad, color_out, bw; 724 | 725 | cap >> color; 726 | imshow(WINDOW_NAME, color); 727 | cvSetWindowProperty(WINDOW_NAME, CV_WND_PROP_FULLSCREEN, CV_WINDOW_FULLSCREEN); 728 | 729 | queue time_queue; 730 | if (!is_live) time_queue.push(tim()); 731 | 732 | 733 | while (true) { 734 | // for (int i=0; i> frame; 744 | // cout << "frame" << endl; 745 | // } 746 | 747 | // } 748 | int a = tim(); 749 | cap >> color; 750 | if (color.empty()) { 751 | continue; 752 | } 753 | if (is_live) { 754 | // Flip along y axis for mirror effect 755 | flip(color, color, 1); 756 | } 757 | 758 | int b = tim(); 759 | 760 | 761 | grad = apply_fast_gradient(color, f); 762 | 763 | if (is_live) { 764 | a = tim(); 765 | imshow(WINDOW_NAME, grad); 766 | waitKey(1); 767 | b = tim(); 768 | } 769 | else { 770 | cvtColor(grad, color_out, CV_GRAY2BGR); // mp4 requires a color frame 771 | outputVideo.write(color_out); 772 | 773 | // int minutes_remaining = (int)round((num_frames-i)*avg_time/60.0); 774 | // cout << "frame " << i << "/" << num_frames << endl; 775 | // cout << minutes_remaining << " minutes remaining" << endl; 776 | } 777 | 778 | time_queue.push(tim()); 779 | if (time_queue.size() > 10) time_queue.pop(); 780 | long avg_time = ((time_queue.back() - time_queue.front()))/(time_queue.size()); 781 | 782 | cout << avg_time << endl; 783 | } 784 | if (!is_live) 785 | cout << "Wrote as " << OUT_VIDEO_FILENAME << endl; 786 | return 0; 787 | } 788 | 789 | 790 | void loop_debug(Mat color, Flags f) { 791 | make_debug_window(); 792 | for(;;) { 793 | Mat grad = apply_fast_gradient(color, f); 794 | imshow(WINDOW_NAME, grad); 795 | waitKey(1000); 796 | } 797 | } 798 | 799 | 800 | int handle_image(Mat color, Flags f) { 801 | if (f.debug) 802 | loop_debug(color, f); 803 | 804 | color = apply_fast_gradient(color, f); 805 | if (!f.color_filename.length()) 806 | imwrite(f.out_filename, color); 807 | cout << "Wrote as " << f.out_filename << endl; 808 | return 0; 809 | } 810 | 811 | int main(int argc, char* argv[]) { 812 | srand (time(NULL)); 813 | 814 | string in_filename = ""; 815 | Flags flags; 816 | 817 | // Loop over arguments 818 | for (int i=1; i