itsyoboieltr commited on
Commit
6519fca
0 Parent(s):

fix: minor changes

Browse files
Files changed (6) hide show
  1. .github/workflows/deploy.yml +34 -0
  2. .gitignore +40 -0
  3. ANPR.ipynb +0 -0
  4. README.md +45 -0
  5. app.py +331 -0
  6. requirements.txt +5 -0
.github/workflows/deploy.yml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face Hub
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - app.py
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ deploy:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Checkout 🛎️
16
+ uses: actions/checkout@v3
17
+ with:
18
+ fetch-depth: 0
19
+ - name: Push to HF ⬆️
20
+ env:
21
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
22
+ HF_USERNAME: itsyoboieltr
23
+ HF_SPACE: itsyoboieltr/anpr
24
+ TARGET_BRANCH: main
25
+ run: |
26
+ rm -fr .git
27
+ git config --global init.defaultBranch main
28
+ git config --global user.email "itsyoboieltr@users.noreply.github.com"
29
+ git config --global user.name "itsyoboieltr"
30
+ git init
31
+ git add . :^main/ANPR.ipynb
32
+ git commit -m "${{ github.event.head_commit.message }}"
33
+ git remote add space https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/$HF_SPACE
34
+ git push --force space $TARGET_BRANCH
.gitignore ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ *.log.mbtree
5
+ npm-debug.log*
6
+ yarn-debug.log*
7
+ yarn-error.log*
8
+ pnpm-debug.log*
9
+ lerna-debug.log*
10
+
11
+ node_modules
12
+ dist
13
+ dist-ssr
14
+ *.local
15
+ pnpm-lock.yaml
16
+
17
+ # Editor directories and files
18
+ .vscode/*
19
+ !.vscode/extensions.json
20
+ .idea
21
+ .DS_Store
22
+ *.suo
23
+ *.ntvs*
24
+ *.njsproj
25
+ *.sln
26
+ *.sw?
27
+
28
+ __pycache__
29
+ .ipynb_checkpoints
30
+ gradio_cached_examples
31
+ *.pt
32
+ deep_sort
33
+ yolov7
34
+ *.mp4
35
+ *.tar.gz
36
+ examples
37
+ anpr-dataset
38
+ runs
39
+ anpr_weights
40
+ anpr_examples_202208
ANPR.ipynb ADDED
The diff for this file is too large to render. See raw diff
README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Automatic Number-Plate Recognition
3
+ emoji: 🚘
4
+ colorFrom: red
5
+ colorTo: gray
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: false
9
+ ---
10
+
11
+ # Automatic Number Plate Recognition
12
+
13
+ > AI to detect and recognize number plates on vehicles.
14
+
15
+ ## Table of contents
16
+
17
+ - [General information](#general-information)
18
+ - [Dataset](#dataset)
19
+ - [How does it work](#how-does-it-work)
20
+
21
+ ## [Live demo](https://huggingface.co/spaces/itsyoboieltr/anpr)
22
+
23
+ ## General information
24
+
25
+ This is an AI that was trained on images of number plates to carry out number plate detection and recognition. It works for both images and videos. Video detection also includes object tracking.
26
+
27
+ <img width="300" src="https://user-images.githubusercontent.com/72046715/183776545-c51843c9-d350-4f4f-aa4f-1168e6922904.png">
28
+
29
+ ## Dataset
30
+
31
+ For this project, I created the [ANPR dataset](https://archive.org/details/anpr-dataset), a dataset of approx. 30k handpicked images of number plates.
32
+
33
+ Annotations are in YOLO format.
34
+
35
+ <img width="600" src="https://user-images.githubusercontent.com/72046715/183776762-7e0d9822-80a1-442e-a111-2fbc03b8213c.png">
36
+
37
+ ## How does it work
38
+
39
+ Technologies used:
40
+
41
+ - [YOLOv8](https://github.com/ultralytics/ultralytics): Object detection model to detect the number plate
42
+ - [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR): OCR to read the number plate
43
+ - [Deep SORT](https://github.com/levan92/deep_sort_realtime): Object tracking algorithm for video detection
44
+
45
+ The YOLOv8 Model was fine-tuned using the ANPR dataset to detect number plates. When a number plate is detected, PaddleOCR is used to read the number plate. For video detection, Deep SORT is used to handle object tracking.
app.py ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr # type: ignore
3
+ from paddleocr import PaddleOCR # type: ignore
4
+ from ultralytics import YOLO # type: ignore
5
+ from pathlib import Path
6
+ from deep_sort_realtime.deepsort_tracker import DeepSort # type: ignore
7
+ import cv2 # type: ignore
8
+ import numpy as np
9
+ import re
10
+ from internetarchive import download # type: ignore
11
+ from tqdm import trange
12
+
13
+ download("anpr_weights", files=["anpr.pt"], verbose=True) # type: ignore
14
+
15
+ download(
16
+ "anpr_examples_202208",
17
+ files=["test_image_1.jpg", "test_image_2.jpg", "test_image_3.jpeg", "test_video_1.mp4"], # type: ignore
18
+ verbose=True,
19
+ )
20
+
21
+ paddle = PaddleOCR(lang="en", use_angle_cls=True, show_log=False)
22
+
23
+ model = YOLO(model="./anpr_weights/anpr.pt", task="detect")
24
+
25
+
26
+ def detect_plates(src):
27
+ predictions = model.predict(src, verbose=False)
28
+
29
+ results = []
30
+
31
+ for prediction in predictions:
32
+ for box in prediction.boxes:
33
+ det_confidence = box.conf.item()
34
+ if det_confidence < 0.6:
35
+ continue
36
+ coords = [int(position) for position in (box.xyxy.view(1, 4)).tolist()[0]]
37
+ results.append({"coords": coords, "det_conf": det_confidence})
38
+
39
+ return results
40
+
41
+
42
+ def crop(img, coords):
43
+ cropped = img[coords[1] : coords[3], coords[0] : coords[2]]
44
+ return cropped
45
+
46
+
47
+ def preprocess_image(src):
48
+ normalize = cv2.normalize(
49
+ src, np.zeros((src.shape[0], src.shape[1])), 0, 255, cv2.NORM_MINMAX
50
+ )
51
+ denoise = cv2.fastNlMeansDenoisingColored(
52
+ normalize, h=10, hColor=10, templateWindowSize=7, searchWindowSize=15
53
+ )
54
+ grayscale = cv2.cvtColor(denoise, cv2.COLOR_BGR2GRAY)
55
+ threshold = cv2.threshold(grayscale, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
56
+ return threshold
57
+
58
+
59
+ def ocr_plate(src):
60
+ # Preprocess the image for better OCR results
61
+ preprocessed = preprocess_image(src)
62
+
63
+ # OCR the preprocessed image
64
+ results = paddle.ocr(preprocessed, det=False, cls=True)
65
+
66
+ # Get the best OCR result
67
+ plate_text, ocr_confidence = max(
68
+ results,
69
+ key=lambda ocr_prediction: max(
70
+ ocr_prediction,
71
+ key=lambda ocr_prediction_result: ocr_prediction_result[1], # type: ignore
72
+ ),
73
+ )[0]
74
+
75
+ # Filter out anything but uppercase letters, digits, hypens and whitespace.
76
+ # Also, remove hypens and whitespaces at the first and last positions
77
+ plate_text_filtered = re.sub(r"[^A-Z0-9- ]", "", plate_text).strip("- ")
78
+
79
+ return {"plate": plate_text_filtered, "ocr_conf": ocr_confidence}
80
+
81
+
82
+ def ocr_plates(src, det_predictions):
83
+ results = []
84
+
85
+ for det_prediction in det_predictions:
86
+ plate_region = crop(src, det_prediction["coords"])
87
+ ocr_prediction = ocr_plate(plate_region)
88
+ results.append(ocr_prediction)
89
+
90
+ return results
91
+
92
+
93
+ def plot_box(img, coords, label=None, color=[0, 150, 255], line_thickness=3):
94
+ # Plots box on image
95
+ c1, c2 = (int(coords[0]), int(coords[1])), (int(coords[2]), int(coords[3]))
96
+ cv2.rectangle(img, c1, c2, color, thickness=line_thickness, lineType=cv2.LINE_AA)
97
+ # Plots label on image, if exists
98
+ if label:
99
+ tf = max(line_thickness - 1, 1) # font thickness
100
+ t_size = cv2.getTextSize(label, 0, fontScale=line_thickness / 3, thickness=tf)[
101
+ 0
102
+ ]
103
+ c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
104
+ cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
105
+ cv2.putText(
106
+ img,
107
+ label,
108
+ (c1[0], c1[1] - 2),
109
+ 0,
110
+ line_thickness / 3,
111
+ [225, 255, 255],
112
+ thickness=tf,
113
+ lineType=cv2.LINE_AA,
114
+ )
115
+
116
+
117
+ def get_plates(src):
118
+ det_predictions = detect_plates(src)
119
+ ocr_predictions = ocr_plates(src, det_predictions)
120
+
121
+ for det_prediction, ocr_prediction in zip(det_predictions, ocr_predictions):
122
+ plot_box(src, det_prediction["coords"], ocr_prediction["plate"])
123
+
124
+ return src, det_predictions, ocr_predictions
125
+
126
+
127
+ def predict_image(src):
128
+ detected_image, det_predictions, ocr_predictions = get_plates(src)
129
+ return detected_image
130
+
131
+
132
+ def predict_image_api(src):
133
+ detected_image, det_predictions, ocr_predictions = get_plates(src)
134
+ return ocr_predictions[0]["plate"]
135
+
136
+
137
+ def pascal_voc_to_coco(x1y1x2y2):
138
+ x1, y1, x2, y2 = x1y1x2y2
139
+ return [x1, y1, x2 - x1, y2 - y1]
140
+
141
+
142
+ def get_best_ocr(preds, rec_conf, ocr_res, track_id):
143
+ for info in preds:
144
+ # Check if it is current track id
145
+ if info["track_id"] == track_id:
146
+ # Check if the ocr confidence is maximum or not
147
+ if info["ocr_conf"] < rec_conf:
148
+ info["ocr_conf"] = rec_conf
149
+ info["ocr_txt"] = ocr_res
150
+ else:
151
+ rec_conf = info["ocr_conf"]
152
+ ocr_res = info["ocr_txt"]
153
+ break
154
+ return preds, rec_conf, ocr_res
155
+
156
+
157
+ def predict_video(src):
158
+ output = f"{Path(src).stem}_detected{Path(src).suffix}"
159
+
160
+ # Create a VideoCapture object
161
+ video = cv2.VideoCapture(src)
162
+
163
+ # Default resolutions of the frame are obtained. The default resolutions are system dependent.
164
+ # We convert the resolutions from float to integer.
165
+ width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
166
+ height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
167
+ fps = video.get(cv2.CAP_PROP_FPS)
168
+ frames_total = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
169
+
170
+ # Define the codec and create VideoWriter object.
171
+ temp = f"{Path(output).stem}_temp{Path(output).suffix}"
172
+ export = cv2.VideoWriter(
173
+ temp, cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height)
174
+ )
175
+
176
+ # Intializing tracker
177
+ tracker = DeepSort()
178
+
179
+ # Initializing some helper variables.
180
+ preds = []
181
+ total_obj = 0
182
+
183
+ for i in trange(frames_total):
184
+ ret, frame = video.read()
185
+ if ret is True:
186
+ # Run the ANPR algorithm
187
+ det_predictions = detect_plates(frame)
188
+ # Convert Pascal VOC detections to COCO
189
+ bboxes = list(
190
+ map(
191
+ lambda bbox: pascal_voc_to_coco(bbox),
192
+ [det_prediction["coords"] for det_prediction in det_predictions],
193
+ )
194
+ )
195
+
196
+ if len(bboxes) > 0:
197
+ # Storing all the required info in a list.
198
+ detections = [
199
+ (bbox, score, "number_plate")
200
+ for bbox, score in zip(
201
+ bboxes,
202
+ [
203
+ det_prediction["det_conf"]
204
+ for det_prediction in det_predictions
205
+ ],
206
+ )
207
+ ]
208
+
209
+ # Applying tracker.
210
+ # The tracker code flow: kalman filter -> target association(using hungarian algorithm) and appearance descriptor.
211
+ tracks = tracker.update_tracks(detections, frame=frame)
212
+
213
+ # Checking if tracks exist.
214
+ for track in tracks:
215
+ if not track.is_confirmed() or track.time_since_update > 1:
216
+ continue
217
+
218
+ # Changing track bbox to top left, bottom right coordinates
219
+ bbox = [int(position) for position in list(track.to_tlbr())]
220
+
221
+ for i in range(len(bbox)):
222
+ if bbox[i] < 0:
223
+ bbox[i] = 0
224
+
225
+ # Cropping the license plate and applying the OCR.
226
+ plate_region = crop(frame, bbox)
227
+ ocr_prediction = ocr_plate(plate_region)
228
+ plate_text, ocr_confidence = (
229
+ ocr_prediction["plate"],
230
+ ocr_prediction["ocr_conf"],
231
+ )
232
+
233
+ # Storing the ocr output for corresponding track id.
234
+ output_frame = {
235
+ "track_id": track.track_id,
236
+ "ocr_txt": plate_text,
237
+ "ocr_conf": ocr_confidence,
238
+ }
239
+
240
+ # Appending track_id to list only if it does not exist in the list
241
+ # else looking for the current track in the list and updating the highest confidence of it.
242
+ if track.track_id not in list(
243
+ set(pred["track_id"] for pred in preds)
244
+ ):
245
+ total_obj += 1
246
+ preds.append(output_frame)
247
+ else:
248
+ preds, ocr_confidence, plate_text = get_best_ocr(
249
+ preds,
250
+ ocr_confidence,
251
+ plate_text,
252
+ track.track_id,
253
+ )
254
+
255
+ # Plotting the prediction.
256
+ plot_box(
257
+ frame,
258
+ bbox,
259
+ f"{str(track.track_id)}. {plate_text}",
260
+ color=[255, 150, 0],
261
+ )
262
+
263
+ # Write the frame into the output file
264
+ export.write(frame)
265
+ else:
266
+ break
267
+
268
+ # When everything done, release the video capture and video write objects
269
+ video.release()
270
+ export.release()
271
+
272
+ # Compressing the video for smaller size and web compatibility.
273
+ os.system(
274
+ f"ffmpeg -y -i {temp} -c:v libx264 -b:v 5000k -minrate 1000k -maxrate 8000k -pass 1 -c:a aac -f mp4 /dev/null && ffmpeg -y -i {temp} -c:v libx264 -b:v 5000k -minrate 1000k -maxrate 8000k -pass 2 -c:a aac -movflags faststart {output}"
275
+ )
276
+ os.system(f"rm -rf {temp} ffmpeg2pass-0.log ffmpeg2pass-0.log.mbtree")
277
+ return output
278
+
279
+
280
+ with gr.Blocks() as demo:
281
+ gr.Markdown('### <h3 align="center">Automatic Number Plate Recognition</h3>')
282
+ gr.Markdown(
283
+ "This AI was trained to detect and recognize number plates on vehicles."
284
+ )
285
+ with gr.Tabs():
286
+ with gr.TabItem("Image"):
287
+ with gr.Row():
288
+ image_input = gr.Image()
289
+ image_output = gr.Image()
290
+ image_input.upload(
291
+ predict_image,
292
+ inputs=[image_input],
293
+ outputs=[image_output],
294
+ )
295
+ with gr.Row(visible=False): # Prediction API
296
+ api_image_input = gr.Image()
297
+ api_prediction_output = gr.Textbox()
298
+ api_image_input.upload(
299
+ predict_image_api,
300
+ inputs=[api_image_input],
301
+ outputs=[api_prediction_output],
302
+ api_name="predict",
303
+ )
304
+ gr.Examples(
305
+ [
306
+ ["./anpr_examples_202208/test_image_1.jpg"],
307
+ ["./anpr_examples_202208/test_image_2.jpg"],
308
+ ["./anpr_examples_202208/test_image_3.jpeg"],
309
+ ],
310
+ [image_input],
311
+ [image_output],
312
+ predict_image,
313
+ cache_examples=True,
314
+ )
315
+ with gr.TabItem("Video"):
316
+ with gr.Row():
317
+ video_input = gr.Video(format="mp4")
318
+ video_output = gr.Video(format="mp4")
319
+ video_input.upload(
320
+ predict_video, inputs=[video_input], outputs=[video_output]
321
+ )
322
+ gr.Examples(
323
+ [["./anpr_examples_202208/test_video_1.mp4"]],
324
+ [video_input],
325
+ [video_output],
326
+ predict_video,
327
+ cache_examples=True,
328
+ )
329
+ gr.Markdown("[@itsyoboieltr](https://github.com/itsyoboieltr)")
330
+
331
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
1
+ ultralytics
2
+ paddlepaddle
3
+ paddleocr
4
+ deep-sort-realtime
5
+ internetarchive