Spaces:
Sleeping
Sleeping
First commit
Browse files- .gitattributes +12 -0
- examples/watch_test1.jpg +3 -0
- examples/watch_test10.jpg +3 -0
- examples/watch_test100.jpg +0 -0
- examples/watch_test101.jpg +0 -0
- examples/watch_test102.jpg +0 -0
- examples/watch_test103.jpg +0 -0
- examples/watch_test104.jpg +0 -0
- examples/watch_test105.jpg +0 -0
- examples/watch_test106.jpg +0 -0
- examples/watch_test107.jpg +3 -0
- examples/watch_test108.jpg +0 -0
- examples/watch_test109.jpg +3 -0
- examples/watch_test11.jpg +0 -0
- examples/watch_test110.jpg +3 -0
- examples/watch_test111.jpg +3 -0
- examples/watch_test112.jpg +3 -0
- examples/watch_test113.jpg +3 -0
- examples/watch_test114.jpg +3 -0
- examples/watch_test115.jpg +3 -0
- examples/watch_test116.jpg +3 -0
- gradio_app.py +211 -0
- img/1.png +0 -0
- img/2.png +3 -0
- img/annotations_utils/add_rotated_gt.ipynb +349 -0
- img/annotations_utils/desfocar.py +68 -0
- img/annotations_utils/name_changer.py +22 -0
- img/annotations_utils/remove_center.py +25 -0
- img/annotations_utils/resized_annotations.py +115 -0
- img/annotations_utils/rotate_img.py +56 -0
- img/annotations_utils/rotate_img_ann.py +148 -0
- img/annotations_utils/train_val_split.py +47 -0
- img/annotations_utils/xml_to_txt.py +59 -0
- img/icon.png +0 -0
- requirements.txt +7 -0
- tune4_best.pt +3 -0
- utils/__pycache__/clock_utils.cpython-311.pyc +0 -0
- utils/__pycache__/clock_utils.cpython-312.pyc +0 -0
- utils/__pycache__/clock_utils.cpython-38.pyc +0 -0
- utils/__pycache__/detections_utils.cpython-311.pyc +0 -0
- utils/__pycache__/detections_utils.cpython-312.pyc +0 -0
- utils/__pycache__/detections_utils.cpython-38.pyc +0 -0
- utils/clock_utils.py +242 -0
- utils/detections_utils.py +158 -0
- utils/train_hiper.py +11 -0
- 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
|
examples/watch_test10.jpg
ADDED
|
Git LFS Details
|
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
|
examples/watch_test108.jpg
ADDED
|
examples/watch_test109.jpg
ADDED
|
Git LFS Details
|
examples/watch_test11.jpg
ADDED
|
examples/watch_test110.jpg
ADDED
|
Git LFS Details
|
examples/watch_test111.jpg
ADDED
|
Git LFS Details
|
examples/watch_test112.jpg
ADDED
|
Git LFS Details
|
examples/watch_test113.jpg
ADDED
|
Git LFS Details
|
examples/watch_test114.jpg
ADDED
|
Git LFS Details
|
examples/watch_test115.jpg
ADDED
|
Git LFS Details
|
examples/watch_test116.jpg
ADDED
|
Git LFS Details
|
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
|
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()
|