@blokcert/node-red-contrib-plate-recognizer 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # @blokcert/node-red-contrib-plate-recognizer
2
+ A Node-RED node for license plate recognizing via [platerecognizer.com](https://app.platerecognizer.com/)
3
+
4
+ > This is a fork of [bartbutenaers/node-red-contrib-plate-recognizer](https://github.com/bartbutenaers/node-red-contrib-plate-recognizer) maintained by [@blokcert](https://gitlab.com/blokcert). The original was created by Bart Butenaers — all credit for the core functionality goes to him. This fork adds **automatic retry on transient `ETIMEDOUT` errors**, which we observed at ~22% failure rate during sustained calls from Asia-Pacific networks (see the [Retry behaviour](#retry-behaviour) section below).
5
+
6
+ ## Install
7
+ Run the following npm command in your Node-RED user directory (typically ~/.node-red):
8
+ ```
9
+ npm install @blokcert/node-red-contrib-plate-recognizer
10
+ ```
11
+
12
+ ## Retry behaviour
13
+ Calls to `api.platerecognizer.com` occasionally fail at the TCP layer with `ETIMEDOUT` after ~260 ms — observed in clusters lasting a few seconds — which surfaces in `node-fetch` as:
14
+
15
+ ```
16
+ FetchError: request to http://api.platerecognizer.com/v1/plate-reader/ failed, reason:
17
+ ```
18
+
19
+ This fork transparently retries up to **5 times with a 1 second delay** when this specific error occurs. While retrying, the node shows a yellow `retry N/5` status. All other errors (HTTP 4xx/5xx, JSON parse failures, etc.) preserve the original behaviour and are sent to the second output unchanged.
20
+
21
+ In a 1 req/sec × 90 second test from Taiwan we observed:
22
+
23
+ | | Failure rate |
24
+ |---|---|
25
+ | Without retry | 27.8% |
26
+ | With 5-retry × 1s delay | 1.1% |
27
+
28
+ Note that you need to ***signup*** for an account on [platerecognizer.com](https://app.platerecognizer.com/start/), and paste your private token into this node's config screen. With a free account there is a limit to recognize 2500 images per month, but they also offer various paid license models.
29
+
30
+ ## Support my Node-RED developments
31
+
32
+ Please buy my wife a coffee to keep her happy, while I am busy developing Node-RED stuff for you ...
33
+
34
+ <a href="https://www.buymeacoffee.com/bartbutenaers" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy my wife a coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
35
+
36
+ ## Node usage
37
+ This node will detect and recognise license plates in an image, using a deep learning (cloud) service. The AI (cloud) service has been trained for license plates for more than 100 countries. They also offer an [SDK](http://docs.platerecognizer.com/#sdk) for local setups, which can easily be installed as a Docker container. See also our [blog](https://platerecognizer.com/blog/anpr-on-node-red/) for an introduction.
38
+
39
+ :warning: When you have an image with an ***incorrect recognition*** result, don't hesitate to contact the people of [platerecognizer.com](https://app.platerecognizer.com/)! They offer great support. When you provide them the image, they will analyse it and try to solve the problem. This way the system can become better and better ...
40
+
41
+ Send an image (as buffer or base64 encoded string) via an input message, to start a recognition:
42
+
43
+ ![Basic flow](https://user-images.githubusercontent.com/14224149/74985812-88e78f00-5438-11ea-8f5c-790730d77047.png)
44
+
45
+ ```
46
+ [{"id":"38586517.a5bf9a","type":"plate-recognizer","z":"d9a54719.b13a88","name":"","inputField":"payload","inputFieldType":"msg","outputField":"payload","outputFieldType":"msg","url":"https://api.platerecognizer.com/v1/plate-reader/","ignoreDuring":false,"makeAndModel":false,"statusText":"none","cameraId":"","regionFilter":false,"timestamp":false,"regionList":"","regionListType":"json","x":880,"y":660,"wires":[["30cb89da.c35546"],[]]},{"id":"e4699284.081c7","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":500,"y":660,"wires":[["9e2c9295.7f9a9"]]},{"id":"30cb89da.c35546","type":"debug","z":"d9a54719.b13a88","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1060,"y":660,"wires":[]},{"id":"9e2c9295.7f9a9","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://www.mercedes-benz.com/en/classic/history/h-number-plate-2020/_jcr_content/root/paragraph/paragraph-right/paragraphimage/image/MQ6-8-image-20191205151927/02-mercedes-benz-classic-h-number-plate-2020-2560x1440.jpeg","tls":"","persist":false,"proxy":"","authType":"","x":680,"y":660,"wires":[["38586517.a5bf9a"]]}]
47
+ ```
48
+
49
+ The output message will contain the recognition results (in json format):
50
+
51
+ ![Basic output](https://user-images.githubusercontent.com/14224149/75106301-521d9e80-561b-11ea-89b1-41dbf53bfac6.png)
52
+
53
+ The *"results*" will be a json array, containing a separate element for each license plate that has been recognised. When the image contains N cars, then the array will contain N elements.
54
+
55
+ For each recognised license plate, some basic information will be delivered:
56
+
57
+ + *box:* the bounding box (coordinates) where the vehicle is located inside the image.
58
+
59
+ + *plate:* the license plate itself as plain text.
60
+
61
+ + *region:* the region of the license plate (e.g. *"be"* for Belgium). This object contains some nested fields:
62
+
63
+ ![region](https://user-images.githubusercontent.com/14224149/75106572-7e86ea00-561e-11ea-9491-40f392e5b5b8.png)
64
+
65
+ + *code:* the region code (from the [region list](http://docs.platerecognizer.com/#regions-supported)).
66
+ + *score:* the confidence level for the region prediction, which is a value between 0 and 1 (with 1 the highest confidence).
67
+
68
+ + *vehicle:* information about the vehicle itself. This object contains some nested fields:
69
+
70
+ ![vehicle](https://user-images.githubusercontent.com/14224149/75106701-8abf7700-561f-11ea-92a2-234bf6cc8887.png)
71
+
72
+ + *type:* the type of vehicle (which can be Ambulance, Bus, Car, Limousine, Motorcycle, Taxi, Truck, Van, Unknown).
73
+ + *score:* the confidence level for the vehicle prediction, which is a value between 0 and 1 (with 1 the highest confidence).
74
+ + *box:* the bounding box (coordinates) where the vehicle is located inside the image.
75
+
76
+ + *score:* the confidence level for the license plate text prediction, which is a value between 0 and 1 (with 1 the highest confidence).
77
+
78
+ + *candidates:* sometimes the service isn't really sure whether it has recognised the license plate correctly. Therefore a list of possible plate *'candidates'* will be supplied. The first candidate is the same plate that has already been offered at the higher level:
79
+
80
+ ![image](https://user-images.githubusercontent.com/14224149/75106803-7b8cf900-5620-11ea-8b05-ee94c093f9b7.png)
81
+
82
+ In this case the AI service thinks (with 90,3% certainty) that the plate its "s0k92h", but it might be that the plate is "sok92h" (with 90,1% certainty). In this case the confusion is between the number "0" and the character "o".
83
+
84
+ + *dscore:* the confidence level for the license plate detection, which is a value between 0 and 1 (with 1 the highest confidence).
85
+
86
+ ## Node properties
87
+ The node can be configured via a series of settings on the config screen:
88
+
89
+ ### Input field
90
+ The field of the input message which will need to contain the input image. By default ```msg.payload``` will be used. The image should be a binary Buffer or a base64 encoded string.
91
+
92
+ ### Output field
93
+ The field of the output message where the recognition result will be stored (in JSON format). By default ```msg.payload``` will be used.
94
+
95
+ ### API token
96
+ Create an account at [platerecogniser.com](https://platerecognizer.com/) and enter your private API token here.
97
+
98
+ ### URL
99
+ Specify the URL of the recognition service, to allow different kind of setups:
100
+ + Use the official *cloud service*, which will be the default (and most used) option.
101
+ + Use a *local installation* (based on the SDK).
102
+ + Use a local *Docker container*.
103
+
104
+ ### Camera ID
105
+ Optionally a camera id can be specified, which will be sent to the recognition service.
106
+
107
+ ### Status text
108
+ Specify how the recognition result needs to be displayed in the node status label:
109
+ + *None:* Show no recognition results.
110
+ + *Plate count:* Show the number of plates that have been recognised in the image.
111
+ + *Plates:* Show a (comma separted) list of the plates that have been recognized in the image.
112
+ + *Plates and scores:* Same as the previous option, but now the 'score' percentage is also added.
113
+
114
+ ### Ignore images arriving during recognition
115
+ When selected images will automatically be skipped, when the previous image is still being recognized. When deselected multiple images can be recognized simultaneously.
116
+
117
+ ### Predict vehicle make and model (MMC)
118
+ When selected not only the plate will be recognized, but there will also be a prediction of the vehicle brand and type.
119
+
120
+ CAUTION: this is only supported for some paid account types!
121
+
122
+ ### Send separate message for each plate:
123
+ When selected a separate output message will be send for each recognized license plate. If not selected a single output message will be send containing an array of ALL recognized license plates. See the section *"Split output messages"* below for more information.
124
+
125
+ ### Specify one or more regions
126
+ When selected, an array of region codes can be specified (see [supported regions](http://docs.platerecognizer.com/#regions-supported).
127
+
128
+ ## Example flow (different cases)
129
+
130
+ The following flow explains some different use cases:
131
+ + Image contain a single car.
132
+ + Image containing a single car, but photografed from an angle. It is important to be able to recognize license plates at angles, because a camera won't always be positioned directly in front of the cars.
133
+ + Image containing two cars, which means the array will contain two individual recognitions.
134
+ + Image containing no cars, which means the array will be empty.
135
+ + Image containing a truck with a license plate, but also some texts on the truck itself. There will be multiple recognitions in the array (because the texts will also be detected!).
136
+
137
+ Note that the [node-red-contrib-image-output](https://github.com/rikukissa/node-red-contrib-image-output) node needs to be installed also!
138
+
139
+ ![image](https://user-images.githubusercontent.com/14224149/75100715-d4817080-55d1-11ea-9c64-2dae43df8fe8.png)
140
+
141
+ ```
142
+ [{"id":"fff72ad.6f8c7d8","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":460,"wires":[["ee55dd94.02b2e"]]},{"id":"28523886.f82428","type":"debug","z":"d9a54719.b13a88","name":"Normal recognitions","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"recognition","targetType":"msg","x":1560,"y":580,"wires":[]},{"id":"ee55dd94.02b2e","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://www.mercedes-benz.com/en/classic/history/h-number-plate-2020/_jcr_content/root/paragraph/paragraph-right/paragraphimage/image/MQ6-8-image-20191205151927/02-mercedes-benz-classic-h-number-plate-2020-2560x1440.jpeg","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":460,"wires":[["231d72b4.5233ae"]]},{"id":"10f189ae.db2d76","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":660,"wires":[["24464bb6.a07b64"]]},{"id":"24464bb6.a07b64","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://image.freepik.com/free-photo/empty-parking-lot_1127-3298.jpg","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":660,"wires":[["231d72b4.5233ae"]]},{"id":"5ee1acd5.4e0784","type":"comment","z":"d9a54719.b13a88","name":"No cars","info":"","x":890,"y":620,"wires":[]},{"id":"231d72b4.5233ae","type":"plate-recognizer","z":"d9a54719.b13a88","name":"","inputField":"payload","inputFieldType":"msg","outputField":"recognition","outputFieldType":"msg","url":"https://api.platerecognizer.com/v1/plate-reader/","ignoreDuring":false,"makeAndModel":false,"statusText":"scores","cameraId":"","regionFilter":false,"timestamp":false,"regionList":"","regionListType":"json","x":1320,"y":660,"wires":[["28523886.f82428","37b1b695.62fa4a"],["100a66ab.4607f9"]]},{"id":"37b1b695.62fa4a","type":"image","z":"d9a54719.b13a88","name":"Show analyzed image","width":"400","data":"payload","dataType":"msg","thumbnail":false,"active":true,"x":1740,"y":640,"wires":[]},{"id":"869cd924.d991d8","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":960,"wires":[["f6b6ac8f.40806"]]},{"id":"f6b6ac8f.40806","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://static.nieuwsblad.be/Assets/Images_Upload/2015/05/24/patton_drivers_2015_1.jpg?maxheight=460&maxwidth=638&scale=both","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":960,"wires":[["231d72b4.5233ae"]]},{"id":"6ac3b436.692fac","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":760,"wires":[["d2c4249d.901fd8"]]},{"id":"d2c4249d.901fd8","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"http://newscrane.com/wp-content/uploads/2019/09/Car-insurance-Newscrane-02.jpg","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":760,"wires":[["231d72b4.5233ae"]]},{"id":"9a21e444.b22fc8","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":560,"wires":[["8e81e18d.39882"]]},{"id":"8e81e18d.39882","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"http://www.piepenbroek.nl/foto2010/baltisch/IMG_1499.JPG","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":560,"wires":[["231d72b4.5233ae"]]},{"id":"5899f099.1b645","type":"comment","z":"d9a54719.b13a88","name":"Two cars","info":"","x":900,"y":520,"wires":[]},{"id":"1db8bd98.23d672","type":"comment","z":"d9a54719.b13a88","name":"One car","info":"","x":890,"y":420,"wires":[]},{"id":"100a66ab.4607f9","type":"debug","z":"d9a54719.b13a88","name":"Errors","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"recognition","targetType":"msg","x":1510,"y":680,"wires":[]},{"id":"cdfaa780.c2fa18","type":"comment","z":"d9a54719.b13a88","name":"Truck with labels","info":"","x":920,"y":920,"wires":[]},{"id":"c1d2f038.82ecc","type":"comment","z":"d9a54719.b13a88","name":"Car at angle","info":"","x":910,"y":720,"wires":[]},{"id":"8c4f0d01.426a2","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":860,"wires":[["7b580c6e.105764"]]},{"id":"7b580c6e.105764","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://askautoexperts.com/wp-content/uploads/1931-Dodge-1024x768.jpg","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":860,"wires":[["231d72b4.5233ae"]]},{"id":"da494e5a.e21ff","type":"comment","z":"d9a54719.b13a88","name":"Another car at angle","info":"","x":930,"y":820,"wires":[]}]
143
+ ```
144
+
145
+ ## Recognition status
146
+ The output message will contain the recognition status (and statusText):
147
+
148
+ ![status](https://user-images.githubusercontent.com/14224149/75118670-4f9f6100-567c-11ea-92fa-98d2801f3dc3.png)
149
+
150
+
151
+ + When the service has finished the recognition without problems, the output message will be send on the *first output* with status 2xx.
152
+ + When the service isn't able to process the recognition, the output message will be send to the *second output* with status 4xx. The status code will explain what went wrong:
153
+ + 403: Forbidden due to incorrect API token.
154
+ + 413: The payload is too large and exceeds their [upload limits](https://app.platerecognizer.com/upload-limit/).
155
+ + 429: Too many requests have been send in a given amount of time. Upgrade your license for higher number of calls per second.
156
+
157
+ ## Plate statistics
158
+ Since the number of recognitions per month is limited (e.g. 2500 for a free account), it is very useful to determine from time to time how many recognitions are left. This way you can avoid situations where you are not aware that you have run out of recognitions...
159
+
160
+ A second node (*"Plate statistics"*) has been provided to get the statistics of your account (with *'URL'* and *'API token'* settings identical as described above):
161
+
162
+ ![Statistics flow](https://user-images.githubusercontent.com/14224149/75119790-6cd92d00-5686-11ea-806d-d755b27eb8d6.png)
163
+
164
+ ```
165
+ [{"id":"9b354f46.f081d","type":"plate-statistics","z":"d9a54719.b13a88","name":"","outputField":"payload","outputFieldType":"msg","url":"https://api.platerecognizer.com/v1/statistics/","x":980,"y":480,"wires":[["54063a77.493ae4"]]},{"id":"5653b69.13c4648","type":"inject","z":"d9a54719.b13a88","name":"Get statistics","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":770,"y":480,"wires":[["9b354f46.f081d"]]},{"id":"54063a77.493ae4","type":"debug","z":"d9a54719.b13a88","name":"Plate statistics","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1190,"y":480,"wires":[]}]
166
+ ```
167
+ The resulting statistics (in json format) contain the maximum number of statistics, and also the used number of statistics of the current month:
168
+
169
+ ![Statistics output](https://user-images.githubusercontent.com/14224149/75119816-a3af4300-5686-11ea-9a7d-fb9a8292b823.png)
170
+
171
+ ## Split output messages
172
+ When the input message contains multiple license plates, then the output message will contain an ***array*** of license plates. Since not all Node-RED nodes can handle arrays as input, it might be required to split the array into separate items. In other words the single output message (containing an array of N license plates) need to be split into N separate output messages (each one containing a single license plate).
173
+
174
+ CAUTION: to avoid conflicts, the original input message will be ***cloned*** N times. But since the output message also contains the input image, that input image will also be cloned N times. As a result extra system resources (CPU and memory) will be used!
175
+
176
+ ### Using a Split node
177
+ The Split node is a Node-RED core node that can be used to split a single message into multiple messages:
178
+
179
+ ![Split node flow](https://user-images.githubusercontent.com/14224149/77010195-9f1a3980-6969-11ea-99e8-112c4b3ea351.png)
180
+
181
+ ```
182
+ [{"id":"ef944a38.bbef38","type":"plate-recognizer","z":"c8a948fc.76ade8","name":"","inputField":"payload","inputFieldType":"msg","outputField":"payload","outputFieldType":"msg","url":"https://api.platerecognizer.com/v1/plate-reader/","ignoreDuring":true,"makeAndModel":false,"statusText":"count","cameraId":"","regionFilter":false,"regionList":"[]","regionListType":"json","x":780,"y":500,"wires":[["78a775a2.7e37cc","60071b7.6a1cae4"],[]]},{"id":"2e44447f.4e42dc","type":"split","z":"c8a948fc.76ade8","name":"Split array","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":1220,"y":500,"wires":[["745a5d4a.ff4e34"]]},{"id":"5a39a5a9.55c97c","type":"http request","z":"c8a948fc.76ade8","name":"Get video stream","method":"GET","ret":"bin","paytoqs":false,"url":"http://www.piepenbroek.nl/foto2010/baltisch/IMG_1499.JPG","tls":"","persist":false,"proxy":"","authType":"","x":570,"y":500,"wires":[["ef944a38.bbef38","dad27e21.9ab27"]]},{"id":"dad27e21.9ab27","type":"image","z":"c8a948fc.76ade8","name":"","width":"400","data":"payload","dataType":"msg","thumbnail":false,"active":true,"x":780,"y":580,"wires":[]},{"id":"745a5d4a.ff4e34","type":"debug","z":"c8a948fc.76ade8","name":"Show messages","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1380,"y":500,"wires":[]},{"id":"15ee9680.b977ea","type":"inject","z":"c8a948fc.76ade8","name":"Start the test","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":370,"y":500,"wires":[["5a39a5a9.55c97c"]]},{"id":"78a775a2.7e37cc","type":"change","z":"c8a948fc.76ade8","name":"payload = payload.results","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.results","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1010,"y":500,"wires":[["2e44447f.4e42dc"]]},{"id":"60071b7.6a1cae4","type":"debug","z":"c8a948fc.76ade8","name":"Show messages","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":980,"y":440,"wires":[]}]
183
+ ```
184
+ The flow explained step by step:
185
+
186
+ 1. Start the flow by pressing the button on the Inject-node
187
+ 2. The second node gets an image (e.g. via a http request from an ip camera)
188
+ 3. In the image-preview node you can see that the image contains two license plates
189
+ 4. The plate recognizer node detects two plates
190
+ 5. Via a debug node you can see the json output: it is a single message containing an array of two license plates.
191
+ 6. I move the payloads.result field to the payload field (because the next node expects the array in the payload field).
192
+ 7. The split node splits the array in the payload, which means the single message will be splitted in two separate messages.
193
+ 8. With a debug node you will see that we now have two separate messages, each one containing a single license plate (which can now be handled easily by other nodes in the flow...).
194
+
195
+ ### Using the build-in splitter
196
+ Since the Split node will cause our flow to become a bit more complex, this node offers a build-in split functionality. When the *"Send separate message for each plate"* checkbox is activated, an input image (containing N license plates) will result in N output messages (each one containing a single license plate).
197
+
198
+ Edge case: when the input message contains *NO* license plate, then a single output message will be sent containing an *empty* result:
199
+
200
+ ![Empty result](https://user-images.githubusercontent.com/14224149/77010828-da693800-696a-11ea-8e64-637e9661ab8a.png)
201
+
202
+ Remark: in the latter case, we could have decided to send no output message (since no license plate has been detected). But when somebody sends a picture to the input, he will expect something back ...
@@ -0,0 +1 @@
1
+ [{"id":"38586517.a5bf9a","type":"plate-recognizer","z":"d9a54719.b13a88","name":"","inputField":"payload","inputFieldType":"msg","outputField":"payload","outputFieldType":"msg","url":"https://api.platerecognizer.com/v1/plate-reader/","ignoreDuring":false,"makeAndModel":false,"statusText":"none","cameraId":"","regionFilter":false,"timestamp":false,"regionList":"","regionListType":"json","x":880,"y":660,"wires":[["30cb89da.c35546"],[]]},{"id":"e4699284.081c7","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":500,"y":660,"wires":[["9e2c9295.7f9a9"]]},{"id":"30cb89da.c35546","type":"debug","z":"d9a54719.b13a88","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1060,"y":660,"wires":[]},{"id":"9e2c9295.7f9a9","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://www.mercedes-benz.com/en/classic/history/h-number-plate-2020/_jcr_content/root/paragraph/paragraph-right/paragraphimage/image/MQ6-8-image-20191205151927/02-mercedes-benz-classic-h-number-plate-2020-2560x1440.jpeg","tls":"","persist":false,"proxy":"","authType":"","x":680,"y":660,"wires":[["38586517.a5bf9a"]]}]
@@ -0,0 +1 @@
1
+ [{"id":"fff72ad.6f8c7d8","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":460,"wires":[["ee55dd94.02b2e"]]},{"id":"28523886.f82428","type":"debug","z":"d9a54719.b13a88","name":"Normal recognitions","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"recognition","targetType":"msg","x":1560,"y":580,"wires":[]},{"id":"ee55dd94.02b2e","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://www.mercedes-benz.com/en/classic/history/h-number-plate-2020/_jcr_content/root/paragraph/paragraph-right/paragraphimage/image/MQ6-8-image-20191205151927/02-mercedes-benz-classic-h-number-plate-2020-2560x1440.jpeg","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":460,"wires":[["231d72b4.5233ae"]]},{"id":"10f189ae.db2d76","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":660,"wires":[["24464bb6.a07b64"]]},{"id":"24464bb6.a07b64","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://image.freepik.com/free-photo/empty-parking-lot_1127-3298.jpg","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":660,"wires":[["231d72b4.5233ae"]]},{"id":"5ee1acd5.4e0784","type":"comment","z":"d9a54719.b13a88","name":"No cars","info":"","x":890,"y":620,"wires":[]},{"id":"231d72b4.5233ae","type":"plate-recognizer","z":"d9a54719.b13a88","name":"","inputField":"payload","inputFieldType":"msg","outputField":"recognition","outputFieldType":"msg","url":"https://api.platerecognizer.com/v1/plate-reader/","ignoreDuring":false,"makeAndModel":false,"statusText":"scores","cameraId":"","regionFilter":false,"timestamp":false,"regionList":"","regionListType":"json","x":1320,"y":660,"wires":[["28523886.f82428","37b1b695.62fa4a"],["100a66ab.4607f9"]]},{"id":"37b1b695.62fa4a","type":"image","z":"d9a54719.b13a88","name":"Show analyzed image","width":"400","data":"payload","dataType":"msg","thumbnail":false,"active":true,"x":1740,"y":640,"wires":[]},{"id":"869cd924.d991d8","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":960,"wires":[["f6b6ac8f.40806"]]},{"id":"f6b6ac8f.40806","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://static.nieuwsblad.be/Assets/Images_Upload/2015/05/24/patton_drivers_2015_1.jpg?maxheight=460&maxwidth=638&scale=both","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":960,"wires":[["231d72b4.5233ae"]]},{"id":"6ac3b436.692fac","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":760,"wires":[["d2c4249d.901fd8"]]},{"id":"d2c4249d.901fd8","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"http://newscrane.com/wp-content/uploads/2019/09/Car-insurance-Newscrane-02.jpg","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":760,"wires":[["231d72b4.5233ae"]]},{"id":"9a21e444.b22fc8","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":560,"wires":[["8e81e18d.39882"]]},{"id":"8e81e18d.39882","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"http://www.piepenbroek.nl/foto2010/baltisch/IMG_1499.JPG","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":560,"wires":[["231d72b4.5233ae"]]},{"id":"5899f099.1b645","type":"comment","z":"d9a54719.b13a88","name":"Two cars","info":"","x":900,"y":520,"wires":[]},{"id":"1db8bd98.23d672","type":"comment","z":"d9a54719.b13a88","name":"One car","info":"","x":890,"y":420,"wires":[]},{"id":"100a66ab.4607f9","type":"debug","z":"d9a54719.b13a88","name":"Errors","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"recognition","targetType":"msg","x":1510,"y":680,"wires":[]},{"id":"cdfaa780.c2fa18","type":"comment","z":"d9a54719.b13a88","name":"Truck with labels","info":"","x":920,"y":920,"wires":[]},{"id":"c1d2f038.82ecc","type":"comment","z":"d9a54719.b13a88","name":"Car at angle","info":"","x":910,"y":720,"wires":[]},{"id":"8c4f0d01.426a2","type":"inject","z":"d9a54719.b13a88","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":920,"y":860,"wires":[["7b580c6e.105764"]]},{"id":"7b580c6e.105764","type":"http request","z":"d9a54719.b13a88","name":"","method":"GET","ret":"bin","paytoqs":false,"url":"https://askautoexperts.com/wp-content/uploads/1931-Dodge-1024x768.jpg","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":860,"wires":[["231d72b4.5233ae"]]},{"id":"da494e5a.e21ff","type":"comment","z":"d9a54719.b13a88","name":"Another car at angle","info":"","x":930,"y":820,"wires":[]}]
@@ -0,0 +1 @@
1
+ [{"id":"9b354f46.f081d","type":"plate-statistics","z":"d9a54719.b13a88","name":"","outputField":"payload","outputFieldType":"msg","url":"https://api.platerecognizer.com/v1/statistics/","x":980,"y":480,"wires":[["54063a77.493ae4"]]},{"id":"5653b69.13c4648","type":"inject","z":"d9a54719.b13a88","name":"Get statistics","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":770,"y":480,"wires":[["9b354f46.f081d"]]},{"id":"54063a77.493ae4","type":"debug","z":"d9a54719.b13a88","name":"Plate statistics","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1190,"y":480,"wires":[]}]
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name" : "@blokcert/node-red-contrib-plate-recognizer",
3
+ "version" : "1.0.4",
4
+ "description" : "A Node Red node for recognising licence plates via platerecognizer.com (fork with automatic retry on ETIMEDOUT)",
5
+ "dependencies": {
6
+ "node-fetch": "2.6.0",
7
+ "form-data": "3.0.0"
8
+ },
9
+ "author": {
10
+ "name": "Bart Butenaers"
11
+ },
12
+ "contributors": [
13
+ {
14
+ "name": "Nemanja Vukmirovic",
15
+ "url": "https://github.com/vukmirovic98"
16
+ },
17
+ {
18
+ "name": "Caspar",
19
+ "email": "handsome0710@gmail.com",
20
+ "url": "https://gitlab.com/blokcert"
21
+ }
22
+ ],
23
+ "license": "Apache-2.0",
24
+ "keywords": [ "node-red", "license", "plate", "recognize", "recognition", "car", "alpr" ],
25
+ "bugs": {
26
+ "url": "https://gitlab.com/blokcert/node-red-contrib-plate-recognizer/-/issues"
27
+ },
28
+ "homepage": "https://gitlab.com/blokcert/node-red-contrib-plate-recognizer",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git@gitlab.com:blokcert/node-red-contrib-plate-recognizer.git"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "node-red" : {
37
+ "nodes": {
38
+ "plate-recognizer": "plate-recognizer.js",
39
+ "plate-statistics": "plate-statistics.js"
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,182 @@
1
+ <!--
2
+ Copyright 2020, Bart Butenaers
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+ http://www.apache.org/licenses/LICENSE-2.0
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
12
+ -->
13
+ <script type="text/javascript">
14
+ RED.nodes.registerType('plate-recognizer',{
15
+ category: 'image',
16
+ color: '#E9967A',
17
+ defaults: {
18
+ name: {value:""},
19
+ inputField: {value: "payload", required: true, validate: RED.validators.typedInput("inputFieldType")},
20
+ inputFieldType: {value: "msg"},
21
+ outputField: {value: "payload", required: true, validate: RED.validators.typedInput("outputFieldType")},
22
+ outputFieldType: {value: "msg"},
23
+ url: {value:"https://api.platerecognizer.com/v1/plate-reader/", required: true},
24
+ ignoreDuring: {value: true},
25
+ makeAndModel: {value: false},
26
+ statusText: {value: "count"},
27
+ cameraId: {value:'', required: false },
28
+ separateMsg: {value: false },
29
+ regionFilter: {value: false},
30
+ regionList: {value: "[]", validate: function(v) {
31
+ // The region list only needs to be specified when the region filter is checked
32
+ var regionFilter = $('#node-input-regionFilter').prop('checked');
33
+ if (regionFilter === true) {
34
+ var json = JSON.parse(v);
35
+ return json && Array.isArray(json) && json.length > 0;
36
+ }
37
+ return true; // Valid otherwise
38
+ }},
39
+ regionListType: {value: "json"}
40
+ },
41
+ credentials: {
42
+ apiToken: {type: "password"}
43
+ },
44
+ inputs:1,
45
+ outputs:2,
46
+ outputLabels: ["recognitions", "errors"],
47
+ icon: "font-awesome/fa-automobile",
48
+ label: function() {
49
+ return this.name || "Plate recognizer";
50
+ },
51
+ oneditprepare: function() {
52
+ $('#node-input-inputField').typedInput({
53
+ typeField: $("#node-input-inputFieldType"),
54
+ types: ['msg']
55
+ });
56
+
57
+
58
+ $('#node-input-outputField').typedInput({
59
+ typeField: $("#node-input-outputField"),
60
+ types: ['msg']
61
+ });
62
+
63
+ $('#node-input-regionList').typedInput({
64
+ typeField: $("#node-input-regionListType"),
65
+ types: ['json']
66
+ });
67
+
68
+ $("#node-input-regionFilter").on("change", function (e) {
69
+ if (this.checked) {
70
+ $(".regionList-row").show();
71
+ }
72
+ else {
73
+ $(".regionList-row").hide();
74
+ }
75
+ });
76
+
77
+ $("#node-input-restoreUrl").on("click", function (e) {
78
+ $("#node-input-url").val("https://api.platerecognizer.com/v1/plate-reader/");
79
+ // Trigger the validators, otherwise the field can stay red
80
+ $("#node-input-url").change();
81
+ });
82
+ }
83
+ });
84
+ </script>
85
+
86
+ <script type="text/x-red" data-template-name="plate-recognizer">
87
+ <div class="form-row">
88
+ <label style="padding-top: 8px" for="node-input-inputField"><i class="fa fa-sign-in"></i> Input field</label>
89
+ <input type="text" id="node-input-inputField" style="width:70%">
90
+ <input type="hidden" id="node-input-inputFieldType">
91
+ </div>
92
+ <div class="form-row">
93
+ <label style="padding-top: 8px" for="node-input-outputField"><i class="fa fa-sign-out"></i> Output field</label>
94
+ <input type="text" id="node-input-outputField" style="width:70%">
95
+ <input type="hidden" id="node-input-outputField">
96
+ </div>
97
+ <div class="form-row">
98
+ <label for="node-input-apiToken"><i class="fa fa-key"></i> API token</label>
99
+ <input type="password" id="node-input-apiToken" placeholder="Enter your token">
100
+ </div>
101
+ <div class="form-row">
102
+ <label for="node-input-url"><i class="fa fa-globe"></i> URL</label>
103
+ <input type="text" id="node-input-url" style="width: 60%;">
104
+ <button id="node-input-restoreUrl" class="editor-button" title="Restore default URL""><i class="fa fa-undo"></i></button>
105
+ </div>
106
+ <div class="form-row">
107
+ <label for="node-input-cameraId"><i class="fa fa-video-camera"></i> Camera ID</label>
108
+ <input type="text" id="node-input-cameraId" placeholder="Enter your camera id">
109
+ <input type="hidden" id="node-input-node-input-cameraId">
110
+ </div>
111
+ <div class="form-row">
112
+ <label for="node-input-statusText"><i class="fa fa-font "></i> Status text</label>
113
+ <select id="node-input-statusText">
114
+ <option value="none">None</option>
115
+ <option value="count">Plate count</option>
116
+ <option value="plates">Plates</option>
117
+ <option value="scores">Plates and scores</option>
118
+ </select>
119
+ </div>
120
+ <div class="form-row">
121
+ <input type="checkbox" id="node-input-ignoreDuring" style="display: inline-block; width: auto; vertical-align: top;">
122
+ <label for="node-input-ignoreDuring" style="width:70%;">Ignore images arriving during recognition</label>
123
+ </div>
124
+ <div class="form-row">
125
+ <input type="checkbox" id="node-input-makeAndModel" style="display: inline-block; width: auto; vertical-align: top;">
126
+ <label for="node-input-makeAndModel" style="width:70%;">Predict vehicle make and model (MMC)</label>
127
+ </div>
128
+ <div class="form-row">
129
+ <input type="checkbox" id="node-input-separateMsg" style="display: inline-block; width: auto; vertical-align: top;">
130
+ <label for="node-input-separateMsg" style="width:70%;">Send separate message for each plate</label>
131
+ </div>
132
+ <div class="form-row">
133
+ <input type="checkbox" id="node-input-regionFilter" style="display: inline-block; width: auto; vertical-align: top;">
134
+ <label for="node-input-regionFilter" style="width:70%;">Specify one or more regions</label>
135
+ </div>
136
+ <div class="form-row regionList-row">
137
+ <label style="padding-top: 8px" for="node-input-regionList"><i class="fa fa-list-ol"></i> Regions</label>
138
+ <input type="text" id="node-input-regionList" style="width:70%">
139
+ <input type="hidden" id="node-input-regionListType">
140
+ </div>
141
+ <br>
142
+ <div class="form-row">
143
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
144
+ <input type="text" id="node-input-name" placeholder="Name">
145
+ </div>
146
+ </script>
147
+
148
+ <script type="text/x-red" data-help-name="plate-recognizer">
149
+ <p>A node for recognizing license plates in images.</p>
150
+ <p><strong>Input field:</strong><br/>
151
+ The field of the input message which will need to contain the input image. By default <code>msg.payload</code> will be used. The image should be a binary Buffer or a base64 encoded string.</p>
152
+ <p><strong>Output field:</strong><br/>
153
+ The field of the output message where the recognition result will be stored (in JSON format). By default <code>msg.payload</code> will be used.</p>
154
+ <p><strong>API token:</strong><br/>
155
+ Create an account at <a target="_blank" href="https://platerecognizer.com/">platerecognizer.com</a> and enter your private API token here.</p>
156
+ <p><strong>URL:</strong><br/>
157
+ Specify the URL of the recognition service, to allow different kind of setups:
158
+ <ul>
159
+ <li>Use the official cloud service, which will be the default (and most used) option.</li>
160
+ <li>Use a local installation (based on the SDK).</li>
161
+ <li>Use a local Docker container.</li>
162
+ </ul></p>
163
+ <p><strong>Camera ID:</strong><br/>
164
+ Optionally specify the camera id, to send it to the recognition service.
165
+ </p>
166
+ <p><strong>Status text:</strong><br/>
167
+ Specify how the recognition result needs to be displayed in the node status label:
168
+ <ul>
169
+ <li><i>None:</i> Show no recognition results.</li>
170
+ <li><i>Plate count:</i> Show the number of plates that have been recognised in the image.</li>
171
+ <li><i>Plates:</i> Show a (comma separted) list of the plates that have been recognized in the image.</i></li>
172
+ <li><i>Plates and scores:</i> Same as the previous option, but now the 'score' percentage is also added.</i></li>
173
+ </ul></p>
174
+ <p><strong>Ignore images arriving during recognition:</strong><br/>
175
+ When selected images will automatically be skipped, when the previous image is still being recognized. When deselected multiple images can be recognized simultaneously.</p>
176
+ <p><strong>Predict vehicle make and model (MMC):</strong><br/>
177
+ When selected not only the plate will be recognized, but there will also be a prediction of the vehicle brand and type. CAUTION: this is only supported for some paid account types!</p>
178
+ <p><strong>Send separate message for each plate:</strong><br/>
179
+ When selected a separate output message will be send for each recognized license plate. If not selected a single output message will be send containing an array of ALL recognized license plates.</p>
180
+ <p><strong>Only allow specific regions:</strong><br/>
181
+ When selected, an array of region codes can be specified (see <a target="_blank" href="http://docs.platerecognizer.com/#regions-supported">supported regions</a>). For example:<code>["fr","gb"]</code></p>
182
+ </script>
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Copyright 2020 Bart Butenaers
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ **/
16
+ module.exports = function(RED) {
17
+ var settings = RED.settings;
18
+ const fetch = require('node-fetch');
19
+ const FormData = require('form-data');
20
+
21
+ function PlateRecognizerNode(config) {
22
+ RED.nodes.createNode(this, config);
23
+ this.url = config.url;
24
+ this.inputField = config.inputField;
25
+ this.outputField = config.outputField;
26
+ this.ignoreDuring = config.ignoreDuring;
27
+ this.makeAndModel = config.makeAndModel;
28
+ this.separateMsg = config.separateMsg;
29
+ this.regionFilter = config.regionFilter;
30
+ this.statusText = config.statusText;
31
+ this.cameraId = config.cameraId;
32
+ this.regionListValue = null;
33
+ this.isRecognizing = false;
34
+
35
+ var node = this;
36
+
37
+ if (node.regionFilter) {
38
+ try {
39
+ // Convert the value list to the correct value, i.e. an array that we start reading from index 0
40
+ node.regionListValue = RED.util.evaluateNodeProperty(config.regionList, config.regionListType, node);
41
+ }
42
+ catch(exc) {
43
+ node.error("Region independent recognition will be executed, due to invalid region list json array format");
44
+ }
45
+ }
46
+
47
+ node.on("input", function(msg) {
48
+ if (node.ignoreDuring && node.isRecognizing) {
49
+ node.status({fill:"yellow",shape:"ring",text:"recognizing"});
50
+ return;
51
+ }
52
+
53
+ node.status({fill:"blue",shape:"dot",text:"recognizing"});
54
+ node.isRecognizing = true;
55
+
56
+ // Get the image specified in the input message
57
+ var image = RED.util.getMessageProperty(msg, node.inputField);
58
+
59
+ // Make sure all images are base64 encoded
60
+ if (Buffer.isBuffer(image)) {
61
+ image = image.toString("base64");
62
+ }
63
+
64
+ // Always send the current timestamp to the service (keep same timestamp across retries)
65
+ var timestamp = new Date().toISOString();
66
+
67
+ // FormData wraps a stream that gets consumed on send, so rebuild it for each retry attempt
68
+ function buildBody() {
69
+ var body = new FormData();
70
+ body.append('upload', image);
71
+ body.append('mmc', node.makeAndModel.toString());
72
+ if (node.cameraId != null) {
73
+ body.append('camera_id', node.cameraId.toString());
74
+ }
75
+ body.append('timestamp', timestamp);
76
+ if (node.regionFilter && node.regionListValue) {
77
+ for (var i = 0; i < node.regionListValue.length; i++) {
78
+ body.append('regions', node.regionListValue[i]);
79
+ }
80
+ }
81
+ return body;
82
+ }
83
+
84
+ // Retry on ETIMEDOUT: PlateRecognizer's edge intermittently drops connections at TCP layer (~260ms)
85
+ var MAX_RETRIES = 5;
86
+ var RETRY_DELAY_MS = 1000;
87
+ function fetchWithRetry(attempt) {
88
+ return fetch(node.url, {
89
+ method: 'POST',
90
+ headers: {
91
+ "Authorization": "Token " + node.credentials.apiToken
92
+ },
93
+ body: buildBody()
94
+ }).catch(function(err) {
95
+ if (err.code === 'ETIMEDOUT' && attempt < MAX_RETRIES) {
96
+ node.status({fill:"yellow",shape:"ring",text:"retry " + (attempt + 1) + "/" + MAX_RETRIES});
97
+ return new Promise(function(resolve) {
98
+ setTimeout(resolve, RETRY_DELAY_MS);
99
+ }).then(function() {
100
+ return fetchWithRetry(attempt + 1);
101
+ });
102
+ }
103
+ throw err;
104
+ });
105
+ }
106
+
107
+ fetchWithRetry(0).then( function(res) {
108
+ if (res.ok) { // res.status >= 200 && res.status < 300
109
+ // Convert the response to a JSON object
110
+ res.json().then( function(resultAsJson) {
111
+ // Make sure the status of the response is available in the output message, for error handling
112
+ resultAsJson.status = res.status;
113
+ resultAsJson.statusText = res.statusText
114
+
115
+ // Store the recognition result (in json format) in the specified output message field
116
+ RED.util.setMessageProperty(msg, node.outputField, resultAsJson, true);
117
+
118
+ // Show the required node status
119
+ switch (node.statusText) {
120
+ case "none":
121
+ node.status({ });
122
+ break;
123
+ case "count":
124
+ var plateCount = resultAsJson.results.length + " plates";
125
+ node.status({ fill: "blue",shape: "dot",text: plateCount });
126
+ break;
127
+ case "plates":
128
+ var plates = "";
129
+
130
+ for (var i = 0; i < resultAsJson.results.length; i++) {
131
+ if (i > 0) plates = plates + ",";
132
+ plates = plates + resultAsJson.results[i].plate.toUpperCase();
133
+ }
134
+
135
+ node.status({ fill: "blue",shape: "dot",text: plates });
136
+ break;
137
+ case "scores":
138
+ var platesAndScores = "";
139
+
140
+ for (var i = 0; i < resultAsJson.results.length; i++) {
141
+ var result = resultAsJson.results[i];
142
+ var score = Math.round(result.score * 10) / 10;
143
+ if (i > 0) platesAndScores = platesAndScores + ",";
144
+ platesAndScores = platesAndScores + result.plate.toUpperCase() + "(" + score * 100 + "%)";
145
+ }
146
+
147
+ node.status({ fill: "blue",shape: "dot",text: platesAndScores });
148
+ break;
149
+ }
150
+
151
+ // Check whether the plates need to be send as separate output messages
152
+ if (node.separateMsg) {
153
+ var plateCount = resultAsJson.results.length;
154
+
155
+ if (plateCount === 0) {
156
+ // When no plate found, replace the empty array by an empty element
157
+ resultAsJson.results = {};
158
+
159
+ // A single output message containing NO plate
160
+ node.send([msg, null]);
161
+ }
162
+ else {
163
+ // All plates (except the first one) will be send as clones
164
+ for (var i = 1; i < plateCount; i++) {
165
+ var clonedMsg = RED.util.cloneMessage(msg);
166
+
167
+ var clonedResultAsJson = RED.util.getMessageProperty(clonedMsg, node.outputField);
168
+ clonedResultAsJson.results = clonedResultAsJson.results[i];
169
+
170
+ // A single output message containing the n-th plate
171
+ node.send([clonedMsg, null]);
172
+ }
173
+
174
+ // For performance the first plate will be send uncloned (i.e. the original msg)
175
+ resultAsJson.results = resultAsJson.results[0];
176
+ node.send([msg, null]);
177
+ }
178
+ }
179
+ else {
180
+ // A single output message containing an array with ALL recognized plates
181
+ node.send([msg, null]);
182
+ }
183
+ }).catch( function(error) {
184
+ // Failed to parse the json
185
+ node.send([null, msg]);
186
+ node.status({ fill: "red",shape: "dot",text: "JSON parse failed" });
187
+ })
188
+ }
189
+ else {
190
+ // An application error happened, i.e. we got result from the service but not an optimistic one...
191
+ // For example we have hit our monthly maximum number of allowed recognitions.
192
+ // Or when the number of license plates is too high, we get an internal server error (so we even cannot parse the json)
193
+ node.send([null, msg]);
194
+ node.status({ fill: "red",shape: "dot",text: res.statusText });
195
+ }
196
+
197
+ node.isRecognizing = false;
198
+ })
199
+ .catch( function(err) {
200
+ // A real failure happened, i.e. we even weren't able to get a result from the service...
201
+ node.isRecognizing = false;
202
+ node.error("License plate recognition failed: " + err);
203
+ node.status({fill:"red",shape:"dot",text:"failed"});
204
+
205
+ node.isRecognizing = false;
206
+ });
207
+ });
208
+
209
+ node.on("close", function() {
210
+ node.status({ });
211
+ node.isRecognizing = false;
212
+ });
213
+ }
214
+
215
+ RED.nodes.registerType("plate-recognizer", PlateRecognizerNode, {
216
+ credentials: {
217
+ apiToken: {type: "password"}
218
+ }
219
+ });
220
+ }
@@ -0,0 +1,83 @@
1
+ <!--
2
+ Copyright 2020, Bart Butenaers
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+ http://www.apache.org/licenses/LICENSE-2.0
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
12
+ -->
13
+ <script type="text/javascript">
14
+ RED.nodes.registerType('plate-statistics',{
15
+ category: 'image',
16
+ color: '#E9967A',
17
+ defaults: {
18
+ name: {value:""},
19
+ outputField: {value: "payload", required: true, validate: RED.validators.typedInput("outputFieldType")},
20
+ outputFieldType: {value: "msg"},
21
+ url: {value:"https://api.platerecognizer.com/v1/statistics/", required: true}
22
+ },
23
+ credentials: {
24
+ apiToken: {type: "password"}
25
+ },
26
+ inputs:1,
27
+ outputs:1,
28
+ icon: "font-awesome/fa-sort-numeric-asc",
29
+ label: function() {
30
+ return this.name || "Plate statistics";
31
+ },
32
+ oneditprepare: function() {
33
+ $('#node-input-outputField').typedInput({
34
+ typeField: $("#node-input-outputField"),
35
+ types: ['msg']
36
+ });
37
+
38
+ $("#node-input-restoreUrl").on("click", function (e) {
39
+ $("#node-input-url").val("https://api.platerecognizer.com/v1/statistics/");
40
+ // Trigger the validators, otherwise the field can stay red
41
+ $("#node-input-url").change();
42
+ });
43
+ }
44
+ });
45
+ </script>
46
+
47
+ <script type="text/x-red" data-template-name="plate-statistics">
48
+ <div class="form-row">
49
+ <label style="padding-top: 8px" for="node-input-outputField"><i class="fa fa-sign-out"></i> Output field</label>
50
+ <input type="text" id="node-input-outputField" style="width:70%">
51
+ <input type="hidden" id="node-input-outputField">
52
+ </div>
53
+ <div class="form-row">
54
+ <label for="node-input-apiToken"><i class="fa fa-key"></i> API token</label>
55
+ <input type="password" id="node-input-apiToken" placeholder="Enter your token">
56
+ </div>
57
+ <div class="form-row">
58
+ <label for="node-input-url"><i class="fa fa-globe"></i> URL</label>
59
+ <input type="text" id="node-input-url" style="width: 60%;">
60
+ <button id="node-input-restoreUrl" class="editor-button" title="Restore default URL""><i class="fa fa-undo"></i></button>
61
+ </div>
62
+ <br>
63
+ <div class="form-row">
64
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
65
+ <input type="text" id="node-input-name" placeholder="Name">
66
+ </div>
67
+ </script>
68
+
69
+ <script type="text/x-red" data-help-name="plate-statistics">
70
+ <p>A node for getting statistics about license plate recognition.</p>
71
+ <p>It returns the maximum number of recognitions, and the current number of recognitions in the current month.</p>
72
+ <p><strong>Output field:</strong><br/>
73
+ The field of the output message where the plate statistics will be stored (in JSON format). By default <code>msg.payload</code> will be used.</p>
74
+ <p><strong>API token:</strong><br/>
75
+ Create an account at <a target="_blank" href="https://platerecognizer.com/">platerecognizer.com</a> and enter your private API token here.</p>
76
+ <p><strong>URL:</strong><br/>
77
+ Specify the URL of the recognition service, to allow different kind of setups:
78
+ <ul>
79
+ <li>Use the official cloud service, which will be the default (and most used) option.</li>
80
+ <li>Use a local installation (based on the SDK).</li>
81
+ <li>Use a local Docker container.</li>
82
+ </ul></p>
83
+ </script>
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Copyright 2020 Bart Butenaers
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ **/
16
+ module.exports = function(RED) {
17
+ var settings = RED.settings;
18
+ const fetch = require('node-fetch');
19
+
20
+ function PlateStatisticsNode(config) {
21
+ RED.nodes.createNode(this, config);
22
+ this.url = config.url;
23
+ this.outputField = config.outputField;
24
+
25
+ var node = this;
26
+
27
+ node.on("input", function(msg) {
28
+ node.status({fill:"blue",shape:"dot",text:"loading"});
29
+
30
+ fetch(node.url, {
31
+ method: 'GET',
32
+ headers: {
33
+ "Authorization": "Token " + node.credentials.apiToken
34
+ }
35
+ }).then( function(res) {
36
+ res.json().then( function(resultAsJson) {
37
+ // Store the statistics result (in json format) in the specified output message field
38
+ RED.util.setMessageProperty(msg, node.outputField, resultAsJson, true);
39
+ node.send(msg);
40
+ node.status({ });
41
+ })
42
+ })
43
+ });
44
+
45
+ node.on("close", function() {
46
+ node.status({ });
47
+ });
48
+ }
49
+
50
+ RED.nodes.registerType("plate-statistics", PlateStatisticsNode, {
51
+ credentials: {
52
+ apiToken: {type: "password"}
53
+ }
54
+ });
55
+ }