pinthoz commited on
Commit
097f176
·
verified ·
1 Parent(s): b97718b

First commit

Browse files
Files changed (46) hide show
  1. .gitattributes +12 -0
  2. examples/watch_test1.jpg +3 -0
  3. examples/watch_test10.jpg +3 -0
  4. examples/watch_test100.jpg +0 -0
  5. examples/watch_test101.jpg +0 -0
  6. examples/watch_test102.jpg +0 -0
  7. examples/watch_test103.jpg +0 -0
  8. examples/watch_test104.jpg +0 -0
  9. examples/watch_test105.jpg +0 -0
  10. examples/watch_test106.jpg +0 -0
  11. examples/watch_test107.jpg +3 -0
  12. examples/watch_test108.jpg +0 -0
  13. examples/watch_test109.jpg +3 -0
  14. examples/watch_test11.jpg +0 -0
  15. examples/watch_test110.jpg +3 -0
  16. examples/watch_test111.jpg +3 -0
  17. examples/watch_test112.jpg +3 -0
  18. examples/watch_test113.jpg +3 -0
  19. examples/watch_test114.jpg +3 -0
  20. examples/watch_test115.jpg +3 -0
  21. examples/watch_test116.jpg +3 -0
  22. gradio_app.py +211 -0
  23. img/1.png +0 -0
  24. img/2.png +3 -0
  25. img/annotations_utils/add_rotated_gt.ipynb +349 -0
  26. img/annotations_utils/desfocar.py +68 -0
  27. img/annotations_utils/name_changer.py +22 -0
  28. img/annotations_utils/remove_center.py +25 -0
  29. img/annotations_utils/resized_annotations.py +115 -0
  30. img/annotations_utils/rotate_img.py +56 -0
  31. img/annotations_utils/rotate_img_ann.py +148 -0
  32. img/annotations_utils/train_val_split.py +47 -0
  33. img/annotations_utils/xml_to_txt.py +59 -0
  34. img/icon.png +0 -0
  35. requirements.txt +7 -0
  36. tune4_best.pt +3 -0
  37. utils/__pycache__/clock_utils.cpython-311.pyc +0 -0
  38. utils/__pycache__/clock_utils.cpython-312.pyc +0 -0
  39. utils/__pycache__/clock_utils.cpython-38.pyc +0 -0
  40. utils/__pycache__/detections_utils.cpython-311.pyc +0 -0
  41. utils/__pycache__/detections_utils.cpython-312.pyc +0 -0
  42. utils/__pycache__/detections_utils.cpython-38.pyc +0 -0
  43. utils/clock_utils.py +242 -0
  44. utils/detections_utils.py +158 -0
  45. utils/train_hiper.py +11 -0
  46. utils/train_model.py +31 -0
.gitattributes CHANGED
@@ -33,3 +33,15 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/watch_test1.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/watch_test10.jpg filter=lfs diff=lfs merge=lfs -text
38
+ examples/watch_test107.jpg filter=lfs diff=lfs merge=lfs -text
39
+ examples/watch_test109.jpg filter=lfs diff=lfs merge=lfs -text
40
+ examples/watch_test110.jpg filter=lfs diff=lfs merge=lfs -text
41
+ examples/watch_test111.jpg filter=lfs diff=lfs merge=lfs -text
42
+ examples/watch_test112.jpg filter=lfs diff=lfs merge=lfs -text
43
+ examples/watch_test113.jpg filter=lfs diff=lfs merge=lfs -text
44
+ examples/watch_test114.jpg filter=lfs diff=lfs merge=lfs -text
45
+ examples/watch_test115.jpg filter=lfs diff=lfs merge=lfs -text
46
+ examples/watch_test116.jpg filter=lfs diff=lfs merge=lfs -text
47
+ img/2.png filter=lfs diff=lfs merge=lfs -text
examples/watch_test1.jpg ADDED

Git LFS Details

  • SHA256: 30af6b36d1fe4f94928816e5e5a436b8a42482afdd67abf9fce740905918496d
  • Pointer size: 131 Bytes
  • Size of remote file: 101 kB
examples/watch_test10.jpg ADDED

Git LFS Details

  • SHA256: 110a2fce72296cc5a8c0d867b32effa74341b646d40f28828c995985fc29ea11
  • Pointer size: 131 Bytes
  • Size of remote file: 146 kB
examples/watch_test100.jpg ADDED
examples/watch_test101.jpg ADDED
examples/watch_test102.jpg ADDED
examples/watch_test103.jpg ADDED
examples/watch_test104.jpg ADDED
examples/watch_test105.jpg ADDED
examples/watch_test106.jpg ADDED
examples/watch_test107.jpg ADDED

Git LFS Details

  • SHA256: d4a9a6c90ec3b182ab4a4e968a2db2f16192540a6a2e8cf5df766864b3f34254
  • Pointer size: 131 Bytes
  • Size of remote file: 153 kB
examples/watch_test108.jpg ADDED
examples/watch_test109.jpg ADDED

Git LFS Details

  • SHA256: 403e946ec40bb1409a678ca88194229a7c0bd5c73a931eaca9e408a5bc0ce0ec
  • Pointer size: 131 Bytes
  • Size of remote file: 104 kB
examples/watch_test11.jpg ADDED
examples/watch_test110.jpg ADDED

Git LFS Details

  • SHA256: 11101307b254b627bf2f77266206ef6f88de4de954d33fe3a6848432b49fc892
  • Pointer size: 132 Bytes
  • Size of remote file: 1.08 MB
examples/watch_test111.jpg ADDED

Git LFS Details

  • SHA256: 7f5a09704a62a5e2f1644769cef905372e87b7e1805de1b6c17046000cf5126b
  • Pointer size: 132 Bytes
  • Size of remote file: 1.17 MB
examples/watch_test112.jpg ADDED

Git LFS Details

  • SHA256: 3ad82113b6aa68c5d5c7f1924b1727aa04749e5e1554af905bfc3628a9b7149f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.11 MB
examples/watch_test113.jpg ADDED

Git LFS Details

  • SHA256: 0f8c57c1f854c9f753a2a14975128c2c233401dbb74bd9c9950b9ded562f50f7
  • Pointer size: 132 Bytes
  • Size of remote file: 1.33 MB
examples/watch_test114.jpg ADDED

Git LFS Details

  • SHA256: 83307badd089f7449450a840b02254df92edb471fd42ed6facad0fb800e7ef0d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.39 MB
examples/watch_test115.jpg ADDED

Git LFS Details

  • SHA256: 6579bd00633efbecf1c8aa9fef7e05dc264c7215f37163aa11fe8738f5cc9838
  • Pointer size: 132 Bytes
  • Size of remote file: 1.62 MB
examples/watch_test116.jpg ADDED

Git LFS Details

  • SHA256: 21acec4459608dc4ff08f15bb4cacfc660fe09bb229fdc184dc5b0b5bca0ad9c
  • Pointer size: 131 Bytes
  • Size of remote file: 991 kB
gradio_app.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio interface for the Analogic Watch Detector model.
2
+
3
+ This module exposes a lightweight Gradio demo that can be used on
4
+ Hugging Face Spaces. It loads the YOLO model once, runs inference on the
5
+ uploaded image and renders the predicted time alongside the annotated
6
+ image.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from typing import Optional, Tuple
13
+
14
+ import cv2
15
+ import gradio as gr
16
+ import numpy as np
17
+ from ultralytics import YOLO
18
+
19
+ from utils.clock_utils import process_clock_time
20
+ from utils.detections_utils import get_latest_train_dir, run_detection
21
+
22
+
23
+ _MODEL: Optional[YOLO] = None
24
+
25
+
26
+ def _resolve_model_path() -> str:
27
+ """Return the best available model path."""
28
+ env_path = os.environ.get("MODEL_PATH")
29
+ if env_path and os.path.exists(env_path):
30
+ return env_path
31
+
32
+ # Priority 1: Specific tuned model for HF deployment
33
+ tune4_model = "tune4_best.pt"
34
+ if os.path.exists(tune4_model):
35
+ return tune4_model
36
+
37
+ default_weight = "yolov8s.pt"
38
+ if os.path.exists(default_weight):
39
+ return default_weight
40
+
41
+ try:
42
+ return os.path.join(get_latest_train_dir(), "weights", "best.pt")
43
+ except FileNotFoundError as exc: # pragma: no cover - defensive path
44
+ raise RuntimeError(
45
+ "Model weights were not found. Provide them via the MODEL_PATH "
46
+ "environment variable or include 'yolov8s.pt' in the repository."
47
+ ) from exc
48
+
49
+
50
+ def _load_model() -> YOLO:
51
+ """Lazy-load the YOLO model to keep the interface responsive."""
52
+ global _MODEL
53
+ if _MODEL is None:
54
+ model_path = _resolve_model_path()
55
+ _MODEL = YOLO(model_path)
56
+ return _MODEL
57
+
58
+
59
+ def _format_time(prediction: Optional[dict]) -> str:
60
+ """Generate a human readable string for the detected time."""
61
+ if not prediction:
62
+ return "Unable to determine the time from the detected clock."
63
+
64
+ hours = prediction.get("hours")
65
+ minutes = prediction.get("minutes")
66
+ seconds = prediction.get("seconds")
67
+
68
+ if hours is None:
69
+ return "Unable to determine the time from the detected clock."
70
+
71
+ if minutes is None:
72
+ return f"Detected hour hand at {hours:02d}."
73
+
74
+ if seconds is None:
75
+ return f"Detected time: {hours:02d}:{minutes:02d}."
76
+
77
+ return f"Detected time: {hours:02d}:{minutes:02d}:{seconds:02d}."
78
+
79
+
80
+ def predict(image: np.ndarray, confidence: float) -> Tuple[np.ndarray, str]:
81
+ """Run detection on the uploaded image and return the annotated preview."""
82
+ if image is None:
83
+ raise gr.Error("Please upload an image of an analog clock.")
84
+
85
+ # Basic guard against oversized inputs to reduce DoS risk
86
+ try:
87
+ if hasattr(image, "nbytes") and (image.nbytes > 40 * 1024 * 1024):
88
+ raise gr.Error("Image is too large. Please upload a smaller file.")
89
+ if image.size and image.size > 20_000_000:
90
+ raise gr.Error("Image resolution is too high. Please downscale and retry.")
91
+ except Exception:
92
+ pass
93
+
94
+ image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
95
+
96
+ detections, results = run_detection(
97
+ image=image_bgr,
98
+ image_path=None,
99
+ confidence=confidence,
100
+ save_path=None,
101
+ save_visualization=False,
102
+ return_prediction_results=True,
103
+ model=_load_model(),
104
+ )
105
+
106
+ if not detections or not detections[0]:
107
+ return image, "No clock components detected in the provided image."
108
+
109
+ prediction = process_clock_time(detections, "uploaded_image")
110
+ annotated = image
111
+ if results:
112
+ annotated_bgr = results[0].plot()
113
+ annotated = cv2.cvtColor(annotated_bgr, cv2.COLOR_BGR2RGB)
114
+
115
+ return annotated, _format_time(prediction)
116
+
117
+
118
+ def build_interface() -> gr.Blocks:
119
+ """Create the Gradio Blocks interface."""
120
+ with gr.Blocks(title="Analog Clock Time Detector") as demo:
121
+ gr.Markdown(
122
+ """
123
+ # Analog Clock Time Detector
124
+ Upload a picture of an analog clock to detect the time displayed on it.
125
+ The model is based on YOLOv8 and predicts the hour, minute and second
126
+ hands when available.
127
+ """
128
+ )
129
+
130
+ with gr.Row():
131
+ with gr.Column():
132
+ image_input = gr.Image(
133
+ type="numpy",
134
+ label="Clock image",
135
+ image_mode="RGB",
136
+ )
137
+ confidence_slider = gr.Slider(
138
+ minimum=0.01,
139
+ maximum=0.5,
140
+ step=0.01,
141
+ value=0.1,
142
+ label="Detection confidence threshold",
143
+ )
144
+ submit_btn = gr.Button("Detect time")
145
+
146
+ with gr.Column():
147
+ annotated_image = gr.Image(
148
+ type="numpy",
149
+ label="Detections",
150
+ )
151
+ time_output = gr.Textbox(
152
+ label="Predicted time",
153
+ placeholder="The predicted time will appear here.",
154
+ )
155
+
156
+ submit_btn.click(
157
+ fn=predict,
158
+ inputs=[image_input, confidence_slider],
159
+ outputs=[annotated_image, time_output],
160
+ )
161
+
162
+ # Load examples from the examples directory
163
+ example_images = []
164
+ if os.path.exists("examples"):
165
+ example_images = [
166
+ os.path.join("examples", f)
167
+ for f in os.listdir("examples")
168
+ if f.lower().endswith(('.png', '.jpg', '.jpeg'))
169
+ ]
170
+ # Sort for consistent order, though not strictly required
171
+ example_images.sort()
172
+
173
+ # Fallback to img directory if no examples found (local dev fallback/legacy)
174
+ if not example_images and os.path.exists("img"):
175
+ example_images = [
176
+ os.path.join("img", "1.png"),
177
+ os.path.join("img", "2.png"),
178
+ ]
179
+
180
+ if example_images:
181
+ gr.Examples(
182
+ examples=example_images,
183
+ inputs=image_input,
184
+ )
185
+
186
+ return demo
187
+
188
+
189
+ if __name__ == "__main__":
190
+ demo = build_interface()
191
+ # Detect Hugging Face Spaces environment
192
+ on_hf = bool(os.environ.get("SPACE_ID"))
193
+
194
+ # Configure queue conservatively; keep broad compatibility
195
+ try:
196
+ demo.queue(concurrency_count=1, max_size=8)
197
+ except TypeError:
198
+ try:
199
+ demo.queue(max_size=8)
200
+ except TypeError:
201
+ demo.queue()
202
+
203
+ if on_hf:
204
+ # Let Spaces manage networking/binding
205
+ demo.launch()
206
+ else:
207
+ # Local dev: bind only to localhost and avoid public share
208
+ try:
209
+ demo.launch(server_name="127.0.0.1", share=False, allowed_paths=["img"])
210
+ except TypeError:
211
+ demo.launch(server_name="127.0.0.1", share=False)
img/1.png ADDED
img/2.png ADDED

Git LFS Details

  • SHA256: 158cc366a4ee4ac0bf5915864ed82bd89cc8a264171eded2bf806552c4551ce3
  • Pointer size: 131 Bytes
  • Size of remote file: 158 kB
img/annotations_utils/add_rotated_gt.ipynb ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "metadata": {},
7
+ "outputs": [
8
+ {
9
+ "name": "stdout",
10
+ "output_type": "stream",
11
+ "text": [
12
+ "<class 'pandas.core.frame.DataFrame'>\n",
13
+ "RangeIndex: 562 entries, 0 to 561\n",
14
+ "Data columns (total 2 columns):\n",
15
+ " # Column Non-Null Count Dtype \n",
16
+ "--- ------ -------------- ----- \n",
17
+ " 0 Watch 562 non-null object\n",
18
+ " 1 Time 562 non-null object\n",
19
+ "dtypes: object(2)\n",
20
+ "memory usage: 8.9+ KB\n"
21
+ ]
22
+ },
23
+ {
24
+ "data": {
25
+ "text/plain": [
26
+ "( Watch Time\n",
27
+ " 0 watch1 10:10:30\n",
28
+ " 1 watch2 11:15:12\n",
29
+ " 2 watch3 10:10:25\n",
30
+ " 3 watch4 10:11:48\n",
31
+ " 4 watch5 10:08:31,\n",
32
+ " None)"
33
+ ]
34
+ },
35
+ "execution_count": 1,
36
+ "metadata": {},
37
+ "output_type": "execute_result"
38
+ }
39
+ ],
40
+ "source": [
41
+ "import pandas as pd\n",
42
+ "\n",
43
+ "# Caminho do arquivo enviado\n",
44
+ "file_path = r'C:\\Users\\anoca\\Documents\\GitHub\\analogic-watch-detector\\ground_truths/watch_times.csv'\n",
45
+ "\n",
46
+ "# Ler o arquivo CSV\n",
47
+ "original_df = pd.read_csv(file_path)\n",
48
+ "\n",
49
+ "# Visualizar as primeiras linhas para entender a estrutura\n",
50
+ "original_df.head(), original_df.info()"
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": 2,
56
+ "metadata": {},
57
+ "outputs": [
58
+ {
59
+ "data": {
60
+ "text/html": [
61
+ "<div>\n",
62
+ "<style scoped>\n",
63
+ " .dataframe tbody tr th:only-of-type {\n",
64
+ " vertical-align: middle;\n",
65
+ " }\n",
66
+ "\n",
67
+ " .dataframe tbody tr th {\n",
68
+ " vertical-align: top;\n",
69
+ " }\n",
70
+ "\n",
71
+ " .dataframe thead th {\n",
72
+ " text-align: right;\n",
73
+ " }\n",
74
+ "</style>\n",
75
+ "<table border=\"1\" class=\"dataframe\">\n",
76
+ " <thead>\n",
77
+ " <tr style=\"text-align: right;\">\n",
78
+ " <th></th>\n",
79
+ " <th>Watch</th>\n",
80
+ " <th>Time</th>\n",
81
+ " </tr>\n",
82
+ " </thead>\n",
83
+ " <tbody>\n",
84
+ " <tr>\n",
85
+ " <th>0</th>\n",
86
+ " <td>watch1</td>\n",
87
+ " <td>10:10:30</td>\n",
88
+ " </tr>\n",
89
+ " <tr>\n",
90
+ " <th>1</th>\n",
91
+ " <td>watch2</td>\n",
92
+ " <td>11:15:12</td>\n",
93
+ " </tr>\n",
94
+ " <tr>\n",
95
+ " <th>2</th>\n",
96
+ " <td>watch3</td>\n",
97
+ " <td>10:10:25</td>\n",
98
+ " </tr>\n",
99
+ " <tr>\n",
100
+ " <th>3</th>\n",
101
+ " <td>watch4</td>\n",
102
+ " <td>10:11:48</td>\n",
103
+ " </tr>\n",
104
+ " <tr>\n",
105
+ " <th>4</th>\n",
106
+ " <td>watch5</td>\n",
107
+ " <td>10:08:31</td>\n",
108
+ " </tr>\n",
109
+ " <tr>\n",
110
+ " <th>...</th>\n",
111
+ " <td>...</td>\n",
112
+ " <td>...</td>\n",
113
+ " </tr>\n",
114
+ " <tr>\n",
115
+ " <th>557</th>\n",
116
+ " <td>watch558</td>\n",
117
+ " <td>10:10:31</td>\n",
118
+ " </tr>\n",
119
+ " <tr>\n",
120
+ " <th>558</th>\n",
121
+ " <td>watch559</td>\n",
122
+ " <td>04:40:01</td>\n",
123
+ " </tr>\n",
124
+ " <tr>\n",
125
+ " <th>559</th>\n",
126
+ " <td>watch560</td>\n",
127
+ " <td>10:01:00</td>\n",
128
+ " </tr>\n",
129
+ " <tr>\n",
130
+ " <th>560</th>\n",
131
+ " <td>watch561</td>\n",
132
+ " <td>10:13:22</td>\n",
133
+ " </tr>\n",
134
+ " <tr>\n",
135
+ " <th>561</th>\n",
136
+ " <td>watch562</td>\n",
137
+ " <td>10:08:00</td>\n",
138
+ " </tr>\n",
139
+ " </tbody>\n",
140
+ "</table>\n",
141
+ "<p>562 rows × 2 columns</p>\n",
142
+ "</div>"
143
+ ],
144
+ "text/plain": [
145
+ " Watch Time\n",
146
+ "0 watch1 10:10:30\n",
147
+ "1 watch2 11:15:12\n",
148
+ "2 watch3 10:10:25\n",
149
+ "3 watch4 10:11:48\n",
150
+ "4 watch5 10:08:31\n",
151
+ ".. ... ...\n",
152
+ "557 watch558 10:10:31\n",
153
+ "558 watch559 04:40:01\n",
154
+ "559 watch560 10:01:00\n",
155
+ "560 watch561 10:13:22\n",
156
+ "561 watch562 10:08:00\n",
157
+ "\n",
158
+ "[562 rows x 2 columns]"
159
+ ]
160
+ },
161
+ "execution_count": 2,
162
+ "metadata": {},
163
+ "output_type": "execute_result"
164
+ }
165
+ ],
166
+ "source": [
167
+ "original_df"
168
+ ]
169
+ },
170
+ {
171
+ "cell_type": "code",
172
+ "execution_count": 3,
173
+ "metadata": {},
174
+ "outputs": [],
175
+ "source": [
176
+ "# Gerar os novos nomes de relógios com rotações\n",
177
+ "rotations = [-90, 180, -270]\n",
178
+ "new_watches = [\n",
179
+ " f\"{row['Watch']}_rotated_{rotation}\"\n",
180
+ " for _, row in original_df.iterrows()\n",
181
+ " for rotation in rotations\n",
182
+ "]\n",
183
+ "\n",
184
+ "# Criar o novo DataFrame para relógios rotacionados\n",
185
+ "new_data = []\n",
186
+ "\n",
187
+ "for new_watch in new_watches:\n",
188
+ " original_watch = new_watch.split(\"_rotated_\")[0] # Extrai o nome base, e.g., \"watch1\"\n",
189
+ " original_time = original_df.loc[original_df[\"Watch\"] == original_watch, \"Time\"].values\n",
190
+ " if len(original_time) > 0:\n",
191
+ " new_data.append({\"Watch\": new_watch, \"Time\": original_time[0]})\n",
192
+ "\n",
193
+ "# Criar DataFrame com os novos dados\n",
194
+ "rotated_df = pd.DataFrame(new_data)\n",
195
+ "\n",
196
+ "# Concatenar os dados originais com os novos\n",
197
+ "final_df = pd.concat([original_df, rotated_df], ignore_index=True)\n"
198
+ ]
199
+ },
200
+ {
201
+ "cell_type": "code",
202
+ "execution_count": 4,
203
+ "metadata": {},
204
+ "outputs": [
205
+ {
206
+ "data": {
207
+ "text/html": [
208
+ "<div>\n",
209
+ "<style scoped>\n",
210
+ " .dataframe tbody tr th:only-of-type {\n",
211
+ " vertical-align: middle;\n",
212
+ " }\n",
213
+ "\n",
214
+ " .dataframe tbody tr th {\n",
215
+ " vertical-align: top;\n",
216
+ " }\n",
217
+ "\n",
218
+ " .dataframe thead th {\n",
219
+ " text-align: right;\n",
220
+ " }\n",
221
+ "</style>\n",
222
+ "<table border=\"1\" class=\"dataframe\">\n",
223
+ " <thead>\n",
224
+ " <tr style=\"text-align: right;\">\n",
225
+ " <th></th>\n",
226
+ " <th>Watch</th>\n",
227
+ " <th>Time</th>\n",
228
+ " </tr>\n",
229
+ " </thead>\n",
230
+ " <tbody>\n",
231
+ " <tr>\n",
232
+ " <th>0</th>\n",
233
+ " <td>watch1</td>\n",
234
+ " <td>10:10:30</td>\n",
235
+ " </tr>\n",
236
+ " <tr>\n",
237
+ " <th>1</th>\n",
238
+ " <td>watch2</td>\n",
239
+ " <td>11:15:12</td>\n",
240
+ " </tr>\n",
241
+ " <tr>\n",
242
+ " <th>2</th>\n",
243
+ " <td>watch3</td>\n",
244
+ " <td>10:10:25</td>\n",
245
+ " </tr>\n",
246
+ " <tr>\n",
247
+ " <th>3</th>\n",
248
+ " <td>watch4</td>\n",
249
+ " <td>10:11:48</td>\n",
250
+ " </tr>\n",
251
+ " <tr>\n",
252
+ " <th>4</th>\n",
253
+ " <td>watch5</td>\n",
254
+ " <td>10:08:31</td>\n",
255
+ " </tr>\n",
256
+ " <tr>\n",
257
+ " <th>...</th>\n",
258
+ " <td>...</td>\n",
259
+ " <td>...</td>\n",
260
+ " </tr>\n",
261
+ " <tr>\n",
262
+ " <th>2243</th>\n",
263
+ " <td>watch561_rotated_180</td>\n",
264
+ " <td>10:13:22</td>\n",
265
+ " </tr>\n",
266
+ " <tr>\n",
267
+ " <th>2244</th>\n",
268
+ " <td>watch561_rotated_-270</td>\n",
269
+ " <td>10:13:22</td>\n",
270
+ " </tr>\n",
271
+ " <tr>\n",
272
+ " <th>2245</th>\n",
273
+ " <td>watch562_rotated_-90</td>\n",
274
+ " <td>10:08:00</td>\n",
275
+ " </tr>\n",
276
+ " <tr>\n",
277
+ " <th>2246</th>\n",
278
+ " <td>watch562_rotated_180</td>\n",
279
+ " <td>10:08:00</td>\n",
280
+ " </tr>\n",
281
+ " <tr>\n",
282
+ " <th>2247</th>\n",
283
+ " <td>watch562_rotated_-270</td>\n",
284
+ " <td>10:08:00</td>\n",
285
+ " </tr>\n",
286
+ " </tbody>\n",
287
+ "</table>\n",
288
+ "<p>2248 rows × 2 columns</p>\n",
289
+ "</div>"
290
+ ],
291
+ "text/plain": [
292
+ " Watch Time\n",
293
+ "0 watch1 10:10:30\n",
294
+ "1 watch2 11:15:12\n",
295
+ "2 watch3 10:10:25\n",
296
+ "3 watch4 10:11:48\n",
297
+ "4 watch5 10:08:31\n",
298
+ "... ... ...\n",
299
+ "2243 watch561_rotated_180 10:13:22\n",
300
+ "2244 watch561_rotated_-270 10:13:22\n",
301
+ "2245 watch562_rotated_-90 10:08:00\n",
302
+ "2246 watch562_rotated_180 10:08:00\n",
303
+ "2247 watch562_rotated_-270 10:08:00\n",
304
+ "\n",
305
+ "[2248 rows x 2 columns]"
306
+ ]
307
+ },
308
+ "execution_count": 4,
309
+ "metadata": {},
310
+ "output_type": "execute_result"
311
+ }
312
+ ],
313
+ "source": [
314
+ "final_df"
315
+ ]
316
+ },
317
+ {
318
+ "cell_type": "code",
319
+ "execution_count": 5,
320
+ "metadata": {},
321
+ "outputs": [],
322
+ "source": [
323
+ "output_path = r'C:\\Users\\anoca\\Documents\\GitHub\\analogic-watch-detector\\ground_truths/ground_truths.csv'\n",
324
+ "final_df.to_csv(output_path, index=False)"
325
+ ]
326
+ }
327
+ ],
328
+ "metadata": {
329
+ "kernelspec": {
330
+ "display_name": "vc",
331
+ "language": "python",
332
+ "name": "python3"
333
+ },
334
+ "language_info": {
335
+ "codemirror_mode": {
336
+ "name": "ipython",
337
+ "version": 3
338
+ },
339
+ "file_extension": ".py",
340
+ "mimetype": "text/x-python",
341
+ "name": "python",
342
+ "nbconvert_exporter": "python",
343
+ "pygments_lexer": "ipython3",
344
+ "version": "3.8.9"
345
+ }
346
+ },
347
+ "nbformat": 4,
348
+ "nbformat_minor": 2
349
+ }
img/annotations_utils/desfocar.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import albumentations as A
4
+
5
+ # Função para ler anotações de um arquivo
6
+ def read_annotations(file_path):
7
+ annotations = []
8
+ with open(file_path, "r") as f:
9
+ for line in f:
10
+ annotations.append(line.strip())
11
+ return annotations
12
+
13
+ # Caminhos das pastas
14
+ images_folder = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\rr"
15
+ labels_folder = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector"
16
+ output_images_folder = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\rr"
17
+ output_labels_folder = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector"
18
+
19
+ # Certificar-se de que as pastas de saída existem
20
+ os.makedirs(output_images_folder, exist_ok=True)
21
+ os.makedirs(output_labels_folder, exist_ok=True)
22
+
23
+ # Configuração de transformação de desfoque
24
+ transform = A.Compose([
25
+ A.Blur(blur_limit=43, p=1.0) # Intensão do desfoque
26
+ ])
27
+
28
+ # Processar todas as imagens na pasta
29
+ for image_filename in os.listdir(images_folder):
30
+ # Ignorar arquivos com "rotated" ou "zoom_out" no nome
31
+ if "rotated" in image_filename or "zoom_out" in image_filename:
32
+ print(f"Ignorado: {image_filename} (contém 'rotated' ou 'zoom_out')")
33
+ continue
34
+
35
+ # Verificar se o arquivo é uma imagem
36
+ if image_filename.endswith((".jpg", ".png", ".jpeg")):
37
+ image_path = os.path.join(images_folder, image_filename)
38
+ label_path = os.path.join(labels_folder, image_filename.replace(".jpg", ".txt").replace(".png", ".txt").replace(".jpeg", ".txt"))
39
+
40
+ # Verificar se o arquivo de anotações correspondente existe
41
+ if not os.path.exists(label_path):
42
+ print(f"Anotação não encontrada para {image_filename}, pulando.")
43
+ continue
44
+
45
+ # Carregar a imagem
46
+ imagem = cv2.imread(image_path)
47
+
48
+ # Aplicar o desfoque
49
+ imagem_desfocada = transform(image=imagem)['image']
50
+
51
+ # Criar novo nome para a imagem e o label
52
+ base_name, ext = os.path.splitext(image_filename) # Separar nome e extensão
53
+ new_image_name = f"{base_name}_blurred{ext}"
54
+ new_label_name = f"{base_name}_blurred.txt"
55
+
56
+ # Salvar a imagem desfocada
57
+ output_image_path = os.path.join(output_images_folder, new_image_name)
58
+ cv2.imwrite(output_image_path, imagem_desfocada)
59
+
60
+ # Copiar o conteúdo das anotações para um novo arquivo
61
+ new_label_path = os.path.join(output_labels_folder, new_label_name)
62
+ annotations = read_annotations(label_path)
63
+ with open(new_label_path, "w") as f:
64
+ for line in annotations:
65
+ f.write(line + "\n")
66
+
67
+ print(f"Imagem processada e salva como: {new_image_name}")
68
+ print(f"Anotações copiadas e salvas como: {new_label_name}")
img/annotations_utils/name_changer.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # Folder containing the images
4
+ folder_path = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\test_set\novos"
5
+
6
+ # Starting number for the renaming
7
+ start_number = 110
8
+
9
+ # Get all .jpg files in the folder
10
+ files = [f for f in os.listdir(folder_path) if f.lower().endswith('.jpg')]
11
+
12
+ # Sort files to ensure consistent renaming
13
+ files.sort()
14
+
15
+ # Rename the files
16
+ for i, file in enumerate(files):
17
+ new_name = f"watch_test{start_number + i}.jpg"
18
+ old_file = os.path.join(folder_path, file)
19
+ new_file = os.path.join(folder_path, new_name)
20
+ os.rename(old_file, new_file)
21
+
22
+ print("Renaming completed!")
img/annotations_utils/remove_center.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # Caminho da pasta onde estão os arquivos
4
+ caminho_pasta = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\dataset\labels\val"
5
+
6
+ # Iterar por todos os arquivos na pasta
7
+ for nome_arquivo in os.listdir(caminho_pasta):
8
+ # Verificar se o arquivo tem extensão .txt
9
+ if nome_arquivo.endswith(".txt"):
10
+ caminho_arquivo = os.path.join(caminho_pasta, nome_arquivo)
11
+
12
+ # Ler o conteúdo do arquivo
13
+ with open(caminho_arquivo, "r") as arquivo:
14
+ linhas = arquivo.readlines()
15
+
16
+ # Filtrar as linhas que não começam com "4"
17
+ linhas_filtradas = [linha for linha in linhas if not linha.startswith("4")]
18
+
19
+ # Sobrescrever o arquivo original com as linhas filtradas
20
+ with open(caminho_arquivo, "w") as arquivo:
21
+ arquivo.writelines(linhas_filtradas)
22
+
23
+ print(f"Processado: {nome_arquivo}")
24
+
25
+ print("Todos os arquivos foram processados.")
img/annotations_utils/resized_annotations.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import albumentations as A
4
+
5
+ # Função para ajustar as anotações após transformação
6
+ def adjust_annotations(annotations, original_size, resized_size, padded_size):
7
+ orig_h, orig_w = original_size
8
+ resized_h, resized_w = resized_size
9
+ padded_h, padded_w = padded_size
10
+
11
+ scale_x = resized_w / orig_w # Escala para largura
12
+ scale_y = resized_h / orig_h # Escala para altura
13
+
14
+ pad_x = (padded_w - resized_w) / 2 # Padding horizontal
15
+ pad_y = (padded_h - resized_h) / 2 # Padding vertical
16
+
17
+ adjusted_annotations = []
18
+ for ann in annotations:
19
+ cls, x_c, y_c, w, h = ann
20
+
21
+ # Ajustar coordenadas normalizadas para o redimensionamento
22
+ x_c = x_c * orig_w * scale_x
23
+ y_c = y_c * orig_h * scale_y
24
+ w = w * orig_w * scale_x
25
+ h = h * orig_h * scale_y
26
+
27
+ # Ajustar para o padding e normalizar para o novo tamanho
28
+ x_c = (x_c + pad_x) / padded_w
29
+ y_c = (y_c + pad_y) / padded_h
30
+ w = w / padded_w
31
+ h = h / padded_h
32
+
33
+ # Adicionar anotação ajustada
34
+ adjusted_annotations.append((cls, x_c, y_c, w, h))
35
+
36
+ return adjusted_annotations
37
+
38
+ # Função para ler as anotações de um arquivo
39
+ def read_annotations(file_path):
40
+ annotations = []
41
+ with open(file_path, "r") as f:
42
+ for line in f:
43
+ parts = line.strip().split()
44
+ cls = int(parts[0]) # Classe
45
+ x_c, y_c, w, h = map(float, parts[1:])
46
+ annotations.append((cls, x_c, y_c, w, h))
47
+ return annotations
48
+
49
+ # Configuração de transformações
50
+ resize_height, resize_width = 128, 128
51
+ padded_height, padded_width = 256, 256
52
+ augmentation = A.Compose([
53
+ A.Resize(height=resize_height, width=resize_width),
54
+ A.PadIfNeeded(min_height=padded_height, min_width=padded_width, border_mode=cv2.BORDER_CONSTANT, value=(0, 0, 0)),
55
+ ])
56
+
57
+ # Caminhos das pastas
58
+ images_folder = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\dataset\images\train"
59
+ labels_folder = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\dataset\labels\train"
60
+ output_images_folder = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\dataset\images\train"
61
+ output_labels_folder = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\dataset\labels\train"
62
+
63
+ # Certificar-se de que as pastas de saída existem
64
+ os.makedirs(output_images_folder, exist_ok=True)
65
+ os.makedirs(output_labels_folder, exist_ok=True)
66
+
67
+ # Processar todos os arquivos na pasta
68
+ for image_filename in os.listdir(images_folder):
69
+ # Pular arquivos com "rotated" no nome
70
+ if "rotated" in image_filename:
71
+ print(f"Pulado: {image_filename} (contém 'rotated').")
72
+ continue
73
+
74
+ if image_filename.endswith((".jpg", ".png", ".jpeg")): # Verifica formatos de imagem
75
+ image_path = os.path.join(images_folder, image_filename)
76
+ label_path = os.path.join(labels_folder, image_filename.replace(".jpg", ".txt").replace(".png", ".txt").replace(".jpeg", ".txt"))
77
+
78
+ # Verificar se o arquivo de anotações correspondente existe
79
+ if not os.path.exists(label_path):
80
+ print(f"Anotação não encontrada para {image_filename}, pulando.")
81
+ continue
82
+
83
+ # Carregar a imagem e as anotações
84
+ image = cv2.imread(image_path)
85
+ original_size = image.shape[:2] # (altura, largura)
86
+ annotations = read_annotations(label_path)
87
+
88
+ # Aplicar transformações
89
+ augmented = augmentation(image=image)
90
+ augmented_image = augmented["image"]
91
+
92
+ # Ajustar as anotações
93
+ new_annotations = adjust_annotations(
94
+ annotations,
95
+ original_size=(original_size[0], original_size[1]),
96
+ resized_size=(resize_height, resize_width),
97
+ padded_size=(padded_height, padded_width)
98
+ )
99
+
100
+ # Criar novo nome para a imagem e as anotações
101
+ base_name, ext = os.path.splitext(image_filename)
102
+ new_image_name = f"{base_name}_zoom_out{ext}"
103
+ new_label_name = f"{base_name}_zoom_out.txt"
104
+
105
+ # Salvar imagem transformada
106
+ output_image_path = os.path.join(output_images_folder, new_image_name)
107
+ cv2.imwrite(output_image_path, augmented_image)
108
+
109
+ # Salvar anotações transformadas
110
+ output_label_path = os.path.join(output_labels_folder, new_label_name)
111
+ with open(output_label_path, "w") as f:
112
+ for ann in new_annotations:
113
+ f.write(f"{ann[0]} {ann[1]:.6f} {ann[2]:.6f} {ann[3]:.6f} {ann[4]:.6f}\n")
114
+
115
+ print(f"Processado e salvo como: {new_image_name}")
img/annotations_utils/rotate_img.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ from PIL import Image
4
+
5
+ def rotate_images(input_dir, output_dir):
6
+ """
7
+ Roda apenas uma imagem do diretório para um ângulo aleatório, salva a nova e remove a antiga.
8
+
9
+ Args:
10
+ input_dir (str): Diretório onde estão as imagens originais.
11
+ output_dir (str): Diretório onde será salva a imagem rotacionada.
12
+ """
13
+ if not os.path.exists(output_dir):
14
+ os.makedirs(output_dir)
15
+
16
+ # Lista todos os arquivos no diretório de entrada
17
+ for filename in os.listdir(input_dir):
18
+ input_path = os.path.join(input_dir, filename)
19
+
20
+ # Verifica se o arquivo é uma imagem
21
+ if not filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tiff')):
22
+ continue
23
+
24
+ try:
25
+ # Escolhe um ângulo aleatório
26
+ ang = [90, 270, 45, 135, 180, 225, 315]
27
+ random_angle = random.choice(ang)
28
+
29
+ # Abrir a imagem
30
+ with Image.open(input_path) as img:
31
+ # Rotacionar a imagem
32
+ rotated_img = img.rotate(random_angle, expand=True)
33
+
34
+ # Gerar nome para a imagem rotacionada
35
+ name, ext = os.path.splitext(filename)
36
+ rotated_filename = f"{name}_rotated_{random_angle}{ext}"
37
+ output_path = os.path.join(output_dir, rotated_filename)
38
+
39
+ # Salvar imagem rotacionada
40
+ rotated_img.save(output_path)
41
+ print(f"Imagem salva: {output_path}")
42
+
43
+ # Remover a imagem original após gerar a versão rotacionada
44
+ os.remove(input_path)
45
+ print(f"Imagem original removida: {input_path}")
46
+
47
+ except Exception as e:
48
+ print(f"Erro ao processar {filename}: {e}")
49
+
50
+ # Configurações
51
+ input_directory = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\dataset\images\train\novos\rodados" # Substitua pelo diretório das imagens originais
52
+ output_directory = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\dataset\images\train\novos\rodados" # Substitua pelo diretório de saída
53
+
54
+ # Executar o script
55
+ rotate_images(input_directory, output_directory)
56
+
img/annotations_utils/rotate_img_ann.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import math
4
+ from PIL import Image, ImageDraw
5
+
6
+ def rotate_point(point, center, angle_deg):
7
+ """
8
+ Rotaciona um ponto ao redor de um centro em um determinado ângulo.
9
+ """
10
+ angle_rad = math.radians(angle_deg)
11
+ x, y = point[0] - center[0], point[1] - center[1]
12
+ x_rotated = x * math.cos(angle_rad) - y * math.sin(angle_rad)
13
+ y_rotated = x * math.sin(angle_rad) + y * math.cos(angle_rad)
14
+ return (x_rotated + center[0], y_rotated + center[1])
15
+
16
+ def draw_bounding_boxes(image_path, annotations_path, output_path):
17
+ """
18
+ Desenha caixas delimitadoras em uma imagem com base nas anotações YOLO.
19
+ """
20
+ with Image.open(image_path) as img:
21
+ draw = ImageDraw.Draw(img)
22
+ img_width, img_height = img.size
23
+
24
+ with open(annotations_path, 'r') as f:
25
+ lines = f.readlines()
26
+
27
+ for line in lines:
28
+ parts = line.strip().split()
29
+ class_id = parts[0]
30
+ x_center = float(parts[1]) * img_width
31
+ y_center = float(parts[2]) * img_height
32
+ width = float(parts[3]) * img_width
33
+ height = float(parts[4]) * img_height
34
+
35
+ x_min = x_center - width / 2
36
+ y_min = y_center - height / 2
37
+ x_max = x_center + width / 2
38
+ y_max = y_center + height / 2
39
+
40
+ # Desenha o retângulo
41
+ draw.rectangle([x_min, y_min, x_max, y_max], outline="red", width=2)
42
+ draw.text((x_min, y_min - 10), f"Class {class_id}", fill="red")
43
+
44
+ img.save(output_path)
45
+ print(f"Imagem com bounding boxes salva: {output_path}")
46
+
47
+ def rotate_yolo_annotations(input_txt, output_txt, angle, original_width, original_height, rotated_width, rotated_height):
48
+ """
49
+ Rotaciona as anotações YOLO para um ângulo específico, considerando o tamanho da imagem rotacionada.
50
+ """
51
+ with open(input_txt, 'r') as f:
52
+ lines = f.readlines()
53
+
54
+ new_annotations = []
55
+
56
+ for line in lines:
57
+ parts = line.strip().split()
58
+ class_id = parts[0]
59
+ x_center = float(parts[1]) * original_width
60
+ y_center = float(parts[2]) * original_height
61
+ width = float(parts[3]) * original_width
62
+ height = float(parts[4]) * original_height
63
+
64
+ if angle == -90:
65
+ new_x_center = original_height - y_center
66
+ new_y_center = x_center
67
+ new_width = height
68
+ new_height = width
69
+ elif angle == 180:
70
+ new_x_center = original_width - x_center
71
+ new_y_center = original_height - y_center
72
+ new_width = width
73
+ new_height = height
74
+ elif angle == -270:
75
+ new_x_center = y_center
76
+ new_y_center = original_width - x_center
77
+ new_width = height
78
+ new_height = width
79
+
80
+ # Normaliza os valores para a escala [0, 1]
81
+ norm_x_center = new_x_center / rotated_width
82
+ norm_y_center = new_y_center / rotated_height
83
+ norm_width = new_width / rotated_width
84
+ norm_height = new_height / rotated_height
85
+
86
+ # Gera a nova anotação YOLO
87
+ new_annotation = f"{class_id} {norm_x_center:.6f} {norm_y_center:.6f} {norm_width:.6f} {norm_height:.6f}\n"
88
+ new_annotations.append(new_annotation)
89
+
90
+ # Salva as novas anotações no arquivo de saída
91
+ with open(output_txt, 'w') as f:
92
+ f.writelines(new_annotations)
93
+
94
+ def rotate_images_and_annotations(input_dir, output_dir):
95
+ """
96
+ Rotaciona imagens e suas anotações correspondentes em todos os ângulos especificados.
97
+ """
98
+ if not os.path.exists(output_dir):
99
+ os.makedirs(output_dir)
100
+
101
+ angles = [-90, 180, -270]
102
+
103
+ for filename in os.listdir(input_dir):
104
+ if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tiff')):
105
+ try:
106
+ input_img_path = os.path.join(input_dir, filename)
107
+ input_txt_path = os.path.splitext(input_img_path)[0] + '.txt'
108
+
109
+ with Image.open(input_img_path) as img:
110
+ original_width, original_height = img.size
111
+
112
+ for angle in angles:
113
+ rotated_img = img.rotate(angle, expand=True)
114
+ rotated_width, rotated_height = rotated_img.size
115
+ name, ext = os.path.splitext(filename)
116
+ rotated_filename = f"{name}_rotated_{angle}{ext}"
117
+ output_img_path = os.path.join(output_dir, rotated_filename)
118
+ rotated_img.save(output_img_path)
119
+ print(f"Imagem salva: {output_img_path}")
120
+
121
+ if os.path.exists(input_txt_path):
122
+ rotated_txt_filename = f"{name}_rotated_{angle}.txt"
123
+ output_txt_path = os.path.join(output_dir, rotated_txt_filename)
124
+ rotate_yolo_annotations(
125
+ input_txt_path,
126
+ output_txt_path,
127
+ angle,
128
+ original_width,
129
+ original_height,
130
+ rotated_width,
131
+ rotated_height
132
+ )
133
+ print(f"Anotação salva: {output_txt_path}")
134
+ # Desenha bounding boxes antes e depois
135
+ #annotated_image_path = os.path.join(output_dir, f"{name}_original_with_boxes{ext}")
136
+ #draw_bounding_boxes(input_img_path, input_txt_path, annotated_image_path)
137
+
138
+ #annotated_rotated_path = os.path.join(output_dir, f"{name}_rotated_with_boxes{ext}")
139
+ #draw_bounding_boxes(output_img_path, output_txt_path, annotated_rotated_path)
140
+
141
+ except Exception as e:
142
+ print(f"Erro ao processar {filename}: {e}")
143
+
144
+
145
+ # Configurações
146
+ input_directory = r"C:\Users\anoca\Downloads\Nova pasta"
147
+ output_directory = r"C:\Users\anoca\Downloads\Nova pasta\rotated"
148
+ rotate_images_and_annotations(input_directory, output_directory)
img/annotations_utils/train_val_split.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ from sklearn.model_selection import train_test_split
4
+
5
+ def split_dataset(images_dir, labels_dir, val_images_dir, val_labels_dir, val_ratio=0.2):
6
+ """
7
+ Divide o conjunto de dados em treino e validação, excluindo imagens "_rotated".
8
+
9
+ :param images_dir: Diretório das imagens originais.
10
+ :param labels_dir: Diretório das labels originais.
11
+ :param val_images_dir: Diretório para salvar imagens de validação.
12
+ :param val_labels_dir: Diretório para salvar labels de validação.
13
+ :param val_ratio: Proporção de imagens a serem usadas na validação.
14
+ """
15
+ # Cria pastas de validação, se não existirem
16
+ os.makedirs(val_images_dir, exist_ok=True)
17
+ os.makedirs(val_labels_dir, exist_ok=True)
18
+
19
+ # Lista de imagens normais (sem _rotated)
20
+ normal_images = [f for f in os.listdir(images_dir) if f.endswith(".jpg") and "_rotated" not in f]
21
+
22
+ # Associa labels existentes
23
+ normal_images = [f for f in normal_images if os.path.exists(os.path.join(labels_dir, f.replace(".jpg", ".txt")))]
24
+
25
+ # Divide em treino e validação
26
+ train_images, val_images = train_test_split(normal_images, test_size=val_ratio, random_state=42)
27
+
28
+ # Move as imagens e labels para o diretório de validação
29
+ for image in val_images:
30
+ label = image.replace(".jpg", ".txt")
31
+
32
+ # Move a imagem
33
+ shutil.move(os.path.join(images_dir, image), os.path.join(val_images_dir, image))
34
+
35
+ # Move a label
36
+ shutil.move(os.path.join(labels_dir, label), os.path.join(val_labels_dir, label))
37
+
38
+ print(f"Divisão concluída! {len(val_images)} imagens movidas para validação.")
39
+
40
+ # Exemplos de uso
41
+ split_dataset(
42
+ images_dir="dataset/images/train",
43
+ labels_dir="dataset/labels/train",
44
+ val_images_dir="dataset/images/val",
45
+ val_labels_dir="dataset/labels/val",
46
+ val_ratio=0.2
47
+ )
img/annotations_utils/xml_to_txt.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import xml.etree.ElementTree as ET
3
+
4
+ # Mapeamento das classes
5
+ class_mapping = {
6
+ "circle": 0,
7
+ "hours": 1,
8
+ "minutes": 2,
9
+ "seconds": 3,
10
+ "12": 4
11
+ }
12
+
13
+ # Diretório contendo os arquivos XML
14
+ input_dir = r"C:\Users\anoca\Documents\GitHub\analogic-watch-detector\dataset\images\train"
15
+
16
+ def convert_xml_to_yolo(xml_file, output_file):
17
+ tree = ET.parse(xml_file)
18
+ root = tree.getroot()
19
+
20
+ image_width = int(root.find('size/width').text)
21
+ image_height = int(root.find('size/height').text)
22
+
23
+ yolo_annotations = []
24
+
25
+ for obj in root.findall('object'):
26
+ class_name = obj.find('name').text
27
+ class_id = class_mapping[class_name]
28
+
29
+ xmin = int(obj.find('bndbox/xmin').text)
30
+ ymin = int(obj.find('bndbox/ymin').text)
31
+ xmax = int(obj.find('bndbox/xmax').text)
32
+ ymax = int(obj.find('bndbox/ymax').text)
33
+
34
+ x_center = ((xmin + xmax) / 2) / image_width
35
+ y_center = ((ymin + ymax) / 2) / image_height
36
+ width = (xmax - xmin) / image_width
37
+ height = (ymax - ymin) / image_height
38
+
39
+ yolo_annotations.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}")
40
+
41
+ with open(output_file, 'w') as f:
42
+ f.write("\n".join(yolo_annotations))
43
+
44
+ # Processar todos os arquivos XML no diretório
45
+ for filename in os.listdir(input_dir):
46
+ if filename.endswith(".xml"):
47
+ xml_path = os.path.join(input_dir, filename)
48
+ txt_filename = os.path.splitext(filename)[0] + ".txt"
49
+ txt_path = os.path.join(input_dir, txt_filename)
50
+
51
+ try:
52
+ convert_xml_to_yolo(xml_path, txt_path)
53
+ print(f"Convertido: {xml_path} -> {txt_path}")
54
+
55
+ # Apagar o arquivo XML após a conversão
56
+ os.remove(xml_path)
57
+ print(f"Arquivo XML apagado: {xml_path}")
58
+ except Exception as e:
59
+ print(f"Erro ao processar {xml_path}: {e}")
img/icon.png ADDED
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio==4.44.0
2
+ ultralytics==8.3.40
3
+ opencv-python-headless
4
+ numpy==1.24.4
5
+ torch==2.2.2
6
+ torchvision==0.17.2
7
+ pillow
tune4_best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:24cc80d63b0532d05e8f088efadacb3132d65fa8c6b1b0c39298ed1115bd0402
3
+ size 22549923
utils/__pycache__/clock_utils.cpython-311.pyc ADDED
Binary file (9.58 kB). View file
 
utils/__pycache__/clock_utils.cpython-312.pyc ADDED
Binary file (8.48 kB). View file
 
utils/__pycache__/clock_utils.cpython-38.pyc ADDED
Binary file (5.15 kB). View file
 
utils/__pycache__/detections_utils.cpython-311.pyc ADDED
Binary file (8.27 kB). View file
 
utils/__pycache__/detections_utils.cpython-312.pyc ADDED
Binary file (6.42 kB). View file
 
utils/__pycache__/detections_utils.cpython-38.pyc ADDED
Binary file (3.79 kB). View file
 
utils/clock_utils.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import math
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ from utils.detections_utils import run_detection
6
+ import os
7
+
8
+
9
+ def get_box_center(box):
10
+ """Calculate the center point of a bounding box"""
11
+ x = (box[0] + box[2]) / 2
12
+ y = (box[1] + box[3]) / 2
13
+ return (x, y)
14
+
15
+ def calculate_angle(center, point, reference_point):
16
+ """Calculate angle between two points relative to 12 o'clock position"""
17
+ # Calculate vectors
18
+ ref_vector = (reference_point[0] - center[0], reference_point[1] - center[1])
19
+ point_vector = (point[0] - center[0], point[1] - center[1])
20
+
21
+ # Calculate angles from vectors
22
+ ref_angle = math.atan2(ref_vector[1], ref_vector[0])
23
+ point_angle = math.atan2(point_vector[1], point_vector[0])
24
+
25
+ # Calculate relative angle in degrees
26
+ angle = math.degrees(point_angle - ref_angle)
27
+
28
+ # Normalize angle to 0-360 range
29
+ angle = (angle + 360) % 360
30
+
31
+ return angle
32
+
33
+ def process_clock_time(detections_data, image_path):
34
+ """Process clock time from detections"""
35
+ # Organize detections by class_name and select the one with highest confidence for each class
36
+ detections_by_class = {}
37
+ for detection in detections_data[0]:
38
+ class_name = detection['class_name']
39
+ if class_name not in detections_by_class or detection['confidence'] > detections_by_class[class_name]['confidence']:
40
+ detections_by_class[class_name] = detection
41
+
42
+ # Validate required keys
43
+ required_keys = ['hours', 'minutes', '12', 'circle']
44
+ for key in required_keys:
45
+ if key not in detections_by_class:
46
+ print(f"Error: Missing required key '{key}' in detection data.")
47
+ return None
48
+
49
+ # Calculate circle center
50
+ circle_box_point = get_box_center(detections_by_class['circle']['box'])
51
+
52
+ # Determine center point: use 'center' if exists, otherwise use circle center
53
+ if 'center' in detections_by_class:
54
+ center_point = get_box_center(detections_by_class['center']['box'])
55
+ else:
56
+ center_point = circle_box_point
57
+
58
+ hours_point = get_box_center(detections_by_class['hours']['box'])
59
+ number_12_point = get_box_center(detections_by_class['12']['box'])
60
+
61
+ # Try to get seconds point with highest confidence
62
+ seconds_point = None
63
+ seconds_angle = None
64
+ calculated_seconds = 0
65
+
66
+ if 'minutes' in detections_by_class:
67
+ minutes_point = get_box_center(detections_by_class['minutes']['box'])
68
+
69
+ if 'seconds' in detections_by_class:
70
+ seconds_point = get_box_center(detections_by_class['seconds']['box'])
71
+
72
+ # Calculate raw angles relative to 12 o'clock position
73
+ hour_angle = calculate_angle(center_point, hours_point, number_12_point)
74
+
75
+ # Calculate minute angle
76
+ if minutes_point:
77
+ minute_angle = calculate_angle(center_point, minutes_point, number_12_point)
78
+
79
+ # Calculate seconds angle if seconds point exists
80
+ if seconds_point:
81
+ seconds_angle = calculate_angle(center_point, seconds_point, number_12_point)
82
+
83
+ # Convert angles to time
84
+ hours = (hour_angle / 30) # Each hour is 30 degrees
85
+
86
+ # Round to nearest hour and minute
87
+ hours = math.floor(hours) % 12
88
+ if hours == 0:
89
+ hours = 12
90
+
91
+ if minute_angle is not None:
92
+ minutes = (minute_angle / 6)
93
+ minutes = round(minutes) % 60
94
+ calculated_minutes = minutes
95
+
96
+ # Calculate seconds if angle exists
97
+ if seconds_angle is not None:
98
+ seconds = (seconds_angle / 6) # Each second is 6 degrees
99
+ seconds = round(seconds) % 60
100
+ calculated_seconds = seconds
101
+
102
+ return {
103
+ 'hours': hours,
104
+ 'minutes': calculated_minutes if minute_angle is not None else None,
105
+ 'seconds': calculated_seconds if seconds_angle is not None else None
106
+ }
107
+
108
+ def draw_clock(image_path, center_point, hours_point, minutes_point, seconds_point, number_12_point, hour_angle, minute_angle, seconds_angle, calculated_hours, calculated_minutes, calculated_seconds, image_name):
109
+ """Draw clock and reference points on the image"""
110
+
111
+ img = cv2.imread(image_path)
112
+
113
+ # To int
114
+ center = (int(center_point[0]), int(center_point[1]))
115
+ hours = (int(hours_point[0]), int(hours_point[1]))
116
+ minutes = (int(minutes_point[0]), int(minutes_point[1])) if minutes_point else None
117
+ seconds = (int(seconds_point[0]), int(seconds_point[1])) if seconds_point else None
118
+ twelve = (int(number_12_point[0]), int(number_12_point[1]))
119
+
120
+ # Draw the reference points
121
+ cv2.circle(img, center, 3, (0, 0, 255), -1) # Centro em vermelho
122
+ cv2.circle(img, twelve, 3, (255, 0, 0), -1) # Ponto 12 em azul
123
+
124
+ # Draw the lines with thicker strokes
125
+ cv2.line(img, center, hours, (0, 0, 255), 5) # Ponteiro das horas em vermelho
126
+ if minutes:
127
+ cv2.line(img, center, minutes, (255, 0, 0), 4) # Ponteiro dos minutos em azul
128
+ if seconds:
129
+ cv2.line(img, center, seconds, (255, 165, 0), 2) # Ponteiro dos segundos em laranja
130
+
131
+ cv2.line(img, center, twelve, (0, 255, 0), 1) # Linha de referência (12h) em verde
132
+
133
+ # Draw the text
134
+ cv2.putText(img, f"Hour angle: {hour_angle:.1f}",
135
+ (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
136
+ if minute_angle is not None:
137
+ cv2.putText(img, f"Minute angle: {minute_angle:.1f}",
138
+ (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
139
+ time_text = f"Time: {int(calculated_hours):02d}:{int(calculated_minutes):02d}"
140
+ else:
141
+ time_text = f"Time: {int(calculated_hours):02d}"
142
+
143
+ if seconds_angle is not None:
144
+ cv2.putText(img, f"Seconds angle: {seconds_angle:.1f}",
145
+ (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
146
+ time_text = f"Time: {int(calculated_hours):02d}:{int(calculated_minutes):02d}:{int(calculated_seconds):02d}"
147
+ else:
148
+ time_text = f"Time: {int(calculated_hours):02d}:{int(calculated_minutes):02d}"
149
+
150
+ cv2.putText(img, time_text,
151
+ (10, 120 if seconds else 90), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
152
+
153
+
154
+ output_path = f'results/images/{image_name}'
155
+ cv2.imwrite(output_path, img)
156
+ print(f"Annotated image saved to {output_path}")
157
+
158
+ def zoom_into_clock_circle(image_path, confidence=0.01):
159
+ """
160
+ Attempt to find the clock circle and zoom into it for more precise detection.
161
+
162
+ Args:
163
+ image_path (str): Path to the input image
164
+ confidence (float): Confidence threshold for detection
165
+
166
+ Returns:
167
+ str: Path to the zoomed-in image, or None if no suitable circle found
168
+ """
169
+ # Read the image
170
+ image = cv2.imread(image_path)
171
+
172
+ # Run detection to find clock circle
173
+ detections = run_detection(image_path, confidence=confidence)
174
+
175
+ # Find the circle detection with highest confidence
176
+ circle_detection = None
177
+ for detection in detections[0]:
178
+ if detection['class_name'] == 'circle' and detection['confidence'] >= confidence:
179
+ if not circle_detection or detection['confidence'] > circle_detection['confidence']:
180
+ circle_detection = detection
181
+
182
+ if not circle_detection:
183
+ return None
184
+
185
+ # Extract bounding box
186
+ x1, y1, x2, y2 = circle_detection['box']
187
+
188
+ # Add some padding (20% on each side)
189
+ height, width = image.shape[:2]
190
+ pad_x = int((x2 - x1) * 0.2)
191
+ pad_y = int((y2 - y1) * 0.2)
192
+
193
+ # Calculate padded coordinates with boundary checks
194
+ x1_pad = max(0, x1 - pad_x)
195
+ y1_pad = max(0, y1 - pad_y)
196
+ x2_pad = min(width, x2 + pad_x)
197
+ y2_pad = min(height, y2 + pad_y)
198
+
199
+ # Crop the image
200
+ zoomed_image = image[int(y1_pad):int(y2_pad), int(x1_pad):int(x2_pad)]
201
+
202
+ # Save the zoomed image
203
+ zoomed_image_path = f'results/zoomed_images/{os.path.splitext(os.path.basename(image_path))[0]}_zoomed.jpg'
204
+ os.makedirs('results/zoomed_images', exist_ok=True)
205
+ cv2.imwrite(zoomed_image_path, zoomed_image)
206
+
207
+ return zoomed_image_path
208
+
209
+ def process_clock_with_fallback(image_path, confidence=0.01):
210
+ """
211
+ Attempt to process clock time with fallback to zoomed detection.
212
+
213
+ Args:
214
+ image_path (str): Path to the input image
215
+ confidence (float): Confidence threshold for detection
216
+
217
+ Returns:
218
+ dict or None: Processed clock time result
219
+ """
220
+ # First attempt with original image
221
+ #original_result = process_clock_time(run_detection(image_path, confidence=confidence), image_path)
222
+
223
+ # If original detection succeeds, return the result
224
+ #if original_result:
225
+ # return original_result
226
+
227
+ # Try zooming into clock circle
228
+ zoomed_image_path = zoom_into_clock_circle(image_path, confidence)
229
+
230
+ # If no zoom possible, return None
231
+ if not zoomed_image_path:
232
+ return None
233
+
234
+ detections = run_detection(zoomed_image_path, confidence=confidence, zoom = True)
235
+ # Attempt detection on zoomed image
236
+ zoomed_result = process_clock_time(detections, zoomed_image_path)
237
+
238
+ return detections, zoomed_result
239
+
240
+
241
+
242
+
utils/detections_utils.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ from ultralytics import YOLO
5
+ import cv2
6
+ import numpy as np
7
+ import torch
8
+
9
+ def get_latest_train_dir(base_path="runs/detect"):
10
+ """Find the most recent training directory (train, train1, train2, etc.)"""
11
+ if not os.path.exists(base_path):
12
+ raise FileNotFoundError(f"Directory {base_path} does not exist")
13
+
14
+ train_dirs = [d for d in os.listdir(base_path)
15
+ if os.path.isdir(os.path.join(base_path, d)) and d.startswith('train')]
16
+
17
+ if not train_dirs:
18
+ raise FileNotFoundError("No 'train' directory found")
19
+
20
+ def get_train_number(dirname):
21
+ match = re.search(r'train(\d+)?$', dirname)
22
+ if not match or not match.group(1):
23
+ return -1
24
+ return int(match.group(1))
25
+
26
+ latest_train = max(train_dirs, key=get_train_number)
27
+ return os.path.join(base_path, latest_train)
28
+
29
+ def run_detection(
30
+ image_path=None,
31
+ model_path=None,
32
+ confidence=0.01,
33
+ save_path=None,
34
+ zoom=False,
35
+ model=None,
36
+ image=None,
37
+ save_visualization=True,
38
+ return_prediction_results=False,
39
+ ):
40
+ """
41
+ Run object detection on an image without Non-Maximum Suppression
42
+
43
+ Args:
44
+ image_path (str): Path to the input image
45
+ model_path (str, optional): Path to the YOLO model weights
46
+ confidence (float, optional): Initial confidence threshold
47
+ save_path (str, optional): Path to save detection results JSON
48
+
49
+ Returns:
50
+ list: Detections from the image
51
+ """
52
+ # Find model path if not provided
53
+ if image_path is None and image is None:
54
+ raise ValueError("Either 'image_path' or 'image' must be provided for detection.")
55
+
56
+ if model is None:
57
+ if not model_path:
58
+ model_path = os.path.join(get_latest_train_dir(), "weights/best.pt")
59
+ model = YOLO(model_path)
60
+
61
+ # Default save path if not specified
62
+ image_identifier = (
63
+ os.path.splitext(os.path.basename(image_path))[0]
64
+ if image_path
65
+ else "uploaded_image"
66
+ )
67
+ if not save_path and image_path:
68
+ save_path = os.path.join('results/detections', f'{image_identifier}_detection.json')
69
+
70
+ # Ensure detections directory exists
71
+ if save_path:
72
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
73
+ if save_visualization and image_path:
74
+ os.makedirs('results/image_detections', exist_ok=True)
75
+
76
+ # Determine prediction source
77
+ source = image if image is not None else image_path
78
+
79
+ # Run detection
80
+ results = model.predict(
81
+ source=source,
82
+ save=save_visualization,
83
+ save_txt=False,
84
+ conf=confidence,
85
+ max_det=50,
86
+ verbose=False,
87
+ )
88
+
89
+ # Convert detections to list format
90
+ detections = []
91
+ for result in results:
92
+ # Extract raw detections
93
+ boxes = result.boxes.xyxy.cpu().numpy()
94
+ confidences = result.boxes.conf.cpu().numpy()
95
+ classes = result.boxes.cls.cpu().numpy()
96
+
97
+ # Get class names
98
+ if hasattr(result.names, 'items'):
99
+ class_names = {int(k): v for k, v in result.names.items()}
100
+ else:
101
+ class_names = {int(cls_id): str(cls_id) for cls_id in np.unique(classes)}
102
+
103
+ # Create list of detections for this image
104
+ image_detections = []
105
+ for box, score, cls_id in zip(boxes, confidences, classes):
106
+ cls_name = class_names.get(int(cls_id), "unknown")
107
+ detection = {
108
+ 'box': box.tolist(), # [x_min, y_min, x_max, y_max]
109
+ 'confidence': float(score),
110
+ 'class_id': int(cls_id),
111
+ 'class_name': cls_name
112
+ }
113
+ image_detections.append(detection)
114
+
115
+ detections.append(image_detections)
116
+
117
+ # Create a visualization only for detections with confidence > 0.1
118
+ if save_visualization and results:
119
+ filtered_results = results.copy()
120
+
121
+ # Filtred results with confidence > 0.1
122
+ filtered_results[0].boxes = filtered_results[0].boxes[filtered_results[0].boxes.conf > 0.1]
123
+
124
+ # Plot the detections
125
+ res_plotted = filtered_results[0].plot()
126
+
127
+ output_path = f"results/image_detections/{image_identifier}_detection.jpg"
128
+ cv2.imwrite(output_path, res_plotted)
129
+ print(f"Imagem salva com as detecções em: results/image_detections/{image_identifier}")
130
+
131
+ # Save to JSON file
132
+ if save_path:
133
+ with open(save_path, 'w') as f:
134
+ json.dump(detections, f, indent=4)
135
+
136
+ print(f"Detections saved to: {save_path}")
137
+
138
+ if return_prediction_results:
139
+ return detections, results
140
+
141
+ return detections
142
+
143
+
144
+
145
+ def load_detections(input_file):
146
+ """
147
+ Load detections from a JSON file
148
+
149
+ Args:
150
+ input_file (str): Path to the JSON detection file
151
+
152
+ Returns:
153
+ list: Loaded detections
154
+ """
155
+ with open(input_file, 'r') as f:
156
+ detections = json.load(f)
157
+ return detections
158
+
utils/train_hiper.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ultralytics import YOLO
2
+ model = YOLO('yolov8s.pt')
3
+
4
+ results = model.tune(data='dataset.yaml', epochs=25,
5
+ iterations=10,
6
+ device=0,
7
+ optimizer='adamw',
8
+ plots=True,
9
+ save=True,
10
+ conf=0.25,
11
+ iou=0.45)
utils/train_model.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yaml
2
+ from ultralytics import YOLO
3
+
4
+ def main():
5
+
6
+ file_path = "runs/detect/tune4/best_hyperparameters.yaml"
7
+
8
+
9
+ with open(file_path, 'r') as file:
10
+ hyperparameters = yaml.safe_load(file)
11
+
12
+ print(hyperparameters)
13
+
14
+ model = YOLO('yolov8s.pt')
15
+ model.train(
16
+ data='dataset.yaml',
17
+ **hyperparameters,
18
+ imgsz=768,
19
+ batch=16,
20
+ device=0,
21
+ optimizer='adamw',
22
+ plots=True,
23
+ save=True,
24
+ conf=0.25,
25
+ iou=0.45,
26
+ epochs=200,
27
+ patience=20
28
+ )
29
+
30
+ if __name__ == "__main__":
31
+ main()