1 00:00:07,417 --> 00:00:10,560 The next library we are going to look at is called Kraken. 2 00:00:10,560 --> 00:00:14,090 Which was developed by the University of PSL in Paris, 3 00:00:14,090 --> 00:00:18,090 it's actually based on a slightly older code base Okrapus. 4 00:00:18,090 --> 00:00:22,610 And you could see how the flexible open source licences allow new ideas to grow 5 00:00:22,610 --> 00:00:24,840 by building upon older ideas. 6 00:00:24,840 --> 00:00:28,626 And in this case, I fully support the idea that the Kraken, 7 00:00:28,626 --> 00:00:33,568 a mythical massive sea creature, is the natural progression of an octopus. 8 00:00:35,052 --> 00:00:37,024 What we're going to use Kraken for 9 00:00:37,024 --> 00:00:41,060 is to detect lines of text as bounding boxes in a given image. 10 00:00:41,060 --> 00:00:46,050 The biggest limitation of Tesseract is the lack of a layout engine inside of it. 11 00:00:46,050 --> 00:00:49,340 Tesseract expects to be using fairly clean text, 12 00:00:49,340 --> 00:00:52,980 it gets confused if we don't crop out other artifacts. 13 00:00:52,980 --> 00:00:57,680 It's not bad, but Kraken can help us by segmenting pages, let's take a look. 14 00:00:58,890 --> 00:01:01,950 First we'll take a look at the Kraken module itself, so 15 00:01:01,950 --> 00:01:05,060 import Kraken and let's run help on Kraken. 16 00:01:06,580 --> 00:01:08,823 So there isn't much of a discussion here, but 17 00:01:08,823 --> 00:01:11,710 there are a number of sub-modules that look interesting. 18 00:01:11,710 --> 00:01:16,154 I spent a bit of time on their website and I think the pageseg module, which handles 19 00:01:16,154 --> 00:01:20,970 all of the page segmentation, is the one we want to use, let's look at it. 20 00:01:20,970 --> 00:01:26,095 So from Kraken, we'll import pageseg and then help on pageseg. 21 00:01:28,840 --> 00:01:32,212 So it looks like there are a few different functions that we can call and 22 00:01:32,212 --> 00:01:35,720 the segment function looks particularly appropriate. 23 00:01:35,720 --> 00:01:39,740 I love how expressive this library is on the documentation front, 24 00:01:39,740 --> 00:01:43,780 I can see immediately that we're working with pill.image files. 25 00:01:43,780 --> 00:01:48,100 And the author is even indicated that we need to pass in either a binarised 26 00:01:48,100 --> 00:01:52,415 example one, or grayscale example L for luminance image. 27 00:01:52,415 --> 00:01:57,870 We can also see that the return value is a dictionary object with two keys, 28 00:01:57,870 --> 00:02:02,510 text direction, which will return to us a string of the direction of text. 29 00:02:02,510 --> 00:02:07,044 And boxes, which appears to be a list of tuples, where each tuple 30 00:02:07,044 --> 00:02:11,751 is a box in the original image, let's try this on the image of text. 31 00:02:11,751 --> 00:02:15,927 I have a simple bit of text in a file called two_col.png which is from 32 00:02:15,927 --> 00:02:18,600 a newspaper on campus here. 33 00:02:18,600 --> 00:02:23,018 So from pill will import images normally, and 34 00:02:23,018 --> 00:02:27,677 then image.open in read only /tocall.ping. 35 00:02:27,677 --> 00:02:32,167 Let's display the image in line, so we'll just call it display with I am, and 36 00:02:32,167 --> 00:02:37,175 let's now convert it to black and white, and segment it up into lines with Kraken. 37 00:02:37,175 --> 00:02:42,683 So for this we'll make some new variable bounding_boxes = pageseg.segment. 38 00:02:42,683 --> 00:02:48,128 And then im.convert, and we'll binarize that, sub boxes. 39 00:02:48,128 --> 00:02:52,377 And let's print those lines to the screen, so I'll just print bounding_boxes. 40 00:02:54,803 --> 00:02:58,460 All right, so we se e the image here, and we see the bounding boxes. 41 00:02:59,880 --> 00:03:03,379 Okay, so pretty simple, two column text and then a list of lists, 42 00:03:03,379 --> 00:03:06,268 which are the bounding boxes of the lines of that text. 43 00:03:06,268 --> 00:03:10,305 Let's write a little routine to try and see the effects a bit more clearly, 44 00:03:10,305 --> 00:03:13,955 I'm going to clean up my act a bit and write real documentation too, 45 00:03:13,955 --> 00:03:15,190 it's good practice. 46 00:03:16,230 --> 00:03:21,020 So def show boxes, we'll call it, and we'll take in a parameter image. 47 00:03:21,020 --> 00:03:24,490 So the docs, I say, modifies the past image to show a series of 48 00:03:24,490 --> 00:03:27,950 bounding boxes on an image as run by Kraken. 49 00:03:27,950 --> 00:03:31,130 Our parameter image is PIL.Image object, 50 00:03:31,130 --> 00:03:34,360 that makes it easier for other people to use this function. 51 00:03:34,360 --> 00:03:40,312 And our return is also going to be an image, the modified PIL.Image object. 52 00:03:40,312 --> 00:03:45,920 Okay, let's bring in our ImageDraw object first, so from PIL import ImageDraw. 53 00:03:45,920 --> 00:03:48,243 And this was covered in our earlier lecturers, 54 00:03:48,243 --> 00:03:50,170 you can go back if you're interested. 55 00:03:50,170 --> 00:03:53,300 And let's grab a drawing object to annotate that image. 56 00:03:53,300 --> 00:03:56,800 So we'll create a new variable, drawing object, imagedraw.draw and 57 00:03:56,800 --> 00:03:59,420 we'll pass in the image that we want to be able to draw in. 58 00:04:00,440 --> 00:04:04,742 We can the create a set of boxes using the page seg.segment. 59 00:04:04,742 --> 00:04:09,510 So bounding boxes = pageseg.segment, we'll convert our image. 60 00:04:09,510 --> 00:04:13,610 Remember we have to binarise our luminance as sub boxes, and 61 00:04:13,610 --> 00:04:16,950 now, let's go through that list of bounding boxes. 62 00:04:16,950 --> 00:04:21,272 So for box and boxes, we're just going to draw a nice rectangle. 63 00:04:21,272 --> 00:04:25,845 So drawing_object.rectangle, we'll give the box we're interested in, 64 00:04:25,845 --> 00:04:28,693 we'll set the fill to none in the outline to red. 65 00:04:28,693 --> 00:04:32,550 And I'll make it easy, we're just going to return that image object, so return img. 66 00:04:34,460 --> 00:04:36,860 To test this, let's use display, so here, 67 00:04:36,860 --> 00:04:41,910 display, then show box is then we'll read in the image, image.open. 68 00:04:41,910 --> 00:04:43,834 We could, of course, reuse the image, but 69 00:04:43,834 --> 00:04:46,542 this is good practice when you're using Jupiter notebooks. 70 00:04:49,990 --> 00:04:53,485 All right, so we see our image here with a bunch of red boxes. 71 00:04:55,651 --> 00:04:58,733 So not bad at all, it's interesting to see that Kraken isn't 72 00:04:58,733 --> 00:05:02,270 completely sure what to do with this two column format. 73 00:05:02,270 --> 00:05:06,488 In some cases Kraken has identified a line in just a single column. 74 00:05:06,488 --> 00:05:10,380 While in other cases Kraken expand the line marker all the way across the page. 75 00:05:11,420 --> 00:05:14,772 This is matter, well it really depends on that goal, 76 00:05:14,772 --> 00:05:18,575 in this case I want to see if we can improve on this a little bit. 77 00:05:18,575 --> 00:05:20,687 So we're going to go a bit of script here, 78 00:05:20,687 --> 00:05:23,360 while this week of lectures is about libraries. 79 00:05:23,360 --> 00:05:26,640 The goal of this last goal is actually to give you confidence that 80 00:05:26,640 --> 00:05:30,320 you can apply your knowledge to actual programming tasks. 81 00:05:30,320 --> 00:05:34,436 Even if the library you're using doesn't quite do what you want. 82 00:05:34,436 --> 00:05:37,884 I'd like to pause the video for a moment and collect your thoughts. 83 00:05:37,884 --> 00:05:41,980 Looking at the image above with the two column example and red boxes. 84 00:05:41,980 --> 00:05:46,540 How do you think we might modify this image to improve cracklings 85 00:05:46,540 --> 00:05:48,405 ability to detect lines? 86 00:05:51,957 --> 00:05:55,480 So thanks for sharing your thoughts, I'm looking forward to seeing the breadth of 87 00:05:55,480 --> 00:05:58,060 ideas that everyone on the course comes up with. 88 00:05:58,060 --> 00:06:00,050 Here's my partial solution, 89 00:06:00,050 --> 00:06:03,890 when looking through the Kraken docs on the page seg function, I saw that there 90 00:06:03,890 --> 00:06:08,490 were a few parameters we can supply in order to improve the segmentation. 91 00:06:08,490 --> 00:06:12,980 One of these is the black call seps parameter, if set to true, 92 00:06:12,980 --> 00:06:17,560 Kraken will assume that the columns will be separated by black lines. 93 00:06:17,560 --> 00:06:18,980 This isn't our case here, 94 00:06:18,980 --> 00:06:23,070 but I think we can have all of the tools that we need to go through and 95 00:06:23,070 --> 00:06:27,140 actually change the source image, to have a black separator between columns. 96 00:06:28,460 --> 00:06:32,460 So the first step is what I want to update the show boxes function. 97 00:06:32,460 --> 00:06:35,542 I'm going to just do a quick copy and past from above but 98 00:06:35,542 --> 00:06:38,099 adding the black call sep = true parameter. 99 00:06:44,990 --> 00:06:47,789 Okay, the next step is to think of the algorithms 100 00:06:47,789 --> 00:06:51,082 that we want to apply to detect a white column separator. 101 00:06:51,082 --> 00:06:55,431 In experimenting a bit, I decided that I only wanted to add the separator if 102 00:06:55,431 --> 00:07:00,262 the space was at least 25 pixels wide, which is roughly the width of a character, 103 00:07:00,262 --> 00:07:02,210 and six lines high. 104 00:07:02,210 --> 00:07:07,150 The width is easy, let's just make a variable, so char_width = 25. 105 00:07:07,150 --> 00:07:10,800 The height is harder since it depends on the height of the text. 106 00:07:10,800 --> 00:07:14,798 I'm going to write a routine to calculate the average height of a line. 107 00:07:14,798 --> 00:07:19,220 So def calculate_line_height, and we'll pass in the img. 108 00:07:19,220 --> 00:07:23,800 So a docs for this calculates the average height of a line form a given image. 109 00:07:23,800 --> 00:07:26,480 And we'll take a pll.image object, 110 00:07:26,480 --> 00:07:29,590 and we'll return the average height of the line in pixels. 111 00:07:31,270 --> 00:07:34,720 Let's get a list of the bounding boxes for this image, 112 00:07:34,720 --> 00:07:38,028 so we'll convert this using page_seg.segment. 113 00:07:38,028 --> 00:07:42,323 Remember, binarize always, and we just want to pull out the boxes. 114 00:07:42,323 --> 00:07:46,038 Each box is a tuple of top, left, bottom, right, so 115 00:07:46,038 --> 00:07:49,310 the height is just top minus bottom. 116 00:07:49,310 --> 00:07:53,140 So let's just calculate this over the set of all boxes, so 117 00:07:53,140 --> 00:07:55,940 we'll set some height accumulator to be 0. 118 00:07:55,940 --> 00:07:58,363 For box and bounding boxes, and 119 00:07:58,363 --> 00:08:04,213 then the heightAccumulator = heightAccumulator + box sub 3- box sub 1. 120 00:08:04,213 --> 00:08:05,583 And this is a bit tricky, 121 00:08:05,583 --> 00:08:09,270 remember that we start counting in the upper left corner in pill. 122 00:08:09,270 --> 00:08:13,299 So for those of you who are used to starting in the lower left corner, 123 00:08:13,299 --> 00:08:16,849 not true with images, we start in the upper left normally. 124 00:08:16,849 --> 00:08:19,449 Now let's just return the average height, and 125 00:08:19,449 --> 00:08:23,790 let's change it to the nearest full pixel by making it an integer. 126 00:08:23,790 --> 00:08:24,840 So we'll just return and 127 00:08:24,840 --> 00:08:28,330 we'll type cast this to an integer which will just cause rounding. 128 00:08:28,330 --> 00:08:33,762 And height accumulated divided by the number of bounding boxes. 129 00:08:33,762 --> 00:08:36,720 And let's test this with the image that we've been using up til now. 130 00:08:36,720 --> 00:08:41,489 S, line_height = calculated_line_heightofimage.openreadonl- 131 00:08:41,489 --> 00:08:45,224 y/2call.png, and we'll just print out the line height. 132 00:08:47,625 --> 00:08:51,845 Okay, so the average hight of a line is 31 pixels. 133 00:08:51,845 --> 00:08:55,985 Now we want to scan through the image looking at each pixel in turn to 134 00:08:55,985 --> 00:08:58,933 determine if there's a block of white space. 135 00:08:58,933 --> 00:09:00,965 How big of a block should we look for? 136 00:09:00,965 --> 00:09:05,455 That's a bit more of an art than science, looking at our sample image, 137 00:09:05,455 --> 00:09:09,857 I'm going to say an appropriate block should be one character width wide, and 138 00:09:09,857 --> 00:09:11,998 six line heights tall. 139 00:09:11,998 --> 00:09:15,380 But honestly, I just made this up by eyeballing the image, so 140 00:09:15,380 --> 00:09:19,080 I would encourage you to play with the values as you explore. 141 00:09:19,080 --> 00:09:23,190 Let's create a new box called gap box that represents this area. 142 00:09:23,190 --> 00:09:26,872 So gap box = 00, and then our car width, and 143 00:09:26,872 --> 00:09:31,980 then our line height times 6, let's just look at that gap box. 144 00:09:33,984 --> 00:09:37,625 It seems we'll want to have a function which, given a pixel and an image, 145 00:09:37,625 --> 00:09:41,808 can check to see if that pixel has white space to the right and below it. 146 00:09:41,808 --> 00:09:46,183 Essentially, we want to test to see if the pixel is in the upper left corner 147 00:09:46,183 --> 00:09:48,711 of something that looks like the gap box. 148 00:09:48,711 --> 00:09:52,791 If so, then we should insert a line to break up this box before sending it 149 00:09:52,791 --> 00:09:53,475 to Kraken. 150 00:09:54,830 --> 00:09:57,735 Let's call this new function Gap Check, so 151 00:09:57,735 --> 00:10:02,038 def Gap Check will pass in an image, and a location. 152 00:10:02,038 --> 00:10:06,422 So here our doc's check see image, in a given xy location, 153 00:10:06,422 --> 00:10:09,767 to see if it fits the description of a gap box. 154 00:10:09,767 --> 00:10:14,284 Our first parameters are PIL.image file, our second parameter 155 00:10:14,284 --> 00:10:19,056 location is a tuple(x.y) which is a pixel location in that image. 156 00:10:19,056 --> 00:10:21,813 So we're going to pass x and y separately, we're going to pass x and 157 00:10:21,813 --> 00:10:23,320 y together on a tuple. 158 00:10:23,320 --> 00:10:27,515 We're going to return true if that fits definition of a gap_box otherwise we'll 159 00:10:27,515 --> 00:10:28,337 return false. 160 00:10:29,980 --> 00:10:34,925 Recall that we can get a pixel using the image.getPixel function from PIL. 161 00:10:34,925 --> 00:10:39,450 It returns a value as a tuple of integers, one for each color channel. 162 00:10:39,450 --> 00:10:43,271 Our tools all work with binarized images, black and white, so 163 00:10:43,271 --> 00:10:45,083 we should just get one value. 164 00:10:45,083 --> 00:10:51,490 If the value is 0, it's a black pixel, if it's white, then the value should be 255. 165 00:10:51,490 --> 00:10:54,870 We're going to assume that the image is in the correct mode already, 166 00:10:54,870 --> 00:10:56,660 in that it already is binarized. 167 00:10:57,980 --> 00:11:01,000 The algorithm to check our bounding box is fairly easy, 168 00:11:01,000 --> 00:11:03,090 we have a single location which is our start. 169 00:11:03,090 --> 00:11:06,440 And then we want to check all the pixels to the right of that location 170 00:11:06,440 --> 00:11:08,650 up to gap_box sub 2. 171 00:11:08,650 --> 00:11:13,174 So for x in range, location sub 0, so that's our x-value, 172 00:11:13,174 --> 00:11:17,706 to location sub 0 + gap_box sub 2, so that's our offset. 173 00:11:17,706 --> 00:11:20,325 And the height is basically the same, so 174 00:11:20,325 --> 00:11:23,413 let's iterate a y-variable to gap box sub 3. 175 00:11:23,413 --> 00:11:30,082 So for y in range location sub 1 to location sub 1 + gap box sub 3. 176 00:11:30,082 --> 00:11:34,920 We want to check if the pixel is white but only if we're still within the image. 177 00:11:34,920 --> 00:11:40,871 So if x is less than the image.width and y is less than the image.height. 178 00:11:40,871 --> 00:11:44,359 If the pixel is white, we don't want to do anything if it's black, 179 00:11:44,359 --> 00:11:46,427 we just want to finish and return false. 180 00:11:46,427 --> 00:11:51,402 So, if img.getPixel[(x,y)] != 255, 181 00:11:51,402 --> 00:11:54,069 then we'll return False. 182 00:11:54,069 --> 00:11:58,380 If we've managed to walk through the whole gap_box without finding any non-white 183 00:11:58,380 --> 00:12:00,175 pixels, then we can return True. 184 00:12:00,175 --> 00:12:02,725 This is actually a gap, so we'll just return True. 185 00:12:05,092 --> 00:12:09,030 All right, we have a function to check for a gap, called gap_check. 186 00:12:09,030 --> 00:12:11,476 What should we do once we find a gap? 187 00:12:11,476 --> 00:12:15,580 For this, let's just draw a line in the middle of it, let's create a new function. 188 00:12:15,580 --> 00:12:20,424 So def I'll call this draw_sep, and it'll take an image and a location. 189 00:12:20,424 --> 00:12:25,055 And this draws a line in the image in the middle of a gap discovered in 190 00:12:25,055 --> 00:12:27,050 the location. 191 00:12:27,050 --> 00:12:30,762 Note that this doesn't draw the line in the location, but 192 00:12:30,762 --> 00:12:34,720 draws it at the middle of the gap box starting at the location. 193 00:12:34,720 --> 00:12:37,004 So the parameter is a pill image file, and 194 00:12:37,004 --> 00:12:39,560 then our tuple xy which is the pixel location. 195 00:12:40,710 --> 00:12:43,150 So, first, let's bring in all of our drawing code. 196 00:12:43,150 --> 00:12:45,540 So from pil we'll import draw and 197 00:12:45,540 --> 00:12:49,295 create a drawing object which equals image_draw.drawimage. 198 00:12:50,480 --> 00:12:55,569 Next, let's decide what the middle means in terms of coordinates and the image. 199 00:12:55,569 --> 00:13:00,604 So x1 is = location sub 0 +, and then we'll just take 200 00:13:00,604 --> 00:13:05,764 our gap_box size x size divided by 2 and round to an int. 201 00:13:05,764 --> 00:13:09,501 And our x2 locations actually just the same thing since 202 00:13:09,501 --> 00:13:13,809 this is a one pixel vertical line, so we'll just say x2 = x1. 203 00:13:13,809 --> 00:13:17,376 Our starting y-coordinate is just the y-coordinate that was passed in which is 204 00:13:17,376 --> 00:13:18,298 the top of the box. 205 00:13:18,298 --> 00:13:21,400 So y1 = location sub 1, but 206 00:13:21,400 --> 00:13:24,780 we want our final y coordinate to be the bottom of the box. 207 00:13:24,780 --> 00:13:31,181 So y2 = y1 + the gap_box height, which is gap_box sub 3. 208 00:13:31,181 --> 00:13:35,700 And then we'll actually do the word, drawing_object.rectangle, 209 00:13:35,700 --> 00:13:39,550 we'll pass in x1, y1, x2, y2, set the fill to black. 210 00:13:39,550 --> 00:13:42,462 Here I'll set the outline to black, and 211 00:13:42,462 --> 00:13:46,497 then I'll draw some nice rule that's a vertical rule. 212 00:13:46,497 --> 00:13:49,176 And we don't have anything we need to return from this, 213 00:13:49,176 --> 00:13:51,750 because actually modify the image directly in line. 214 00:13:53,640 --> 00:13:58,589 All right, now let's try it up, this is pretty easy, we can just iterate through 215 00:13:58,589 --> 00:14:03,211 each pixel in the image check if there's a gap then insert a line if there is. 216 00:14:03,211 --> 00:14:07,533 So def process image, take an image, so we're going to take in an image of text 217 00:14:07,533 --> 00:14:10,605 and now this black vertical bars to break up columns. 218 00:14:10,605 --> 00:14:15,700 pil.imagefile both in and out, we're going to start with 219 00:14:15,700 --> 00:14:22,610 a familiar iteration process, so for x in range width and for y in range height. 220 00:14:22,610 --> 00:14:25,910 I'm going to check to see if there's a gap at this point. 221 00:14:25,910 --> 00:14:29,710 So if gap check sub, or sorry, if gap_check, and 222 00:14:29,710 --> 00:14:34,530 then we'll pass it the image and the tupple is True, 223 00:14:34,530 --> 00:14:37,850 then we're going to update the image and draw a separator in it. 224 00:14:37,850 --> 00:14:40,195 So then we'll just call our draw sep_imagexy. 225 00:14:41,580 --> 00:14:45,272 And for good measure we'll return the image we modified, so return image. 226 00:14:46,637 --> 00:14:50,358 All right, let's test it out, so let's read in our test image and 227 00:14:50,358 --> 00:14:53,470 convert it through the binarization process. 228 00:14:53,470 --> 00:14:58,842 So i is our new image, we'll read this in and we'll convert it to luminance here. 229 00:14:58,842 --> 00:15:01,116 And then we're going to call process_image, and 230 00:15:01,116 --> 00:15:03,897 then since we returned it, we're going to display_image. 231 00:15:06,775 --> 00:15:11,380 Now, you can notice immediately that this function didn't return right away. 232 00:15:11,380 --> 00:15:14,580 In fact, you're sitting there kind of wondering what's happening, 233 00:15:14,580 --> 00:15:17,135 and you can see the asterisk and the margin in Jupyter. 234 00:15:17,135 --> 00:15:21,250 And that tells you that the back-end processor is still working, 235 00:15:21,250 --> 00:15:26,700 this will actually take a fair bit of time on the course era system. 236 00:15:26,700 --> 00:15:31,860 And so, reflect a little bit on what's happening in the code that we've written. 237 00:15:31,860 --> 00:15:37,093 We're iterating over every pixel in the image both through the x and y directions. 238 00:15:37,093 --> 00:15:41,623 And we're looking to see if that there's a gap marks to the right and 239 00:15:41,623 --> 00:15:44,530 to the lower side of that pixel. 240 00:15:44,530 --> 00:15:47,970 Now we're going to try and draw a line if there is, and 241 00:15:47,970 --> 00:15:50,410 then we just go immediately to the next pixel. 242 00:15:50,410 --> 00:15:53,867 So you can see there's lots of opportunities for 243 00:15:53,867 --> 00:15:55,898 optimization of this code. 244 00:15:55,898 --> 00:15:59,928 And it's really meant to be a demonstration of what you can do yourself 245 00:15:59,928 --> 00:16:02,465 when you start combining these libraries. 246 00:16:05,198 --> 00:16:09,816 We're going to use the magic of video to speed this up a little bit for you, for 247 00:16:09,816 --> 00:16:11,070 the video lecture. 248 00:16:11,070 --> 00:16:15,436 But if you're following in the Jupiter notebooks and I hope that you are, 249 00:16:15,436 --> 00:16:20,620 please think about how you might change that to modify the image, not bad at all. 250 00:16:20,620 --> 00:16:25,230 The effect of the bottom of the image is bit unexpected to me but it makes sense. 251 00:16:25,230 --> 00:16:28,185 You can imagine that there's several ways we might try and 252 00:16:28,185 --> 00:16:31,803 control for this, but let's see how this new image works when we try and 253 00:16:31,803 --> 00:16:33,928 run it through the Kraken layout engine. 254 00:16:33,928 --> 00:16:38,540 So we'll say displayshow_boxes, and because we stored i it makes it easy. 255 00:16:44,293 --> 00:16:48,521 So it looks like that's actually pretty accurate and fixes the problems we faced. 256 00:16:48,521 --> 00:16:51,557 Feel free to experiment with different settings for 257 00:16:51,557 --> 00:16:54,462 the gap heights and width and share on the forums. 258 00:16:54,462 --> 00:16:57,920 You'll notice through this method it's really quite slow, 259 00:16:57,920 --> 00:17:02,030 which is a bit of a problem if we wanted to use this on larger text. 260 00:17:02,030 --> 00:17:05,170 But I wanted you to see how you could mix your own logic and 261 00:17:05,170 --> 00:17:07,750 work with libraries you're using. 262 00:17:07,750 --> 00:17:11,175 Just because CrackIt didn't work perfectly it doesn't 263 00:17:11,175 --> 00:17:15,537 mean we can't build something more specific to our use case on top of it. 264 00:17:15,537 --> 00:17:17,444 I want to end this lecture with a pause, and 265 00:17:17,444 --> 00:17:20,460 ask you reflect on the code we've written here. 266 00:17:20,460 --> 00:17:24,720 We started this course with some pretty simple use of libraries, but now we're 267 00:17:24,720 --> 00:17:28,500 digging in deeper and solving problems ourselves with the help of libraries. 268 00:17:29,610 --> 00:17:32,873 Before we go to our last library, how well prepared do 269 00:17:32,873 --> 00:17:36,730 you think you are to take your Python skills out into the world?