Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
Dockerfile
README.md
# Arquivos e pastas que não precisam ir pro container

# Git
.git
.gitignore

# Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
__pycache__
.pytest_cache
*.pdb
.pytest_cache/
*.pytest_cache

# Virtualenvs
env/
venv/
.venv/

# Build / distribuições
build/
dist/
*.egg-info/
*.egg

# Logs e DB locais
*.log
*.sqlite3
*.db

# Node
node_modules/

# Outros
.DS_Store
*.swp
23 changes: 17 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
FROM python:3.11-slim

ENV PYTHONUNBUFFERED True
ENV APP_HOME /app
ENV PORT 5000
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
APP_HOME=/app \
PORT=8080

WORKDIR $APP_HOME
COPY . ./

RUN pip install --no-cache-dir -r requirements.txt
COPY requirements.txt .

CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 wsgi:app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& pip install --no-cache-dir -r requirements.txt \
&& apt-get purge -y --auto-remove build-essential \
&& rm -rf /var/lib/apt/lists/*

COPY . .

EXPOSE 8080

# Usando JSON array no CMD (mais seguro)
CMD ["gunicorn", "--bind", ":8080", "--workers", "1", "--threads", "8", "--timeout", "0", "wsgi:app"]
10 changes: 7 additions & 3 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ def calib_validation():
"""
if request.method == "POST":
return session_route.calib_results()
return Response(
"Invalid request method for route", status=405, mimetype="application/json"
)
return Response('Invalid request method for route', status=405, mimetype='application/json')

@app.route('/api/session/batch_predict', methods=['POST'])
def batch_predict():
if request.method == 'POST':
return session_route.batch_predict()
return Response('Invalid request method for route', status=405, mimetype='application/json')
20 changes: 20 additions & 0 deletions app/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
blinker==1.9.0
click==8.1.8
Flask==3.1.0
flask-cors==5.0.1
itsdangerous==2.2.0
Jinja2==3.1.6
joblib==1.4.2
MarkupSafe==3.0.2
numpy==2.2.4
pandas==2.2.3
python-dateutil==2.9.0.post0
pytz==2025.2
scikit-learn==1.6.1
scipy==1.15.2
six==1.17.0
threadpoolctl==3.6.0
tzdata==2025.2
Werkzeug==3.1.3
gunicorn==23.0.0
requests==2.31.0
137 changes: 112 additions & 25 deletions app/routes/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
import csv

from pathlib import Path
import os
import pandas as pd
import traceback
import re
import requests
from flask import Flask, request, Response, send_file

# Local imports from app
Expand Down Expand Up @@ -147,26 +152,15 @@


def calib_results():
"""
Generate calibration results.

This function generates calibration results based on the provided form data.
It saves the calibration points to a CSV file. Then, it uses the gaze_tracker module to predict the calibration results.

Returns:
Response: A JSON response containing the calibration results.

Raises:
IOError: If there is an error while writing to the CSV files.
"""
# Get form data from request
file_name = json.loads(request.form["file_name"])
fixed_points = json.loads(request.form["fixed_circle_iris_points"])
calib_points = json.loads(request.form["calib_circle_iris_points"])
screen_height = json.loads(request.form["screen_height"])
screen_width = json.loads(request.form["screen_width"])
k = json.loads(request.form["k"])
model = json.loads(request.form["model"])
from_ruxailab = json.loads(request.form['from_ruxailab'])
file_name = json.loads(request.form['file_name'])
fixed_points = json.loads(request.form['fixed_circle_iris_points'])
calib_points = json.loads(request.form['calib_circle_iris_points'])
screen_height = json.loads(request.form['screen_height'])
screen_width = json.loads(request.form['screen_width'])
model_X = json.loads(request.form.get('model', '"Linear Regression"'))
model_Y = json.loads(request.form.get('model', '"Linear Regression"'))
k = json.loads(request.form['k'])

# Generate csv dataset of calibration points
os.makedirs(
Expand Down Expand Up @@ -219,14 +213,107 @@ def calib_results():
except IOError:
print("I/O error")

# data = gaze_tracker.train_to_validate_calib(calib_csv_file, predict_csv_file)
# Run prediction
data = gaze_tracker.predict(calib_csv_file, k, model_X, model_Y)

if from_ruxailab:
try:
payload = {
"session_id": file_name,
"model": data,
"screen_height": screen_height,
"screen_width": screen_width,
"k": k
}

RUXAILAB_WEBHOOK_URL = "https://receivecalibration-ffptzpxikq-uc.a.run.app"

# Predict calibration results
data = gaze_tracker.predict(calib_csv_file, k, model_X=model, model_Y=model)
print("file_name:", file_name)

# Return calibration results
return Response(json.dumps(data), status=200, mimetype="application/json")
resp = requests.post(RUXAILAB_WEBHOOK_URL, json=payload)
print("Enviado para RuxaiLab:", resp.status_code, resp.text)
except Exception as e:
print("Erro ao enviar para RuxaiLab:", e)

return Response(json.dumps(data), status=200, mimetype='application/json')

def batch_predict():
try:
data = request.get_json()
iris_data = data['iris_tracking_data']
k = data.get('k', 3)
screen_height = data.get('screen_height')
screen_width = data.get('screen_width')
model_X = data.get('model_X', 'Linear Regression')
model_Y = data.get('model_Y', 'Linear Regression')
calib_id = data.get('calib_id')
if not calib_id:
return Response("Missing 'calib_id' in request", status=400)

base_path = Path().absolute() / 'app/services/calib_validation/csv/data'
calib_csv_path = base_path / f"{calib_id}_fixed_train_data.csv"
predict_csv_path = base_path / 'temp_batch_predict.csv'

print(f"Calib CSV Path: {calib_csv_path}")
print(f"Predict CSV Path: {predict_csv_path}")
print(f"Iris data sample (até 3): {iris_data[:3]}")

# Gera CSV temporário com os dados de íris
with open(predict_csv_path, 'w', newline='') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=[
'left_iris_x', 'left_iris_y', 'right_iris_x', 'right_iris_y'
])
writer.writeheader()
for item in iris_data:
writer.writerow({
'left_iris_x': item['left_iris_x'],
'left_iris_y': item['left_iris_y'],
'right_iris_x': item['right_iris_x'],
'right_iris_y': item['right_iris_y']
})

# Chama a função de predição corretamente
predictions_raw = gaze_tracker.predict_new_data(
calib_csv_path,
predict_csv_path,
model_X,
model_Y,
k
)

# Constrói uma resposta mais visual e direta
result = []
if isinstance(predictions_raw, dict):
# Percorre o dicionário retornado e transforma em lista plana
for true_x, inner_dict in predictions_raw.items():
if true_x == "centroids":
continue
for true_y, info in inner_dict.items():
pred_x_list = info.get("predicted_x", [])
pred_y_list = info.get("predicted_y", [])
precision = info.get("PrecisionSD")
accuracy = info.get("Accuracy")

for i, (px, py) in enumerate(zip(pred_x_list, pred_y_list)):
timestamp = iris_data[i].get("timestamp") if i < len(iris_data) else None
result.append({
"timestamp": timestamp,
"predicted_x": px,
"predicted_y": py,
"precision": precision,
"accuracy": accuracy,
"screen_width": screen_width,
"screen_height": screen_height
})
else:
print("Retorno inesperado da função predict:", type(predictions_raw))

return Response(json.dumps(result), status=200, mimetype='application/json')

except Exception as e:
print("Erro na batch_predict:", e)
traceback.print_exc()
return Response("Erro interno na predição", status=500)

# def session_results():
# session_id = request.args.__getitem__('id')
Expand Down
84 changes: 84 additions & 0 deletions app/services/gaze_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,90 @@ def predict(data, k, model_X, model_Y):
# Return the data
return data

def predict_new_data_simple(calib_csv_path, predict_csv_path, model_X, model_Y, k=3):
"""
Versão simplificada de predict_new_data.
Treina modelos nos dados de calibração e prevê coordenadas nos novos dados.
Retorna o mesmo formato que a função `predict`.
"""
# -------------------- SCALERS --------------------
sc_x = StandardScaler()
sc_y = StandardScaler()

# -------------------- TREINO --------------------
df_train = pd.read_csv(calib_csv_path).drop(["screen_height", "screen_width"], axis=1)

X_train_x = df_train[["left_iris_x", "right_iris_x"]].values
y_train_x = df_train["point_x"].values
X_train_y = df_train[["left_iris_y", "right_iris_y"]].values
y_train_y = df_train["point_y"].values

X_train_x_scaled = sc_x.fit_transform(X_train_x)
X_train_y_scaled = sc_y.fit_transform(X_train_y)

# Modelos
model_fit_x = models[model_X].fit(X_train_x_scaled, y_train_x)
model_fit_y = models[model_Y].fit(X_train_y_scaled, y_train_y)

# -------------------- NOVOS DADOS --------------------
df_predict = pd.read_csv(predict_csv_path)
X_pred_x = sc_x.transform(df_predict[["left_iris_x", "right_iris_x"]].values)
X_pred_y = sc_y.transform(df_predict[["left_iris_y", "right_iris_y"]].values)

y_pred_x = model_fit_x.predict(X_pred_x)
y_pred_y = model_fit_y.predict(X_pred_y)

# Garantir valores não-negativos
y_pred_x = np.clip(y_pred_x, 0, None)
y_pred_y = np.clip(y_pred_y, 0, None)

# -------------------- KMEANS --------------------
data_pred = np.array([y_pred_x, y_pred_y]).T
kmeans_model = KMeans(n_clusters=k, n_init="auto", init="k-means++")
y_kmeans = kmeans_model.fit_predict(data_pred)

# -------------------- FORMATA DADOS --------------------
df_data = pd.DataFrame({
"Predicted X": y_pred_x,
"Predicted Y": y_pred_y,
"True X": df_predict["point_x"] if "point_x" in df_predict else y_pred_x,
"True Y": df_predict["point_y"] if "point_y" in df_predict else y_pred_y
})

# Calcular métricas
precision_x = df_data.groupby(["True X", "True Y"]).apply(func_precision_x)
precision_y = df_data.groupby(["True X", "True Y"]).apply(func_presicion_y)
precision_xy = (precision_x + precision_y) / 2
precision_xy /= np.mean(precision_xy)

accuracy_x = df_data.groupby(["True X", "True Y"]).apply(func_accuracy_x)
accuracy_y = df_data.groupby(["True X", "True Y"]).apply(func_accuracy_y)
accuracy_xy = (accuracy_x + accuracy_y) / 2
accuracy_xy /= np.mean(accuracy_xy)

# Estrutura final
data = {}
for index, row in df_data.iterrows():
outer_key = str(int(row["True X"]))
inner_key = str(int(row["True Y"]))
if outer_key not in data:
data[outer_key] = {}
data[outer_key][inner_key] = {
"predicted_x": df_data[
(df_data["True X"] == row["True X"]) &
(df_data["True Y"] == row["True Y"])
]["Predicted X"].tolist(),
"predicted_y": df_data[
(df_data["True X"] == row["True X"]) &
(df_data["True Y"] == row["True Y"])
]["Predicted Y"].tolist(),
"PrecisionSD": precision_xy[(row["True X"], row["True Y"])],
"Accuracy": accuracy_xy[(row["True X"], row["True Y"])],
}

data["centroids"] = kmeans_model.cluster_centers_.tolist()
return data


def train_to_validate_calib(calib_csv_file, predict_csv_file):
dataset_train_path = calib_csv_file
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ scipy==1.15.2
six==1.17.0
threadpoolctl==3.6.0
tzdata==2025.2
Werkzeug==3.1.3
Werkzeug==3.1.3
gunicorn==23.0.0
requests==2.31.0
3 changes: 1 addition & 2 deletions wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,5 @@
import os
from app.main import app


if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
Loading