├── .gitignore ├── LICENSE ├── README.md ├── config.ini ├── detector ├── PlateDetector.py ├── __init__.py ├── config │ └── yolov3-plates.cfg ├── models.py ├── sort.py └── utils │ ├── __init__.py │ ├── logger.py │ ├── parse_config.py │ └── utils.py ├── http_stream.py ├── lev_weights.csv ├── reader ├── PlateReader.py ├── __init__.py ├── efficientnet.py ├── train.py ├── utils.py └── weights │ └── training.log ├── sample ├── 01.jpg ├── 02.jpg ├── KRC8D12.jpg ├── OMD6805.jpg ├── cnn.png ├── confusion_matrix.png ├── feed_example.png ├── plate01.jpeg └── sample_gif.gif ├── test.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | /rtsp_stream.py 2 | /video_parse.py 3 | /forever.py 4 | **/__pycache__ 5 | *.ipynb 6 | *.pth -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AccessALPR 2 | ### Deep Learning-based ALPR system for vehicle access control trained on brazilian license plates 3 | 4 | ![gif](sample/sample_gif.gif) 5 | AccessALPR is a semi ad-hoc system developped for robust vehicular access control via images in the wild. 6 | 7 | 1. [ Features. ](#feats) 8 | 2. [ Usage. ](#usage) 9 | 3. [ Implementation Details. ](#implementation) 10 | 4. [ System Requirements. ](#requirements) 11 | 5. [ References. ](#references) 12 | 13 | 14 | #### Features: 15 | - Frontal and angled plate location powered by YoloV3; 16 | - Plate recognition independent of character segmentation; 17 | - Plate recognition for classic brazilian plates and new Mercosul plates in a single model; 18 | - Majority vote heuristic algorithm for video stream recognition performance increase; 19 | - Weighted Levenshtein distance costs for plate distance calculation 20 | - Modular structure (meaning you can easily replace the detection and/or recognition modules to test your own solutions!). 21 | 22 | 23 | #### Usage: 24 | 25 | For infering on single images, use `test.py` like this: 26 | ``` 27 | $ python3 test.py -i sample/01.jpg -o sample/ 28 | ``` 29 | | Argument | Description | 30 | | ----------- | ----------- | 31 | | --input_img, -i | Path to input image to be analyzed | 32 | | --output, -o | Folder where the results are stored. If passed, an image will be saved with the predicted word as the name and the detected plate bounding box plotted. | 33 | | --read_only, -r | If passing an already cropped plate, this will pass the image directly to the plate reader | 34 | 35 | Examples: 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
Brazilian PlateNew Mercosul PlateRead Only Mode
Input Imagetest image 1test image 2
Output Imageoutput 1output 2'ABC1234'
57 | 58 | For usage on a live video feed, we provide `http_stream.py`, capable of parsing and logging detected plates in real time. Since the camera is fixed, we don't need to pass the whole frame to de detector network, and instead we define an anchor as the top left corner and grab a 416x416 square from that area. 59 | The anchor, HTTP URL, authentication and other configurations can be done by editing the `config.ini` file on the project's root. 60 | ``` 61 | $ python3 http_stream.py 62 | ``` 63 | ![Feed Example](sample/feed_example.png) 64 | The red square is the section that is being passed to the plate detection network, and the blue square is the detected plate bbox. (Plate partially censored due to privacy concerns) 65 | 66 | 67 | #### Implementation details: 68 | The weights for the plate detector and plate reader can be downloaded here: [Google Drive](https://drive.google.com/file/d/1KvIcIMOZ0o9xeW6_Q037Lo8S5bfWUrfz/view?usp=sharing). Their paths should be respectively `detector/weights/detector-w.pth` and 69 | `reader/weights/reader-w.pth`. 70 | Due to the proprietary nature of the datasets used, I'm not at liberty to release them for usage. 71 | 72 | ##### Plate Detector 73 | YoloV3 (checkout [@eriklindernoren's implementation](https://github.com/eriklindernoren/PyTorch-YOLOv3) he did a great job) on COCO pre-trained and retrained/finetuned for detection of brazilian license plates. Usage via the PlateDetector object (`detector/PlateDetector.py`). 74 | Accuracy is greatly improved if input images have a 1:1 aspect ratio. 75 | 76 | ##### Plate Reader 77 | Unlike most ALPR systems, we don't use a character segmentation stage prior to recognition. Segmentation is usually the performance bottleneck in most systems, being the most error prone of them. 78 | We employ an Efficient-Net B0 backbone to extract 252 features from an input image, and then parse that into 7 sets of 36 probabilities. The maximum of each of the 7 is the output letter guessed by the network. This is also very efficient, taking an average of 16ms per prediction (counting the majority vote overhead). Implementation details can be seen on the `reader/PlateReader.py` file. 79 | ![CNN](sample/cnn.png) 80 | 81 | This approach is arguably less accurate than systems with very accurate segmentation steps on high resolution images, but for our specific applications we achieve competitive results by infering on multiple sequential frames and employing a majority vote algorithm to parse the best result. This is done by using [the Sort tracker](https://github.com/abewley/sort) on the detected bounding boxes. Check `http_stream.py` for an example. 82 | 83 | 84 | 85 | ##### Plate Matching 86 | 100% match is not always a realistic expectation for unconstraind environments, especially for easily mistakable characters like I and T, K and R, etc. In order to use the prediction for access control, we recommend using Weighted Levenshtein's distance. We computed the [confusion matrix](https://github.com/glefundes/AccessALPR/blob/master/sample/confusion_matrix.png) for individual characters on our dataset, and used it to define substitution costs for the weighted Levenshtein distance algorithm according to the following rule: 87 | ![equation](http://www.sciweavers.org/tex2img.php?eq=%24%24cost%20%3D%201%20-%205%20%5Ccdot%20n%24%24&bc=White&fc=Black&im=jpg&fs=12&ff=arev&edit=0) 88 | where *n* is the value for the pairing in the normalized confusion matrix. 89 | 90 | To obtain the Levenshtein distance, we call the `utils.lev_distance(plate1, plate2)` function to obtain the value. We establish a threshold of 0.2 for considering the plates a match. 91 | 92 | 93 | ##### Pre-Processing (Experimental) 94 | 95 | There are multiple optional pre-process algorithms available in `reader/utils.py` in the `PreProcessor` class. CLAHE histogram normalization and different RETINEX algorithms are available and can possibly improve performance in low-light situations. Due to limited dataset resources, we could not prove significant quantitative results, but there is a clear visual improvement when applying the filters. To use them, initialize the plate reader object by passing the `filter=` keyword like this: 96 | 97 | ``` 98 | # Filter options: 99 | # 100 | # 'CLAHE'; 101 | # 'RETINEX-CP'; 102 | # 'RETINEX-CR'; 103 | # 'RETINEX-AUTO'. 104 | 105 | plate_reader = PlateReader(filter='CLAHE') 106 | ``` 107 | 108 | It is worth noting that while the best qualitative results are obtained with the Automated RETINEX algorithm, it also introduces significant overhead to the reading process. The RETINEX implementation was taken from [here](https://github.com/dongb5/Retinex). 109 | 110 | 111 | 112 | #### System Requirements: 113 | The code was implemented using Ubuntu 16.04, Python 3.5, Pytorch 1.1.0 and tested to run in real-time on a NVIDIA GTX TITAN 1080. 114 | 115 | 116 | #### Other projects and repositories used during implementation: 117 | https://github.com/eriklindernoren/PyTorch-YOLOv3 118 | https://github.com/abewley/sort 119 | https://github.com/lukemelas/EfficientNet-PyTorch 120 | https://github.com/takeitallsource/cnn-traffic-light-evaluation 121 | https://github.com/dongb5/Retinex 122 | https://github.com/infoscout/weighted-levenshtein 123 | 124 | Shout out to them, and please check out their great work :) 125 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | InputSize = 416 3 | LogFolder = /mnt/hd-docker/gabriel/GUARITA-RUNNING 4 | DetectorWeights = detector/weights/detector-w.pth 5 | ReaderWeights = reader/weights/reader-w.pth 6 | DetectorConfig = detector/config/yolov3-plates.cfg 7 | LevenshteinWeights = lev_weights.csv 8 | 9 | [STREAM] 10 | Anchor = 250,300 11 | Url = http://10.131.20.35/GetImage.cgi?CH=1 12 | Username = admin 13 | Password = admin -------------------------------------------------------------------------------- /detector/PlateDetector.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | 4 | from PIL import Image 5 | from detector.models import * 6 | from detector.utils.utils import * 7 | from torchvision import transforms 8 | 9 | 10 | class PlateDetector(object): 11 | def __init__(self, cfg, weights_path, input_size=416, conf_thresh=0.98): 12 | """ 13 | Construct model to predict plate position in image. 14 | 15 | """ 16 | self.input_size = 416 17 | self.conf_thresh = conf_thresh 18 | self.trans_to_tensor = transforms.Compose([ 19 | transforms.Resize((416,416)), 20 | transforms.ToTensor() 21 | ]) 22 | self.trans_to_img = transforms.ToPILImage() 23 | self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 24 | 25 | self.model = Darknet(cfg, img_size=self.input_size).to(self.device) 26 | self.model.load_state_dict(torch.load(weights_path)) 27 | self.model.eval() 28 | 29 | def predict(self, img): 30 | img = self.trans_to_tensor(Image.fromarray(img)).to(self.device) 31 | img = img[np.newaxis, :] 32 | detections = self.model(img) 33 | detections = non_max_suppression(detections, 0.4, 0.4) 34 | 35 | for detection in detections: 36 | if(detection is not None): 37 | for bbox in detection: 38 | x1, y1, x2, y2, conf, _, _= bbox 39 | if(conf > self.conf_thresh): 40 | return np.asarray([[x1,y1,x2,y2,conf]]) 41 | 42 | return [] 43 | 44 | def rescale_boxes(self, boxes, current_dim, original_shape): 45 | """ Rescales bounding boxes to the original shape """ 46 | orig_h, orig_w = original_shape 47 | # The amount of padding that was added 48 | pad_x = max(orig_h - orig_w, 0) * (current_dim / max(original_shape)) 49 | pad_y = max(orig_w - orig_h, 0) * (current_dim / max(original_shape)) 50 | # Image height and width after padding is removed 51 | unpad_h = current_dim - pad_y 52 | unpad_w = current_dim - pad_x 53 | # Rescale bounding boxes to dimension of original image 54 | boxes[:, 0] = ((boxes[:, 0] - pad_x // 2) / unpad_w) * orig_w 55 | boxes[:, 1] = ((boxes[:, 1] - pad_y // 2) / unpad_h) * orig_h 56 | boxes[:, 2] = ((boxes[:, 2] - pad_x // 2) / unpad_w) * orig_w 57 | boxes[:, 3] = ((boxes[:, 3] - pad_y // 2) / unpad_h) * orig_h 58 | return boxes -------------------------------------------------------------------------------- /detector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/detector/__init__.py -------------------------------------------------------------------------------- /detector/config/yolov3-plates.cfg: -------------------------------------------------------------------------------- 1 | 2 | [net] 3 | # Testing 4 | #batch=1 5 | #subdivisions=1 6 | # Training 7 | batch=16 8 | subdivisions=1 9 | width=416 10 | height=416 11 | channels=3 12 | momentum=0.9 13 | decay=0.0005 14 | angle=0 15 | saturation = 1.5 16 | exposure = 1.5 17 | hue=.1 18 | 19 | learning_rate=0.001 20 | burn_in=1000 21 | max_batches = 500200 22 | policy=steps 23 | steps=400000,450000 24 | scales=.1,.1 25 | 26 | [convolutional] 27 | batch_normalize=1 28 | filters=32 29 | size=3 30 | stride=1 31 | pad=1 32 | activation=leaky 33 | 34 | # Downsample 35 | 36 | [convolutional] 37 | batch_normalize=1 38 | filters=64 39 | size=3 40 | stride=2 41 | pad=1 42 | activation=leaky 43 | 44 | [convolutional] 45 | batch_normalize=1 46 | filters=32 47 | size=1 48 | stride=1 49 | pad=1 50 | activation=leaky 51 | 52 | [convolutional] 53 | batch_normalize=1 54 | filters=64 55 | size=3 56 | stride=1 57 | pad=1 58 | activation=leaky 59 | 60 | [shortcut] 61 | from=-3 62 | activation=linear 63 | 64 | # Downsample 65 | 66 | [convolutional] 67 | batch_normalize=1 68 | filters=128 69 | size=3 70 | stride=2 71 | pad=1 72 | activation=leaky 73 | 74 | [convolutional] 75 | batch_normalize=1 76 | filters=64 77 | size=1 78 | stride=1 79 | pad=1 80 | activation=leaky 81 | 82 | [convolutional] 83 | batch_normalize=1 84 | filters=128 85 | size=3 86 | stride=1 87 | pad=1 88 | activation=leaky 89 | 90 | [shortcut] 91 | from=-3 92 | activation=linear 93 | 94 | [convolutional] 95 | batch_normalize=1 96 | filters=64 97 | size=1 98 | stride=1 99 | pad=1 100 | activation=leaky 101 | 102 | [convolutional] 103 | batch_normalize=1 104 | filters=128 105 | size=3 106 | stride=1 107 | pad=1 108 | activation=leaky 109 | 110 | [shortcut] 111 | from=-3 112 | activation=linear 113 | 114 | # Downsample 115 | 116 | [convolutional] 117 | batch_normalize=1 118 | filters=256 119 | size=3 120 | stride=2 121 | pad=1 122 | activation=leaky 123 | 124 | [convolutional] 125 | batch_normalize=1 126 | filters=128 127 | size=1 128 | stride=1 129 | pad=1 130 | activation=leaky 131 | 132 | [convolutional] 133 | batch_normalize=1 134 | filters=256 135 | size=3 136 | stride=1 137 | pad=1 138 | activation=leaky 139 | 140 | [shortcut] 141 | from=-3 142 | activation=linear 143 | 144 | [convolutional] 145 | batch_normalize=1 146 | filters=128 147 | size=1 148 | stride=1 149 | pad=1 150 | activation=leaky 151 | 152 | [convolutional] 153 | batch_normalize=1 154 | filters=256 155 | size=3 156 | stride=1 157 | pad=1 158 | activation=leaky 159 | 160 | [shortcut] 161 | from=-3 162 | activation=linear 163 | 164 | [convolutional] 165 | batch_normalize=1 166 | filters=128 167 | size=1 168 | stride=1 169 | pad=1 170 | activation=leaky 171 | 172 | [convolutional] 173 | batch_normalize=1 174 | filters=256 175 | size=3 176 | stride=1 177 | pad=1 178 | activation=leaky 179 | 180 | [shortcut] 181 | from=-3 182 | activation=linear 183 | 184 | [convolutional] 185 | batch_normalize=1 186 | filters=128 187 | size=1 188 | stride=1 189 | pad=1 190 | activation=leaky 191 | 192 | [convolutional] 193 | batch_normalize=1 194 | filters=256 195 | size=3 196 | stride=1 197 | pad=1 198 | activation=leaky 199 | 200 | [shortcut] 201 | from=-3 202 | activation=linear 203 | 204 | 205 | [convolutional] 206 | batch_normalize=1 207 | filters=128 208 | size=1 209 | stride=1 210 | pad=1 211 | activation=leaky 212 | 213 | [convolutional] 214 | batch_normalize=1 215 | filters=256 216 | size=3 217 | stride=1 218 | pad=1 219 | activation=leaky 220 | 221 | [shortcut] 222 | from=-3 223 | activation=linear 224 | 225 | [convolutional] 226 | batch_normalize=1 227 | filters=128 228 | size=1 229 | stride=1 230 | pad=1 231 | activation=leaky 232 | 233 | [convolutional] 234 | batch_normalize=1 235 | filters=256 236 | size=3 237 | stride=1 238 | pad=1 239 | activation=leaky 240 | 241 | [shortcut] 242 | from=-3 243 | activation=linear 244 | 245 | [convolutional] 246 | batch_normalize=1 247 | filters=128 248 | size=1 249 | stride=1 250 | pad=1 251 | activation=leaky 252 | 253 | [convolutional] 254 | batch_normalize=1 255 | filters=256 256 | size=3 257 | stride=1 258 | pad=1 259 | activation=leaky 260 | 261 | [shortcut] 262 | from=-3 263 | activation=linear 264 | 265 | [convolutional] 266 | batch_normalize=1 267 | filters=128 268 | size=1 269 | stride=1 270 | pad=1 271 | activation=leaky 272 | 273 | [convolutional] 274 | batch_normalize=1 275 | filters=256 276 | size=3 277 | stride=1 278 | pad=1 279 | activation=leaky 280 | 281 | [shortcut] 282 | from=-3 283 | activation=linear 284 | 285 | # Downsample 286 | 287 | [convolutional] 288 | batch_normalize=1 289 | filters=512 290 | size=3 291 | stride=2 292 | pad=1 293 | activation=leaky 294 | 295 | [convolutional] 296 | batch_normalize=1 297 | filters=256 298 | size=1 299 | stride=1 300 | pad=1 301 | activation=leaky 302 | 303 | [convolutional] 304 | batch_normalize=1 305 | filters=512 306 | size=3 307 | stride=1 308 | pad=1 309 | activation=leaky 310 | 311 | [shortcut] 312 | from=-3 313 | activation=linear 314 | 315 | 316 | [convolutional] 317 | batch_normalize=1 318 | filters=256 319 | size=1 320 | stride=1 321 | pad=1 322 | activation=leaky 323 | 324 | [convolutional] 325 | batch_normalize=1 326 | filters=512 327 | size=3 328 | stride=1 329 | pad=1 330 | activation=leaky 331 | 332 | [shortcut] 333 | from=-3 334 | activation=linear 335 | 336 | 337 | [convolutional] 338 | batch_normalize=1 339 | filters=256 340 | size=1 341 | stride=1 342 | pad=1 343 | activation=leaky 344 | 345 | [convolutional] 346 | batch_normalize=1 347 | filters=512 348 | size=3 349 | stride=1 350 | pad=1 351 | activation=leaky 352 | 353 | [shortcut] 354 | from=-3 355 | activation=linear 356 | 357 | 358 | [convolutional] 359 | batch_normalize=1 360 | filters=256 361 | size=1 362 | stride=1 363 | pad=1 364 | activation=leaky 365 | 366 | [convolutional] 367 | batch_normalize=1 368 | filters=512 369 | size=3 370 | stride=1 371 | pad=1 372 | activation=leaky 373 | 374 | [shortcut] 375 | from=-3 376 | activation=linear 377 | 378 | [convolutional] 379 | batch_normalize=1 380 | filters=256 381 | size=1 382 | stride=1 383 | pad=1 384 | activation=leaky 385 | 386 | [convolutional] 387 | batch_normalize=1 388 | filters=512 389 | size=3 390 | stride=1 391 | pad=1 392 | activation=leaky 393 | 394 | [shortcut] 395 | from=-3 396 | activation=linear 397 | 398 | 399 | [convolutional] 400 | batch_normalize=1 401 | filters=256 402 | size=1 403 | stride=1 404 | pad=1 405 | activation=leaky 406 | 407 | [convolutional] 408 | batch_normalize=1 409 | filters=512 410 | size=3 411 | stride=1 412 | pad=1 413 | activation=leaky 414 | 415 | [shortcut] 416 | from=-3 417 | activation=linear 418 | 419 | 420 | [convolutional] 421 | batch_normalize=1 422 | filters=256 423 | size=1 424 | stride=1 425 | pad=1 426 | activation=leaky 427 | 428 | [convolutional] 429 | batch_normalize=1 430 | filters=512 431 | size=3 432 | stride=1 433 | pad=1 434 | activation=leaky 435 | 436 | [shortcut] 437 | from=-3 438 | activation=linear 439 | 440 | [convolutional] 441 | batch_normalize=1 442 | filters=256 443 | size=1 444 | stride=1 445 | pad=1 446 | activation=leaky 447 | 448 | [convolutional] 449 | batch_normalize=1 450 | filters=512 451 | size=3 452 | stride=1 453 | pad=1 454 | activation=leaky 455 | 456 | [shortcut] 457 | from=-3 458 | activation=linear 459 | 460 | # Downsample 461 | 462 | [convolutional] 463 | batch_normalize=1 464 | filters=1024 465 | size=3 466 | stride=2 467 | pad=1 468 | activation=leaky 469 | 470 | [convolutional] 471 | batch_normalize=1 472 | filters=512 473 | size=1 474 | stride=1 475 | pad=1 476 | activation=leaky 477 | 478 | [convolutional] 479 | batch_normalize=1 480 | filters=1024 481 | size=3 482 | stride=1 483 | pad=1 484 | activation=leaky 485 | 486 | [shortcut] 487 | from=-3 488 | activation=linear 489 | 490 | [convolutional] 491 | batch_normalize=1 492 | filters=512 493 | size=1 494 | stride=1 495 | pad=1 496 | activation=leaky 497 | 498 | [convolutional] 499 | batch_normalize=1 500 | filters=1024 501 | size=3 502 | stride=1 503 | pad=1 504 | activation=leaky 505 | 506 | [shortcut] 507 | from=-3 508 | activation=linear 509 | 510 | [convolutional] 511 | batch_normalize=1 512 | filters=512 513 | size=1 514 | stride=1 515 | pad=1 516 | activation=leaky 517 | 518 | [convolutional] 519 | batch_normalize=1 520 | filters=1024 521 | size=3 522 | stride=1 523 | pad=1 524 | activation=leaky 525 | 526 | [shortcut] 527 | from=-3 528 | activation=linear 529 | 530 | [convolutional] 531 | batch_normalize=1 532 | filters=512 533 | size=1 534 | stride=1 535 | pad=1 536 | activation=leaky 537 | 538 | [convolutional] 539 | batch_normalize=1 540 | filters=1024 541 | size=3 542 | stride=1 543 | pad=1 544 | activation=leaky 545 | 546 | [shortcut] 547 | from=-3 548 | activation=linear 549 | 550 | ###################### 551 | 552 | [convolutional] 553 | batch_normalize=1 554 | filters=512 555 | size=1 556 | stride=1 557 | pad=1 558 | activation=leaky 559 | 560 | [convolutional] 561 | batch_normalize=1 562 | size=3 563 | stride=1 564 | pad=1 565 | filters=1024 566 | activation=leaky 567 | 568 | [convolutional] 569 | batch_normalize=1 570 | filters=512 571 | size=1 572 | stride=1 573 | pad=1 574 | activation=leaky 575 | 576 | [convolutional] 577 | batch_normalize=1 578 | size=3 579 | stride=1 580 | pad=1 581 | filters=1024 582 | activation=leaky 583 | 584 | [convolutional] 585 | batch_normalize=1 586 | filters=512 587 | size=1 588 | stride=1 589 | pad=1 590 | activation=leaky 591 | 592 | [convolutional] 593 | batch_normalize=1 594 | size=3 595 | stride=1 596 | pad=1 597 | filters=1024 598 | activation=leaky 599 | 600 | [convolutional] 601 | size=1 602 | stride=1 603 | pad=1 604 | filters=18 605 | activation=linear 606 | 607 | 608 | [yolo] 609 | mask = 6,7,8 610 | anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326 611 | classes=1 612 | num=9 613 | jitter=.3 614 | ignore_thresh = .7 615 | truth_thresh = 1 616 | random=1 617 | 618 | 619 | [route] 620 | layers = -4 621 | 622 | [convolutional] 623 | batch_normalize=1 624 | filters=256 625 | size=1 626 | stride=1 627 | pad=1 628 | activation=leaky 629 | 630 | [upsample] 631 | stride=2 632 | 633 | [route] 634 | layers = -1, 61 635 | 636 | 637 | 638 | [convolutional] 639 | batch_normalize=1 640 | filters=256 641 | size=1 642 | stride=1 643 | pad=1 644 | activation=leaky 645 | 646 | [convolutional] 647 | batch_normalize=1 648 | size=3 649 | stride=1 650 | pad=1 651 | filters=512 652 | activation=leaky 653 | 654 | [convolutional] 655 | batch_normalize=1 656 | filters=256 657 | size=1 658 | stride=1 659 | pad=1 660 | activation=leaky 661 | 662 | [convolutional] 663 | batch_normalize=1 664 | size=3 665 | stride=1 666 | pad=1 667 | filters=512 668 | activation=leaky 669 | 670 | [convolutional] 671 | batch_normalize=1 672 | filters=256 673 | size=1 674 | stride=1 675 | pad=1 676 | activation=leaky 677 | 678 | [convolutional] 679 | batch_normalize=1 680 | size=3 681 | stride=1 682 | pad=1 683 | filters=512 684 | activation=leaky 685 | 686 | [convolutional] 687 | size=1 688 | stride=1 689 | pad=1 690 | filters=18 691 | activation=linear 692 | 693 | 694 | [yolo] 695 | mask = 3,4,5 696 | anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326 697 | classes=1 698 | num=9 699 | jitter=.3 700 | ignore_thresh = .7 701 | truth_thresh = 1 702 | random=1 703 | 704 | 705 | 706 | [route] 707 | layers = -4 708 | 709 | [convolutional] 710 | batch_normalize=1 711 | filters=128 712 | size=1 713 | stride=1 714 | pad=1 715 | activation=leaky 716 | 717 | [upsample] 718 | stride=2 719 | 720 | [route] 721 | layers = -1, 36 722 | 723 | 724 | 725 | [convolutional] 726 | batch_normalize=1 727 | filters=128 728 | size=1 729 | stride=1 730 | pad=1 731 | activation=leaky 732 | 733 | [convolutional] 734 | batch_normalize=1 735 | size=3 736 | stride=1 737 | pad=1 738 | filters=256 739 | activation=leaky 740 | 741 | [convolutional] 742 | batch_normalize=1 743 | filters=128 744 | size=1 745 | stride=1 746 | pad=1 747 | activation=leaky 748 | 749 | [convolutional] 750 | batch_normalize=1 751 | size=3 752 | stride=1 753 | pad=1 754 | filters=256 755 | activation=leaky 756 | 757 | [convolutional] 758 | batch_normalize=1 759 | filters=128 760 | size=1 761 | stride=1 762 | pad=1 763 | activation=leaky 764 | 765 | [convolutional] 766 | batch_normalize=1 767 | size=3 768 | stride=1 769 | pad=1 770 | filters=256 771 | activation=leaky 772 | 773 | [convolutional] 774 | size=1 775 | stride=1 776 | pad=1 777 | filters=18 778 | activation=linear 779 | 780 | 781 | [yolo] 782 | mask = 0,1,2 783 | anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326 784 | classes=1 785 | num=9 786 | jitter=.3 787 | ignore_thresh = .7 788 | truth_thresh = 1 789 | random=1 790 | 791 | -------------------------------------------------------------------------------- /detector/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python -W ignore::FutureWarning 2 | #!/usr/bin/env python -W ignore::UserWarning 3 | 4 | from __future__ import division 5 | 6 | import torch 7 | import torch.nn as nn 8 | import torch.nn.functional as F 9 | from torch.autograd import Variable 10 | import numpy as np 11 | 12 | from detector.utils.parse_config import * 13 | from detector.utils.utils import build_targets, to_cpu, non_max_suppression 14 | 15 | import matplotlib.pyplot as plt 16 | import matplotlib.patches as patches 17 | 18 | 19 | def create_modules(module_defs): 20 | """ 21 | Constructs module list of layer blocks from module configuration in module_defs 22 | """ 23 | hyperparams = module_defs.pop(0) 24 | output_filters = [int(hyperparams["channels"])] 25 | module_list = nn.ModuleList() 26 | for module_i, module_def in enumerate(module_defs): 27 | modules = nn.Sequential() 28 | 29 | if module_def["type"] == "convolutional": 30 | bn = int(module_def["batch_normalize"]) 31 | filters = int(module_def["filters"]) 32 | kernel_size = int(module_def["size"]) 33 | pad = (kernel_size - 1) // 2 34 | modules.add_module( 35 | "conv_{}".format(module_i), 36 | nn.Conv2d( 37 | in_channels=output_filters[-1], 38 | out_channels=filters, 39 | kernel_size=kernel_size, 40 | stride=int(module_def["stride"]), 41 | padding=pad, 42 | bias=not bn, 43 | ), 44 | ) 45 | if bn: 46 | modules.add_module("batch_norm_{}".format(module_i), nn.BatchNorm2d(filters, momentum=0.9, eps=1e-5)) 47 | if module_def["activation"] == "leaky": 48 | modules.add_module("leaky_{}".format(module_i), nn.LeakyReLU(0.1)) 49 | 50 | elif module_def["type"] == "maxpool": 51 | kernel_size = int(module_def["size"]) 52 | stride = int(module_def["stride"]) 53 | if kernel_size == 2 and stride == 1: 54 | modules.add_module("_debug_padding_{}".format(module_i), nn.ZeroPad2d((0, 1, 0, 1))) 55 | maxpool = nn.MaxPool2d(kernel_size=kernel_size, stride=stride, padding=int((kernel_size - 1) // 2)) 56 | modules.add_module("maxpool_{}".format(module_i), maxpool) 57 | 58 | elif module_def["type"] == "upsample": 59 | upsample = Upsample(scale_factor=int(module_def["stride"]), mode="nearest") 60 | modules.add_module("upsample_{}".format(module_i), upsample) 61 | 62 | elif module_def["type"] == "route": 63 | layers = [int(x) for x in module_def["layers"].split(",")] 64 | filters = sum([output_filters[1:][i] for i in layers]) 65 | modules.add_module("route_{}".format(module_i), EmptyLayer()) 66 | 67 | elif module_def["type"] == "shortcut": 68 | filters = output_filters[1:][int(module_def["from"])] 69 | modules.add_module("shortcut_{}".format(module_i), EmptyLayer()) 70 | 71 | elif module_def["type"] == "yolo": 72 | anchor_idxs = [int(x) for x in module_def["mask"].split(",")] 73 | # Extract anchors 74 | anchors = [int(x) for x in module_def["anchors"].split(",")] 75 | anchors = [(anchors[i], anchors[i + 1]) for i in range(0, len(anchors), 2)] 76 | anchors = [anchors[i] for i in anchor_idxs] 77 | num_classes = int(module_def["classes"]) 78 | img_size = int(hyperparams["height"]) 79 | # Define detection layer 80 | yolo_layer = YOLOLayer(anchors, num_classes, img_size) 81 | modules.add_module("yolo_{}".format(module_i), yolo_layer) 82 | # Register module list and number of output filters 83 | module_list.append(modules) 84 | output_filters.append(filters) 85 | 86 | return hyperparams, module_list 87 | 88 | 89 | class Upsample(nn.Module): 90 | """ nn.Upsample is deprecated """ 91 | 92 | def __init__(self, scale_factor, mode="nearest"): 93 | super(Upsample, self).__init__() 94 | self.scale_factor = scale_factor 95 | self.mode = mode 96 | 97 | def forward(self, x): 98 | x = F.interpolate(x, scale_factor=self.scale_factor, mode=self.mode) 99 | return x 100 | 101 | 102 | class EmptyLayer(nn.Module): 103 | """Placeholder for 'route' and 'shortcut' layers""" 104 | 105 | def __init__(self): 106 | super(EmptyLayer, self).__init__() 107 | 108 | 109 | class YOLOLayer(nn.Module): 110 | """Detection layer""" 111 | 112 | def __init__(self, anchors, num_classes, img_dim=416): 113 | super(YOLOLayer, self).__init__() 114 | self.anchors = anchors 115 | self.num_anchors = len(anchors) 116 | self.num_classes = num_classes 117 | self.ignore_thres = 0.5 118 | self.mse_loss = nn.MSELoss() 119 | self.bce_loss = nn.BCELoss() 120 | self.obj_scale = 1 121 | self.noobj_scale = 100 122 | self.metrics = {} 123 | self.img_dim = img_dim 124 | self.grid_size = 0 # grid size 125 | 126 | def compute_grid_offsets(self, grid_size, cuda=True): 127 | self.grid_size = grid_size 128 | g = self.grid_size 129 | FloatTensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor 130 | self.stride = self.img_dim / self.grid_size 131 | # Calculate offsets for each grid 132 | self.grid_x = torch.arange(g).repeat(g, 1).view([1, 1, g, g]).type(FloatTensor) 133 | self.grid_y = torch.arange(g).repeat(g, 1).t().view([1, 1, g, g]).type(FloatTensor) 134 | self.scaled_anchors = FloatTensor([(a_w / self.stride, a_h / self.stride) for a_w, a_h in self.anchors]) 135 | self.anchor_w = self.scaled_anchors[:, 0:1].view((1, self.num_anchors, 1, 1)) 136 | self.anchor_h = self.scaled_anchors[:, 1:2].view((1, self.num_anchors, 1, 1)) 137 | 138 | def forward(self, x, targets=None, img_dim=None): 139 | 140 | # Tensors for cuda support 141 | FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor 142 | LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor 143 | ByteTensor = torch.cuda.ByteTensor if x.is_cuda else torch.ByteTensor 144 | 145 | self.img_dim = img_dim 146 | num_samples = x.size(0) 147 | grid_size = x.size(2) 148 | 149 | prediction = ( 150 | x.view(num_samples, self.num_anchors, self.num_classes + 5, grid_size, grid_size) 151 | .permute(0, 1, 3, 4, 2) 152 | .contiguous() 153 | ) 154 | 155 | # Get outputs 156 | x = torch.sigmoid(prediction[..., 0]) # Center x 157 | y = torch.sigmoid(prediction[..., 1]) # Center y 158 | w = prediction[..., 2] # Width 159 | h = prediction[..., 3] # Height 160 | pred_conf = torch.sigmoid(prediction[..., 4]) # Conf 161 | pred_cls = torch.sigmoid(prediction[..., 5:]) # Cls pred. 162 | 163 | # If grid size does not match current we compute new offsets 164 | if grid_size != self.grid_size: 165 | self.compute_grid_offsets(grid_size, cuda=x.is_cuda) 166 | 167 | # Add offset and scale with anchors 168 | pred_boxes = FloatTensor(prediction[..., :4].shape) 169 | pred_boxes[..., 0] = x.data + self.grid_x 170 | pred_boxes[..., 1] = y.data + self.grid_y 171 | pred_boxes[..., 2] = torch.exp(w.data) * self.anchor_w 172 | pred_boxes[..., 3] = torch.exp(h.data) * self.anchor_h 173 | 174 | output = torch.cat( 175 | ( 176 | pred_boxes.view(num_samples, -1, 4) * self.stride, 177 | pred_conf.view(num_samples, -1, 1), 178 | pred_cls.view(num_samples, -1, self.num_classes), 179 | ), 180 | -1, 181 | ) 182 | 183 | if targets is None: 184 | return output, 0 185 | else: 186 | iou_scores, class_mask, obj_mask, noobj_mask, tx, ty, tw, th, tcls, tconf = build_targets( 187 | pred_boxes=pred_boxes, 188 | pred_cls=pred_cls, 189 | target=targets, 190 | anchors=self.scaled_anchors, 191 | ignore_thres=self.ignore_thres, 192 | ) 193 | # Loss : Mask outputs to ignore non-existing objects (except with conf. loss) 194 | loss_x = self.mse_loss(x[obj_mask], tx[obj_mask]) 195 | loss_y = self.mse_loss(y[obj_mask], ty[obj_mask]) 196 | loss_w = self.mse_loss(w[obj_mask], tw[obj_mask]) 197 | loss_h = self.mse_loss(h[obj_mask], th[obj_mask]) 198 | loss_conf_obj = self.bce_loss(pred_conf[obj_mask], tconf[obj_mask]) 199 | loss_conf_noobj = self.bce_loss(pred_conf[noobj_mask], tconf[noobj_mask]) 200 | loss_conf = self.obj_scale * loss_conf_obj + self.noobj_scale * loss_conf_noobj 201 | loss_cls = self.bce_loss(pred_cls[obj_mask], tcls[obj_mask]) 202 | total_loss = loss_x + loss_y + loss_w + loss_h + loss_conf + loss_cls 203 | 204 | # Metrics 205 | cls_acc = 100 * class_mask[obj_mask].mean() 206 | conf_obj = pred_conf[obj_mask].mean() 207 | conf_noobj = pred_conf[noobj_mask].mean() 208 | conf50 = (pred_conf > 0.5).float() 209 | iou50 = (iou_scores > 0.5).float() 210 | iou75 = (iou_scores > 0.75).float() 211 | detected_mask = conf50 * class_mask * tconf 212 | precision = torch.sum(iou50 * detected_mask) / (conf50.sum() + 1e-16) 213 | recall50 = torch.sum(iou50 * detected_mask) / (obj_mask.sum() + 1e-16) 214 | recall75 = torch.sum(iou75 * detected_mask) / (obj_mask.sum() + 1e-16) 215 | 216 | self.metrics = { 217 | "loss": to_cpu(total_loss).item(), 218 | "x": to_cpu(loss_x).item(), 219 | "y": to_cpu(loss_y).item(), 220 | "w": to_cpu(loss_w).item(), 221 | "h": to_cpu(loss_h).item(), 222 | "conf": to_cpu(loss_conf).item(), 223 | "cls": to_cpu(loss_cls).item(), 224 | "cls_acc": to_cpu(cls_acc).item(), 225 | "recall50": to_cpu(recall50).item(), 226 | "recall75": to_cpu(recall75).item(), 227 | "precision": to_cpu(precision).item(), 228 | "conf_obj": to_cpu(conf_obj).item(), 229 | "conf_noobj": to_cpu(conf_noobj).item(), 230 | "grid_size": grid_size, 231 | } 232 | 233 | return output, total_loss 234 | 235 | 236 | class Darknet(nn.Module): 237 | """YOLOv3 object detection model""" 238 | 239 | def __init__(self, config_path, img_size=416): 240 | super(Darknet, self).__init__() 241 | self.module_defs = parse_model_config(config_path) 242 | self.hyperparams, self.module_list = create_modules(self.module_defs) 243 | self.yolo_layers = [layer[0] for layer in self.module_list if hasattr(layer[0], "metrics")] 244 | self.img_size = img_size 245 | self.seen = 0 246 | self.header_info = np.array([0, 0, 0, self.seen, 0], dtype=np.int32) 247 | 248 | def forward(self, x, targets=None): 249 | img_dim = x.shape[2] 250 | loss = 0 251 | layer_outputs, yolo_outputs = [], [] 252 | for i, (module_def, module) in enumerate(zip(self.module_defs, self.module_list)): 253 | if module_def["type"] in ["convolutional", "upsample", "maxpool"]: 254 | x = module(x) 255 | elif module_def["type"] == "route": 256 | x = torch.cat([layer_outputs[int(layer_i)] for layer_i in module_def["layers"].split(",")], 1) 257 | elif module_def["type"] == "shortcut": 258 | layer_i = int(module_def["from"]) 259 | x = layer_outputs[-1] + layer_outputs[layer_i] 260 | elif module_def["type"] == "yolo": 261 | 262 | x, layer_loss = module[0](x, targets, img_dim) 263 | 264 | loss += layer_loss 265 | yolo_outputs.append(x) 266 | layer_outputs.append(x) 267 | yolo_outputs = to_cpu(torch.cat(yolo_outputs, 1)) 268 | return yolo_outputs if targets is None else (loss, yolo_outputs) 269 | 270 | def load_darknet_weights(self, weights_path): 271 | """Parses and loads the weights stored in 'weights_path'""" 272 | 273 | # Open the weights file 274 | with open(weights_path, "rb") as f: 275 | header = np.fromfile(f, dtype=np.int32, count=5) # First five are header values 276 | self.header_info = header # Needed to write header when saving weights 277 | self.seen = header[3] # number of images seen during training 278 | weights = np.fromfile(f, dtype=np.float32) # The rest are weights 279 | 280 | # Establish cutoff for loading backbone weights 281 | cutoff = None 282 | if "darknet53.conv.74" in weights_path: 283 | cutoff = 75 284 | 285 | ptr = 0 286 | for i, (module_def, module) in enumerate(zip(self.module_defs, self.module_list)): 287 | if i == cutoff: 288 | break 289 | if module_def["type"] == "convolutional": 290 | conv_layer = module[0] 291 | if module_def["batch_normalize"]: 292 | # Load BN bias, weights, running mean and running variance 293 | bn_layer = module[1] 294 | num_b = bn_layer.bias.numel() # Number of biases 295 | # Bias 296 | bn_b = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.bias) 297 | bn_layer.bias.data.copy_(bn_b) 298 | ptr += num_b 299 | # Weight 300 | bn_w = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.weight) 301 | bn_layer.weight.data.copy_(bn_w) 302 | ptr += num_b 303 | # Running Mean 304 | bn_rm = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.running_mean) 305 | bn_layer.running_mean.data.copy_(bn_rm) 306 | ptr += num_b 307 | # Running Var 308 | bn_rv = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.running_var) 309 | bn_layer.running_var.data.copy_(bn_rv) 310 | ptr += num_b 311 | else: 312 | # Load conv. bias 313 | num_b = conv_layer.bias.numel() 314 | conv_b = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(conv_layer.bias) 315 | conv_layer.bias.data.copy_(conv_b) 316 | ptr += num_b 317 | # Load conv. weights 318 | num_w = conv_layer.weight.numel() 319 | conv_w = torch.from_numpy(weights[ptr : ptr + num_w]).view_as(conv_layer.weight) 320 | conv_layer.weight.data.copy_(conv_w) 321 | ptr += num_w 322 | 323 | def save_darknet_weights(self, path, cutoff=-1): 324 | """ 325 | @:param path - path of the new weights file 326 | @:param cutoff - save layers between 0 and cutoff (cutoff = -1 -> all are saved) 327 | """ 328 | fp = open(path, "wb") 329 | self.header_info[3] = self.seen 330 | self.header_info.tofile(fp) 331 | 332 | # Iterate through layers 333 | for i, (module_def, module) in enumerate(zip(self.module_defs[:cutoff], self.module_list[:cutoff])): 334 | if module_def["type"] == "convolutional": 335 | conv_layer = module[0] 336 | # If batch norm, load bn first 337 | if module_def["batch_normalize"]: 338 | bn_layer = module[1] 339 | bn_layer.bias.data.cpu().numpy().tofile(fp) 340 | bn_layer.weight.data.cpu().numpy().tofile(fp) 341 | bn_layer.running_mean.data.cpu().numpy().tofile(fp) 342 | bn_layer.running_var.data.cpu().numpy().tofile(fp) 343 | # Load conv bias 344 | else: 345 | conv_layer.bias.data.cpu().numpy().tofile(fp) 346 | # Load conv weights 347 | conv_layer.weight.data.cpu().numpy().tofile(fp) 348 | 349 | fp.close() 350 | -------------------------------------------------------------------------------- /detector/sort.py: -------------------------------------------------------------------------------- 1 | """ 2 | SORT: A Simple, Online and Realtime Tracker 3 | Copyright (C) 2016 Alex Bewley alex@dynamicdetection.com 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | """ 18 | from __future__ import print_function 19 | 20 | from numba import jit 21 | import os.path 22 | import numpy as np 23 | import matplotlib.pyplot as plt 24 | import matplotlib.patches as patches 25 | from skimage import io 26 | from sklearn.utils.linear_assignment_ import linear_assignment 27 | import glob 28 | import time 29 | import argparse 30 | from filterpy.kalman import KalmanFilter 31 | 32 | @jit 33 | def iou(bb_test,bb_gt): 34 | """ 35 | Computes IUO between two bboxes in the form [x1,y1,x2,y2] 36 | """ 37 | xx1 = np.maximum(bb_test[0], bb_gt[0]) 38 | yy1 = np.maximum(bb_test[1], bb_gt[1]) 39 | xx2 = np.minimum(bb_test[2], bb_gt[2]) 40 | yy2 = np.minimum(bb_test[3], bb_gt[3]) 41 | w = np.maximum(0., xx2 - xx1) 42 | h = np.maximum(0., yy2 - yy1) 43 | wh = w * h 44 | o = wh / ((bb_test[2]-bb_test[0])*(bb_test[3]-bb_test[1]) 45 | + (bb_gt[2]-bb_gt[0])*(bb_gt[3]-bb_gt[1]) - wh) 46 | return(o) 47 | 48 | def convert_bbox_to_z(bbox): 49 | """ 50 | Takes a bounding box in the form [x1,y1,x2,y2] and returns z in the form 51 | [x,y,s,r] where x,y is the centre of the box and s is the scale/area and r is 52 | the aspect ratio 53 | """ 54 | w = bbox[2]-bbox[0] 55 | h = bbox[3]-bbox[1] 56 | x = bbox[0]+w/2. 57 | y = bbox[1]+h/2. 58 | s = w*h #scale is just area 59 | r = w/float(h) 60 | return np.array([x,y,s,r]).reshape((4,1)) 61 | 62 | def convert_x_to_bbox(x,score=None): 63 | """ 64 | Takes a bounding box in the centre form [x,y,s,r] and returns it in the form 65 | [x1,y1,x2,y2] where x1,y1 is the top left and x2,y2 is the bottom right 66 | """ 67 | w = np.sqrt(x[2]*x[3]) 68 | h = x[2]/w 69 | if(score==None): 70 | return np.array([x[0]-w/2.,x[1]-h/2.,x[0]+w/2.,x[1]+h/2.]).reshape((1,4)) 71 | else: 72 | return np.array([x[0]-w/2.,x[1]-h/2.,x[0]+w/2.,x[1]+h/2.,score]).reshape((1,5)) 73 | 74 | 75 | class KalmanBoxTracker(object): 76 | """ 77 | This class represents the internel state of individual tracked objects observed as bbox. 78 | """ 79 | count = 0 80 | def __init__(self,bbox): 81 | """ 82 | Initialises a tracker using initial bounding box. 83 | """ 84 | #define constant velocity model 85 | self.kf = KalmanFilter(dim_x=7, dim_z=4) 86 | self.kf.F = np.array([[1,0,0,0,1,0,0],[0,1,0,0,0,1,0],[0,0,1,0,0,0,1],[0,0,0,1,0,0,0], [0,0,0,0,1,0,0],[0,0,0,0,0,1,0],[0,0,0,0,0,0,1]]) 87 | self.kf.H = np.array([[1,0,0,0,0,0,0],[0,1,0,0,0,0,0],[0,0,1,0,0,0,0],[0,0,0,1,0,0,0]]) 88 | 89 | self.kf.R[2:,2:] *= 10. 90 | self.kf.P[4:,4:] *= 1000. #give high uncertainty to the unobservable initial velocities 91 | self.kf.P *= 10. 92 | self.kf.Q[-1,-1] *= 0.01 93 | self.kf.Q[4:,4:] *= 0.01 94 | 95 | self.kf.x[:4] = convert_bbox_to_z(bbox) 96 | self.time_since_update = 0 97 | self.id = KalmanBoxTracker.count 98 | KalmanBoxTracker.count += 1 99 | self.history = [] 100 | self.hits = 0 101 | self.hit_streak = 0 102 | self.age = 0 103 | 104 | def update(self,bbox): 105 | """ 106 | Updates the state vector with observed bbox. 107 | """ 108 | self.time_since_update = 0 109 | self.history = [] 110 | self.hits += 1 111 | self.hit_streak += 1 112 | self.kf.update(convert_bbox_to_z(bbox)) 113 | 114 | def predict(self): 115 | """ 116 | Advances the state vector and returns the predicted bounding box estimate. 117 | """ 118 | if((self.kf.x[6]+self.kf.x[2])<=0): 119 | self.kf.x[6] *= 0.0 120 | self.kf.predict() 121 | self.age += 1 122 | if(self.time_since_update>0): 123 | self.hit_streak = 0 124 | self.time_since_update += 1 125 | self.history.append(convert_x_to_bbox(self.kf.x)) 126 | return self.history[-1] 127 | 128 | def get_state(self): 129 | """ 130 | Returns the current bounding box estimate. 131 | """ 132 | return convert_x_to_bbox(self.kf.x) 133 | 134 | def associate_detections_to_trackers(detections,trackers,iou_threshold = 0.3): 135 | """ 136 | Assigns detections to tracked object (both represented as bounding boxes) 137 | 138 | Returns 3 lists of matches, unmatched_detections and unmatched_trackers 139 | """ 140 | if(len(trackers)==0): 141 | return np.empty((0,2),dtype=int), np.arange(len(detections)), np.empty((0,5),dtype=int) 142 | iou_matrix = np.zeros((len(detections),len(trackers)),dtype=np.float32) 143 | 144 | for d,det in enumerate(detections): 145 | for t,trk in enumerate(trackers): 146 | iou_matrix[d,t] = iou(det,trk) 147 | matched_indices = linear_assignment(-iou_matrix) 148 | 149 | unmatched_detections = [] 150 | for d,det in enumerate(detections): 151 | if(d not in matched_indices[:,0]): 152 | unmatched_detections.append(d) 153 | unmatched_trackers = [] 154 | for t,trk in enumerate(trackers): 155 | if(t not in matched_indices[:,1]): 156 | unmatched_trackers.append(t) 157 | 158 | #filter out matched with low IOU 159 | matches = [] 160 | for m in matched_indices: 161 | if(iou_matrix[m[0],m[1]]= self.min_hits or self.frame_count <= self.min_hits)): 224 | ret.append(np.concatenate((d,[trk.id+1])).reshape(1,-1)) # +1 as MOT benchmark requires positive 225 | i -= 1 226 | #remove dead tracklet 227 | if(trk.time_since_update > self.max_age): 228 | self.trackers.pop(i) 229 | if(len(ret)>0): 230 | return np.concatenate(ret) 231 | return np.empty((0,5)) 232 | 233 | def parse_args(): 234 | """Parse input arguments.""" 235 | parser = argparse.ArgumentParser(description='SORT demo') 236 | parser.add_argument('--display', dest='display', help='Display online tracker output (slow) [False]',action='store_true') 237 | args = parser.parse_args() 238 | return args 239 | 240 | if __name__ == '__main__': 241 | # all train 242 | sequences = ['PETS09-S2L1','TUD-Campus','TUD-Stadtmitte','ETH-Bahnhof','ETH-Sunnyday','ETH-Pedcross2','KITTI-13','KITTI-17','ADL-Rundle-6','ADL-Rundle-8','Venice-2'] 243 | args = parse_args() 244 | display = args.display 245 | phase = 'train' 246 | total_time = 0.0 247 | total_frames = 0 248 | colours = np.random.rand(32,3) #used only for display 249 | if(display): 250 | if not os.path.exists('mot_benchmark'): 251 | print('\n\tERROR: mot_benchmark link not found!\n\n Create a symbolic link to the MOT benchmark\n (https://motchallenge.net/data/2D_MOT_2015/#download). E.g.:\n\n $ ln -s /path/to/MOT2015_challenge/2DMOT2015 mot_benchmark\n\n') 252 | exit() 253 | plt.ion() 254 | fig = plt.figure() 255 | 256 | if not os.path.exists('output'): 257 | os.makedirs('output') 258 | 259 | for seq in sequences: 260 | mot_tracker = Sort() #create instance of the SORT tracker 261 | seq_dets = np.loadtxt('data/%s/det.txt'%(seq),delimiter=',') #load detections 262 | with open('output/%s.txt'%(seq),'w') as out_file: 263 | print("Processing %s."%(seq)) 264 | for frame in range(int(seq_dets[:,0].max())): 265 | frame += 1 #detection and frame numbers begin at 1 266 | dets = seq_dets[seq_dets[:,0]==frame,2:7] 267 | dets[:,2:4] += dets[:,0:2] #convert to [x1,y1,w,h] to [x1,y1,x2,y2] 268 | total_frames += 1 269 | 270 | if(display): 271 | ax1 = fig.add_subplot(111, aspect='equal') 272 | fn = 'mot_benchmark/%s/%s/img1/%06d.jpg'%(phase,seq,frame) 273 | im =io.imread(fn) 274 | ax1.imshow(im) 275 | plt.title(seq+' Tracked Targets') 276 | 277 | start_time = time.time() 278 | trackers = mot_tracker.update(dets) 279 | cycle_time = time.time() - start_time 280 | total_time += cycle_time 281 | 282 | for d in trackers: 283 | print('%d,%d,%.2f,%.2f,%.2f,%.2f,1,-1,-1,-1'%(frame,d[4],d[0],d[1],d[2]-d[0],d[3]-d[1]),file=out_file) 284 | if(display): 285 | d = d.astype(np.int32) 286 | ax1.add_patch(patches.Rectangle((d[0],d[1]),d[2]-d[0],d[3]-d[1],fill=False,lw=3,ec=colours[d[4]%32,:])) 287 | ax1.set_adjustable('box-forced') 288 | 289 | if(display): 290 | fig.canvas.flush_events() 291 | plt.draw() 292 | ax1.cla() 293 | 294 | print("Total Tracking took: %.3f for %d frames or %.1f FPS"%(total_time,total_frames,total_frames/total_time)) 295 | if(display): 296 | print("Note: to get real runtime results run without the option: --display") 297 | 298 | 299 | 300 | -------------------------------------------------------------------------------- /detector/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/detector/utils/__init__.py -------------------------------------------------------------------------------- /detector/utils/logger.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | 3 | 4 | class Logger(object): 5 | def __init__(self, log_dir): 6 | """Create a summary writer logging to log_dir.""" 7 | self.writer = tf.summary.FileWriter(log_dir) 8 | 9 | def scalar_summary(self, tag, value, step): 10 | """Log a scalar variable.""" 11 | summary = tf.Summary(value=[tf.Summary.Value(tag=tag, simple_value=value)]) 12 | self.writer.add_summary(summary, step) 13 | 14 | def list_of_scalars_summary(self, tag_value_pairs, step): 15 | """Log scalar variables.""" 16 | summary = tf.Summary(value=[tf.Summary.Value(tag=tag, simple_value=value) for tag, value in tag_value_pairs]) 17 | self.writer.add_summary(summary, step) 18 | -------------------------------------------------------------------------------- /detector/utils/parse_config.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def parse_model_config(path): 4 | """Parses the yolo-v3 layer configuration file and returns module definitions""" 5 | file = open(path, 'r') 6 | lines = file.read().split('\n') 7 | lines = [x for x in lines if x and not x.startswith('#')] 8 | lines = [x.rstrip().lstrip() for x in lines] # get rid of fringe whitespaces 9 | module_defs = [] 10 | for line in lines: 11 | if line.startswith('['): # This marks the start of a new block 12 | module_defs.append({}) 13 | module_defs[-1]['type'] = line[1:-1].rstrip() 14 | if module_defs[-1]['type'] == 'convolutional': 15 | module_defs[-1]['batch_normalize'] = 0 16 | else: 17 | key, value = line.split("=") 18 | value = value.strip() 19 | module_defs[-1][key.rstrip()] = value.strip() 20 | 21 | return module_defs 22 | 23 | def parse_data_config(path): 24 | """Parses the data configuration file""" 25 | options = dict() 26 | options['gpus'] = '0,1,2,3' 27 | options['num_workers'] = '10' 28 | with open(path, 'r') as fp: 29 | lines = fp.readlines() 30 | for line in lines: 31 | line = line.strip() 32 | if line == '' or line.startswith('#'): 33 | continue 34 | key, value = line.split('=') 35 | options[key.strip()] = value.strip() 36 | return options 37 | -------------------------------------------------------------------------------- /detector/utils/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import math 3 | import time 4 | import tqdm 5 | import torch 6 | import torch.nn as nn 7 | import torch.nn.functional as F 8 | from torch.autograd import Variable 9 | import numpy as np 10 | import matplotlib.pyplot as plt 11 | import matplotlib.patches as patches 12 | 13 | 14 | def to_cpu(tensor): 15 | return tensor.detach().cpu() 16 | 17 | def xywh2xyxy(x): 18 | y = x.new(x.shape) 19 | y[..., 0] = x[..., 0] - x[..., 2] / 2 20 | y[..., 1] = x[..., 1] - x[..., 3] / 2 21 | y[..., 2] = x[..., 0] + x[..., 2] / 2 22 | y[..., 3] = x[..., 1] + x[..., 3] / 2 23 | return y 24 | 25 | 26 | def bbox_wh_iou(wh1, wh2): 27 | wh2 = wh2.t() 28 | w1, h1 = wh1[0], wh1[1] 29 | w2, h2 = wh2[0], wh2[1] 30 | inter_area = torch.min(w1, w2) * torch.min(h1, h2) 31 | union_area = (w1 * h1 + 1e-16) + w2 * h2 - inter_area 32 | return inter_area / union_area 33 | 34 | 35 | def bbox_iou(box1, box2, x1y1x2y2=True): 36 | """ 37 | Returns the IoU of two bounding boxes 38 | """ 39 | if not x1y1x2y2: 40 | # Transform from center and width to exact coordinates 41 | b1_x1, b1_x2 = box1[:, 0] - box1[:, 2] / 2, box1[:, 0] + box1[:, 2] / 2 42 | b1_y1, b1_y2 = box1[:, 1] - box1[:, 3] / 2, box1[:, 1] + box1[:, 3] / 2 43 | b2_x1, b2_x2 = box2[:, 0] - box2[:, 2] / 2, box2[:, 0] + box2[:, 2] / 2 44 | b2_y1, b2_y2 = box2[:, 1] - box2[:, 3] / 2, box2[:, 1] + box2[:, 3] / 2 45 | else: 46 | # Get the coordinates of bounding boxes 47 | b1_x1, b1_y1, b1_x2, b1_y2 = box1[:, 0], box1[:, 1], box1[:, 2], box1[:, 3] 48 | b2_x1, b2_y1, b2_x2, b2_y2 = box2[:, 0], box2[:, 1], box2[:, 2], box2[:, 3] 49 | 50 | # get the corrdinates of the intersection rectangle 51 | inter_rect_x1 = torch.max(b1_x1, b2_x1) 52 | inter_rect_y1 = torch.max(b1_y1, b2_y1) 53 | inter_rect_x2 = torch.min(b1_x2, b2_x2) 54 | inter_rect_y2 = torch.min(b1_y2, b2_y2) 55 | # Intersection area 56 | inter_area = torch.clamp(inter_rect_x2 - inter_rect_x1 + 1, min=0) * torch.clamp( 57 | inter_rect_y2 - inter_rect_y1 + 1, min=0 58 | ) 59 | # Union Area 60 | b1_area = (b1_x2 - b1_x1 + 1) * (b1_y2 - b1_y1 + 1) 61 | b2_area = (b2_x2 - b2_x1 + 1) * (b2_y2 - b2_y1 + 1) 62 | 63 | iou = inter_area / (b1_area + b2_area - inter_area + 1e-16) 64 | 65 | return iou 66 | 67 | 68 | def non_max_suppression(prediction, conf_thres=0.5, nms_thres=0.4): 69 | """ 70 | Removes detections with lower object confidence score than 'conf_thres' and performs 71 | Non-Maximum Suppression to further filter detections. 72 | Returns detections with shape: 73 | (x1, y1, x2, y2, object_conf, class_score, class_pred) 74 | """ 75 | 76 | # From (center x, center y, width, height) to (x1, y1, x2, y2) 77 | prediction[..., :4] = xywh2xyxy(prediction[..., :4]) 78 | output = [None for _ in range(len(prediction))] 79 | for image_i, image_pred in enumerate(prediction): 80 | # Filter out confidence scores below threshold 81 | image_pred = image_pred[image_pred[:, 4] >= conf_thres] 82 | # If none are remaining => process next image 83 | if not image_pred.size(0): 84 | continue 85 | # Object confidence times class confidence 86 | score = image_pred[:, 4] * image_pred[:, 5:].max(1)[0] 87 | # Sort by it 88 | image_pred = image_pred[(-score).argsort()] 89 | class_confs, class_preds = image_pred[:, 5:].max(1, keepdim=True) 90 | detections = torch.cat((image_pred[:, :5], class_confs.float(), class_preds.float()), 1) 91 | # Perform non-maximum suppression 92 | keep_boxes = [] 93 | while detections.size(0): 94 | large_overlap = bbox_iou(detections[0, :4].unsqueeze(0), detections[:, :4]) > nms_thres 95 | label_match = detections[0, -1] == detections[:, -1] 96 | # Indices of boxes with lower confidence scores, large IOUs and matching labels 97 | invalid = large_overlap & label_match 98 | weights = detections[invalid, 4:5] 99 | # Merge overlapping bboxes by order of confidence 100 | detections[0, :4] = (weights * detections[invalid, :4]).sum(0) / weights.sum() 101 | keep_boxes += [detections[0]] 102 | detections = detections[~invalid] 103 | if keep_boxes: 104 | output[image_i] = torch.stack(keep_boxes) 105 | 106 | return output 107 | 108 | 109 | def build_targets(pred_boxes, pred_cls, target, anchors, ignore_thres): 110 | 111 | ByteTensor = torch.cuda.ByteTensor if pred_boxes.is_cuda else torch.ByteTensor 112 | FloatTensor = torch.cuda.FloatTensor if pred_boxes.is_cuda else torch.FloatTensor 113 | 114 | nB = pred_boxes.size(0) 115 | nA = pred_boxes.size(1) 116 | nC = pred_cls.size(-1) 117 | nG = pred_boxes.size(2) 118 | 119 | # Output tensors 120 | obj_mask = ByteTensor(nB, nA, nG, nG).fill_(0) 121 | noobj_mask = ByteTensor(nB, nA, nG, nG).fill_(1) 122 | class_mask = FloatTensor(nB, nA, nG, nG).fill_(0) 123 | iou_scores = FloatTensor(nB, nA, nG, nG).fill_(0) 124 | tx = FloatTensor(nB, nA, nG, nG).fill_(0) 125 | ty = FloatTensor(nB, nA, nG, nG).fill_(0) 126 | tw = FloatTensor(nB, nA, nG, nG).fill_(0) 127 | th = FloatTensor(nB, nA, nG, nG).fill_(0) 128 | tcls = FloatTensor(nB, nA, nG, nG, nC).fill_(0) 129 | 130 | # Convert to position relative to box 131 | 132 | target_boxes = target[:, 2:6] * nG 133 | # gxy = target_boxes[:, :2] 134 | gwh = target_boxes[:, 2:] 135 | gxy = torch.clamp(target_boxes[:, :2], 0, nG-1e-5) 136 | # Get anchors with best iou 137 | ious = torch.stack([bbox_wh_iou(anchor, gwh) for anchor in anchors]) 138 | best_ious, best_n = ious.max(0) 139 | # Separate target values 140 | b, target_labels = target[:, :2].long().t() 141 | gx, gy = gxy.t() 142 | gw, gh = gwh.t() 143 | gi, gj = gxy.long().t() 144 | ########## TODO(arthur77wang): 145 | gi[gi < 0] = 0 146 | gj[gj < 0] = 0 147 | gi[gi > nG - 1] = nG - 1 148 | gj[gj > nG - 1] = nG - 1 149 | ################### 150 | # Set masks 151 | obj_mask[b, best_n, gj, gi] = 1 152 | noobj_mask[b, best_n, gj, gi] = 0 153 | # b, target_labels = target[:, :2].long().t() 154 | # gx, gy = gxy.t() 155 | # gw, gh = gwh.t() 156 | # gi, gj = gxy.long().t() 157 | # # Set masks 158 | # gi = torch.clamp(gi, 0, noobj_mask.size()[2]-1) 159 | # gj = torch.clamp(gj, 0, noobj_mask.size()[3]-1) 160 | # obj_mask[b, best_n, gj, gi] = 1 161 | # noobj_mask[b, best_n, gj, gi] = 0 162 | 163 | 164 | # Set noobj mask to zero where iou exceeds ignore threshold 165 | for i, anchor_ious in enumerate(ious.t()): 166 | noobj_mask[b[i], anchor_ious > ignore_thres, gj[i], gi[i]] = 0 167 | 168 | # Coordinates 169 | tx[b, best_n, gj, gi] = gx - gx.floor() 170 | ty[b, best_n, gj, gi] = gy - gy.floor() 171 | # Width and height 172 | tw[b, best_n, gj, gi] = torch.log(gw / anchors[best_n][:, 0] + 1e-16) 173 | th[b, best_n, gj, gi] = torch.log(gh / anchors[best_n][:, 1] + 1e-16) 174 | # One-hot encoding of label 175 | tcls[b, best_n, gj, gi, target_labels] = 1 176 | # Compute label correctness and iou at best anchor 177 | class_mask[b, best_n, gj, gi] = (pred_cls[b, best_n, gj, gi].argmax(-1) == target_labels).float() 178 | iou_scores[b, best_n, gj, gi] = bbox_iou(pred_boxes[b, best_n, gj, gi], target_boxes, x1y1x2y2=False) 179 | 180 | tconf = obj_mask.float() 181 | return iou_scores, class_mask, obj_mask, noobj_mask, tx, ty, tw, th, tcls, tconf 182 | -------------------------------------------------------------------------------- /http_stream.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | import sys 4 | import torch 5 | import random 6 | import datetime 7 | import argparse 8 | import traceback 9 | import numpy as np 10 | import configparser 11 | 12 | import reader.PlateReader 13 | import detector.PlateDetector 14 | import urllib.request, urllib.parse 15 | 16 | from PIL import Image 17 | from art import tprint 18 | from detector.sort import Sort 19 | 20 | config = configparser.ConfigParser() 21 | try: 22 | config.read('config.ini') 23 | except: 24 | print('Configuration file not found :(') 25 | sys.exit(1) 26 | 27 | tprint('DeepALPR',font='slant') 28 | print(datetime.datetime.now().strftime("%d-%b-%Y (%H:%M:%S)")) 29 | print('Author: Gabriel Lefundes Vieira (Novandrie)') 30 | print('IVision Research Lab - Universidade Federal da Bahia @ 2019') 31 | print('Project Page: https://github.com/glefundes/AccessALPR') 32 | print('=============================\n') 33 | 34 | INPUT_SIZE = int(config['DEFAULT']['InputSize']) 35 | READER_WEIGHTS = config['DEFAULT']['ReaderWeights'] 36 | DETECTOR_CFG = config['DEFAULT']['DetectorConfig'] 37 | PLATE_OUTPUT_ROOT = config['DEFAULT']['LogFolder'] 38 | DETECTOR_WEIGHTS = config['DEFAULT']['DetectorWeights'] 39 | 40 | URL = config['STREAM']['Url'] 41 | ANCHOR = tuple([int(c) for c in config['STREAM']['Anchor'].split(',')]) 42 | USERNAME = config['STREAM']['Username'] 43 | PASSWORD = config['STREAM']['Password'] 44 | PADDING = 1 45 | 46 | print('Loading detection model and tracker...') 47 | try: 48 | plate_detector = detector.PlateDetector.PlateDetector(DETECTOR_CFG, DETECTOR_WEIGHTS, input_size=INPUT_SIZE, conf_thresh=0.4) 49 | tracker = Sort() 50 | except Exception as e: 51 | print('\n Failed on loading plate detector :( Please check stack trace:\n') 52 | traceback.print_exc() 53 | sys.exit(1) 54 | 55 | print('Loading OCR model...') 56 | try: 57 | plate_reader = reader.PlateReader.PlateReader() 58 | plate_reader.load_state_dict(torch.load(READER_WEIGHTS)) 59 | plate_reader.eval() 60 | except Exception as e: 61 | print('\n Failed on loading plate reader :( Please check stack trace:\n') 62 | traceback.print_exc() 63 | sys.exit(1) 64 | 65 | print('Opening {} with authentication for: {}'.format(URL, USERNAME)) 66 | try: 67 | base_url = urllib.parse.urlparse(URL) 68 | pw = urllib.request.HTTPPasswordMgrWithDefaultRealm() 69 | pw.add_password(None, base_url, USERNAME, PASSWORD) 70 | handler = urllib.request.HTTPBasicAuthHandler(pw) 71 | opener = urllib.request.build_opener(handler) 72 | opener.open(URL) 73 | urllib.request.install_opener(opener) 74 | except Exception as e: 75 | print('\n Failed on opening HTTP stream :( Please check stack trace:\n') 76 | traceback.print_exc() 77 | sys.exit(1) 78 | 79 | print('All done! Starting ALPR system...') 80 | print('Logging files to: {}'.format(PLATE_OUTPUT_ROOT)) 81 | print('Have fun!') 82 | n = 1 83 | SKIP_FRAME = 1 84 | 85 | current_tracker_id = 0 86 | img_log_folder = os.path.join(PLATE_OUTPUT_ROOT, 'temp:{}-{}'.format(current_tracker_id, random.randint(1, 1999))) 87 | os.mkdir(img_log_folder) 88 | 89 | track_preds = [] 90 | while(True): 91 | try: 92 | imgResp = urllib.request.urlopen(URL) 93 | imgNp = np.array(bytearray(imgResp.read()),dtype=np.uint8) 94 | frame = cv2.imdecode(imgNp,-1) 95 | if(frame is not None): 96 | display = np.copy(frame) 97 | if not(n % SKIP_FRAME): 98 | n = 1 99 | # Crop ROI square 100 | frame = frame[ANCHOR[1]:ANCHOR[1]+(INPUT_SIZE), ANCHOR[0]:ANCHOR[0]+(INPUT_SIZE)] 101 | # Plate detection 102 | yolo_frame = frame.copy() 103 | bbox = plate_detector.predict(yolo_frame) 104 | bbox = tracker.update(bbox) 105 | # Detection processing 106 | if (len(bbox) != 0): 107 | x1, y1, x2, y2, det_id = [int(d) for d in bbox[0]] 108 | # Creates folder for img storing if track id is new 109 | if current_tracker_id < det_id: 110 | print('Logging track {}...'.format(det_id)) 111 | current_tracker_id = det_id 112 | # Majority vote on previous detection 113 | if(len(track_preds) != 0): 114 | mv = plate_reader.majority_vote(track_preds) 115 | timestamp = datetime.datetime.now().strftime("%d-%b-%Y(%H:%M:%S)") 116 | os.rename(img_log_folder, os.path.join(PLATE_OUTPUT_ROOT, '{}{}'.format(timestamp,mv))) 117 | # Start new log location 118 | track_preds = [] 119 | img_log_folder = os.path.join(PLATE_OUTPUT_ROOT, 'temp:{}'.format(current_tracker_id)) 120 | os.mkdir(img_log_folder) 121 | 122 | plate = frame[int(y1-PADDING):int(y2+PADDING),int(x1-PADDING):int(x2+PADDING)] 123 | try: 124 | # Plate OCR reading 125 | pil_plate = Image.fromarray(plate) 126 | word = plate_reader.predict(pil_plate) 127 | track_preds.append(word) 128 | timestamp = datetime.datetime.now().strftime("%d-%b-%Y(%H:%M:%S)") 129 | cv2.imwrite(os.path.join(img_log_folder, '{}_{}_{}.jpg'.format(len(track_preds), word, timestamp)), plate) 130 | except Exception as e: 131 | traceback.print_exc() 132 | continue 133 | 134 | frame = cv2.rectangle(frame, (x1-PADDING,y1-PADDING), (x2+PADDING,y2+PADDING), (255,255,0),thickness=2) 135 | #Display the resulting frame 136 | display[ANCHOR[1]:ANCHOR[1]+(INPUT_SIZE), ANCHOR[0]:ANCHOR[0]+INPUT_SIZE] = frame 137 | else: 138 | n +=1 139 | 140 | display = cv2.rectangle(display, (ANCHOR), (ANCHOR[0]+INPUT_SIZE, ANCHOR[1]+INPUT_SIZE), (0,0,255), thickness=2) 141 | cv2.imshow('frame',display) 142 | 143 | except Exception as e: 144 | cv2.destroyAllWindows() 145 | traceback.print_exc() 146 | sys.exit(1) 147 | 148 | if cv2.waitKey(1) & 0xFF == ord('q'): 149 | break 150 | 151 | cv2.destroyAllWindows() 152 | -------------------------------------------------------------------------------- /lev_weights.csv: -------------------------------------------------------------------------------- 1 | gt,sub,cost 2 | 0,1,0.8461538461538461 3 | 0,8,0.9137931034482758 4 | 0,9,0.9305555555555556 5 | 1,0,0.9242424242424242 6 | 1,7,0.9180327868852459 7 | 2,3,0.9253731343283582 8 | 2,9,0.8611111111111112 9 | 3,2,0.8529411764705882 10 | 3,5,0.9242424242424242 11 | 4,1,0.8461538461538461 12 | 4,2,0.9264705882352942 13 | 4,5,0.9242424242424242 14 | 4,7,0.7540983606557377 15 | 5,4,0.9285714285714286 16 | 5,6,0.927536231884058 17 | 6,0,0.9242424242424242 18 | 6,4,0.9285714285714286 19 | 6,5,0.9242424242424242 20 | 6,9,0.8611111111111112 21 | 8,0,0.9242424242424242 22 | 8,1,0.9230769230769231 23 | 8,3,0.9253731343283582 24 | 8,6,0.927536231884058 25 | 9,0,0.9242424242424242 26 | 9,2,0.9264705882352942 27 | 9,3,0.9253731343283582 28 | 9,5,0.9242424242424242 29 | A,O,0.9 30 | C,G,0.5833333333333334 31 | C,L,0.8 32 | D,B,0.2857142857142858 33 | D,O,0.9 34 | D,T,0.375 35 | F,E,0.6153846153846154 36 | I,T,0.375 37 | J,D,0.6153846153846154 38 | J,I,0.16666666666666674 39 | J,T,0.375 40 | K,R,0.16666666666666674 41 | K,V,0.5454545454545454 42 | L,C,0.5 43 | M,W,0.16666666666666674 44 | O,Q,0.6428571428571429 45 | P,O,0.9 46 | Q,O,0.7 47 | Q,U,0.7222222222222222 48 | T,I,0.16666666666666674 49 | W,M,0.4444444444444444 50 | Y,V,0.5454545454545454 51 | -------------------------------------------------------------------------------- /reader/PlateReader.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import torch 3 | import torch.nn as nn 4 | import torch.nn.functional as F 5 | 6 | import numpy as np 7 | 8 | from torchvision import transforms 9 | from PIL import Image 10 | 11 | from reader.efficientnet import * 12 | import reader.utils as utils 13 | 14 | class Conv_block(nn.Module): 15 | def __init__(self,in_c,out_c,kernel=[2,2],stride=[1,1],padding=(1,1),groups=1): 16 | super(Conv_block, self).__init__() 17 | self.conv = nn.Conv2d(in_c, out_c, kernel_size=kernel, stride=stride, padding=padding, groups=groups,bias=False) 18 | self.batchnorm = nn.BatchNorm2d(out_c) 19 | self.relu = nn.ReLU(out_c) 20 | 21 | def forward(self, x): 22 | x = self.conv(x) 23 | x = self.batchnorm(x) 24 | x = self.relu(x) 25 | return x 26 | 27 | class PlateReader(nn.Module): 28 | def __init__(self, training=False, filter=None): 29 | super().__init__() 30 | use_cuda = torch.cuda.is_available() and True 31 | self.training = training 32 | self.device = torch.device("cuda" if use_cuda else "cpu") 33 | self.dropout_rate = 0.4 34 | self.filter = filter 35 | if(filter): 36 | self.preprocessor = utils.PreProcessor() 37 | self.backbone = EfficientNet.from_pretrained('efficientnet-b0') 38 | 39 | 40 | self.transf = transforms.Compose([transforms.Resize((50,120),interpolation=2), 41 | transforms.ToTensor(), 42 | transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),]) 43 | 44 | self.conv = Conv_block(1280, 512) 45 | self.fc = nn.Linear(512,252) 46 | 47 | self.to(self.device) 48 | self.backbone.to(self.device) 49 | 50 | def forward(self, x): 51 | x = x.to(self.device).float() #NCHW 52 | x = self.backbone.extract_features(x) 53 | x = self.conv(x) 54 | 55 | x = F.adaptive_avg_pool2d(x, 1).squeeze(-1).squeeze(-1) 56 | x = F.dropout(x, p=self.dropout_rate, training=self.training) 57 | x = self.fc(x) 58 | x = x.view(-1,7,36) 59 | 60 | return x 61 | 62 | def preprocess(self, img, filter=None): 63 | if filter: 64 | img = np.array(img) 65 | if filter.upper() == 'CLAHE': 66 | img = self.preprocessor.clahe_adaptive_histogram_equalization(img) 67 | elif filter.upper() == 'RETINEX-CP': 68 | img = self.preprocessor.MSRCP(img) 69 | elif filter.upper() == 'RETINEX-CR': 70 | img = self.preprocessor.MSRCR(img) 71 | elif filter.upper() == 'RETINEX-AUTO': 72 | img = self.preprocessor.automatedMSRCR(img) 73 | 74 | img = Image.fromarray(img) 75 | return img 76 | 77 | def predict(self, img): 78 | img = self.preprocess(img,self.filter) 79 | 80 | x = self.transf(img) 81 | x = self(x[np.newaxis,:,:,:]) 82 | x = utils.translate_prediction(x) 83 | 84 | return x 85 | 86 | def majority_vote(self, words): 87 | result = [] 88 | maj_vote_counts = [{},{},{},{},{},{},{}] 89 | maj_vote_candidate = '' 90 | for word in words: 91 | for i in range(len(word)): 92 | # Update count dictionary 93 | if word[i] in maj_vote_counts[i]: maj_vote_counts[i][word[i]] += 1 94 | else: maj_vote_counts[i][str(word[i])] = 1 95 | # Get most frequent occurence 96 | for i in range(7): 97 | choice = max(maj_vote_counts[i], key=lambda key: maj_vote_counts[i][key]) 98 | result.append(choice) 99 | 100 | maj_vote_candidate = "".join(result) 101 | return maj_vote_candidate 102 | 103 | -------------------------------------------------------------------------------- /reader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/reader/__init__.py -------------------------------------------------------------------------------- /reader/efficientnet.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch import nn 3 | from torch.nn import functional as F 4 | 5 | import re 6 | import math 7 | import collections 8 | from functools import partial 9 | from torch.utils import model_zoo 10 | 11 | 12 | ######################################################################## 13 | ############### HELPERS FUNCTIONS FOR MODEL ARCHITECTURE ############### 14 | ######################################################################## 15 | 16 | 17 | # Parameters for the entire model (stem, all blocks, and head) 18 | GlobalParams = collections.namedtuple('GlobalParams', [ 19 | 'batch_norm_momentum', 'batch_norm_epsilon', 'dropout_rate', 20 | 'num_classes', 'width_coefficient', 'depth_coefficient', 21 | 'depth_divisor', 'min_depth', 'drop_connect_rate', 'image_size']) 22 | 23 | 24 | # Parameters for an individual model block 25 | BlockArgs = collections.namedtuple('BlockArgs', [ 26 | 'kernel_size', 'num_repeat', 'input_filters', 'output_filters', 27 | 'expand_ratio', 'id_skip', 'stride', 'se_ratio']) 28 | 29 | 30 | # Change namedtuple defaults 31 | GlobalParams.__new__.__defaults__ = (None,) * len(GlobalParams._fields) 32 | BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) 33 | 34 | 35 | def relu_fn(x): 36 | """ Swish activation function """ 37 | return x * torch.sigmoid(x) 38 | 39 | 40 | def round_filters(filters, global_params): 41 | """ Calculate and round number of filters based on depth multiplier. """ 42 | multiplier = global_params.width_coefficient 43 | if not multiplier: 44 | return filters 45 | divisor = global_params.depth_divisor 46 | min_depth = global_params.min_depth 47 | filters *= multiplier 48 | min_depth = min_depth or divisor 49 | new_filters = max(min_depth, int(filters + divisor / 2) // divisor * divisor) 50 | if new_filters < 0.9 * filters: # prevent rounding by more than 10% 51 | new_filters += divisor 52 | return int(new_filters) 53 | 54 | 55 | def round_repeats(repeats, global_params): 56 | """ Round number of filters based on depth multiplier. """ 57 | multiplier = global_params.depth_coefficient 58 | if not multiplier: 59 | return repeats 60 | return int(math.ceil(multiplier * repeats)) 61 | 62 | 63 | def drop_connect(inputs, p, training): 64 | """ Drop connect. """ 65 | if not training: return inputs 66 | batch_size = inputs.shape[0] 67 | keep_prob = 1 - p 68 | random_tensor = keep_prob 69 | random_tensor += torch.rand([batch_size, 1, 1, 1], dtype=inputs.dtype, device=inputs.device) 70 | binary_tensor = torch.floor(random_tensor) 71 | output = inputs / keep_prob * binary_tensor 72 | return output 73 | 74 | 75 | def get_same_padding_conv2d(image_size=None): 76 | """ Chooses static padding if you have specified an image size, and dynamic padding otherwise. 77 | Static padding is necessary for ONNX exporting of models. """ 78 | if image_size is None: 79 | return Conv2dDynamicSamePadding 80 | else: 81 | return partial(Conv2dStaticSamePadding, image_size=image_size) 82 | 83 | class Conv2dDynamicSamePadding(nn.Conv2d): 84 | """ 2D Convolutions like TensorFlow, for a dynamic image size """ 85 | def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, groups=1, bias=True): 86 | super().__init__(in_channels, out_channels, kernel_size, stride, 0, dilation, groups, bias) 87 | self.stride = self.stride if len(self.stride) == 2 else [self.stride[0]]*2 88 | 89 | def forward(self, x): 90 | ih, iw = x.size()[-2:] 91 | kh, kw = self.weight.size()[-2:] 92 | sh, sw = self.stride 93 | oh, ow = math.ceil(ih / sh), math.ceil(iw / sw) 94 | pad_h = max((oh - 1) * self.stride[0] + (kh - 1) * self.dilation[0] + 1 - ih, 0) 95 | pad_w = max((ow - 1) * self.stride[1] + (kw - 1) * self.dilation[1] + 1 - iw, 0) 96 | if pad_h > 0 or pad_w > 0: 97 | x = F.pad(x, [pad_w//2, pad_w - pad_w//2, pad_h//2, pad_h - pad_h//2]) 98 | return F.conv2d(x, self.weight, self.bias, self.stride, self.padding, self.dilation, self.groups) 99 | 100 | 101 | class Conv2dStaticSamePadding(nn.Conv2d): 102 | """ 2D Convolutions like TensorFlow, for a fixed image size""" 103 | def __init__(self, in_channels, out_channels, kernel_size, image_size=None, **kwargs): 104 | super().__init__(in_channels, out_channels, kernel_size, **kwargs) 105 | self.stride = self.stride if len(self.stride) == 2 else [self.stride[0]] * 2 106 | 107 | # Calculate padding based on image size and save it 108 | assert image_size is not None 109 | ih, iw = image_size if type(image_size) == list else [image_size, image_size] 110 | kh, kw = self.weight.size()[-2:] 111 | sh, sw = self.stride 112 | oh, ow = math.ceil(ih / sh), math.ceil(iw / sw) 113 | pad_h = max((oh - 1) * self.stride[0] + (kh - 1) * self.dilation[0] + 1 - ih, 0) 114 | pad_w = max((ow - 1) * self.stride[1] + (kw - 1) * self.dilation[1] + 1 - iw, 0) 115 | if pad_h > 0 or pad_w > 0: 116 | self.static_padding = nn.ZeroPad2d((pad_w // 2, pad_w - pad_w // 2, pad_h // 2, pad_h - pad_h // 2)) 117 | else: 118 | self.static_padding = Identity() 119 | 120 | def forward(self, x): 121 | x = self.static_padding(x) 122 | x = F.conv2d(x, self.weight, self.bias, self.stride, self.padding, self.dilation, self.groups) 123 | return x 124 | 125 | 126 | class Identity(nn.Module): 127 | def __init__(self,): 128 | super(Identity, self).__init__() 129 | 130 | def forward(self, input): 131 | return input 132 | 133 | 134 | ######################################################################## 135 | ############## HELPERS FUNCTIONS FOR LOADING MODEL PARAMS ############## 136 | ######################################################################## 137 | 138 | 139 | def efficientnet_params(model_name): 140 | """ Map EfficientNet model name to parameter coefficients. """ 141 | params_dict = { 142 | # Coefficients: width,depth,res,dropout 143 | 'efficientnet-b0': (1.0, 1.0, 224, 0.2), 144 | 'efficientnet-b1': (1.0, 1.1, 240, 0.2), 145 | 'efficientnet-b2': (1.1, 1.2, 260, 0.3), 146 | 'efficientnet-b3': (1.2, 1.4, 300, 0.3), 147 | 'efficientnet-b4': (1.4, 1.8, 380, 0.4), 148 | 'efficientnet-b5': (1.6, 2.2, 456, 0.4), 149 | 'efficientnet-b6': (1.8, 2.6, 528, 0.5), 150 | 'efficientnet-b7': (2.0, 3.1, 600, 0.5), 151 | } 152 | return params_dict[model_name] 153 | 154 | 155 | class BlockDecoder(object): 156 | """ Block Decoder for readability, straight from the official TensorFlow repository """ 157 | 158 | @staticmethod 159 | def _decode_block_string(block_string): 160 | """ Gets a block through a string notation of arguments. """ 161 | assert isinstance(block_string, str) 162 | 163 | ops = block_string.split('_') 164 | options = {} 165 | for op in ops: 166 | splits = re.split(r'(\d.*)', op) 167 | if len(splits) >= 2: 168 | key, value = splits[:2] 169 | options[key] = value 170 | 171 | # Check stride 172 | assert (('s' in options and len(options['s']) == 1) or 173 | (len(options['s']) == 2 and options['s'][0] == options['s'][1])) 174 | 175 | return BlockArgs( 176 | kernel_size=int(options['k']), 177 | num_repeat=int(options['r']), 178 | input_filters=int(options['i']), 179 | output_filters=int(options['o']), 180 | expand_ratio=int(options['e']), 181 | id_skip=('noskip' not in block_string), 182 | se_ratio=float(options['se']) if 'se' in options else None, 183 | stride=[int(options['s'][0])]) 184 | 185 | @staticmethod 186 | def _encode_block_string(block): 187 | """Encodes a block to a string.""" 188 | args = [ 189 | 'r%d' % block.num_repeat, 190 | 'k%d' % block.kernel_size, 191 | 's%d%d' % (block.strides[0], block.strides[1]), 192 | 'e%s' % block.expand_ratio, 193 | 'i%d' % block.input_filters, 194 | 'o%d' % block.output_filters 195 | ] 196 | if 0 < block.se_ratio <= 1: 197 | args.append('se%s' % block.se_ratio) 198 | if block.id_skip is False: 199 | args.append('noskip') 200 | return '_'.join(args) 201 | 202 | @staticmethod 203 | def decode(string_list): 204 | """ 205 | Decodes a list of string notations to specify blocks inside the network. 206 | 207 | :param string_list: a list of strings, each string is a notation of block 208 | :return: a list of BlockArgs namedtuples of block args 209 | """ 210 | assert isinstance(string_list, list) 211 | blocks_args = [] 212 | for block_string in string_list: 213 | blocks_args.append(BlockDecoder._decode_block_string(block_string)) 214 | return blocks_args 215 | 216 | @staticmethod 217 | def encode(blocks_args): 218 | """ 219 | Encodes a list of BlockArgs to a list of strings. 220 | 221 | :param blocks_args: a list of BlockArgs namedtuples of block args 222 | :return: a list of strings, each string is a notation of block 223 | """ 224 | block_strings = [] 225 | for block in blocks_args: 226 | block_strings.append(BlockDecoder._encode_block_string(block)) 227 | return block_strings 228 | 229 | 230 | def efficientnet(width_coefficient=None, depth_coefficient=None, dropout_rate=0.2, 231 | drop_connect_rate=0.2, image_size=None, num_classes=1000): 232 | """ Creates a efficientnet model. """ 233 | 234 | blocks_args = [ 235 | 'r1_k3_s11_e1_i32_o16_se0.25', 'r2_k3_s22_e6_i16_o24_se0.25', 236 | 'r2_k5_s22_e6_i24_o40_se0.25', 'r3_k3_s22_e6_i40_o80_se0.25', 237 | 'r3_k5_s11_e6_i80_o112_se0.25', 'r4_k5_s22_e6_i112_o192_se0.25', 238 | 'r1_k3_s11_e6_i192_o320_se0.25', 239 | ] 240 | blocks_args = BlockDecoder.decode(blocks_args) 241 | 242 | global_params = GlobalParams( 243 | batch_norm_momentum=0.99, 244 | batch_norm_epsilon=1e-3, 245 | dropout_rate=dropout_rate, 246 | drop_connect_rate=drop_connect_rate, 247 | # data_format='channels_last', # removed, this is always true in PyTorch 248 | num_classes=num_classes, 249 | width_coefficient=width_coefficient, 250 | depth_coefficient=depth_coefficient, 251 | depth_divisor=8, 252 | min_depth=None, 253 | image_size=image_size, 254 | ) 255 | 256 | return blocks_args, global_params 257 | 258 | 259 | def get_model_params(model_name, override_params): 260 | """ Get the block args and global params for a given model """ 261 | if model_name.startswith('efficientnet'): 262 | w, d, s, p = efficientnet_params(model_name) 263 | # note: all models have drop connect rate = 0.2 264 | blocks_args, global_params = efficientnet( 265 | width_coefficient=w, depth_coefficient=d, dropout_rate=p, image_size=s) 266 | else: 267 | raise NotImplementedError('model name is not pre-defined: %s' % model_name) 268 | if override_params: 269 | # ValueError will be raised here if override_params has fields not included in global_params. 270 | global_params = global_params._replace(**override_params) 271 | return blocks_args, global_params 272 | 273 | 274 | url_map = { 275 | 'efficientnet-b0': 'http://storage.googleapis.com/public-models/efficientnet/efficientnet-b0-355c32eb.pth', 276 | 'efficientnet-b1': 'http://storage.googleapis.com/public-models/efficientnet/efficientnet-b1-f1951068.pth', 277 | 'efficientnet-b2': 'http://storage.googleapis.com/public-models/efficientnet/efficientnet-b2-8bb594d6.pth', 278 | 'efficientnet-b3': 'http://storage.googleapis.com/public-models/efficientnet/efficientnet-b3-5fb5a3c3.pth', 279 | 'efficientnet-b4': 'http://storage.googleapis.com/public-models/efficientnet/efficientnet-b4-6ed6700e.pth', 280 | 'efficientnet-b5': 'http://storage.googleapis.com/public-models/efficientnet/efficientnet-b5-b6417697.pth', 281 | 'efficientnet-b6': 'http://storage.googleapis.com/public-models/efficientnet/efficientnet-b6-c76e70fd.pth', 282 | 'efficientnet-b7': 'http://storage.googleapis.com/public-models/efficientnet/efficientnet-b7-dcc49843.pth', 283 | } 284 | 285 | def load_pretrained_weights(model, model_name, load_fc=True): 286 | """ Loads pretrained weights, and downloads if loading for the first time. """ 287 | state_dict = model_zoo.load_url(url_map[model_name]) 288 | if load_fc: 289 | model.load_state_dict(state_dict) 290 | else: 291 | state_dict.pop('_fc.weight') 292 | state_dict.pop('_fc.bias') 293 | res = model.load_state_dict(state_dict, strict=False) 294 | assert str(res.missing_keys) == str(['_fc.weight', '_fc.bias']), 'issue loading pretrained weights' 295 | # print('Loaded pretrained weights for {}'.format(model_name)) 296 | 297 | 298 | class MBConvBlock(nn.Module): 299 | """ 300 | Mobile Inverted Residual Bottleneck Block 301 | 302 | Args: 303 | block_args (namedtuple): BlockArgs, see above 304 | global_params (namedtuple): GlobalParam, see above 305 | 306 | Attributes: 307 | has_se (bool): Whether the block contains a Squeeze and Excitation layer. 308 | """ 309 | 310 | def __init__(self, block_args, global_params): 311 | super().__init__() 312 | self._block_args = block_args 313 | self._bn_mom = 1 - global_params.batch_norm_momentum 314 | self._bn_eps = global_params.batch_norm_epsilon 315 | self.has_se = (self._block_args.se_ratio is not None) and (0 < self._block_args.se_ratio <= 1) 316 | self.id_skip = block_args.id_skip # skip connection and drop connect 317 | 318 | # Get static or dynamic convolution depending on image size 319 | Conv2d = get_same_padding_conv2d(image_size=global_params.image_size) 320 | 321 | # Expansion phase 322 | inp = self._block_args.input_filters # number of input channels 323 | oup = self._block_args.input_filters * self._block_args.expand_ratio # number of output channels 324 | if self._block_args.expand_ratio != 1: 325 | self._expand_conv = Conv2d(in_channels=inp, out_channels=oup, kernel_size=1, bias=False) 326 | self._bn0 = nn.BatchNorm2d(num_features=oup, momentum=self._bn_mom, eps=self._bn_eps) 327 | 328 | # Depthwise convolution phase 329 | k = self._block_args.kernel_size 330 | s = self._block_args.stride 331 | self._depthwise_conv = Conv2d( 332 | in_channels=oup, out_channels=oup, groups=oup, # groups makes it depthwise 333 | kernel_size=k, stride=s, bias=False) 334 | self._bn1 = nn.BatchNorm2d(num_features=oup, momentum=self._bn_mom, eps=self._bn_eps) 335 | 336 | # Squeeze and Excitation layer, if desired 337 | if self.has_se: 338 | num_squeezed_channels = max(1, int(self._block_args.input_filters * self._block_args.se_ratio)) 339 | self._se_reduce = Conv2d(in_channels=oup, out_channels=num_squeezed_channels, kernel_size=1) 340 | self._se_expand = Conv2d(in_channels=num_squeezed_channels, out_channels=oup, kernel_size=1) 341 | 342 | # Output phase 343 | final_oup = self._block_args.output_filters 344 | self._project_conv = Conv2d(in_channels=oup, out_channels=final_oup, kernel_size=1, bias=False) 345 | self._bn2 = nn.BatchNorm2d(num_features=final_oup, momentum=self._bn_mom, eps=self._bn_eps) 346 | 347 | def forward(self, inputs, drop_connect_rate=None): 348 | """ 349 | :param inputs: input tensor 350 | :param drop_connect_rate: drop connect rate (float, between 0 and 1) 351 | :return: output of block 352 | """ 353 | 354 | # Expansion and Depthwise Convolution 355 | x = inputs 356 | if self._block_args.expand_ratio != 1: 357 | x = relu_fn(self._bn0(self._expand_conv(inputs))) 358 | x = relu_fn(self._bn1(self._depthwise_conv(x))) 359 | 360 | # Squeeze and Excitation 361 | if self.has_se: 362 | x_squeezed = F.adaptive_avg_pool2d(x, 1) 363 | x_squeezed = self._se_expand(relu_fn(self._se_reduce(x_squeezed))) 364 | x = torch.sigmoid(x_squeezed) * x 365 | 366 | x = self._bn2(self._project_conv(x)) 367 | 368 | # Skip connection and drop connect 369 | input_filters, output_filters = self._block_args.input_filters, self._block_args.output_filters 370 | if self.id_skip and self._block_args.stride == 1 and input_filters == output_filters: 371 | if drop_connect_rate: 372 | x = drop_connect(x, p=drop_connect_rate, training=self.training) 373 | x = x + inputs # skip connection 374 | return x 375 | 376 | 377 | class EfficientNet(nn.Module): 378 | """ 379 | An EfficientNet model. Most easily loaded with the .from_name or .from_pretrained methods 380 | 381 | Args: 382 | blocks_args (list): A list of BlockArgs to construct blocks 383 | global_params (namedtuple): A set of GlobalParams shared between blocks 384 | 385 | Example: 386 | model = EfficientNet.from_pretrained('efficientnet-b0') 387 | 388 | """ 389 | 390 | def __init__(self, blocks_args=None, global_params=None): 391 | super().__init__() 392 | assert isinstance(blocks_args, list), 'blocks_args should be a list' 393 | assert len(blocks_args) > 0, 'block args must be greater than 0' 394 | self._global_params = global_params 395 | self._blocks_args = blocks_args 396 | 397 | # Get static or dynamic convolution depending on image size 398 | Conv2d = get_same_padding_conv2d(image_size=global_params.image_size) 399 | 400 | # Batch norm parameters 401 | bn_mom = 1 - self._global_params.batch_norm_momentum 402 | bn_eps = self._global_params.batch_norm_epsilon 403 | 404 | # Stem 405 | in_channels = 3 # rgb 406 | out_channels = round_filters(32, self._global_params) # number of output channels 407 | self._conv_stem = Conv2d(in_channels, out_channels, kernel_size=3, stride=2, bias=False) 408 | self._bn0 = nn.BatchNorm2d(num_features=out_channels, momentum=bn_mom, eps=bn_eps) 409 | 410 | # Build blocks 411 | self._blocks = nn.ModuleList([]) 412 | for block_args in self._blocks_args: 413 | 414 | # Update block input and output filters based on depth multiplier. 415 | block_args = block_args._replace( 416 | input_filters=round_filters(block_args.input_filters, self._global_params), 417 | output_filters=round_filters(block_args.output_filters, self._global_params), 418 | num_repeat=round_repeats(block_args.num_repeat, self._global_params) 419 | ) 420 | 421 | # The first block needs to take care of stride and filter size increase. 422 | self._blocks.append(MBConvBlock(block_args, self._global_params)) 423 | if block_args.num_repeat > 1: 424 | block_args = block_args._replace(input_filters=block_args.output_filters, stride=1) 425 | for _ in range(block_args.num_repeat - 1): 426 | self._blocks.append(MBConvBlock(block_args, self._global_params)) 427 | 428 | # Head 429 | in_channels = block_args.output_filters # output of final block 430 | out_channels = round_filters(1280, self._global_params) 431 | self._conv_head = Conv2d(in_channels, out_channels, kernel_size=1, bias=False) 432 | self._bn1 = nn.BatchNorm2d(num_features=out_channels, momentum=bn_mom, eps=bn_eps) 433 | 434 | # Final linear layer 435 | self._dropout = self._global_params.dropout_rate 436 | self._fc = nn.Linear(out_channels, self._global_params.num_classes) 437 | 438 | def extract_features(self, inputs): 439 | """ Returns output of the final convolution layer """ 440 | 441 | # Stem 442 | x = relu_fn(self._bn0(self._conv_stem(inputs))) 443 | 444 | # Blocks 445 | for idx, block in enumerate(self._blocks): 446 | drop_connect_rate = self._global_params.drop_connect_rate 447 | if drop_connect_rate: 448 | drop_connect_rate *= float(idx) / len(self._blocks) 449 | x = block(x, drop_connect_rate=drop_connect_rate) 450 | 451 | # Head 452 | x = relu_fn(self._bn1(self._conv_head(x))) 453 | 454 | return x 455 | 456 | def skip_connection_features(self,inputs): 457 | # Stem 458 | x = relu_fn(self._bn0(self._conv_stem(inputs))) 459 | 460 | # Accumulate features from differente net stages (3, 7, 11, 15) 461 | feats = [] 462 | feats_to_save = [3, 7, 11] 463 | # Blocks 464 | for idx, block in enumerate(self._blocks): 465 | drop_connect_rate = self._global_params.drop_connect_rate 466 | if drop_connect_rate: 467 | drop_connect_rate *= float(idx) / len(self._blocks) 468 | x = block(x, drop_connect_rate=drop_connect_rate) 469 | 470 | if idx in feats_to_save: 471 | feats.append(x) 472 | # Head 473 | x = relu_fn(self._bn1(self._conv_head(x))) 474 | feats.append(x) 475 | 476 | return feats[0], feats[1], feats[2], feats[3] 477 | 478 | def forward(self, inputs): 479 | """ Calls extract_features to extract features, applies final linear layer, and returns logits. """ 480 | 481 | # Convolution layers 482 | x = self.extract_features(inputs) 483 | 484 | # Pooling and final linear layer 485 | x = F.adaptive_avg_pool2d(x, 1).squeeze(-1).squeeze(-1) 486 | if self._dropout: 487 | x = F.dropout(x, p=self._dropout, training=self.training) 488 | x = self._fc(x) 489 | return x 490 | 491 | @classmethod 492 | def from_name(cls, model_name, override_params=None): 493 | cls._check_model_name_is_valid(model_name) 494 | blocks_args, global_params = get_model_params(model_name, override_params) 495 | return EfficientNet(blocks_args, global_params) 496 | 497 | @classmethod 498 | def from_pretrained(cls, model_name, num_classes=1000): 499 | model = EfficientNet.from_name(model_name, override_params={'num_classes': num_classes}) 500 | load_pretrained_weights(model, model_name, load_fc=(num_classes == 1000)) 501 | return model 502 | 503 | @classmethod 504 | def get_image_size(cls, model_name): 505 | cls._check_model_name_is_valid(model_name) 506 | _, _, res, _ = efficientnet_params(model_name) 507 | return res 508 | 509 | @classmethod 510 | def _check_model_name_is_valid(cls, model_name, also_need_pretrained_weights=False): 511 | """ Validates model name. None that pretrained weights are only available for 512 | the first four models (efficientnet-b{i} for i in 0,1,2,3) at the moment. """ 513 | num_models = 4 if also_need_pretrained_weights else 8 514 | valid_models = ['efficientnet_b'+str(i) for i in range(num_models)] 515 | if model_name.replace('-','_') not in valid_models: 516 | raise ValueError('model_name should be one of: ' + ', '.join(valid_models)) 517 | -------------------------------------------------------------------------------- /reader/train.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | import time 4 | import numpy as np 5 | import pandas as pd 6 | import json 7 | import torch 8 | import torch.nn as nn 9 | import torch.nn.functional as F 10 | import utils 11 | import argparse 12 | import sys 13 | 14 | from torch.utils.data import Dataset 15 | from torch.utils.data import DataLoader 16 | from torchvision import transforms 17 | 18 | from pprint import pprint 19 | from PlateReader import PlateReader 20 | from PIL import Image 21 | 22 | 23 | torch.backends.cudnn.deterministic = True 24 | 25 | 26 | # Argparse helper 27 | 28 | parser = argparse.ArgumentParser() 29 | parser.add_argument('--numworkers', type=int, default=3) 30 | parser.add_argument('--outpath','-o', type=str, required=True) 31 | parser.add_argument('--lr', type=float, default=0.0001) 32 | parser.add_argument('--epochs', type=int, default=30) 33 | parser.add_argument('--batch_size', type=int, default=128) 34 | parser.add_argument('--save_from_epoch','-s', type=int, default=0) 35 | parser.add_argument('--freeze', type=bool, default=False) 36 | parser.add_argument('--resume', type=str, default=None) 37 | parser.add_argument('--filter', type=str, default=None) 38 | parser.add_argument('--train_csv', type=str, default=None) 39 | parser.add_argument('--val_csv', type=str, default=None) 40 | 41 | args = parser.parse_args() 42 | 43 | PATH = args.outpath 44 | if not os.path.exists(PATH): 45 | os.mkdir(PATH) 46 | LOGFILE = os.path.join(PATH, 'training.log') 47 | 48 | EPOCHS = args.epochs 49 | LR = args.lr 50 | 51 | 52 | # Logging 53 | header = [] 54 | header.append('PyTorch Version: %s' % torch.__version__) 55 | header.append('CUDA device available: %s' % torch.cuda.is_available()) 56 | header.append('Output Path: %s' % PATH) 57 | header.append('Learning Rate: %s' % LR) 58 | header.append('Number of epochs to train: %s' % EPOCHS) 59 | header.append('Batch Size: %s' % args.batch_size) 60 | header.append('Using preprocess filter: %s' % args.filter) 61 | 62 | if args.resume: 63 | header.append('Resuming from: %s' % args.resume) 64 | 65 | with open(LOGFILE, 'w') as f: 66 | for entry in header: 67 | print(entry) 68 | f.write('%s\n' % entry) 69 | f.flush() 70 | 71 | ########################################### 72 | # Initialize Cost, Model, and Optimizer 73 | ########################################### 74 | model = PlateReader(training=True, filter=args.filter) 75 | 76 | if args.resume: 77 | print('Loading {}'.format(args.resume)) 78 | model.load_state_dict(torch.load(args.resume)) 79 | 80 | if args.freeze: 81 | print('Freezing backbone layers...') 82 | # Freeze layers 83 | for name, param in model.named_parameters(): 84 | if(name.split('.')[0] == 'backbone'): 85 | param.requires_grad = False 86 | # print(name, param.requires_grad) 87 | # Trainable Parameters 88 | total_params = sum(p.numel() for p in model.parameters()) 89 | print('{} total parameters.'.format(total_params)) 90 | total_trainable_params = sum( 91 | p.numel() for p in model.parameters() if p.requires_grad) 92 | 93 | 94 | for name, param in model.named_parameters(): 95 | if(param.requires_grad): 96 | print('Training layer: ', name) 97 | 98 | print('{} training parameters.'.format(total_trainable_params)) 99 | 100 | 101 | model.train() 102 | print('Loaded model') 103 | optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=LR) 104 | 105 | 106 | train_dataset = utils.PlateReaderDataset(csv_path=args.train_csv, filter=args.filter) 107 | val_dataset = utils.PlateReaderDataset(csv_path=args.val_csv, filter=args.filter) 108 | 109 | train_loader = DataLoader(dataset=train_dataset, 110 | batch_size=128, 111 | shuffle=True, 112 | num_workers=0) 113 | 114 | val_loader = DataLoader(dataset=val_dataset, 115 | batch_size=128, 116 | shuffle=True, 117 | num_workers=0) 118 | 119 | 120 | start_time = time.time() 121 | cost_fn = nn.BCELoss() 122 | sig = nn.Sigmoid() 123 | for epoch in range(EPOCHS): 124 | model.train() 125 | 126 | for batch_idx, (img,gt) in enumerate(train_loader): 127 | 128 | optimizer.zero_grad() 129 | # FORWARD AND BACK PROP 130 | x = model(img) 131 | target = utils.get_targets(gt).to(model.device) 132 | cost = cost_fn(sig(x), target) 133 | cost.backward() 134 | 135 | # UPDATE MODEL PARAMETERS 136 | optimizer.step() 137 | if not batch_idx % 10: 138 | s = ('Batch:{}/{} | Epoch: {}/{} | Cost: {:.4f} | Time elapsed: {:.2f} min'.format(batch_idx, len(train_loader), epoch, args.epochs, cost, (time.time() - start_time)/60)) 139 | print(s) 140 | 141 | with open(LOGFILE, 'a') as f: 142 | f.write('{}\n'.format(s)) 143 | 144 | if epoch >= args.save_from_epoch and cost < 0.01: 145 | print('Evaluation for epoch {}:'.format(epoch)) 146 | model.eval() 147 | with torch.set_grad_enabled(False): # save memory during inference 148 | 149 | train_eval = utils.evaluate(model, train_loader) 150 | test_eval = utils.evaluate(model, val_loader) 151 | 152 | model.train() 153 | print('Test set:') 154 | pprint(test_eval) 155 | print('Training set:') 156 | pprint(train_eval) 157 | 158 | with open(LOGFILE, 'a') as f: 159 | f.write('Evaluation for epoch {}:'.format(epoch)) 160 | f.write('Test\n') 161 | f.write(json.dumps(test_eval)) 162 | f.write('Train\n') 163 | f.write(json.dumps(train_eval)) 164 | 165 | model = model.to(torch.device('cpu')) 166 | torch.save(model.state_dict(), os.path.join(PATH, 'model-{}.pth'.format(epoch))) 167 | # torch.save(model, os.path.join(PATH, 'fullmodel-{}.pt'.format(epoch))) 168 | model.to(model.device) 169 | 170 | model = model.to(torch.device('cpu')) 171 | torch.save(model.state_dict(), os.path.join(PATH, 'model-{}.pth'.format(epoch))) 172 | -------------------------------------------------------------------------------- /reader/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | import numpy as np 4 | import pandas as pd 5 | import torch 6 | import torch.nn as nn 7 | from PIL import Image 8 | from torchvision import transforms 9 | from torch.utils.data import Dataset 10 | from torch.utils.data import DataLoader 11 | 12 | ## PREPROCESS 13 | class PreProcessor(Dataset): 14 | # https://github.com/dongb5/Retinex 15 | # https://github.com/takeitallsource/cnn-traffic-light-evaluation 16 | def __init__(self): 17 | self.retinex_params = { 18 | "sigma_list": [15, 80, 250], 19 | "G" : 5.0, 20 | "b" : 25.0, 21 | "alpha" : 125.0, 22 | "beta" : 46.0, 23 | "low_clip" : 0.01, 24 | "high_clip" : 0.99 25 | } 26 | self.clahe_intensity_component = 1 27 | 28 | def clahe_adaptive_histogram_equalization(self, image): 29 | image = cv2.cvtColor(image, cv2.COLOR_BGR2LAB) 30 | x, y, z = cv2.split(image) 31 | 32 | adaptive_histogram_equalizer = cv2.createCLAHE(clipLimit=3, tileGridSize=(4,4)) 33 | 34 | if self.clahe_intensity_component == 1: 35 | x = adaptive_histogram_equalizer.apply(x) 36 | elif self.clahe_intensity_component == 2: 37 | y = adaptive_histogram_equalizer.apply(y) 38 | elif self.clahe_intensity_component == 3: 39 | z = adaptive_histogram_equalizer.apply(z) 40 | 41 | return cv2.cvtColor(cv2.merge((x, y, z)), cv2.COLOR_LAB2BGR) 42 | 43 | def multiScaleRetinex(self, img): 44 | retinex = np.zeros_like(img) 45 | for sigma in self.retinex_params['sigma_list']: 46 | retinex += self.singleScaleRetinex(img, sigma) 47 | 48 | retinex = retinex / len(self.retinex_params['sigma_list']) 49 | return retinex 50 | 51 | def singleScaleRetinex(self, img, sigma): 52 | retinex = np.log10(img) - np.log10(cv2.GaussianBlur(img, (0, 0), sigma)) 53 | return retinex 54 | 55 | def colorRestoration(self, img): 56 | img_sum = np.sum(img, axis=2, keepdims=True) 57 | color_restoration = self.retinex_params['beta'] * (np.log10(self.retinex_params['alpha'] * img) - np.log10(img_sum)) 58 | return color_restoration 59 | 60 | def simplestColorBalance(self, img): 61 | total = img.shape[0] * img.shape[1] 62 | for i in range(img.shape[2]): 63 | unique, counts = np.unique(img[:, :, i], return_counts=True) 64 | current = 0 65 | for u, c in zip(unique, counts): 66 | if float(current) / total < self.retinex_params['low_clip']: 67 | low_val = u 68 | if float(current) / total < self.retinex_params['high_clip']: 69 | high_val = u 70 | current += c 71 | img[:, :, i] = np.maximum(np.minimum(img[:, :, i], high_val), low_val) 72 | return img 73 | 74 | def MSRCR(self, img): 75 | img = np.float64(img) + 1.0 76 | img_retinex = self.multiScaleRetinex(img) 77 | img_color = self.colorRestoration(img) 78 | img_msrcr = self.retinex_params['G'] * (img_retinex * img_color + self.retinex_params['b']) 79 | for i in range(img_msrcr.shape[2]): 80 | img_msrcr[:, :, i] = (img_msrcr[:, :, i] - np.min(img_msrcr[:, :, i])) / \ 81 | (np.max(img_msrcr[:, :, i]) - np.min(img_msrcr[:, :, i])) * \ 82 | 255 83 | img_msrcr = np.uint8(np.minimum(np.maximum(img_msrcr, 0), 255)) 84 | img_msrcr = self.simplestColorBalance(img_msrcr) 85 | return img_msrcr 86 | 87 | def automatedMSRCR(self, img): 88 | img = np.float64(img) + 1.0 89 | img_retinex = self.multiScaleRetinex(img) 90 | for i in range(img_retinex.shape[2]): 91 | unique, count = np.unique(np.int32(img_retinex[:, :, i] * 100), return_counts=True) 92 | for u, c in zip(unique, count): 93 | if u == 0: 94 | zero_count = c 95 | break 96 | low_val = unique[0] / 100.0 97 | high_val = unique[-1] / 100.0 98 | for u, c in zip(unique, count): 99 | if u < 0 and c < zero_count * 0.1: 100 | low_val = u / 100.0 101 | if u > 0 and c < zero_count * 0.1: 102 | high_val = u / 100.0 103 | break 104 | img_retinex[:, :, i] = np.maximum(np.minimum(img_retinex[:, :, i], high_val), low_val) 105 | 106 | img_retinex[:, :, i] = (img_retinex[:, :, i] - np.min(img_retinex[:, :, i])) / \ 107 | (np.max(img_retinex[:, :, i]) - np.min(img_retinex[:, :, i])) \ 108 | * 255 109 | img_retinex = np.uint8(img_retinex) 110 | return img_retinex 111 | 112 | def MSRCP(self, img): 113 | img = np.float64(img) + 1.0 114 | intensity = np.sum(img, axis=2) / img.shape[2] 115 | retinex = self.multiScaleRetinex(intensity) 116 | intensity = np.expand_dims(intensity, 2) 117 | retinex = np.expand_dims(retinex, 2) 118 | intensity1 = self.simplestColorBalance(retinex) 119 | intensity1 = (intensity1 - np.min(intensity1)) / \ 120 | (np.max(intensity1) - np.min(intensity1)) * \ 121 | 255.0 + 1.0 122 | img_msrcp = np.zeros_like(img) 123 | for y in range(img_msrcp.shape[0]): 124 | for x in range(img_msrcp.shape[1]): 125 | B = np.max(img[y, x]) 126 | A = np.minimum(256.0 / B, intensity1[y, x, 0] / intensity[y, x, 0]) 127 | img_msrcp[y, x, 0] = A * img[y, x, 0] 128 | img_msrcp[y, x, 1] = A * img[y, x, 1] 129 | img_msrcp[y, x, 2] = A * img[y, x, 2] 130 | img_msrcp = np.uint8(img_msrcp - 1.0) 131 | return img_msrcp 132 | 133 | 134 | ## TRAIN UTILS 135 | def get_targets(gts): 136 | targets = [] 137 | for gt in gts: 138 | target = np.zeros((7,36)) 139 | # print(gt) 140 | gt = [class_dict[c] for c in gt] 141 | for char_pos, char in enumerate(gt): 142 | target[char_pos][char] = 1 143 | targets.append(target) 144 | return torch.Tensor(np.array(targets)) 145 | 146 | def evaluate(model, dataloader): 147 | hits = 0 148 | six_and_over = 0 149 | five_and_over = 0 150 | total = 0 151 | 152 | sig = nn.Sigmoid() 153 | 154 | for batch_idx, (img,gt) in (enumerate(dataloader)): 155 | # Predict over batches 156 | x = model(img) 157 | # Iterate predictions/GTs 158 | for pred, label in zip(x, gt): 159 | 160 | num_label = [class_dict[c] for c in label] # Numerical label 161 | # Codify each position 162 | num_pred = [] 163 | for pos in pred: 164 | _, char_num = torch.max(sig(pos),0) 165 | num_pred.append(char_num.item()) 166 | score = np.sum(np.array(num_pred) == np.array(num_label)) 167 | if(score >= 5): 168 | five_and_over += 1 169 | if(score >= 6): 170 | six_and_over += 1 171 | if(score == 7): 172 | hits += 1 173 | total += 1 174 | s = 'Hits: {}({:.2f}) | >=6: {}({:.2f}) | >=5: {}({:.2f}) | Total examples: {}'.format(hits, (hits/total)*100, 175 | six_and_over, (six_and_over/total)*100, 176 | five_and_over, (five_and_over/total)*100, 177 | total) 178 | return s 179 | 180 | 181 | class PlateReaderDataset(Dataset): 182 | """Custom Dataset for loading plate images""" 183 | def __init__(self,csv_path, filter=None): 184 | if(type(csv_path) == list): 185 | df = pd.concat([pd.read_csv(f) for f in csv_path ]) 186 | else: 187 | df = pd.read_csv(csv_path) 188 | 189 | self.csv_path = csv_path 190 | self.img_paths = df['path'].values 191 | self.gts = df['gt'].values 192 | self.filter = filter 193 | self.preprocessor = PreProcessor() 194 | self.transf = transforms.Compose([transforms.Resize((50,120),interpolation=2), 195 | transforms.ToTensor(), 196 | transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),]) 197 | 198 | def __getitem__(self, index): 199 | 200 | if self.filter: 201 | img = cv2.imread(self.img_paths[index]) 202 | if self.filter.upper() == 'CLAHE': 203 | img = self.preprocessor.clahe_adaptive_histogram_equalization(img) 204 | elif self.filter.upper() == 'RETINEX-CP': 205 | img = self.preprocessor.MSRCP(img) 206 | elif self.filter.upper() == 'RETINEX-CR': 207 | img = self.preprocessor.MSRCR(img) 208 | elif self.filter.upper() == 'RETINEX-AUTO': 209 | img = self.preprocessor.automatedMSRCR(img) 210 | img = Image.fromarray(img) 211 | 212 | else: 213 | img = Image.open(self.img_paths[index]) 214 | 215 | 216 | if img.mode is not ('RGB'): 217 | img = img.convert('RGB') 218 | gt = self.gts[index] 219 | 220 | return self.transf(img), gt 221 | 222 | def __len__(self): 223 | return self.img_paths.shape[0] 224 | 225 | 226 | 227 | 228 | 229 | ## GENERAL 230 | def output2word(x): 231 | string = '' 232 | for char in x: 233 | for k,v in class_dict.items(): 234 | if v == char: 235 | string += k 236 | return string 237 | 238 | sig = nn.Sigmoid() 239 | def translate_prediction(x): 240 | pred = [] 241 | for pos in sig(x)[0]: 242 | _, idx = torch.max(pos,0) 243 | pred.append(idx.item()) 244 | return output2word(pred) 245 | 246 | class_dict = { 247 | '0':0, 248 | '1':1, 249 | '2':2, 250 | '3':3, 251 | '4':4, 252 | '5':5, 253 | '6':6, 254 | '7':7, 255 | '8':8, 256 | '9':9, 257 | 'A':10, 258 | 'B':11, 259 | 'C':12, 260 | 'D':13, 261 | 'E':14, 262 | 'F':15, 263 | 'G':16, 264 | 'H':17, 265 | 'I':18, 266 | 'J':19, 267 | 'K':20, 268 | 'L':21, 269 | 'M':22, 270 | 'N':23, 271 | 'O':24, 272 | 'P':25, 273 | 'Q':26, 274 | 'R':27, 275 | 'S':28, 276 | 'T':29, 277 | 'U':30, 278 | 'V':31, 279 | 'W':32, 280 | 'X':33, 281 | 'Y':34, 282 | 'Z':35 283 | } -------------------------------------------------------------------------------- /reader/weights/training.log: -------------------------------------------------------------------------------- 1 | PyTorch Version: 1.1.0 2 | CUDA device available: True 3 | Output Path: transfer_learning/unfrozen/realplates/ 4 | Learning Rate: 1e-05 5 | Number of epochs to train: 30 6 | Batch:0/90 | Epoch: 0/30 | Cost: 0.1005 | Time elapsed: 0.05 min 7 | Batch:10/90 | Epoch: 0/30 | Cost: 0.1024 | Time elapsed: 0.47 min 8 | Batch:20/90 | Epoch: 0/30 | Cost: 0.0901 | Time elapsed: 0.88 min 9 | Batch:30/90 | Epoch: 0/30 | Cost: 0.0961 | Time elapsed: 1.28 min 10 | Batch:40/90 | Epoch: 0/30 | Cost: 0.0932 | Time elapsed: 1.69 min 11 | Batch:50/90 | Epoch: 0/30 | Cost: 0.0862 | Time elapsed: 2.06 min 12 | Batch:60/90 | Epoch: 0/30 | Cost: 0.0866 | Time elapsed: 2.43 min 13 | Batch:70/90 | Epoch: 0/30 | Cost: 0.0838 | Time elapsed: 2.79 min 14 | Batch:80/90 | Epoch: 0/30 | Cost: 0.0805 | Time elapsed: 3.14 min 15 | Evaluation for epoch 0:Test 16 | "Hits: 921(13.15) | >=6: 2327(33.22) | >=5: 3712(52.99) | Total examples: 7005"Train 17 | "Hits: 1564(13.65) | >=6: 3857(33.67) | >=5: 6141(53.61) | Total examples: 11454"Batch:0/90 | Epoch: 1/30 | Cost: 0.0728 | Time elapsed: 5.94 min 18 | Batch:10/90 | Epoch: 1/30 | Cost: 0.0741 | Time elapsed: 6.20 min 19 | Batch:20/90 | Epoch: 1/30 | Cost: 0.0776 | Time elapsed: 6.56 min 20 | Batch:30/90 | Epoch: 1/30 | Cost: 0.0732 | Time elapsed: 7.04 min 21 | Batch:40/90 | Epoch: 1/30 | Cost: 0.0699 | Time elapsed: 7.44 min 22 | Batch:50/90 | Epoch: 1/30 | Cost: 0.0700 | Time elapsed: 7.81 min 23 | Batch:60/90 | Epoch: 1/30 | Cost: 0.0687 | Time elapsed: 8.19 min 24 | Batch:70/90 | Epoch: 1/30 | Cost: 0.0644 | Time elapsed: 8.55 min 25 | Batch:80/90 | Epoch: 1/30 | Cost: 0.0613 | Time elapsed: 8.92 min 26 | Evaluation for epoch 1:Test 27 | "Hits: 1517(21.66) | >=6: 3281(46.84) | >=5: 4629(66.08) | Total examples: 7005"Train 28 | "Hits: 2884(25.18) | >=6: 5752(50.22) | >=5: 7862(68.64) | Total examples: 11454"Batch:0/90 | Epoch: 2/30 | Cost: 0.0616 | Time elapsed: 13.56 min 29 | Batch:10/90 | Epoch: 2/30 | Cost: 0.0670 | Time elapsed: 14.02 min 30 | Batch:20/90 | Epoch: 2/30 | Cost: 0.0613 | Time elapsed: 14.51 min 31 | Batch:30/90 | Epoch: 2/30 | Cost: 0.0624 | Time elapsed: 14.98 min 32 | Batch:40/90 | Epoch: 2/30 | Cost: 0.0602 | Time elapsed: 15.47 min 33 | Batch:50/90 | Epoch: 2/30 | Cost: 0.0601 | Time elapsed: 15.94 min 34 | Batch:60/90 | Epoch: 2/30 | Cost: 0.0584 | Time elapsed: 16.39 min 35 | Batch:70/90 | Epoch: 2/30 | Cost: 0.0596 | Time elapsed: 16.84 min 36 | Batch:80/90 | Epoch: 2/30 | Cost: 0.0522 | Time elapsed: 17.28 min 37 | Evaluation for epoch 2:Test 38 | "Hits: 1818(25.95) | >=6: 3657(52.21) | >=5: 4926(70.32) | Total examples: 7005"Train 39 | "Hits: 3730(32.57) | >=6: 6751(58.94) | >=5: 8684(75.82) | Total examples: 11454"Batch:0/90 | Epoch: 3/30 | Cost: 0.0550 | Time elapsed: 23.19 min 40 | Batch:10/90 | Epoch: 3/30 | Cost: 0.0515 | Time elapsed: 23.65 min 41 | Batch:20/90 | Epoch: 3/30 | Cost: 0.0573 | Time elapsed: 24.10 min 42 | Batch:30/90 | Epoch: 3/30 | Cost: 0.0500 | Time elapsed: 24.53 min 43 | Batch:40/90 | Epoch: 3/30 | Cost: 0.0509 | Time elapsed: 24.93 min 44 | Batch:50/90 | Epoch: 3/30 | Cost: 0.0541 | Time elapsed: 25.41 min 45 | Batch:60/90 | Epoch: 3/30 | Cost: 0.0535 | Time elapsed: 25.80 min 46 | Batch:70/90 | Epoch: 3/30 | Cost: 0.0481 | Time elapsed: 26.17 min 47 | Batch:80/90 | Epoch: 3/30 | Cost: 0.0497 | Time elapsed: 26.56 min 48 | Evaluation for epoch 3:Test 49 | "Hits: 1988(28.38) | >=6: 3928(56.07) | >=5: 5174(73.86) | Total examples: 7005"Train 50 | "Hits: 4385(38.28) | >=6: 7459(65.12) | >=5: 9192(80.25) | Total examples: 11454"Batch:0/90 | Epoch: 4/30 | Cost: 0.0485 | Time elapsed: 30.09 min 51 | Batch:10/90 | Epoch: 4/30 | Cost: 0.0456 | Time elapsed: 30.50 min 52 | Batch:20/90 | Epoch: 4/30 | Cost: 0.0485 | Time elapsed: 30.95 min 53 | Batch:30/90 | Epoch: 4/30 | Cost: 0.0481 | Time elapsed: 31.35 min 54 | Batch:40/90 | Epoch: 4/30 | Cost: 0.0444 | Time elapsed: 31.74 min 55 | Batch:50/90 | Epoch: 4/30 | Cost: 0.0422 | Time elapsed: 32.17 min 56 | Batch:60/90 | Epoch: 4/30 | Cost: 0.0440 | Time elapsed: 32.62 min 57 | Batch:70/90 | Epoch: 4/30 | Cost: 0.0425 | Time elapsed: 33.10 min 58 | Batch:80/90 | Epoch: 4/30 | Cost: 0.0428 | Time elapsed: 33.55 min 59 | Evaluation for epoch 4:Test 60 | "Hits: 2185(31.19) | >=6: 4134(59.01) | >=5: 5333(76.13) | Total examples: 7005"Train 61 | "Hits: 4963(43.33) | >=6: 7993(69.78) | >=5: 9538(83.27) | Total examples: 11454"Batch:0/90 | Epoch: 5/30 | Cost: 0.0488 | Time elapsed: 38.54 min 62 | Batch:10/90 | Epoch: 5/30 | Cost: 0.0413 | Time elapsed: 38.58 min 63 | Batch:20/90 | Epoch: 5/30 | Cost: 0.0416 | Time elapsed: 38.62 min 64 | Batch:30/90 | Epoch: 5/30 | Cost: 0.0416 | Time elapsed: 38.67 min 65 | Batch:40/90 | Epoch: 5/30 | Cost: 0.0407 | Time elapsed: 38.71 min 66 | Batch:50/90 | Epoch: 5/30 | Cost: 0.0438 | Time elapsed: 38.75 min 67 | Batch:60/90 | Epoch: 5/30 | Cost: 0.0389 | Time elapsed: 38.81 min 68 | Batch:70/90 | Epoch: 5/30 | Cost: 0.0409 | Time elapsed: 38.86 min 69 | Batch:80/90 | Epoch: 5/30 | Cost: 0.0389 | Time elapsed: 38.92 min 70 | Evaluation for epoch 5:Test 71 | "Hits: 2349(33.53) | >=6: 4311(61.54) | >=5: 5447(77.76) | Total examples: 7005"Train 72 | "Hits: 5506(48.07) | >=6: 8393(73.28) | >=5: 9817(85.71) | Total examples: 11454"Batch:0/90 | Epoch: 6/30 | Cost: 0.0400 | Time elapsed: 39.49 min 73 | Batch:10/90 | Epoch: 6/30 | Cost: 0.0410 | Time elapsed: 39.53 min 74 | Batch:20/90 | Epoch: 6/30 | Cost: 0.0423 | Time elapsed: 39.58 min 75 | Batch:30/90 | Epoch: 6/30 | Cost: 0.0395 | Time elapsed: 39.62 min 76 | Batch:40/90 | Epoch: 6/30 | Cost: 0.0393 | Time elapsed: 39.66 min 77 | Batch:50/90 | Epoch: 6/30 | Cost: 0.0376 | Time elapsed: 39.71 min 78 | Batch:60/90 | Epoch: 6/30 | Cost: 0.0411 | Time elapsed: 39.75 min 79 | Batch:70/90 | Epoch: 6/30 | Cost: 0.0356 | Time elapsed: 39.79 min 80 | Batch:80/90 | Epoch: 6/30 | Cost: 0.0407 | Time elapsed: 39.83 min 81 | Evaluation for epoch 6:Test 82 | "Hits: 2485(35.47) | >=6: 4442(63.41) | >=5: 5560(79.37) | Total examples: 7005"Train 83 | "Hits: 5992(52.31) | >=6: 8797(76.80) | >=5: 10079(88.00) | Total examples: 11454"Batch:0/90 | Epoch: 7/30 | Cost: 0.0407 | Time elapsed: 40.32 min 84 | Batch:10/90 | Epoch: 7/30 | Cost: 0.0390 | Time elapsed: 40.36 min 85 | Batch:20/90 | Epoch: 7/30 | Cost: 0.0398 | Time elapsed: 40.41 min 86 | Batch:30/90 | Epoch: 7/30 | Cost: 0.0390 | Time elapsed: 40.45 min 87 | Batch:40/90 | Epoch: 7/30 | Cost: 0.0353 | Time elapsed: 40.49 min 88 | Batch:50/90 | Epoch: 7/30 | Cost: 0.0354 | Time elapsed: 40.54 min 89 | Batch:60/90 | Epoch: 7/30 | Cost: 0.0349 | Time elapsed: 40.58 min 90 | Batch:70/90 | Epoch: 7/30 | Cost: 0.0360 | Time elapsed: 40.62 min 91 | Batch:80/90 | Epoch: 7/30 | Cost: 0.0326 | Time elapsed: 40.66 min 92 | Evaluation for epoch 7:Test 93 | "Hits: 2604(37.17) | >=6: 4579(65.37) | >=5: 5641(80.53) | Total examples: 7005"Train 94 | "Hits: 6394(55.82) | >=6: 9095(79.40) | >=5: 10263(89.60) | Total examples: 11454"Batch:0/90 | Epoch: 8/30 | Cost: 0.0334 | Time elapsed: 41.14 min 95 | Batch:10/90 | Epoch: 8/30 | Cost: 0.0361 | Time elapsed: 41.18 min 96 | Batch:20/90 | Epoch: 8/30 | Cost: 0.0358 | Time elapsed: 41.22 min 97 | Batch:30/90 | Epoch: 8/30 | Cost: 0.0334 | Time elapsed: 41.26 min 98 | Batch:40/90 | Epoch: 8/30 | Cost: 0.0320 | Time elapsed: 41.31 min 99 | Batch:50/90 | Epoch: 8/30 | Cost: 0.0321 | Time elapsed: 41.35 min 100 | Batch:60/90 | Epoch: 8/30 | Cost: 0.0312 | Time elapsed: 41.39 min 101 | Batch:70/90 | Epoch: 8/30 | Cost: 0.0307 | Time elapsed: 41.44 min 102 | Batch:80/90 | Epoch: 8/30 | Cost: 0.0346 | Time elapsed: 41.48 min 103 | Evaluation for epoch 8:Test 104 | "Hits: 2702(38.57) | >=6: 4664(66.58) | >=5: 5727(81.76) | Total examples: 7005"Train 105 | "Hits: 6780(59.19) | >=6: 9349(81.62) | >=5: 10366(90.50) | Total examples: 11454"Batch:0/90 | Epoch: 9/30 | Cost: 0.0345 | Time elapsed: 42.01 min 106 | Batch:10/90 | Epoch: 9/30 | Cost: 0.0320 | Time elapsed: 42.06 min 107 | Batch:20/90 | Epoch: 9/30 | Cost: 0.0333 | Time elapsed: 42.10 min 108 | Batch:30/90 | Epoch: 9/30 | Cost: 0.0318 | Time elapsed: 42.14 min 109 | Batch:40/90 | Epoch: 9/30 | Cost: 0.0285 | Time elapsed: 42.18 min 110 | Batch:50/90 | Epoch: 9/30 | Cost: 0.0329 | Time elapsed: 42.23 min 111 | Batch:60/90 | Epoch: 9/30 | Cost: 0.0340 | Time elapsed: 42.27 min 112 | Batch:70/90 | Epoch: 9/30 | Cost: 0.0288 | Time elapsed: 42.31 min 113 | Batch:80/90 | Epoch: 9/30 | Cost: 0.0310 | Time elapsed: 42.35 min 114 | Evaluation for epoch 9:Test 115 | "Hits: 2784(39.74) | >=6: 4770(68.09) | >=5: 5806(82.88) | Total examples: 7005"Train 116 | "Hits: 7130(62.25) | >=6: 9558(83.45) | >=5: 10480(91.50) | Total examples: 11454"Batch:0/90 | Epoch: 10/30 | Cost: 0.0295 | Time elapsed: 46.21 min 117 | Batch:10/90 | Epoch: 10/30 | Cost: 0.0334 | Time elapsed: 46.58 min 118 | Batch:20/90 | Epoch: 10/30 | Cost: 0.0301 | Time elapsed: 46.94 min 119 | Batch:30/90 | Epoch: 10/30 | Cost: 0.0306 | Time elapsed: 47.29 min 120 | Batch:40/90 | Epoch: 10/30 | Cost: 0.0259 | Time elapsed: 47.63 min 121 | Batch:50/90 | Epoch: 10/30 | Cost: 0.0271 | Time elapsed: 47.98 min 122 | Batch:60/90 | Epoch: 10/30 | Cost: 0.0285 | Time elapsed: 48.32 min 123 | Batch:70/90 | Epoch: 10/30 | Cost: 0.0279 | Time elapsed: 48.69 min 124 | Batch:80/90 | Epoch: 10/30 | Cost: 0.0274 | Time elapsed: 49.07 min 125 | Evaluation for epoch 10:Test 126 | "Hits: 2897(41.36) | >=6: 4834(69.01) | >=5: 5875(83.87) | Total examples: 7005"Train 127 | "Hits: 7429(64.86) | >=6: 9721(84.87) | >=5: 10580(92.37) | Total examples: 11454"Batch:0/90 | Epoch: 11/30 | Cost: 0.0268 | Time elapsed: 51.92 min 128 | Batch:10/90 | Epoch: 11/30 | Cost: 0.0283 | Time elapsed: 52.19 min 129 | Batch:20/90 | Epoch: 11/30 | Cost: 0.0312 | Time elapsed: 52.62 min 130 | Batch:30/90 | Epoch: 11/30 | Cost: 0.0305 | Time elapsed: 53.03 min 131 | Batch:40/90 | Epoch: 11/30 | Cost: 0.0277 | Time elapsed: 53.43 min 132 | Batch:50/90 | Epoch: 11/30 | Cost: 0.0320 | Time elapsed: 53.81 min 133 | Batch:60/90 | Epoch: 11/30 | Cost: 0.0272 | Time elapsed: 54.19 min 134 | Batch:70/90 | Epoch: 11/30 | Cost: 0.0282 | Time elapsed: 54.55 min 135 | Batch:80/90 | Epoch: 11/30 | Cost: 0.0269 | Time elapsed: 54.93 min 136 | Evaluation for epoch 11:Test 137 | "Hits: 2971(42.41) | >=6: 4911(70.11) | >=5: 5957(85.04) | Total examples: 7005"Train 138 | "Hits: 7707(67.29) | >=6: 9870(86.17) | >=5: 10666(93.12) | Total examples: 11454"Batch:0/90 | Epoch: 12/30 | Cost: 0.0264 | Time elapsed: 58.09 min 139 | Batch:10/90 | Epoch: 12/30 | Cost: 0.0300 | Time elapsed: 58.27 min 140 | Batch:20/90 | Epoch: 12/30 | Cost: 0.0278 | Time elapsed: 58.47 min 141 | Batch:30/90 | Epoch: 12/30 | Cost: 0.0272 | Time elapsed: 58.67 min 142 | Batch:40/90 | Epoch: 12/30 | Cost: 0.0243 | Time elapsed: 58.87 min 143 | Batch:50/90 | Epoch: 12/30 | Cost: 0.0230 | Time elapsed: 59.07 min 144 | Batch:60/90 | Epoch: 12/30 | Cost: 0.0284 | Time elapsed: 59.25 min 145 | Batch:70/90 | Epoch: 12/30 | Cost: 0.0281 | Time elapsed: 59.44 min 146 | Batch:80/90 | Epoch: 12/30 | Cost: 0.0271 | Time elapsed: 59.65 min 147 | Evaluation for epoch 12:Test 148 | "Hits: 3042(43.43) | >=6: 4979(71.08) | >=5: 6002(85.68) | Total examples: 7005"Train 149 | "Hits: 7969(69.57) | >=6: 10004(87.34) | >=5: 10733(93.71) | Total examples: 11454"Batch:0/90 | Epoch: 13/30 | Cost: 0.0264 | Time elapsed: 60.29 min 150 | Batch:10/90 | Epoch: 13/30 | Cost: 0.0267 | Time elapsed: 60.33 min 151 | Batch:20/90 | Epoch: 13/30 | Cost: 0.0263 | Time elapsed: 60.37 min 152 | Batch:30/90 | Epoch: 13/30 | Cost: 0.0261 | Time elapsed: 60.41 min 153 | Batch:40/90 | Epoch: 13/30 | Cost: 0.0268 | Time elapsed: 60.45 min 154 | Batch:50/90 | Epoch: 13/30 | Cost: 0.0290 | Time elapsed: 60.49 min 155 | Batch:60/90 | Epoch: 13/30 | Cost: 0.0291 | Time elapsed: 60.53 min 156 | Batch:70/90 | Epoch: 13/30 | Cost: 0.0264 | Time elapsed: 60.57 min 157 | Batch:80/90 | Epoch: 13/30 | Cost: 0.0257 | Time elapsed: 60.61 min 158 | Evaluation for epoch 13:Test 159 | "Hits: 3086(44.05) | >=6: 5027(71.76) | >=5: 6066(86.60) | Total examples: 7005"Train 160 | "Hits: 8177(71.39) | >=6: 10114(88.30) | >=5: 10797(94.26) | Total examples: 11454"Batch:0/90 | Epoch: 14/30 | Cost: 0.0235 | Time elapsed: 61.09 min 161 | Batch:10/90 | Epoch: 14/30 | Cost: 0.0275 | Time elapsed: 61.13 min 162 | Batch:20/90 | Epoch: 14/30 | Cost: 0.0242 | Time elapsed: 61.17 min 163 | Batch:30/90 | Epoch: 14/30 | Cost: 0.0254 | Time elapsed: 61.21 min 164 | Batch:40/90 | Epoch: 14/30 | Cost: 0.0261 | Time elapsed: 61.25 min 165 | Batch:50/90 | Epoch: 14/30 | Cost: 0.0259 | Time elapsed: 61.29 min 166 | Batch:60/90 | Epoch: 14/30 | Cost: 0.0267 | Time elapsed: 61.33 min 167 | Batch:70/90 | Epoch: 14/30 | Cost: 0.0231 | Time elapsed: 61.37 min 168 | Batch:80/90 | Epoch: 14/30 | Cost: 0.0230 | Time elapsed: 61.42 min 169 | Evaluation for epoch 14:Test 170 | "Hits: 3160(45.11) | >=6: 5078(72.49) | >=5: 6105(87.15) | Total examples: 7005"Train 171 | "Hits: 8393(73.28) | >=6: 10226(89.28) | >=5: 10853(94.75) | Total examples: 11454"Batch:0/90 | Epoch: 15/30 | Cost: 0.0262 | Time elapsed: 61.90 min 172 | Batch:10/90 | Epoch: 15/30 | Cost: 0.0228 | Time elapsed: 61.94 min 173 | Batch:20/90 | Epoch: 15/30 | Cost: 0.0232 | Time elapsed: 61.98 min 174 | Batch:30/90 | Epoch: 15/30 | Cost: 0.0253 | Time elapsed: 62.02 min 175 | Batch:40/90 | Epoch: 15/30 | Cost: 0.0242 | Time elapsed: 62.06 min 176 | Batch:50/90 | Epoch: 15/30 | Cost: 0.0228 | Time elapsed: 62.10 min 177 | Batch:60/90 | Epoch: 15/30 | Cost: 0.0266 | Time elapsed: 62.14 min 178 | Batch:70/90 | Epoch: 15/30 | Cost: 0.0240 | Time elapsed: 62.18 min 179 | Batch:80/90 | Epoch: 15/30 | Cost: 0.0253 | Time elapsed: 62.22 min 180 | Evaluation for epoch 15:Test 181 | "Hits: 3194(45.60) | >=6: 5136(73.32) | >=5: 6135(87.58) | Total examples: 7005"Train 182 | "Hits: 8550(74.65) | >=6: 10310(90.01) | >=5: 10891(95.08) | Total examples: 11454"Batch:0/90 | Epoch: 16/30 | Cost: 0.0268 | Time elapsed: 62.69 min 183 | Batch:10/90 | Epoch: 16/30 | Cost: 0.0264 | Time elapsed: 62.73 min 184 | Batch:20/90 | Epoch: 16/30 | Cost: 0.0238 | Time elapsed: 62.77 min 185 | Batch:30/90 | Epoch: 16/30 | Cost: 0.0252 | Time elapsed: 62.81 min 186 | Batch:40/90 | Epoch: 16/30 | Cost: 0.0239 | Time elapsed: 62.85 min 187 | Batch:50/90 | Epoch: 16/30 | Cost: 0.0237 | Time elapsed: 62.89 min 188 | Batch:60/90 | Epoch: 16/30 | Cost: 0.0212 | Time elapsed: 62.93 min 189 | Batch:70/90 | Epoch: 16/30 | Cost: 0.0273 | Time elapsed: 62.97 min 190 | Batch:80/90 | Epoch: 16/30 | Cost: 0.0197 | Time elapsed: 63.01 min 191 | Evaluation for epoch 16:Test 192 | "Hits: 3247(46.35) | >=6: 5176(73.89) | >=5: 6170(88.08) | Total examples: 7005"Train 193 | "Hits: 8710(76.04) | >=6: 10373(90.56) | >=5: 10948(95.58) | Total examples: 11454"Batch:0/90 | Epoch: 17/30 | Cost: 0.0212 | Time elapsed: 63.47 min 194 | Batch:10/90 | Epoch: 17/30 | Cost: 0.0209 | Time elapsed: 63.51 min 195 | Batch:20/90 | Epoch: 17/30 | Cost: 0.0252 | Time elapsed: 63.55 min 196 | Batch:30/90 | Epoch: 17/30 | Cost: 0.0238 | Time elapsed: 63.59 min 197 | Batch:40/90 | Epoch: 17/30 | Cost: 0.0196 | Time elapsed: 63.63 min 198 | Batch:50/90 | Epoch: 17/30 | Cost: 0.0231 | Time elapsed: 63.67 min 199 | Batch:60/90 | Epoch: 17/30 | Cost: 0.0255 | Time elapsed: 63.71 min 200 | Batch:70/90 | Epoch: 17/30 | Cost: 0.0204 | Time elapsed: 63.76 min 201 | Batch:80/90 | Epoch: 17/30 | Cost: 0.0190 | Time elapsed: 63.79 min 202 | Evaluation for epoch 17:Test 203 | "Hits: 3289(46.95) | >=6: 5218(74.49) | >=5: 6187(88.32) | Total examples: 7005"Train 204 | "Hits: 8859(77.34) | >=6: 10450(91.23) | >=5: 10970(95.77) | Total examples: 11454"Batch:0/90 | Epoch: 18/30 | Cost: 0.0203 | Time elapsed: 64.42 min 205 | Batch:10/90 | Epoch: 18/30 | Cost: 0.0213 | Time elapsed: 64.46 min 206 | Batch:20/90 | Epoch: 18/30 | Cost: 0.0203 | Time elapsed: 64.50 min 207 | Batch:30/90 | Epoch: 18/30 | Cost: 0.0209 | Time elapsed: 64.54 min 208 | Batch:40/90 | Epoch: 18/30 | Cost: 0.0219 | Time elapsed: 64.58 min 209 | Batch:50/90 | Epoch: 18/30 | Cost: 0.0215 | Time elapsed: 64.62 min 210 | Batch:60/90 | Epoch: 18/30 | Cost: 0.0217 | Time elapsed: 64.66 min 211 | Batch:70/90 | Epoch: 18/30 | Cost: 0.0238 | Time elapsed: 64.70 min 212 | Batch:80/90 | Epoch: 18/30 | Cost: 0.0205 | Time elapsed: 64.74 min 213 | Evaluation for epoch 18:Test 214 | "Hits: 3340(47.68) | >=6: 5246(74.89) | >=5: 6213(88.69) | Total examples: 7005"Train 215 | "Hits: 9027(78.81) | >=6: 10511(91.77) | >=5: 11012(96.14) | Total examples: 11454"Batch:0/90 | Epoch: 19/30 | Cost: 0.0220 | Time elapsed: 67.28 min 216 | Batch:10/90 | Epoch: 19/30 | Cost: 0.0217 | Time elapsed: 67.73 min 217 | Batch:20/90 | Epoch: 19/30 | Cost: 0.0238 | Time elapsed: 68.15 min 218 | Batch:30/90 | Epoch: 19/30 | Cost: 0.0222 | Time elapsed: 68.58 min 219 | Batch:40/90 | Epoch: 19/30 | Cost: 0.0204 | Time elapsed: 68.99 min 220 | Batch:50/90 | Epoch: 19/30 | Cost: 0.0201 | Time elapsed: 69.40 min 221 | Batch:60/90 | Epoch: 19/30 | Cost: 0.0200 | Time elapsed: 69.83 min 222 | Batch:70/90 | Epoch: 19/30 | Cost: 0.0203 | Time elapsed: 70.25 min 223 | Batch:80/90 | Epoch: 19/30 | Cost: 0.0232 | Time elapsed: 70.67 min 224 | Evaluation for epoch 19:Test 225 | "Hits: 3377(48.21) | >=6: 5299(75.65) | >=5: 6236(89.02) | Total examples: 7005"Train 226 | "Hits: 9138(79.78) | >=6: 10563(92.22) | >=5: 11049(96.46) | Total examples: 11454"Batch:0/90 | Epoch: 20/30 | Cost: 0.0216 | Time elapsed: 74.76 min 227 | Batch:10/90 | Epoch: 20/30 | Cost: 0.0200 | Time elapsed: 75.18 min 228 | Batch:20/90 | Epoch: 20/30 | Cost: 0.0193 | Time elapsed: 75.61 min 229 | Batch:30/90 | Epoch: 20/30 | Cost: 0.0178 | Time elapsed: 76.03 min 230 | Batch:40/90 | Epoch: 20/30 | Cost: 0.0217 | Time elapsed: 76.44 min 231 | Batch:50/90 | Epoch: 20/30 | Cost: 0.0205 | Time elapsed: 76.85 min 232 | Batch:60/90 | Epoch: 20/30 | Cost: 0.0225 | Time elapsed: 77.23 min 233 | Batch:70/90 | Epoch: 20/30 | Cost: 0.0195 | Time elapsed: 77.63 min 234 | Batch:80/90 | Epoch: 20/30 | Cost: 0.0205 | Time elapsed: 78.03 min 235 | Evaluation for epoch 20:Test 236 | "Hits: 3404(48.59) | >=6: 5342(76.26) | >=5: 6246(89.16) | Total examples: 7005"Train 237 | "Hits: 9260(80.85) | >=6: 10611(92.64) | >=5: 11073(96.67) | Total examples: 11454"Batch:0/90 | Epoch: 21/30 | Cost: 0.0198 | Time elapsed: 80.06 min 238 | Batch:10/90 | Epoch: 21/30 | Cost: 0.0176 | Time elapsed: 80.10 min 239 | Batch:20/90 | Epoch: 21/30 | Cost: 0.0222 | Time elapsed: 80.14 min 240 | Batch:30/90 | Epoch: 21/30 | Cost: 0.0191 | Time elapsed: 80.18 min 241 | Batch:40/90 | Epoch: 21/30 | Cost: 0.0222 | Time elapsed: 80.22 min 242 | Batch:50/90 | Epoch: 21/30 | Cost: 0.0231 | Time elapsed: 80.26 min 243 | Batch:60/90 | Epoch: 21/30 | Cost: 0.0197 | Time elapsed: 80.30 min 244 | Batch:70/90 | Epoch: 21/30 | Cost: 0.0179 | Time elapsed: 80.34 min 245 | Batch:80/90 | Epoch: 21/30 | Cost: 0.0195 | Time elapsed: 80.38 min 246 | Evaluation for epoch 21:Test 247 | "Hits: 3449(49.24) | >=6: 5364(76.57) | >=5: 6270(89.51) | Total examples: 7005"Train 248 | "Hits: 9401(82.08) | >=6: 10667(93.13) | >=5: 11099(96.90) | Total examples: 11454"Batch:0/90 | Epoch: 22/30 | Cost: 0.0172 | Time elapsed: 80.85 min 249 | Batch:10/90 | Epoch: 22/30 | Cost: 0.0172 | Time elapsed: 80.89 min 250 | Batch:20/90 | Epoch: 22/30 | Cost: 0.0184 | Time elapsed: 80.93 min 251 | Batch:30/90 | Epoch: 22/30 | Cost: 0.0177 | Time elapsed: 80.97 min 252 | Batch:40/90 | Epoch: 22/30 | Cost: 0.0168 | Time elapsed: 81.01 min 253 | Batch:50/90 | Epoch: 22/30 | Cost: 0.0197 | Time elapsed: 81.05 min 254 | Batch:60/90 | Epoch: 22/30 | Cost: 0.0179 | Time elapsed: 81.09 min 255 | Batch:70/90 | Epoch: 22/30 | Cost: 0.0199 | Time elapsed: 81.14 min 256 | Batch:80/90 | Epoch: 22/30 | Cost: 0.0173 | Time elapsed: 81.18 min 257 | Evaluation for epoch 22:Test 258 | "Hits: 3486(49.76) | >=6: 5411(77.24) | >=5: 6282(89.68) | Total examples: 7005"Train 259 | "Hits: 9523(83.14) | >=6: 10715(93.55) | >=5: 11114(97.03) | Total examples: 11454"Batch:0/90 | Epoch: 23/30 | Cost: 0.0204 | Time elapsed: 81.65 min 260 | Batch:10/90 | Epoch: 23/30 | Cost: 0.0185 | Time elapsed: 81.69 min 261 | Batch:20/90 | Epoch: 23/30 | Cost: 0.0174 | Time elapsed: 81.73 min 262 | Batch:30/90 | Epoch: 23/30 | Cost: 0.0198 | Time elapsed: 81.77 min 263 | Batch:40/90 | Epoch: 23/30 | Cost: 0.0191 | Time elapsed: 81.81 min 264 | Batch:50/90 | Epoch: 23/30 | Cost: 0.0206 | Time elapsed: 81.85 min 265 | Batch:60/90 | Epoch: 23/30 | Cost: 0.0204 | Time elapsed: 81.89 min 266 | Batch:70/90 | Epoch: 23/30 | Cost: 0.0170 | Time elapsed: 81.93 min 267 | Batch:80/90 | Epoch: 23/30 | Cost: 0.0170 | Time elapsed: 81.97 min 268 | Evaluation for epoch 23:Test 269 | "Hits: 3508(50.08) | >=6: 5438(77.63) | >=5: 6303(89.98) | Total examples: 7005"Train 270 | "Hits: 9625(84.03) | >=6: 10758(93.92) | >=5: 11133(97.20) | Total examples: 11454"Batch:0/90 | Epoch: 24/30 | Cost: 0.0176 | Time elapsed: 82.44 min 271 | Batch:10/90 | Epoch: 24/30 | Cost: 0.0184 | Time elapsed: 82.48 min 272 | Batch:20/90 | Epoch: 24/30 | Cost: 0.0199 | Time elapsed: 82.52 min 273 | Batch:30/90 | Epoch: 24/30 | Cost: 0.0162 | Time elapsed: 82.56 min 274 | Batch:40/90 | Epoch: 24/30 | Cost: 0.0189 | Time elapsed: 82.60 min 275 | Batch:50/90 | Epoch: 24/30 | Cost: 0.0187 | Time elapsed: 82.64 min 276 | Batch:60/90 | Epoch: 24/30 | Cost: 0.0161 | Time elapsed: 82.68 min 277 | Batch:70/90 | Epoch: 24/30 | Cost: 0.0171 | Time elapsed: 82.73 min 278 | Batch:80/90 | Epoch: 24/30 | Cost: 0.0175 | Time elapsed: 82.77 min 279 | Evaluation for epoch 24:Test 280 | "Hits: 3538(50.51) | >=6: 5481(78.24) | >=5: 6309(90.06) | Total examples: 7005"Train 281 | "Hits: 9709(84.77) | >=6: 10809(94.37) | >=5: 11151(97.35) | Total examples: 11454"Batch:0/90 | Epoch: 25/30 | Cost: 0.0161 | Time elapsed: 84.21 min 282 | Batch:10/90 | Epoch: 25/30 | Cost: 0.0185 | Time elapsed: 84.63 min 283 | Batch:20/90 | Epoch: 25/30 | Cost: 0.0183 | Time elapsed: 85.04 min 284 | Batch:30/90 | Epoch: 25/30 | Cost: 0.0187 | Time elapsed: 85.47 min 285 | Batch:40/90 | Epoch: 25/30 | Cost: 0.0204 | Time elapsed: 85.90 min 286 | Batch:50/90 | Epoch: 25/30 | Cost: 0.0164 | Time elapsed: 86.32 min 287 | Batch:60/90 | Epoch: 25/30 | Cost: 0.0172 | Time elapsed: 86.76 min 288 | Batch:70/90 | Epoch: 25/30 | Cost: 0.0178 | Time elapsed: 87.18 min 289 | Batch:80/90 | Epoch: 25/30 | Cost: 0.0165 | Time elapsed: 87.57 min 290 | Evaluation for epoch 25:Test 291 | "Hits: 3567(50.92) | >=6: 5497(78.47) | >=5: 6322(90.25) | Total examples: 7005"Train 292 | "Hits: 9790(85.47) | >=6: 10854(94.76) | >=5: 11170(97.52) | Total examples: 11454"Batch:0/90 | Epoch: 26/30 | Cost: 0.0186 | Time elapsed: 90.37 min 293 | Batch:10/90 | Epoch: 26/30 | Cost: 0.0187 | Time elapsed: 90.52 min 294 | Batch:20/90 | Epoch: 26/30 | Cost: 0.0173 | Time elapsed: 90.69 min 295 | Batch:30/90 | Epoch: 26/30 | Cost: 0.0193 | Time elapsed: 90.85 min 296 | Batch:40/90 | Epoch: 26/30 | Cost: 0.0163 | Time elapsed: 91.02 min 297 | Batch:50/90 | Epoch: 26/30 | Cost: 0.0178 | Time elapsed: 91.22 min 298 | Batch:60/90 | Epoch: 26/30 | Cost: 0.0157 | Time elapsed: 91.59 min 299 | Batch:70/90 | Epoch: 26/30 | Cost: 0.0174 | Time elapsed: 91.95 min 300 | Batch:80/90 | Epoch: 26/30 | Cost: 0.0170 | Time elapsed: 92.32 min 301 | Evaluation for epoch 26:Test 302 | "Hits: 3595(51.32) | >=6: 5525(78.87) | >=5: 6330(90.36) | Total examples: 7005"Train 303 | "Hits: 9875(86.21) | >=6: 10889(95.07) | >=5: 11182(97.63) | Total examples: 11454"Batch:0/90 | Epoch: 27/30 | Cost: 0.0164 | Time elapsed: 94.97 min 304 | Batch:10/90 | Epoch: 27/30 | Cost: 0.0162 | Time elapsed: 95.04 min 305 | Batch:20/90 | Epoch: 27/30 | Cost: 0.0166 | Time elapsed: 95.12 min 306 | Batch:30/90 | Epoch: 27/30 | Cost: 0.0161 | Time elapsed: 95.20 min 307 | Batch:40/90 | Epoch: 27/30 | Cost: 0.0162 | Time elapsed: 95.25 min 308 | Batch:50/90 | Epoch: 27/30 | Cost: 0.0168 | Time elapsed: 95.30 min 309 | Batch:60/90 | Epoch: 27/30 | Cost: 0.0175 | Time elapsed: 95.34 min 310 | Batch:70/90 | Epoch: 27/30 | Cost: 0.0185 | Time elapsed: 95.39 min 311 | Batch:80/90 | Epoch: 27/30 | Cost: 0.0175 | Time elapsed: 95.44 min 312 | Evaluation for epoch 27:Test 313 | "Hits: 3629(51.81) | >=6: 5547(79.19) | >=5: 6339(90.49) | Total examples: 7005"Train 314 | "Hits: 9965(87.00) | >=6: 10920(95.34) | >=5: 11203(97.81) | Total examples: 11454"Batch:0/90 | Epoch: 28/30 | Cost: 0.0175 | Time elapsed: 96.36 min 315 | Batch:10/90 | Epoch: 28/30 | Cost: 0.0164 | Time elapsed: 96.43 min 316 | Batch:20/90 | Epoch: 28/30 | Cost: 0.0182 | Time elapsed: 96.51 min 317 | Batch:30/90 | Epoch: 28/30 | Cost: 0.0153 | Time elapsed: 96.58 min 318 | Batch:40/90 | Epoch: 28/30 | Cost: 0.0163 | Time elapsed: 96.66 min 319 | Batch:50/90 | Epoch: 28/30 | Cost: 0.0150 | Time elapsed: 96.72 min 320 | Batch:60/90 | Epoch: 28/30 | Cost: 0.0139 | Time elapsed: 96.77 min 321 | Batch:70/90 | Epoch: 28/30 | Cost: 0.0166 | Time elapsed: 96.82 min 322 | Batch:80/90 | Epoch: 28/30 | Cost: 0.0178 | Time elapsed: 96.87 min 323 | Evaluation for epoch 28:Test 324 | "Hits: 3650(52.11) | >=6: 5555(79.30) | >=5: 6355(90.72) | Total examples: 7005"Train 325 | "Hits: 10033(87.59) | >=6: 10953(95.63) | >=5: 11226(98.01) | Total examples: 11454"Batch:0/90 | Epoch: 29/30 | Cost: 0.0153 | Time elapsed: 97.35 min 326 | Batch:10/90 | Epoch: 29/30 | Cost: 0.0176 | Time elapsed: 97.39 min 327 | Batch:20/90 | Epoch: 29/30 | Cost: 0.0151 | Time elapsed: 97.43 min 328 | Batch:30/90 | Epoch: 29/30 | Cost: 0.0166 | Time elapsed: 97.47 min 329 | Batch:40/90 | Epoch: 29/30 | Cost: 0.0161 | Time elapsed: 97.51 min 330 | Batch:50/90 | Epoch: 29/30 | Cost: 0.0172 | Time elapsed: 97.55 min 331 | Batch:60/90 | Epoch: 29/30 | Cost: 0.0163 | Time elapsed: 97.59 min 332 | Batch:70/90 | Epoch: 29/30 | Cost: 0.0157 | Time elapsed: 97.63 min 333 | Batch:80/90 | Epoch: 29/30 | Cost: 0.0172 | Time elapsed: 97.67 min 334 | Evaluation for epoch 29:Test 335 | "Hits: 3672(52.42) | >=6: 5573(79.56) | >=5: 6362(90.82) | Total examples: 7005"Train 336 | "Hits: 10095(88.14) | >=6: 10975(95.82) | >=5: 11239(98.12) | Total examples: 11454" 337 | Batch:0/90 | Epoch: 3/30 | Cost: 0.0113 | Time elapsed: 0.03 min 338 | Batch:10/90 | Epoch: 0/30 | Cost: 0.0112 | Time elapsed: 0.29 min 339 | Batch:20/90 | Epoch: 0/30 | Cost: 0.0135 | Time elapsed: 0.54 min 340 | Batch:30/90 | Epoch: 0/30 | Cost: 0.0141 | Time elapsed: 0.81 min 341 | Batch:40/90 | Epoch: 0/30 | Cost: 0.0131 | Time elapsed: 1.08 min 342 | Batch:50/90 | Epoch: 0/30 | Cost: 0.0129 | Time elapsed: 1.35 min 343 | Batch:60/90 | Epoch: 0/30 | Cost: 0.0132 | Time elapsed: 1.60 min 344 | Batch:70/90 | Epoch: 0/30 | Cost: 0.0129 | Time elapsed: 1.86 min 345 | Batch:80/90 | Epoch: 0/30 | Cost: 0.0122 | Time elapsed: 2.11 min 346 | Evaluation for epoch 30:Test 347 | "Hits: 1615(23.05) | >=6: 3736(53.33) | >=5: 5277(75.33) | Total examples: 7005"Train 348 | "Hits: 10229(89.31) | >=6: 10971(95.78) | >=5: 11228(98.03) | Total examples: 11454"Batch:0/90 | Epoch: 1/30 | Cost: 0.0129 | Time elapsed: 3.70 min 349 | Batch:10/90 | Epoch: 1/30 | Cost: 0.0110 | Time elapsed: 3.78 min 350 | Batch:20/90 | Epoch: 1/30 | Cost: 0.0126 | Time elapsed: 3.86 min 351 | Batch:30/90 | Epoch: 1/30 | Cost: 0.0095 | Time elapsed: 3.94 min 352 | Batch:40/90 | Epoch: 1/30 | Cost: 0.0164 | Time elapsed: 4.02 min 353 | Batch:50/90 | Epoch: 1/30 | Cost: 0.0140 | Time elapsed: 4.11 min 354 | Batch:60/90 | Epoch: 1/30 | Cost: 0.0114 | Time elapsed: 4.20 min 355 | Batch:70/90 | Epoch: 1/30 | Cost: 0.0114 | Time elapsed: 4.28 min 356 | Batch:80/90 | Epoch: 1/30 | Cost: 0.0103 | Time elapsed: 4.37 min 357 | Evaluation for epoch 31:Test 358 | "Hits: 1637(23.37) | >=6: 3776(53.90) | >=5: 5316(75.89) | Total examples: 7005"Train 359 | "Hits: 10335(90.23) | >=6: 11016(96.18) | >=5: 11249(98.21) | Total examples: 11454"Batch:0/90 | Epoch: 2/30 | Cost: 0.0104 | Time elapsed: 4.92 min 360 | Batch:10/90 | Epoch: 2/30 | Cost: 0.0126 | Time elapsed: 4.99 min 361 | Batch:20/90 | Epoch: 2/30 | Cost: 0.0129 | Time elapsed: 5.07 min 362 | Batch:30/90 | Epoch: 2/30 | Cost: 0.0111 | Time elapsed: 5.15 min 363 | Batch:40/90 | Epoch: 2/30 | Cost: 0.0110 | Time elapsed: 5.24 min 364 | Batch:50/90 | Epoch: 2/30 | Cost: 0.0100 | Time elapsed: 5.32 min 365 | Batch:60/90 | Epoch: 2/30 | Cost: 0.0113 | Time elapsed: 5.41 min 366 | Batch:70/90 | Epoch: 2/30 | Cost: 0.0136 | Time elapsed: 5.49 min 367 | Batch:80/90 | Epoch: 2/30 | Cost: 0.0114 | Time elapsed: 5.57 min 368 | Evaluation for epoch 32:Test 369 | "Hits: 1648(23.53) | >=6: 3779(53.95) | >=5: 5327(76.05) | Total examples: 7005"Train 370 | "Hits: 10435(91.10) | >=6: 11057(96.53) | >=5: 11276(98.45) | Total examples: 11454"Batch:0/90 | Epoch: 3/30 | Cost: 0.0126 | Time elapsed: 6.12 min 371 | Batch:10/90 | Epoch: 3/30 | Cost: 0.0121 | Time elapsed: 6.20 min 372 | Batch:20/90 | Epoch: 3/30 | Cost: 0.0134 | Time elapsed: 6.28 min 373 | Batch:30/90 | Epoch: 3/30 | Cost: 0.0105 | Time elapsed: 6.37 min 374 | Batch:40/90 | Epoch: 3/30 | Cost: 0.0092 | Time elapsed: 6.45 min 375 | Batch:50/90 | Epoch: 3/30 | Cost: 0.0112 | Time elapsed: 6.53 min 376 | Batch:60/90 | Epoch: 3/30 | Cost: 0.0113 | Time elapsed: 6.62 min 377 | Batch:70/90 | Epoch: 3/30 | Cost: 0.0094 | Time elapsed: 6.70 min 378 | Batch:80/90 | Epoch: 3/30 | Cost: 0.0111 | Time elapsed: 6.79 min 379 | Evaluation for epoch 33:Test 380 | "Hits: 1678(23.95) | >=6: 3818(54.50) | >=5: 5345(76.30) | Total examples: 7005"Train 381 | "Hits: 10524(91.88) | >=6: 11094(96.86) | >=5: 11302(98.67) | Total examples: 11454"Batch:0/90 | Epoch: 4/30 | Cost: 0.0103 | Time elapsed: 7.35 min 382 | Batch:10/90 | Epoch: 4/30 | Cost: 0.0105 | Time elapsed: 7.43 min 383 | Batch:20/90 | Epoch: 4/30 | Cost: 0.0108 | Time elapsed: 7.51 min 384 | Batch:30/90 | Epoch: 4/30 | Cost: 0.0114 | Time elapsed: 7.59 min 385 | Batch:40/90 | Epoch: 4/30 | Cost: 0.0112 | Time elapsed: 7.68 min 386 | Batch:50/90 | Epoch: 4/30 | Cost: 0.0093 | Time elapsed: 7.77 min 387 | Batch:60/90 | Epoch: 4/30 | Cost: 0.0090 | Time elapsed: 7.85 min 388 | Batch:70/90 | Epoch: 4/30 | Cost: 0.0105 | Time elapsed: 7.94 min 389 | Batch:80/90 | Epoch: 4/30 | Cost: 0.0123 | Time elapsed: 8.02 min 390 | Evaluation for epoch 34:Test 391 | "Hits: 1666(23.78) | >=6: 3842(54.85) | >=5: 5352(76.40) | Total examples: 7005"Train 392 | "Hits: 10587(92.43) | >=6: 11119(97.08) | >=5: 11322(98.85) | Total examples: 11454"Batch:0/90 | Epoch: 5/30 | Cost: 0.0093 | Time elapsed: 8.59 min 393 | Batch:10/90 | Epoch: 5/30 | Cost: 0.0106 | Time elapsed: 8.68 min 394 | Batch:20/90 | Epoch: 5/30 | Cost: 0.0090 | Time elapsed: 8.76 min 395 | Batch:30/90 | Epoch: 5/30 | Cost: 0.0105 | Time elapsed: 8.84 min 396 | Batch:40/90 | Epoch: 5/30 | Cost: 0.0111 | Time elapsed: 8.93 min 397 | Batch:50/90 | Epoch: 5/30 | Cost: 0.0093 | Time elapsed: 9.01 min 398 | Batch:60/90 | Epoch: 5/30 | Cost: 0.0109 | Time elapsed: 9.10 min 399 | Batch:70/90 | Epoch: 5/30 | Cost: 0.0103 | Time elapsed: 9.19 min 400 | Batch:80/90 | Epoch: 5/30 | Cost: 0.0094 | Time elapsed: 9.27 min 401 | Evaluation for epoch 35:Test 402 | "Hits: 1687(24.08) | >=6: 3872(55.27) | >=5: 5372(76.69) | Total examples: 7005"Train 403 | "Hits: 10650(92.98) | >=6: 11142(97.28) | >=5: 11332(98.93) | Total examples: 11454"Batch:0/90 | Epoch: 6/30 | Cost: 0.0088 | Time elapsed: 9.82 min 404 | Batch:10/90 | Epoch: 6/30 | Cost: 0.0098 | Time elapsed: 9.91 min 405 | Batch:20/90 | Epoch: 6/30 | Cost: 0.0117 | Time elapsed: 9.99 min 406 | Batch:30/90 | Epoch: 6/30 | Cost: 0.0116 | Time elapsed: 10.07 min 407 | Batch:40/90 | Epoch: 6/30 | Cost: 0.0074 | Time elapsed: 10.15 min 408 | Batch:50/90 | Epoch: 6/30 | Cost: 0.0088 | Time elapsed: 10.24 min 409 | Batch:60/90 | Epoch: 6/30 | Cost: 0.0067 | Time elapsed: 10.32 min 410 | Batch:70/90 | Epoch: 6/30 | Cost: 0.0097 | Time elapsed: 10.41 min 411 | Batch:80/90 | Epoch: 6/30 | Cost: 0.0102 | Time elapsed: 10.49 min 412 | Evaluation for epoch 36:Test 413 | "Hits: 1695(24.20) | >=6: 3883(55.43) | >=5: 5372(76.69) | Total examples: 7005"Train 414 | "Hits: 10715(93.55) | >=6: 11174(97.56) | >=5: 11348(99.07) | Total examples: 11454"Batch:0/90 | Epoch: 7/30 | Cost: 0.0082 | Time elapsed: 11.07 min 415 | Batch:10/90 | Epoch: 7/30 | Cost: 0.0091 | Time elapsed: 11.31 min 416 | Batch:20/90 | Epoch: 7/30 | Cost: 0.0081 | Time elapsed: 11.56 min 417 | Batch:30/90 | Epoch: 7/30 | Cost: 0.0080 | Time elapsed: 11.80 min 418 | Batch:40/90 | Epoch: 7/30 | Cost: 0.0088 | Time elapsed: 12.03 min 419 | Batch:50/90 | Epoch: 7/30 | Cost: 0.0090 | Time elapsed: 12.26 min 420 | Batch:60/90 | Epoch: 7/30 | Cost: 0.0087 | Time elapsed: 12.48 min 421 | Batch:70/90 | Epoch: 7/30 | Cost: 0.0105 | Time elapsed: 12.71 min 422 | Batch:80/90 | Epoch: 7/30 | Cost: 0.0085 | Time elapsed: 12.91 min 423 | Evaluation for epoch 37:Test 424 | "Hits: 1708(24.38) | >=6: 3912(55.85) | >=5: 5391(76.96) | Total examples: 7005"Train 425 | "Hits: 10768(94.01) | >=6: 11200(97.78) | >=5: 11361(99.19) | Total examples: 11454"Batch:0/90 | Epoch: 8/30 | Cost: 0.0066 | Time elapsed: 13.61 min 426 | Batch:10/90 | Epoch: 8/30 | Cost: 0.0081 | Time elapsed: 13.69 min 427 | Batch:20/90 | Epoch: 8/30 | Cost: 0.0090 | Time elapsed: 13.78 min 428 | Batch:30/90 | Epoch: 8/30 | Cost: 0.0077 | Time elapsed: 13.87 min 429 | Batch:40/90 | Epoch: 8/30 | Cost: 0.0094 | Time elapsed: 13.95 min 430 | Batch:50/90 | Epoch: 8/30 | Cost: 0.0074 | Time elapsed: 14.04 min 431 | Batch:60/90 | Epoch: 8/30 | Cost: 0.0116 | Time elapsed: 14.13 min 432 | Batch:70/90 | Epoch: 8/30 | Cost: 0.0094 | Time elapsed: 14.21 min 433 | Batch:80/90 | Epoch: 8/30 | Cost: 0.0086 | Time elapsed: 14.30 min 434 | Evaluation for epoch 38:Test 435 | "Hits: 1724(24.61) | >=6: 3938(56.22) | >=5: 5422(77.40) | Total examples: 7005"Train 436 | "Hits: 10836(94.60) | >=6: 11231(98.05) | >=5: 11372(99.28) | Total examples: 11454"Batch:0/90 | Epoch: 9/30 | Cost: 0.0094 | Time elapsed: 14.85 min 437 | Batch:10/90 | Epoch: 9/30 | Cost: 0.0075 | Time elapsed: 14.93 min 438 | Batch:20/90 | Epoch: 9/30 | Cost: 0.0070 | Time elapsed: 15.01 min 439 | Batch:30/90 | Epoch: 9/30 | Cost: 0.0084 | Time elapsed: 15.09 min 440 | Batch:40/90 | Epoch: 9/30 | Cost: 0.0080 | Time elapsed: 15.18 min 441 | Batch:50/90 | Epoch: 9/30 | Cost: 0.0088 | Time elapsed: 15.27 min 442 | Batch:60/90 | Epoch: 9/30 | Cost: 0.0098 | Time elapsed: 15.35 min 443 | Batch:70/90 | Epoch: 9/30 | Cost: 0.0090 | Time elapsed: 15.44 min 444 | Batch:80/90 | Epoch: 9/30 | Cost: 0.0087 | Time elapsed: 15.52 min 445 | Evaluation for epoch 39:Test 446 | "Hits: 1753(25.02) | >=6: 3948(56.36) | >=5: 5420(77.37) | Total examples: 7005"Train 447 | "Hits: 10890(95.08) | >=6: 11256(98.27) | >=5: 11381(99.36) | Total examples: 11454"Batch:0/90 | Epoch: 10/30 | Cost: 0.0066 | Time elapsed: 16.08 min 448 | Batch:10/90 | Epoch: 10/30 | Cost: 0.0082 | Time elapsed: 16.17 min 449 | Batch:20/90 | Epoch: 10/30 | Cost: 0.0079 | Time elapsed: 16.25 min 450 | Batch:30/90 | Epoch: 10/30 | Cost: 0.0083 | Time elapsed: 16.33 min 451 | Batch:40/90 | Epoch: 10/30 | Cost: 0.0079 | Time elapsed: 16.42 min 452 | Batch:50/90 | Epoch: 10/30 | Cost: 0.0097 | Time elapsed: 16.51 min 453 | Batch:60/90 | Epoch: 10/30 | Cost: 0.0065 | Time elapsed: 16.60 min 454 | Batch:70/90 | Epoch: 10/30 | Cost: 0.0066 | Time elapsed: 16.69 min 455 | Batch:80/90 | Epoch: 10/30 | Cost: 0.0074 | Time elapsed: 16.78 min 456 | Evaluation for epoch 40:Test 457 | "Hits: 1769(25.25) | >=6: 3966(56.62) | >=5: 5431(77.53) | Total examples: 7005"Train 458 | "Hits: 10926(95.39) | >=6: 11270(98.39) | >=5: 11393(99.47) | Total examples: 11454"Batch:0/90 | Epoch: 11/30 | Cost: 0.0095 | Time elapsed: 17.37 min 459 | Batch:10/90 | Epoch: 11/30 | Cost: 0.0071 | Time elapsed: 17.45 min 460 | Batch:20/90 | Epoch: 11/30 | Cost: 0.0079 | Time elapsed: 17.53 min 461 | Batch:30/90 | Epoch: 11/30 | Cost: 0.0063 | Time elapsed: 17.62 min 462 | Batch:40/90 | Epoch: 11/30 | Cost: 0.0071 | Time elapsed: 17.70 min 463 | Batch:50/90 | Epoch: 11/30 | Cost: 0.0066 | Time elapsed: 17.79 min 464 | Batch:60/90 | Epoch: 11/30 | Cost: 0.0072 | Time elapsed: 17.88 min 465 | Batch:70/90 | Epoch: 11/30 | Cost: 0.0073 | Time elapsed: 17.97 min 466 | Batch:80/90 | Epoch: 11/30 | Cost: 0.0069 | Time elapsed: 18.06 min 467 | Evaluation for epoch 41:Test 468 | "Hits: 1770(25.27) | >=6: 3990(56.96) | >=5: 5448(77.77) | Total examples: 7005"Train 469 | "Hits: 10963(95.71) | >=6: 11291(98.58) | >=5: 11400(99.53) | Total examples: 11454"Batch:0/90 | Epoch: 12/30 | Cost: 0.0075 | Time elapsed: 18.63 min 470 | Batch:10/90 | Epoch: 12/30 | Cost: 0.0065 | Time elapsed: 18.71 min 471 | Batch:20/90 | Epoch: 12/30 | Cost: 0.0055 | Time elapsed: 18.79 min 472 | Batch:30/90 | Epoch: 12/30 | Cost: 0.0071 | Time elapsed: 18.87 min 473 | Batch:40/90 | Epoch: 12/30 | Cost: 0.0073 | Time elapsed: 18.96 min 474 | Batch:50/90 | Epoch: 12/30 | Cost: 0.0078 | Time elapsed: 19.05 min 475 | Batch:60/90 | Epoch: 12/30 | Cost: 0.0056 | Time elapsed: 19.13 min 476 | Batch:70/90 | Epoch: 12/30 | Cost: 0.0073 | Time elapsed: 19.22 min 477 | Batch:80/90 | Epoch: 12/30 | Cost: 0.0071 | Time elapsed: 19.31 min 478 | Evaluation for epoch 42:Test 479 | "Hits: 1787(25.51) | >=6: 4004(57.16) | >=5: 5456(77.89) | Total examples: 7005"Train 480 | "Hits: 11016(96.18) | >=6: 11309(98.73) | >=5: 11403(99.55) | Total examples: 11454"Batch:0/90 | Epoch: 13/30 | Cost: 0.0065 | Time elapsed: 19.86 min 481 | Batch:10/90 | Epoch: 13/30 | Cost: 0.0065 | Time elapsed: 19.95 min 482 | Batch:20/90 | Epoch: 13/30 | Cost: 0.0056 | Time elapsed: 20.03 min 483 | Batch:30/90 | Epoch: 13/30 | Cost: 0.0090 | Time elapsed: 20.11 min 484 | Batch:40/90 | Epoch: 13/30 | Cost: 0.0077 | Time elapsed: 20.20 min 485 | Batch:50/90 | Epoch: 13/30 | Cost: 0.0074 | Time elapsed: 20.29 min 486 | Batch:60/90 | Epoch: 13/30 | Cost: 0.0071 | Time elapsed: 20.38 min 487 | Batch:70/90 | Epoch: 13/30 | Cost: 0.0058 | Time elapsed: 20.47 min 488 | Batch:80/90 | Epoch: 13/30 | Cost: 0.0074 | Time elapsed: 20.56 min 489 | Evaluation for epoch 43:Test 490 | "Hits: 1797(25.65) | >=6: 4032(57.56) | >=5: 5465(78.02) | Total examples: 7005"Train 491 | "Hits: 11041(96.39) | >=6: 11320(98.83) | >=5: 11410(99.62) | Total examples: 11454"Batch:0/90 | Epoch: 14/30 | Cost: 0.0075 | Time elapsed: 21.16 min 492 | Batch:10/90 | Epoch: 14/30 | Cost: 0.0068 | Time elapsed: 21.24 min 493 | Batch:20/90 | Epoch: 14/30 | Cost: 0.0063 | Time elapsed: 21.33 min 494 | Batch:30/90 | Epoch: 14/30 | Cost: 0.0069 | Time elapsed: 21.41 min 495 | Batch:40/90 | Epoch: 14/30 | Cost: 0.0068 | Time elapsed: 21.50 min 496 | Batch:50/90 | Epoch: 14/30 | Cost: 0.0080 | Time elapsed: 21.59 min 497 | Batch:60/90 | Epoch: 14/30 | Cost: 0.0059 | Time elapsed: 21.69 min 498 | Batch:70/90 | Epoch: 14/30 | Cost: 0.0076 | Time elapsed: 21.78 min 499 | Batch:80/90 | Epoch: 14/30 | Cost: 0.0079 | Time elapsed: 21.87 min 500 | Evaluation for epoch 44:Test 501 | "Hits: 1801(25.71) | >=6: 4043(57.72) | >=5: 5489(78.36) | Total examples: 7005"Train 502 | "Hits: 11075(96.69) | >=6: 11330(98.92) | >=5: 11414(99.65) | Total examples: 11454"Batch:0/90 | Epoch: 15/30 | Cost: 0.0062 | Time elapsed: 22.48 min 503 | Batch:10/90 | Epoch: 15/30 | Cost: 0.0053 | Time elapsed: 22.56 min 504 | Batch:20/90 | Epoch: 15/30 | Cost: 0.0066 | Time elapsed: 22.65 min 505 | Batch:30/90 | Epoch: 15/30 | Cost: 0.0064 | Time elapsed: 22.73 min 506 | Batch:40/90 | Epoch: 15/30 | Cost: 0.0069 | Time elapsed: 22.82 min 507 | Batch:50/90 | Epoch: 15/30 | Cost: 0.0082 | Time elapsed: 22.90 min 508 | Batch:60/90 | Epoch: 15/30 | Cost: 0.0071 | Time elapsed: 22.99 min 509 | Batch:70/90 | Epoch: 15/30 | Cost: 0.0073 | Time elapsed: 23.08 min 510 | Batch:80/90 | Epoch: 15/30 | Cost: 0.0077 | Time elapsed: 23.17 min 511 | Evaluation for epoch 45:Test 512 | "Hits: 1810(25.84) | >=6: 4055(57.89) | >=5: 5501(78.53) | Total examples: 7005"Train 513 | "Hits: 11105(96.95) | >=6: 11339(99.00) | >=5: 11420(99.70) | Total examples: 11454"Batch:0/90 | Epoch: 16/30 | Cost: 0.0069 | Time elapsed: 23.76 min 514 | Batch:10/90 | Epoch: 16/30 | Cost: 0.0058 | Time elapsed: 24.02 min 515 | Batch:20/90 | Epoch: 16/30 | Cost: 0.0073 | Time elapsed: 24.26 min 516 | Batch:30/90 | Epoch: 16/30 | Cost: 0.0071 | Time elapsed: 24.50 min 517 | Batch:40/90 | Epoch: 16/30 | Cost: 0.0068 | Time elapsed: 24.77 min 518 | Batch:50/90 | Epoch: 16/30 | Cost: 0.0074 | Time elapsed: 25.02 min 519 | Batch:60/90 | Epoch: 16/30 | Cost: 0.0068 | Time elapsed: 25.26 min 520 | Batch:70/90 | Epoch: 16/30 | Cost: 0.0051 | Time elapsed: 25.52 min 521 | Batch:80/90 | Epoch: 16/30 | Cost: 0.0077 | Time elapsed: 25.76 min 522 | Evaluation for epoch 46:Test 523 | "Hits: 1814(25.90) | >=6: 4079(58.23) | >=5: 5496(78.46) | Total examples: 7005"Train 524 | "Hits: 11134(97.21) | >=6: 11356(99.14) | >=5: 11426(99.76) | Total examples: 11454"Batch:0/90 | Epoch: 17/30 | Cost: 0.0057 | Time elapsed: 27.07 min 525 | Batch:10/90 | Epoch: 17/30 | Cost: 0.0055 | Time elapsed: 27.33 min 526 | Batch:20/90 | Epoch: 17/30 | Cost: 0.0086 | Time elapsed: 27.59 min 527 | Batch:30/90 | Epoch: 17/30 | Cost: 0.0054 | Time elapsed: 27.87 min 528 | Batch:40/90 | Epoch: 17/30 | Cost: 0.0061 | Time elapsed: 28.12 min 529 | Batch:50/90 | Epoch: 17/30 | Cost: 0.0066 | Time elapsed: 28.40 min 530 | Batch:60/90 | Epoch: 17/30 | Cost: 0.0050 | Time elapsed: 28.66 min 531 | Batch:70/90 | Epoch: 17/30 | Cost: 0.0061 | Time elapsed: 28.94 min 532 | Batch:80/90 | Epoch: 17/30 | Cost: 0.0059 | Time elapsed: 29.20 min 533 | Evaluation for epoch 47:Test 534 | "Hits: 1817(25.94) | >=6: 4077(58.20) | >=5: 5500(78.52) | Total examples: 7005"Train 535 | "Hits: 11169(97.51) | >=6: 11367(99.24) | >=5: 11431(99.80) | Total examples: 11454"Batch:0/90 | Epoch: 18/30 | Cost: 0.0066 | Time elapsed: 31.58 min 536 | Batch:10/90 | Epoch: 18/30 | Cost: 0.0052 | Time elapsed: 31.88 min 537 | Batch:20/90 | Epoch: 18/30 | Cost: 0.0069 | Time elapsed: 32.21 min 538 | Batch:30/90 | Epoch: 18/30 | Cost: 0.0062 | Time elapsed: 32.48 min 539 | Batch:40/90 | Epoch: 18/30 | Cost: 0.0052 | Time elapsed: 32.74 min 540 | Batch:50/90 | Epoch: 18/30 | Cost: 0.0058 | Time elapsed: 32.99 min 541 | Batch:60/90 | Epoch: 18/30 | Cost: 0.0068 | Time elapsed: 33.30 min 542 | Batch:70/90 | Epoch: 18/30 | Cost: 0.0045 | Time elapsed: 33.57 min 543 | Batch:80/90 | Epoch: 18/30 | Cost: 0.0061 | Time elapsed: 33.84 min 544 | Evaluation for epoch 48:Test 545 | "Hits: 1837(26.22) | >=6: 4080(58.24) | >=5: 5513(78.70) | Total examples: 7005"Train 546 | "Hits: 11190(97.70) | >=6: 11375(99.31) | >=5: 11435(99.83) | Total examples: 11454"Batch:0/90 | Epoch: 19/30 | Cost: 0.0061 | Time elapsed: 36.46 min 547 | Batch:10/90 | Epoch: 19/30 | Cost: 0.0049 | Time elapsed: 36.54 min 548 | Batch:20/90 | Epoch: 19/30 | Cost: 0.0065 | Time elapsed: 36.63 min 549 | Batch:30/90 | Epoch: 19/30 | Cost: 0.0055 | Time elapsed: 36.71 min 550 | Batch:40/90 | Epoch: 19/30 | Cost: 0.0054 | Time elapsed: 36.80 min 551 | Batch:50/90 | Epoch: 19/30 | Cost: 0.0069 | Time elapsed: 36.89 min 552 | Batch:60/90 | Epoch: 19/30 | Cost: 0.0065 | Time elapsed: 36.98 min 553 | Batch:70/90 | Epoch: 19/30 | Cost: 0.0059 | Time elapsed: 37.07 min 554 | Batch:80/90 | Epoch: 19/30 | Cost: 0.0047 | Time elapsed: 37.16 min 555 | Evaluation for epoch 49:Test 556 | "Hits: 1847(26.37) | >=6: 4112(58.70) | >=5: 5535(79.01) | Total examples: 7005"Train 557 | "Hits: 11220(97.96) | >=6: 11379(99.35) | >=5: 11438(99.86) | Total examples: 11454"Batch:0/90 | Epoch: 20/30 | Cost: 0.0048 | Time elapsed: 37.72 min 558 | Batch:10/90 | Epoch: 20/30 | Cost: 0.0065 | Time elapsed: 37.80 min 559 | Batch:20/90 | Epoch: 20/30 | Cost: 0.0057 | Time elapsed: 37.89 min 560 | Batch:30/90 | Epoch: 20/30 | Cost: 0.0058 | Time elapsed: 37.97 min 561 | Batch:40/90 | Epoch: 20/30 | Cost: 0.0052 | Time elapsed: 38.05 min 562 | Batch:50/90 | Epoch: 20/30 | Cost: 0.0049 | Time elapsed: 38.14 min 563 | Batch:60/90 | Epoch: 20/30 | Cost: 0.0048 | Time elapsed: 38.23 min 564 | Batch:70/90 | Epoch: 20/30 | Cost: 0.0065 | Time elapsed: 38.32 min 565 | Batch:80/90 | Epoch: 20/30 | Cost: 0.0061 | Time elapsed: 38.41 min 566 | Evaluation for epoch 50:Test 567 | "Hits: 1858(26.52) | >=6: 4110(58.67) | >=5: 5545(79.16) | Total examples: 7005"Train 568 | "Hits: 11242(98.15) | >=6: 11389(99.43) | >=5: 11440(99.88) | Total examples: 11454"Batch:0/90 | Epoch: 21/30 | Cost: 0.0044 | Time elapsed: 38.97 min 569 | -------------------------------------------------------------------------------- /sample/01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/sample/01.jpg -------------------------------------------------------------------------------- /sample/02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/sample/02.jpg -------------------------------------------------------------------------------- /sample/KRC8D12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/sample/KRC8D12.jpg -------------------------------------------------------------------------------- /sample/OMD6805.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/sample/OMD6805.jpg -------------------------------------------------------------------------------- /sample/cnn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/sample/cnn.png -------------------------------------------------------------------------------- /sample/confusion_matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/sample/confusion_matrix.png -------------------------------------------------------------------------------- /sample/feed_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/sample/feed_example.png -------------------------------------------------------------------------------- /sample/plate01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/sample/plate01.jpeg -------------------------------------------------------------------------------- /sample/sample_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glefundes/AccessALPR/bbba05dc07e7558c51dca2b1873e9f4d7bdb2bce/sample/sample_gif.gif -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | import sys 4 | import torch 5 | import random 6 | import datetime 7 | import argparse 8 | import traceback 9 | import numpy as np 10 | import configparser 11 | 12 | import reader.PlateReader 13 | import detector.PlateDetector 14 | import urllib.request, urllib.parse 15 | 16 | from PIL import Image 17 | from art import tprint 18 | 19 | config = configparser.ConfigParser() 20 | try: 21 | config.read('config.ini') 22 | except: 23 | print('Configuration file not found :(') 24 | sys.exit(1) 25 | 26 | tprint('AccessALPR',font='slant') 27 | print(datetime.datetime.now().strftime("%d-%b-%Y (%H:%M:%S)")) 28 | print('Author: Gabriel Lefundes Vieira (Novandrie)') 29 | print('IVision Research Lab - Universidade Federal da Bahia @ 2019') 30 | print('Project Page: https://github.com/glefundes/AccessALPR') 31 | print('=============================\n') 32 | 33 | parser = argparse.ArgumentParser() 34 | parser.add_argument('--img_path', '-i', type=str, default='sample/01.jpg') 35 | parser.add_argument('--output','-o', type=str, default=None) 36 | parser.add_argument('--read_only','-r', type=bool, default=False) 37 | args = parser.parse_args() 38 | 39 | INPUT_IMG = args.img_path 40 | OUTPUT = args.output 41 | READ_ONLY = args.read_only 42 | 43 | PADDING = 1 44 | 45 | INPUT_SIZE = int(config['DEFAULT']['InputSize']) 46 | READER_WEIGHTS = config['DEFAULT']['ReaderWeights'] 47 | DETECTOR_CFG = config['DEFAULT']['DetectorConfig'] 48 | DETECTOR_WEIGHTS = config['DEFAULT']['DetectorWeights'] 49 | 50 | print('Loading detection model...') 51 | try: 52 | plate_detector = detector.PlateDetector.PlateDetector(DETECTOR_CFG, DETECTOR_WEIGHTS, input_size=INPUT_SIZE, conf_thresh=0.4) 53 | except Exception as e: 54 | print('\n Failed on loading plate detector :( Please check stack trace:\n') 55 | traceback.print_exc() 56 | sys.exit(1) 57 | 58 | print('Loading OCR model...') 59 | try: 60 | plate_reader = reader.PlateReader.PlateReader() 61 | plate_reader.load_state_dict(torch.load(READER_WEIGHTS)) 62 | plate_reader.eval() 63 | except Exception as e: 64 | print('\n Failed on loading plate reader :( Please check stack trace:\n') 65 | traceback.print_exc() 66 | sys.exit(1) 67 | 68 | print('All done! Starting ALPR system...') 69 | 70 | try: 71 | if not READ_ONLY: 72 | print(INPUT_IMG) 73 | frame = cv2.imread(INPUT_IMG) 74 | # Plate detection 75 | yolo_frame = frame.copy() 76 | bbox = plate_detector.predict(yolo_frame) 77 | # Detection processing 78 | if (len(bbox) != 0): 79 | bbox = plate_detector.rescale_boxes(bbox, INPUT_SIZE, yolo_frame.shape[:2]) 80 | x1, y1, x2, y2, conf = [int(d) for d in bbox[0]] 81 | plate = frame[int(y1-PADDING):int(y2+PADDING),int(x1-PADDING):int(x2+PADDING)] 82 | # # Plate OCR reading 83 | pil_plate = Image.fromarray(plate) 84 | else: 85 | print('No plates found on input image. Exiting...') 86 | sys.exit(1) 87 | else: 88 | pil_plate = Image.open(INPUT_IMG) 89 | 90 | 91 | word = plate_reader.predict(pil_plate) 92 | print('Found plate: {}'.format(word)) 93 | if(OUTPUT and not READ_ONLY): 94 | frame = cv2.rectangle(frame, (x1-PADDING,y1-PADDING), (x2+PADDING,y2+PADDING), (0,255,255),thickness=2) 95 | frame = cv2.putText(frame,word,(x1-2,y1-2), cv2.FONT_HERSHEY_SIMPLEX, 1,(0,255,255),2) 96 | outpath = os.path.join(OUTPUT,word+'.jpg') 97 | cv2.imwrite(outpath, frame) 98 | 99 | 100 | 101 | except Exception as e: 102 | traceback.print_exc() 103 | sys.exit(1) 104 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import configparser 3 | import weighted_levenshtein 4 | 5 | config = configparser.ConfigParser() 6 | 7 | LEV_WEIGHTS = config['DEFAULT']['LevenshteinWeights'] 8 | LEV_TRESHOLD = 0.2 9 | 10 | with open(LevenshteinWeights, 'r') as readFile: 11 | csvreader = csv.reader(readFile) 12 | lines = list(csvreader)[1:] 13 | for l in lines: 14 | substitute_costs[ord(l[0]), ord(l[1])] = l[2] 15 | 16 | 17 | def lev_distance(a, b, weights=LEV_WEIGHTS) 18 | wlev_dist = weighted_levenshtein.lev(a, b, substitute_costs=weights) 19 | return wlev_dist --------------------------------------------------------------------------------