Commit ·
a2b5362
1
Parent(s): 58ae82a
feat(yolo): add object detection app with YOLOv3-tiny integration
Browse files- .env.example +6 -0
- Dockerfile +10 -4
- models/coco.names +80 -0
- models/yolov3-tiny.cfg +182 -0
- models/yolov3-tiny.weights +3 -0
- requirements.txt +13 -3
- src/streamlit_app.py +0 -40
- streamlit_app.py +140 -0
- yolo_inference.py +156 -0
.env.example
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
YOLO_CFG_PATH=darknet/cfg/yolov3-tiny-obj.cfg
|
| 2 |
+
YOLO_WEIGHTS_PATH=darknet/backup/yolov3-tiny-obj_final.weights
|
| 3 |
+
YOLO_NAMES_PATH=darknet/cfg/obj.names
|
| 4 |
+
YOLO_CONF_THRESHOLD=0.5
|
| 5 |
+
YOLO_NMS_THRESHOLD=0.4
|
| 6 |
+
YOLO_USE_GPU=false
|
Dockerfile
CHANGED
|
@@ -1,20 +1,26 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
|
|
|
| 5 |
RUN apt-get update && apt-get install -y \
|
| 6 |
build-essential \
|
| 7 |
curl \
|
| 8 |
git \
|
|
|
|
|
|
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
|
|
|
|
| 11 |
COPY requirements.txt ./
|
| 12 |
-
|
| 13 |
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
EXPOSE 8501
|
| 17 |
|
| 18 |
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# Instalar dependências de sistema
|
| 6 |
RUN apt-get update && apt-get install -y \
|
| 7 |
build-essential \
|
| 8 |
curl \
|
| 9 |
git \
|
| 10 |
+
libgl1 \
|
| 11 |
+
libglib2.0-0 \
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
+
# Copiar apenas requirements primeiro para aproveitar o cache do Docker
|
| 15 |
COPY requirements.txt ./
|
| 16 |
+
RUN pip3 install --no-cache-dir -r requirements.txt
|
| 17 |
|
| 18 |
+
# Copiar o restante dos arquivos (isso invalidará o cache se algum arquivo mudar)
|
| 19 |
+
COPY . .
|
| 20 |
|
| 21 |
EXPOSE 8501
|
| 22 |
|
| 23 |
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
|
| 24 |
|
| 25 |
+
# Flags para evitar erro 403 e garantir funcionamento em proxies
|
| 26 |
+
ENTRYPOINT ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.enableCORS=false", "--server.enableXsrfProtection=false"]
|
models/coco.names
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
person
|
| 2 |
+
bicycle
|
| 3 |
+
car
|
| 4 |
+
motorbike
|
| 5 |
+
aeroplane
|
| 6 |
+
bus
|
| 7 |
+
train
|
| 8 |
+
truck
|
| 9 |
+
boat
|
| 10 |
+
traffic light
|
| 11 |
+
fire hydrant
|
| 12 |
+
stop sign
|
| 13 |
+
parking meter
|
| 14 |
+
bench
|
| 15 |
+
bird
|
| 16 |
+
cat
|
| 17 |
+
dog
|
| 18 |
+
horse
|
| 19 |
+
sheep
|
| 20 |
+
cow
|
| 21 |
+
elephant
|
| 22 |
+
bear
|
| 23 |
+
zebra
|
| 24 |
+
giraffe
|
| 25 |
+
backpack
|
| 26 |
+
umbrella
|
| 27 |
+
handbag
|
| 28 |
+
tie
|
| 29 |
+
suitcase
|
| 30 |
+
frisbee
|
| 31 |
+
skis
|
| 32 |
+
snowboard
|
| 33 |
+
sports ball
|
| 34 |
+
kite
|
| 35 |
+
baseball bat
|
| 36 |
+
baseball glove
|
| 37 |
+
skateboard
|
| 38 |
+
surfboard
|
| 39 |
+
tennis racket
|
| 40 |
+
bottle
|
| 41 |
+
wine glass
|
| 42 |
+
cup
|
| 43 |
+
fork
|
| 44 |
+
knife
|
| 45 |
+
spoon
|
| 46 |
+
bowl
|
| 47 |
+
banana
|
| 48 |
+
apple
|
| 49 |
+
sandwich
|
| 50 |
+
orange
|
| 51 |
+
broccoli
|
| 52 |
+
carrot
|
| 53 |
+
hot dog
|
| 54 |
+
pizza
|
| 55 |
+
donut
|
| 56 |
+
cake
|
| 57 |
+
chair
|
| 58 |
+
sofa
|
| 59 |
+
pottedplant
|
| 60 |
+
bed
|
| 61 |
+
diningtable
|
| 62 |
+
toilet
|
| 63 |
+
tvmonitor
|
| 64 |
+
laptop
|
| 65 |
+
mouse
|
| 66 |
+
remote
|
| 67 |
+
keyboard
|
| 68 |
+
cell phone
|
| 69 |
+
microwave
|
| 70 |
+
oven
|
| 71 |
+
toaster
|
| 72 |
+
sink
|
| 73 |
+
refrigerator
|
| 74 |
+
book
|
| 75 |
+
clock
|
| 76 |
+
vase
|
| 77 |
+
scissors
|
| 78 |
+
teddy bear
|
| 79 |
+
hair drier
|
| 80 |
+
toothbrush
|
models/yolov3-tiny.cfg
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[net]
|
| 2 |
+
# Testing
|
| 3 |
+
batch=1
|
| 4 |
+
subdivisions=1
|
| 5 |
+
# Training
|
| 6 |
+
# batch=64
|
| 7 |
+
# subdivisions=2
|
| 8 |
+
width=416
|
| 9 |
+
height=416
|
| 10 |
+
channels=3
|
| 11 |
+
momentum=0.9
|
| 12 |
+
decay=0.0005
|
| 13 |
+
angle=0
|
| 14 |
+
saturation = 1.5
|
| 15 |
+
exposure = 1.5
|
| 16 |
+
hue=.1
|
| 17 |
+
|
| 18 |
+
learning_rate=0.001
|
| 19 |
+
burn_in=1000
|
| 20 |
+
max_batches = 500200
|
| 21 |
+
policy=steps
|
| 22 |
+
steps=400000,450000
|
| 23 |
+
scales=.1,.1
|
| 24 |
+
|
| 25 |
+
[convolutional]
|
| 26 |
+
batch_normalize=1
|
| 27 |
+
filters=16
|
| 28 |
+
size=3
|
| 29 |
+
stride=1
|
| 30 |
+
pad=1
|
| 31 |
+
activation=leaky
|
| 32 |
+
|
| 33 |
+
[maxpool]
|
| 34 |
+
size=2
|
| 35 |
+
stride=2
|
| 36 |
+
|
| 37 |
+
[convolutional]
|
| 38 |
+
batch_normalize=1
|
| 39 |
+
filters=32
|
| 40 |
+
size=3
|
| 41 |
+
stride=1
|
| 42 |
+
pad=1
|
| 43 |
+
activation=leaky
|
| 44 |
+
|
| 45 |
+
[maxpool]
|
| 46 |
+
size=2
|
| 47 |
+
stride=2
|
| 48 |
+
|
| 49 |
+
[convolutional]
|
| 50 |
+
batch_normalize=1
|
| 51 |
+
filters=64
|
| 52 |
+
size=3
|
| 53 |
+
stride=1
|
| 54 |
+
pad=1
|
| 55 |
+
activation=leaky
|
| 56 |
+
|
| 57 |
+
[maxpool]
|
| 58 |
+
size=2
|
| 59 |
+
stride=2
|
| 60 |
+
|
| 61 |
+
[convolutional]
|
| 62 |
+
batch_normalize=1
|
| 63 |
+
filters=128
|
| 64 |
+
size=3
|
| 65 |
+
stride=1
|
| 66 |
+
pad=1
|
| 67 |
+
activation=leaky
|
| 68 |
+
|
| 69 |
+
[maxpool]
|
| 70 |
+
size=2
|
| 71 |
+
stride=2
|
| 72 |
+
|
| 73 |
+
[convolutional]
|
| 74 |
+
batch_normalize=1
|
| 75 |
+
filters=256
|
| 76 |
+
size=3
|
| 77 |
+
stride=1
|
| 78 |
+
pad=1
|
| 79 |
+
activation=leaky
|
| 80 |
+
|
| 81 |
+
[maxpool]
|
| 82 |
+
size=2
|
| 83 |
+
stride=2
|
| 84 |
+
|
| 85 |
+
[convolutional]
|
| 86 |
+
batch_normalize=1
|
| 87 |
+
filters=512
|
| 88 |
+
size=3
|
| 89 |
+
stride=1
|
| 90 |
+
pad=1
|
| 91 |
+
activation=leaky
|
| 92 |
+
|
| 93 |
+
[maxpool]
|
| 94 |
+
size=2
|
| 95 |
+
stride=1
|
| 96 |
+
|
| 97 |
+
[convolutional]
|
| 98 |
+
batch_normalize=1
|
| 99 |
+
filters=1024
|
| 100 |
+
size=3
|
| 101 |
+
stride=1
|
| 102 |
+
pad=1
|
| 103 |
+
activation=leaky
|
| 104 |
+
|
| 105 |
+
###########
|
| 106 |
+
|
| 107 |
+
[convolutional]
|
| 108 |
+
batch_normalize=1
|
| 109 |
+
filters=256
|
| 110 |
+
size=1
|
| 111 |
+
stride=1
|
| 112 |
+
pad=1
|
| 113 |
+
activation=leaky
|
| 114 |
+
|
| 115 |
+
[convolutional]
|
| 116 |
+
batch_normalize=1
|
| 117 |
+
filters=512
|
| 118 |
+
size=3
|
| 119 |
+
stride=1
|
| 120 |
+
pad=1
|
| 121 |
+
activation=leaky
|
| 122 |
+
|
| 123 |
+
[convolutional]
|
| 124 |
+
size=1
|
| 125 |
+
stride=1
|
| 126 |
+
pad=1
|
| 127 |
+
filters=255
|
| 128 |
+
activation=linear
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
[yolo]
|
| 133 |
+
mask = 3,4,5
|
| 134 |
+
anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319
|
| 135 |
+
classes=80
|
| 136 |
+
num=6
|
| 137 |
+
jitter=.3
|
| 138 |
+
ignore_thresh = .7
|
| 139 |
+
truth_thresh = 1
|
| 140 |
+
random=1
|
| 141 |
+
|
| 142 |
+
[route]
|
| 143 |
+
layers = -4
|
| 144 |
+
|
| 145 |
+
[convolutional]
|
| 146 |
+
batch_normalize=1
|
| 147 |
+
filters=128
|
| 148 |
+
size=1
|
| 149 |
+
stride=1
|
| 150 |
+
pad=1
|
| 151 |
+
activation=leaky
|
| 152 |
+
|
| 153 |
+
[upsample]
|
| 154 |
+
stride=2
|
| 155 |
+
|
| 156 |
+
[route]
|
| 157 |
+
layers = -1, 8
|
| 158 |
+
|
| 159 |
+
[convolutional]
|
| 160 |
+
batch_normalize=1
|
| 161 |
+
filters=256
|
| 162 |
+
size=3
|
| 163 |
+
stride=1
|
| 164 |
+
pad=1
|
| 165 |
+
activation=leaky
|
| 166 |
+
|
| 167 |
+
[convolutional]
|
| 168 |
+
size=1
|
| 169 |
+
stride=1
|
| 170 |
+
pad=1
|
| 171 |
+
filters=255
|
| 172 |
+
activation=linear
|
| 173 |
+
|
| 174 |
+
[yolo]
|
| 175 |
+
mask = 0,1,2
|
| 176 |
+
anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319
|
| 177 |
+
classes=80
|
| 178 |
+
num=6
|
| 179 |
+
jitter=.3
|
| 180 |
+
ignore_thresh = .7
|
| 181 |
+
truth_thresh = 1
|
| 182 |
+
random=1
|
models/yolov3-tiny.weights
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dccea06f59b781ec1234ddf8d1e94b9519a97f4245748a7d4db75d5b7080a42c
|
| 3 |
+
size 35434956
|
requirements.txt
CHANGED
|
@@ -1,3 +1,13 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
opencv-python>=4.8.0
|
| 2 |
+
numpy>=1.24.0
|
| 3 |
+
python-dotenv>=1.0.0
|
| 4 |
+
jupyter>=1.0.0
|
| 5 |
+
ipywidgets>=8.0.0
|
| 6 |
+
matplotlib>=3.7.0
|
| 7 |
+
pytest>=7.0.0
|
| 8 |
+
ultralytics>=8.0.0
|
| 9 |
+
PyYAML>=6.0.0
|
| 10 |
+
requests>=2.31.0
|
| 11 |
+
tqdm>=4.66.0
|
| 12 |
+
streamlit>=1.25.0
|
| 13 |
+
pillow>=10.0.0
|
src/streamlit_app.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
import altair as alt
|
| 2 |
-
import numpy as np
|
| 3 |
-
import pandas as pd
|
| 4 |
-
import streamlit as st
|
| 5 |
-
|
| 6 |
-
"""
|
| 7 |
-
# Welcome to Streamlit!
|
| 8 |
-
|
| 9 |
-
Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
|
| 10 |
-
If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
|
| 11 |
-
forums](https://discuss.streamlit.io).
|
| 12 |
-
|
| 13 |
-
In the meantime, below is an example of what you can do with just a few lines of code:
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
|
| 17 |
-
num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
|
| 18 |
-
|
| 19 |
-
indices = np.linspace(0, 1, num_points)
|
| 20 |
-
theta = 2 * np.pi * num_turns * indices
|
| 21 |
-
radius = indices
|
| 22 |
-
|
| 23 |
-
x = radius * np.cos(theta)
|
| 24 |
-
y = radius * np.sin(theta)
|
| 25 |
-
|
| 26 |
-
df = pd.DataFrame({
|
| 27 |
-
"x": x,
|
| 28 |
-
"y": y,
|
| 29 |
-
"idx": indices,
|
| 30 |
-
"rand": np.random.randn(num_points),
|
| 31 |
-
})
|
| 32 |
-
|
| 33 |
-
st.altair_chart(alt.Chart(df, height=700, width=700)
|
| 34 |
-
.mark_point(filled=True)
|
| 35 |
-
.encode(
|
| 36 |
-
x=alt.X("x", axis=None),
|
| 37 |
-
y=alt.Y("y", axis=None),
|
| 38 |
-
color=alt.Color("idx", legend=None, scale=alt.Scale()),
|
| 39 |
-
size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
|
| 40 |
-
))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
streamlit_app.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Importações necessárias para Streamlit, OpenCV e processamento de imagem
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from yolo_inference import build_detector_from_env
|
| 7 |
+
|
| 8 |
+
# Configuração inicial da página do Streamlit (Título e Layout)
|
| 9 |
+
st.set_page_config(page_title="YOLO Detection - Streamlit", layout="wide", page_icon="🚗")
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
"""
|
| 13 |
+
Função principal que gerencia a interface Streamlit.
|
| 14 |
+
Permite alternar entre detecção em imagens estáticas e vídeo em tempo real via webcam.
|
| 15 |
+
"""
|
| 16 |
+
st.title("🚀 YOLO Object Detection")
|
| 17 |
+
st.markdown("---")
|
| 18 |
+
st.markdown("### Interface interativa para detecção de objetos usando YOLOv3-tiny.")
|
| 19 |
+
|
| 20 |
+
# Sidebar: Painel lateral para controle de parâmetros e seleção de modo
|
| 21 |
+
st.sidebar.header("🛠️ Configurações do Modelo")
|
| 22 |
+
|
| 23 |
+
# Sliders para ajuste dinâmico dos limiares de detecção
|
| 24 |
+
conf_threshold = st.sidebar.slider("Confiança Mínima (Threshold)", 0.0, 1.0, 0.5, 0.05,
|
| 25 |
+
help="Nível mínimo de certeza para exibir uma detecção.")
|
| 26 |
+
nms_threshold = st.sidebar.slider("NMS Threshold", 0.0, 1.0, 0.4, 0.05,
|
| 27 |
+
help="Limiar para supressão de não-máximos (remove bboxes sobrepostas).")
|
| 28 |
+
|
| 29 |
+
st.sidebar.markdown("---")
|
| 30 |
+
# Seleção do modo de operação
|
| 31 |
+
mode = st.sidebar.radio("📡 Escolha o Modo de Entrada", ["Imagem", "Câmera (Real-time)"])
|
| 32 |
+
|
| 33 |
+
# Inicializa o detector YOLO
|
| 34 |
+
# A função build_detector_from_env gerencia o download automático dos pesos se necessário.
|
| 35 |
+
try:
|
| 36 |
+
detector = build_detector_from_env(conf_threshold=conf_threshold, nms_threshold=nms_threshold)
|
| 37 |
+
except Exception as e:
|
| 38 |
+
st.error(f"❌ Erro ao inicializar detector: {e}")
|
| 39 |
+
return
|
| 40 |
+
|
| 41 |
+
# Lista de classes do dataset personalizado para monitoramento especial
|
| 42 |
+
CUSTOM_CLASSES = {"car", "truck", "bus", "motorbike", "bicycle", "van", "threewheel"}
|
| 43 |
+
|
| 44 |
+
if mode == "Imagem":
|
| 45 |
+
st.subheader("📁 Upload e Detecção em Imagem")
|
| 46 |
+
uploaded_file = st.file_uploader("Arraste ou selecione uma imagem...", type=["jpg", "jpeg", "png"])
|
| 47 |
+
|
| 48 |
+
if uploaded_file is not None:
|
| 49 |
+
# Converte o arquivo carregado (BytesIO) para uma imagem PIL e depois para array numpy
|
| 50 |
+
image = Image.open(uploaded_file)
|
| 51 |
+
image_np = np.array(image)
|
| 52 |
+
|
| 53 |
+
# Streamlit/PIL trabalham em RGB, mas o detector OpenCV espera BGR
|
| 54 |
+
frame_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
|
| 55 |
+
|
| 56 |
+
# Realiza a detecção de objetos
|
| 57 |
+
with st.spinner('Processando imagem...'):
|
| 58 |
+
detections = detector.detect(frame_bgr)
|
| 59 |
+
|
| 60 |
+
# Filtra e exibe classes encontradas que pertencem ao dataset customizado
|
| 61 |
+
hits = sorted({d['class_name'] for d in detections if d['class_name'] in CUSTOM_CLASSES})
|
| 62 |
+
|
| 63 |
+
# Layout em duas colunas: Imagem original vs Resultado
|
| 64 |
+
col1, col2 = st.columns(2)
|
| 65 |
+
|
| 66 |
+
with col1:
|
| 67 |
+
st.image(image, caption="Imagem Original", use_column_width=True)
|
| 68 |
+
|
| 69 |
+
with col2:
|
| 70 |
+
# Desenha os retângulos e labels no frame BGR
|
| 71 |
+
result_bgr = detector.draw(frame_bgr, detections)
|
| 72 |
+
# Converte de volta para RGB para exibição correta no Streamlit
|
| 73 |
+
result_rgb = cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB)
|
| 74 |
+
st.image(result_rgb, caption="Detecções Encontradas", use_column_width=True)
|
| 75 |
+
|
| 76 |
+
# Exibe alertas baseados nas classes detectadas
|
| 77 |
+
if hits:
|
| 78 |
+
st.success(f"✅ Objetos do dataset detectados: **{', '.join(hits)}**")
|
| 79 |
+
else:
|
| 80 |
+
st.info("ℹ️ Nenhuma classe do dataset específico foi detectada nesta imagem.")
|
| 81 |
+
|
| 82 |
+
elif mode == "Câmera (Real-time)":
|
| 83 |
+
st.subheader("🎥 Detecção via Webcam em Tempo Real")
|
| 84 |
+
st.warning("⚠️ Certifique-se de que sua webcam não está sendo usada por outro aplicativo.")
|
| 85 |
+
|
| 86 |
+
# Checkbox para ligar/desligar o loop da câmera
|
| 87 |
+
run = st.checkbox("Ativar Câmera")
|
| 88 |
+
|
| 89 |
+
# Placeholders para atualização dinâmica do frame e status sem recarregar a página toda
|
| 90 |
+
frame_placeholder = st.empty()
|
| 91 |
+
status_placeholder = st.empty()
|
| 92 |
+
|
| 93 |
+
if run:
|
| 94 |
+
# Inicializa a captura de vídeo (ID 0 costuma ser a webcam padrão)
|
| 95 |
+
cap = cv2.VideoCapture(0)
|
| 96 |
+
if not cap.isOpened():
|
| 97 |
+
st.error("Não foi possível acessar a câmera. Verifique as permissões.")
|
| 98 |
+
return
|
| 99 |
+
|
| 100 |
+
while run:
|
| 101 |
+
ret, frame = cap.read()
|
| 102 |
+
if not ret:
|
| 103 |
+
st.error("Falha ao capturar vídeo.")
|
| 104 |
+
break
|
| 105 |
+
|
| 106 |
+
# Processa o frame atual
|
| 107 |
+
detections = detector.detect(frame)
|
| 108 |
+
|
| 109 |
+
# Renderiza as detecções no frame
|
| 110 |
+
frame_out = detector.draw(frame, detections)
|
| 111 |
+
|
| 112 |
+
# Adiciona overlay de instrução no frame (estilo solicitado anteriormente)
|
| 113 |
+
cv2.putText(frame_out, "Desmarque 'Ativar Camera' para sair", (20, 40),
|
| 114 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
| 115 |
+
|
| 116 |
+
# Identifica classes do dataset para exibição de status dinâmico
|
| 117 |
+
hits = sorted({d['class_name'] for d in detections if d['class_name'] in CUSTOM_CLASSES})
|
| 118 |
+
if hits:
|
| 119 |
+
status_placeholder.success(f"Detectado: **{', '.join(hits)}**")
|
| 120 |
+
else:
|
| 121 |
+
status_placeholder.empty()
|
| 122 |
+
|
| 123 |
+
# Conversão BGR -> RGB para o Streamlit renderizar corretamente
|
| 124 |
+
frame_rgb = cv2.cvtColor(frame_out, cv2.COLOR_BGR2RGB)
|
| 125 |
+
frame_placeholder.image(frame_rgb, channels="RGB", use_column_width=True)
|
| 126 |
+
|
| 127 |
+
# Pequeno delay opcional para sincronia (cv2.waitKey não é necessário aqui para exibição,
|
| 128 |
+
# mas ajuda a liberar CPU)
|
| 129 |
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
| 130 |
+
break
|
| 131 |
+
|
| 132 |
+
# Libera recursos ao encerrar
|
| 133 |
+
cap.release()
|
| 134 |
+
st.write("🏁 Captura encerrada.")
|
| 135 |
+
else:
|
| 136 |
+
st.write("💤 Câmera em espera.")
|
| 137 |
+
|
| 138 |
+
if __name__ == "__main__":
|
| 139 |
+
main()
|
| 140 |
+
|
yolo_inference.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
from typing import List, Tuple, Dict, Optional
|
| 5 |
+
import urllib.request
|
| 6 |
+
import pathlib
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
except Exception:
|
| 11 |
+
load_dotenv = None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _load_classes(names_path: str) -> List[str]:
|
| 15 |
+
# Lê arquivo .names e retorna lista de classes
|
| 16 |
+
if not os.path.isfile(names_path):
|
| 17 |
+
raise FileNotFoundError(f"Arquivo de classes não encontrado: {names_path}")
|
| 18 |
+
with open(names_path, "r", encoding="utf-8") as f:
|
| 19 |
+
classes = [line.strip() for line in f if line.strip()]
|
| 20 |
+
if not classes:
|
| 21 |
+
raise ValueError("Lista de classes vazia")
|
| 22 |
+
return classes
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _get_output_layer_names(net: cv2.dnn_Net) -> List[str]:
|
| 26 |
+
# Extrai nomes das camadas de saída (YOLO) para forward
|
| 27 |
+
layer_names = net.getLayerNames()
|
| 28 |
+
out_layers = net.getUnconnectedOutLayers()
|
| 29 |
+
return [layer_names[i - 1] for i in out_layers.flatten()]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class YoloDetector:
|
| 33 |
+
# Wrapper para inferência com OpenCV DNN + Darknet cfg/weights
|
| 34 |
+
def __init__(
|
| 35 |
+
self,
|
| 36 |
+
cfg_path: str,
|
| 37 |
+
weights_path: str,
|
| 38 |
+
names_path: str,
|
| 39 |
+
conf_threshold: float = 0.5,
|
| 40 |
+
nms_threshold: float = 0.4,
|
| 41 |
+
use_gpu: bool = False,
|
| 42 |
+
):
|
| 43 |
+
if not os.path.isfile(cfg_path):
|
| 44 |
+
raise FileNotFoundError(f"CFG não encontrado: {cfg_path}")
|
| 45 |
+
if not os.path.isfile(weights_path):
|
| 46 |
+
raise FileNotFoundError(f"Pesos não encontrados: {weights_path}")
|
| 47 |
+
self.classes = _load_classes(names_path)
|
| 48 |
+
self.net = cv2.dnn.readNetFromDarknet(cfg_path, weights_path)
|
| 49 |
+
if use_gpu:
|
| 50 |
+
self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
|
| 51 |
+
self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
|
| 52 |
+
else:
|
| 53 |
+
self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
|
| 54 |
+
self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
|
| 55 |
+
self.conf_threshold = conf_threshold
|
| 56 |
+
self.nms_threshold = nms_threshold
|
| 57 |
+
self.output_layer_names = _get_output_layer_names(self.net)
|
| 58 |
+
|
| 59 |
+
def detect(
|
| 60 |
+
self,
|
| 61 |
+
image_bgr: np.ndarray,
|
| 62 |
+
input_size: Tuple[int, int] = (416, 416),
|
| 63 |
+
) -> List[Dict]:
|
| 64 |
+
# Executa inferência e retorna lista de detecções com bbox, classe e confiança
|
| 65 |
+
if image_bgr is None or image_bgr.size == 0:
|
| 66 |
+
raise ValueError("Imagem inválida para detecção")
|
| 67 |
+
h, w = image_bgr.shape[:2]
|
| 68 |
+
blob = cv2.dnn.blobFromImage(image_bgr, 1 / 255.0, input_size, swapRB=True, crop=False)
|
| 69 |
+
self.net.setInput(blob)
|
| 70 |
+
layer_outputs = self.net.forward(self.output_layer_names)
|
| 71 |
+
|
| 72 |
+
boxes: List[List[int]] = []
|
| 73 |
+
confidences: List[float] = []
|
| 74 |
+
class_ids: List[int] = []
|
| 75 |
+
|
| 76 |
+
for output in layer_outputs:
|
| 77 |
+
for detection in output:
|
| 78 |
+
scores = detection[5:]
|
| 79 |
+
class_id = int(np.argmax(scores))
|
| 80 |
+
confidence = float(scores[class_id])
|
| 81 |
+
if confidence >= self.conf_threshold:
|
| 82 |
+
center_x = int(detection[0] * w)
|
| 83 |
+
center_y = int(detection[1] * h)
|
| 84 |
+
width = int(detection[2] * w)
|
| 85 |
+
height = int(detection[3] * h)
|
| 86 |
+
x = int(center_x - width / 2)
|
| 87 |
+
y = int(center_y - height / 2)
|
| 88 |
+
boxes.append([x, y, width, height])
|
| 89 |
+
confidences.append(confidence)
|
| 90 |
+
class_ids.append(class_id)
|
| 91 |
+
|
| 92 |
+
indices = cv2.dnn.NMSBoxes(boxes, confidences, self.conf_threshold, self.nms_threshold)
|
| 93 |
+
|
| 94 |
+
detections: List[Dict] = []
|
| 95 |
+
if len(indices) > 0:
|
| 96 |
+
for i in indices.flatten():
|
| 97 |
+
x, y, w_box, h_box = boxes[i]
|
| 98 |
+
detections.append(
|
| 99 |
+
{
|
| 100 |
+
"class_id": class_ids[i],
|
| 101 |
+
"class_name": self.classes[class_ids[i]] if 0 <= class_ids[i] < len(self.classes) else str(class_ids[i]),
|
| 102 |
+
"confidence": confidences[i],
|
| 103 |
+
"box": (max(0, x), max(0, y), max(0, w_box), max(0, h_box)),
|
| 104 |
+
}
|
| 105 |
+
)
|
| 106 |
+
return detections
|
| 107 |
+
|
| 108 |
+
def draw(self, image_bgr: np.ndarray, detections: List[Dict]) -> np.ndarray:
|
| 109 |
+
# Desenha retângulos e labels no frame
|
| 110 |
+
out = image_bgr.copy()
|
| 111 |
+
for det in detections:
|
| 112 |
+
x, y, w, h = det["box"]
|
| 113 |
+
label = f"{det['class_name']} {det['confidence']:.2f}"
|
| 114 |
+
color = (0, 255, 0)
|
| 115 |
+
cv2.rectangle(out, (x, y), (x + w, y + h), color, 2)
|
| 116 |
+
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
|
| 117 |
+
cv2.rectangle(out, (x, y - th - 6), (x + tw + 4, y), color, -1)
|
| 118 |
+
cv2.putText(out, label, (x + 2, y - 4), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
|
| 119 |
+
return out
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def build_detector_from_env(
|
| 123 |
+
conf_threshold: Optional[float] = None,
|
| 124 |
+
nms_threshold: Optional[float] = None,
|
| 125 |
+
use_gpu: Optional[bool] = None,
|
| 126 |
+
) -> YoloDetector:
|
| 127 |
+
# Inicializa via .env; se faltarem caminhos/arquivos, baixa YOLOv3-tiny automaticamente (models/)
|
| 128 |
+
if load_dotenv is not None:
|
| 129 |
+
load_dotenv()
|
| 130 |
+
cfg_path = os.getenv("YOLO_CFG_PATH", "").strip()
|
| 131 |
+
weights_path = os.getenv("YOLO_WEIGHTS_PATH", "").strip()
|
| 132 |
+
names_path = os.getenv("YOLO_NAMES_PATH", "").strip()
|
| 133 |
+
# Se variáveis não existirem OU arquivos não existirem, usar fallback auto-download
|
| 134 |
+
if (not cfg_path or not weights_path or not names_path
|
| 135 |
+
or not os.path.isfile(cfg_path)
|
| 136 |
+
or not os.path.isfile(weights_path)
|
| 137 |
+
or not os.path.isfile(names_path)):
|
| 138 |
+
models_dir = pathlib.Path("models")
|
| 139 |
+
models_dir.mkdir(exist_ok=True)
|
| 140 |
+
cfg_path = str(models_dir / "yolov3-tiny.cfg")
|
| 141 |
+
weights_path = str(models_dir / "yolov3-tiny.weights")
|
| 142 |
+
names_path = str(models_dir / "coco.names")
|
| 143 |
+
if not os.path.isfile(cfg_path):
|
| 144 |
+
url_cfg = "https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3-tiny.cfg"
|
| 145 |
+
urllib.request.urlretrieve(url_cfg, cfg_path)
|
| 146 |
+
if not os.path.isfile(weights_path):
|
| 147 |
+
url_weights = "https://pjreddie.com/media/files/yolov3-tiny.weights"
|
| 148 |
+
urllib.request.urlretrieve(url_weights, weights_path)
|
| 149 |
+
if not os.path.isfile(names_path):
|
| 150 |
+
url_names = "https://raw.githubusercontent.com/pjreddie/darknet/master/data/coco.names"
|
| 151 |
+
urllib.request.urlretrieve(url_names, names_path)
|
| 152 |
+
ct = float(os.getenv("YOLO_CONF_THRESHOLD", conf_threshold if conf_threshold is not None else 0.5))
|
| 153 |
+
nt = float(os.getenv("YOLO_NMS_THRESHOLD", nms_threshold if nms_threshold is not None else 0.4))
|
| 154 |
+
gpu_flag = os.getenv("YOLO_USE_GPU", "false").lower() in {"1", "true", "yes"} if use_gpu is None else use_gpu
|
| 155 |
+
return YoloDetector(cfg_path=cfg_path, weights_path=weights_path, names_path=names_path, conf_threshold=ct, nms_threshold=nt, use_gpu=gpu_flag)
|
| 156 |
+
|