pluto90 commited on
Commit
50386b1
·
1 Parent(s): d1abef1

files upload

Browse files
app/api/__pycache__/main.cpython-312.pyc ADDED
Binary file (2.11 kB). View file
 
app/api/main.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+
3
+ from fastapi import FastAPI, UploadFile, File
4
+ import shutil
5
+ import os
6
+ from src.services.inference import detect_license_plate
7
+ from fastapi.staticfiles import StaticFiles
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+
10
+ app = FastAPI()
11
+
12
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
13
+ UPLOAD_DIR = os.path.join(BASE_DIR, "temp")
14
+
15
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["*"], # for development
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+
25
+ app.mount("/temp", StaticFiles(directory=UPLOAD_DIR), name="temp")
26
+
27
+ @app.post("/detect")
28
+ async def detect(file: UploadFile = File(...)):
29
+ file_path = os.path.join(UPLOAD_DIR, file.filename)
30
+
31
+ # Save uploaded file
32
+ with open(file_path, "wb") as buffer:
33
+ shutil.copyfileobj(file.file, buffer)
34
+
35
+ # Run inference
36
+ detections, output_path = detect_license_plate(file_path)
37
+
38
+ # return ONLY filename
39
+ output_filename = os.path.basename(output_path)
40
+
41
+ return {
42
+ "filename": file.filename,
43
+ "detections": detections,
44
+ "output_image": output_filename
45
+ }
app/evaluate/evaluate.py ADDED
File without changes
app/models/best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e458f1e9a5a077e954e3adbc964f4416a3ebbcb9bff24e7883c3fd4d527694af
3
+ size 22542250
app/models/last.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:080592f1c43e63cadfc39202c7aa82838aad5868a8805e1bb5ba0ea5c4c722f9
3
+ size 22542250
app/services/__pycache__/inference.cpython-312.pyc ADDED
Binary file (2.83 kB). View file
 
app/services/inference.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # # inference.py
2
+
3
+ # from ultralytics import YOLO
4
+ # import os
5
+ # import cv2
6
+ # import easyocr
7
+ # import numpy as np
8
+
9
+
10
+
11
+ # # Load model once (global)
12
+ # MODEL_PATH = os.path.join("src", "models", "best.pt")
13
+ # model = YOLO(MODEL_PATH)
14
+
15
+ # reader= easyocr.Reader(
16
+ # ['en'],
17
+ # gpu=True,
18
+ # )
19
+
20
+ # # Plate characters only — kills J→] Z→z O→0 confusion
21
+ # PLATE_ALLOWLIST = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 '
22
+
23
+ # CONF_THRESHOLD= 0.3
24
+
25
+
26
+ # def preprocess_plate(crop: np.ndarray) -> np.ndarray:
27
+ # """
28
+ # Clean up a plate crop before passing to OCR.
29
+ # Steps: upscale if small → grayscale → denoise → sharpen → adaptive threshold
30
+ # """
31
+ # h, w = crop.shape[:2]
32
+
33
+ # # 1. Upscale only if the crop is genuinely small
34
+ # # Target: at least 100px tall so characters are readable
35
+ # if h < 100:
36
+ # scale = 100 / h
37
+ # crop = cv2.resize(crop, None, fx=scale, fy=scale,
38
+ # interpolation=cv2.INTER_CUBIC)
39
+ # elif h < 200:
40
+ # # Modest 1.5x for medium crops
41
+ # crop = cv2.resize(crop, None, fx=1.5, fy=1.5,
42
+ # interpolation=cv2.INTER_CUBIC)
43
+ # # If already large enough, don't upscale — it can blur
44
+
45
+ # # 2. Grayscale
46
+ # gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
47
+
48
+ # # 3. Denoise (fastNlMeans: removes sensor noise without destroying edges)
49
+ # gray = cv2.fastNlMeansDenoising(gray, h=15, templateWindowSize=7, searchWindowSize=21)
50
+
51
+ # # 4. Sharpen — unsharp mask style
52
+ # blurred = cv2.GaussianBlur(gray, (0, 0), 2)
53
+ # gray = cv2.addWeighted(gray, 1.8, blurred, -0.8, 0)
54
+
55
+ # # 5. Adaptive threshold → clean black-on-white binary image
56
+ # # Works much better than global threshold for varying lighting
57
+ # binary = cv2.adaptiveThreshold(
58
+ # gray, 255,
59
+ # cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
60
+ # cv2.THRESH_BINARY,
61
+ # blockSize=15,
62
+ # C=8
63
+ # )
64
+
65
+ # # 6. Add a small white border — prevents edge characters from being clipped
66
+ # binary = cv2.copyMakeBorder(binary, 10, 10, 10, 10,
67
+ # cv2.BORDER_CONSTANT, value=255)
68
+
69
+ # return binary
70
+
71
+
72
+
73
+
74
+
75
+ # def read_plate_text(crop: np.ndarray) -> tuple[str, float]:
76
+ # """
77
+ # Run OCR on a plate crop. Returns (text, ocr_confidence).
78
+ # Tries preprocessed binary first; falls back to color crop if no result.
79
+ # """
80
+ # processed = preprocess_plate(crop)
81
+
82
+ # results = reader.readtext(
83
+ # processed,
84
+ # detail=1,
85
+ # paragraph=False, # treat each text region independently
86
+ # decoder='beamsearch', # more accurate than greedy
87
+ # beamWidth=10,
88
+ # batch_size=1,
89
+ # allowlist=PLATE_ALLOWLIST,
90
+ # # EasyOCR hint: plate text is usually 1-2 lines, wide aspect
91
+ # width_ths=0.8, # merge nearby text boxes horizontally
92
+ # contrast_ths=0.05,
93
+ # adjust_contrast=0.7,
94
+ # text_threshold=0.6,
95
+ # low_text=0.3,
96
+ # )
97
+
98
+ # if not results:
99
+ # # Fallback: try on the raw color crop
100
+ # results = reader.readtext(
101
+ # crop,
102
+ # detail=1,
103
+ # allowlist=PLATE_ALLOWLIST,
104
+ # decoder='beamsearch',
105
+ # beamWidth=10,
106
+ # )
107
+
108
+ # if not results:
109
+ # return "", 0.0
110
+
111
+ # # Sort by confidence descending, take best
112
+ # results.sort(key=lambda x: x[2], reverse=True)
113
+ # best = results[0]
114
+ # text = best[1].upper().strip()
115
+ # conf = float(best[2])
116
+
117
+ # # If multiple boxes detected, try joining them in left-to-right order
118
+ # # (handles split plates like "KV67" + "HUJ" as separate regions)
119
+ # if len(results) > 1:
120
+ # # Sort all boxes by their x-coordinate (left edge of bbox)
121
+ # sorted_by_x = sorted(results, key=lambda x: x[0][0][0])
122
+ # joined = " ".join(r[1].upper().strip() for r in sorted_by_x)
123
+ # avg_conf = sum(r[2] for r in sorted_by_x) / len(sorted_by_x)
124
+ # # Use joined version only if average confidence is decent
125
+ # if avg_conf >= 0.5:
126
+ # text = joined
127
+ # conf = avg_conf
128
+
129
+ # return text, round(conf, 3)
130
+
131
+
132
+
133
+
134
+ # def detect_license_plate(image_path):
135
+ # results= model(image_path)
136
+ # image= cv2.imread(image_path)
137
+
138
+ # detections= []
139
+
140
+ # for result in results:
141
+ # for box in result.boxes:
142
+ # x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
143
+ # conf = float(box.conf[0])
144
+
145
+
146
+ # if conf < CONF_THRESHOLD:
147
+ # continue
148
+
149
+
150
+ # # CROPPING
151
+ # # Crop with a small margin to avoid clipping plate edges
152
+ # margin = 4
153
+ # h_img, w_img = image.shape[:2]
154
+ # cx1 = max(0, x1 - margin)
155
+ # cy1 = max(0, y1 - margin)
156
+ # cx2 = min(w_img, x2 + margin)
157
+ # cy2 = min(h_img, y2 + margin)
158
+
159
+ # plate_crop = image[cy1:cy2, cx1:cx2]
160
+
161
+ # plate_text, ocr_conf = read_plate_text(plate_crop)
162
+
163
+ # # Draw bounding box
164
+ # cv2.rectangle(image, (x1, y1), (x2, y2), (0, 0, 220), 2)
165
+
166
+
167
+ # # Label: text + detection confidence
168
+ # label = f"{plate_text} ({round(conf, 2)})" if plate_text else f"({round(conf, 2)})"
169
+ # (lw, lh), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 2)
170
+
171
+
172
+ # # Background rect for label so it's always readable
173
+ # cv2.rectangle(image, (x1, y1 - lh - baseline - 6), (x1 + lw + 4, y1), (0, 0, 220), -1)
174
+ # cv2.putText(
175
+ # image, label,
176
+ # (x1 + 2, y1 - baseline - 2),
177
+ # cv2.FONT_HERSHEY_SIMPLEX, 0.55,
178
+ # (255, 255, 255), 2
179
+ # )
180
+
181
+
182
+ # detections.append({
183
+ # "bbox": {
184
+ # "x1": int(x1),
185
+ # "y1": int(y1),
186
+ # "x2": int(x2),
187
+ # "y2": int(y2)
188
+ # },
189
+ # "confidence": round(conf, 3),
190
+ # "text": plate_text,
191
+ # "ocr_confidence": round(ocr_conf, 3) if ocr_conf else None,
192
+ # })
193
+
194
+ # # output image
195
+ # name, ext= os.path.splitext(image_path)
196
+ # output_path= f"{name}_output{ext}"
197
+ # cv2.imwrite(output_path, image)
198
+
199
+
200
+ # return detections, output_path
201
+
202
+
203
+
204
+
205
+
206
+
207
+
208
+
209
+ from ultralytics import YOLO
210
+ import os
211
+ import cv2
212
+ import numpy as np
213
+ import easyocr
214
+ import re
215
+ from fast_plate_ocr import LicensePlateRecognizer
216
+
217
+ # ── Init ──────────────────────────────────────────────────────────────────────
218
+ MODEL_PATH = os.path.join("src", "models", "best.pt")
219
+ model = YOLO(MODEL_PATH)
220
+ # reader = easyocr.Reader(['en'], gpu=True)
221
+
222
+ ocr= LicensePlateRecognizer("cct-s-v2-global-model")
223
+
224
+ CONF_THRESHOLD = 0.255
225
+ PLATE_ALLOWLIST = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 '
226
+
227
+ # ── Preprocessing ─────────────────────────────────────────────────────────────
228
+
229
+ # def preprocess_plate(crop: np.ndarray) -> list[np.ndarray]:
230
+ # """
231
+ # Returns multiple processed versions of the crop.
232
+ # OCR is run on all of them and best result is picked.
233
+ # """
234
+ # h, w = crop.shape[:2]
235
+
236
+ # # Upscale only if genuinely small — target 80px height minimum
237
+ # scale = max(1.0, 80 / h)
238
+ # if scale > 1.0:
239
+ # crop = cv2.resize(crop, None, fx=scale, fy=scale,
240
+ # interpolation=cv2.INTER_CUBIC)
241
+
242
+ # gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
243
+
244
+ # # Version 1: CLAHE — improves local contrast without over-brightening
245
+ # clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(4, 4))
246
+ # v1 = clahe.apply(gray)
247
+
248
+ # # Version 2: Otsu threshold — works well on clean plates
249
+ # _, v2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
250
+
251
+ # # Version 3: Inverted Otsu — for dark-on-light plates
252
+ # v3 = cv2.bitwise_not(v2)
253
+
254
+ # # Version 4: Sharpened grayscale — good for slightly blurry crops
255
+ # blurred = cv2.GaussianBlur(gray, (0, 0), 1.5)
256
+ # v4 = cv2.addWeighted(gray, 2.0, blurred, -1.0, 0)
257
+
258
+ # # Add white padding to all versions so edge chars aren't clipped
259
+ # pad = lambda img: cv2.copyMakeBorder(img, 12, 12, 12, 12,
260
+ # cv2.BORDER_CONSTANT, value=255)
261
+ # return [pad(v) for v in [v1, v2, v3, v4]]
262
+
263
+
264
+ # def clean_text(text: str) -> str:
265
+ # """Strip non-plate characters and normalize spacing."""
266
+ # text = text.upper().strip()
267
+ # # Remove anything that's not A-Z, 0-9, or space
268
+ # text = re.sub(r'[^A-Z0-9 ]', '', text)
269
+ # # Collapse multiple spaces
270
+ # text = re.sub(r' +', ' ', text).strip()
271
+ # return text
272
+
273
+
274
+ # def run_ocr_on_versions(versions: list[np.ndarray]) -> tuple[str, float]:
275
+ # """
276
+ # Run OCR on each preprocessed version, collect all results,
277
+ # return the highest-confidence clean result.
278
+ # """
279
+ # candidates = []
280
+
281
+ # for img in versions:
282
+ # try:
283
+ # results = reader.readtext(
284
+ # img,
285
+ # detail=1,
286
+ # allowlist=PLATE_ALLOWLIST,
287
+ # paragraph=True, # merge into one line — avoids multi-box noise
288
+ # decoder='greedy', # greedy is actually more stable for short strings
289
+ # text_threshold=0.5,
290
+ # low_text=0.3,
291
+ # width_ths=1.0, # aggressive merge: treat plate as single region
292
+ # mag_ratio=1.0,
293
+ # )
294
+
295
+ # for (_, text, conf) in results:
296
+ # cleaned = clean_text(text)
297
+ # if len(cleaned) >= 4: # ignore single chars / noise
298
+ # candidates.append((cleaned, float(conf)))
299
+
300
+ # except Exception:
301
+ # continue
302
+
303
+ # if not candidates:
304
+ # return "", 0.0
305
+
306
+ # # Pick highest confidence
307
+ # candidates.sort(key=lambda x: x[1], reverse=True)
308
+ # return candidates[0]
309
+
310
+
311
+
312
+
313
+ # ── Main ──────────────────────────────────────────────────────────────────────
314
+
315
+ def detect_license_plate(image_path: str):
316
+ results = model(image_path)
317
+ image = cv2.imread(image_path)
318
+ h_img, w_img = image.shape[:2]
319
+
320
+ detections = []
321
+
322
+ for result in results:
323
+ for box in result.boxes:
324
+ x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
325
+ conf = float(box.conf[0])
326
+
327
+ if conf < CONF_THRESHOLD:
328
+ continue
329
+
330
+ # Small margin to avoid clipping plate edges
331
+ margin = 4
332
+ cx1 = max(0, x1 - margin)
333
+ cy1 = max(0, y1 - margin)
334
+ cx2 = min(w_img, x2 + margin)
335
+ cy2 = min(h_img, y2 + margin)
336
+
337
+ plate_crop = image[cy1:cy2, cx1:cx2]
338
+ # versions = preprocess_plate(plate_crop)
339
+ # plate_text, ocr_conf = run_ocr_on_versions(versions)
340
+
341
+
342
+
343
+ # fast-plate-ocr expects BGR numpy array — no preprocessing needed
344
+ result_ocr = ocr.run(plate_crop)
345
+
346
+ # run() returns a list of predictions, one per image — take first
347
+ plate_text = result_ocr[0].plate if result_ocr else ""
348
+
349
+
350
+ # Draw bounding box
351
+ cv2.rectangle(image, (x1, y1), (x2, y2), (255, 218, 105), 2)
352
+
353
+ label = f"{plate_text} ({round(conf, 2)})" if plate_text else f"({round(conf, 2)})"
354
+ (lw, lh), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 2)
355
+
356
+ # Solid background behind label for readability
357
+ # cv2.rectangle(image,
358
+ # (x1, y1 - lh - baseline - 6),
359
+ # (x1 + lw + 6, y1),
360
+ # (0, 0, 220), -1)
361
+ # cv2.putText(image, label,
362
+ # (x1 + 3, y1 - baseline - 2),
363
+ # cv2.FONT_HERSHEY_SIMPLEX, 0.55,
364
+ # (255, 255, 255), 2)
365
+
366
+ detections.append({
367
+ "bbox": {"x1": x1, "y1": y1, "x2": x2, "y2": y2},
368
+ "confidence": round(conf, 3),
369
+ "text": plate_text,
370
+ # "ocr_confidence": round(ocr_conf, 3) if ocr_conf else None,
371
+ })
372
+
373
+ name, ext = os.path.splitext(image_path)
374
+ output_path = f"{name}_output{ext}"
375
+ cv2.imwrite(output_path, image)
376
+
377
+ return detections, output_path
app/training/train.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from ultralytics import YOLO
3
+ import torch
4
+ import mlflow
5
+
6
+
7
+
8
+ device= 0 if torch.cuda.is_available() else "cpu"
9
+ if device==0:
10
+ print("GPU")
11
+ else:
12
+ print("CPU")
13
+
14
+
15
+ def train():
16
+ # Project root
17
+ ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))
18
+ data_path = os.path.join(ROOT_DIR, "data/raw/data.yaml")
19
+
20
+
21
+ # Output directory (YOLO saves here)
22
+ project_name= "experiments"
23
+ run_name= "yolov8s_768_v2_run"
24
+
25
+ output_dir= os.path.join(ROOT_DIR, project_name, run_name)
26
+
27
+
28
+ # MLflow Setup
29
+ mlflow.set_tracking_uri("sqlite:///mlflow.db")
30
+ mlflow.set_experiment("license-plate-detection")
31
+
32
+
33
+ # Training Config
34
+ params= {
35
+ "model": "yolov8s",
36
+ "epochs": 40,
37
+ "imgsz": 768,
38
+ "batch": 6,
39
+ "optimizer": "auto",
40
+ "mosaic": 0.3,
41
+ "device": device,
42
+ }
43
+
44
+
45
+ # Start MLflow run
46
+ with mlflow.start_run(run_name=run_name):
47
+
48
+ # log parameters
49
+ mlflow.log_params(params)
50
+
51
+
52
+ # load model
53
+ model = YOLO("yolov8s.pt")
54
+
55
+ # train
56
+ results= model.train(
57
+ data=data_path,
58
+ epochs=params["epochs"],
59
+ imgsz=params["imgsz"],
60
+ device=params["device"],
61
+ batch=params["batch"],
62
+ cache=False,
63
+ workers=0,
64
+ patience=10,
65
+ mosaic=params["mosaic"],
66
+ project=project_name,
67
+ name=run_name
68
+ )
69
+
70
+ # log metrics
71
+ metrics = results.results_dict
72
+
73
+ mlflow.log_metric("mAP50", metrics.get("metrics/mAP50(B)", 0))
74
+ mlflow.log_metric("mAP50-95", metrics.get("metrics/mAP50-95(B)", 0))
75
+ mlflow.log_metric("precision", metrics.get("metrics/precision(B)", 0))
76
+ mlflow.log_metric("recall", metrics.get("metrics/recall(B)", 0))
77
+
78
+
79
+ # log artifacts
80
+ # -------------
81
+
82
+ # 1. Best model
83
+ best_model_path= os.path.join(output_dir, "weights/best.pt")
84
+ if os.path.exists(best_model_path):
85
+ mlflow.log_artifact(best_model_path, artifact_path="model")
86
+
87
+
88
+ # 2. Training results csv
89
+ results_csv= os.path.join(output_dir, "results.csv")
90
+ if os.path.exists(results_csv):
91
+ mlflow.log_artifact(results_csv, artifact_path="metrics")
92
+
93
+
94
+ # 3. labels plot / confusion matrix (if generated)
95
+ labels_img= os.path.join(output_dir, "labels.jpg")
96
+ if os.path.exists(labels_img):
97
+ mlflow.log_artifact(labels_img, artifact_path="plots")
98
+
99
+
100
+ print("Training + MLflow logging completed")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ train()
app/training/yolo26n.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9b09cc8bf347f0fc8a5f7657480587f25db09b34bf33b0652110fb03a8ad4fef
3
+ size 5544453
app/training/yolov8n.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f59b3d833e2ff32e194b5bb8e08d211dc7c5bdf144b90d2c8412c47ccfc83b36
3
+ size 6549796
app/training/yolov8s.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1f47a78bf100391c2a140b7ac73a1caae18c32779be7d310658112f7ac9aa78a
3
+ size 22588772