@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 +201 -0
- package/README.md +202 -0
- package/examples/basic-flow.json +1 -0
- package/examples/different-cases.json +1 -0
- package/examples/plate-statistics.json +1 -0
- package/package.json +42 -0
- package/plate-recognizer.html +182 -0
- package/plate-recognizer.js +220 -0
- package/plate-statistics.html +83 -0
- package/plate-statistics.js +55 -0
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+
}
|