From 3e1b5d1ba3c8a672fa24c8917f699d854ceda260 Mon Sep 17 00:00:00 2001 From: nguentrungthat Date: Tue, 11 Mar 2025 09:25:24 +0700 Subject: [PATCH] Update Config label --- .gitignore | 4 +- public/index.html | 37 ++- src/App.js | 454 ++++++++++++++++++++--------- src/pages/components/ImageLabel.js | 29 +- src/pages/components/ModalLabel.js | 72 +++++ src/pages/components/PointsBlur.js | 109 ++++--- src/pages/components/SaveButton.js | 48 +-- src/server/main.py | 2 + src/server/routes/detect.py | 16 +- src/server/routes/label.py | 141 +++++++++ src/server/train.py | 50 +++- src/ultils/index.js | 3 +- src/workers/detect-worker.js | 12 +- 13 files changed, 733 insertions(+), 244 deletions(-) create mode 100644 src/pages/components/ModalLabel.js create mode 100644 src/server/routes/label.py diff --git a/.gitignore b/.gitignore index 6bf66a7..0240c44 100755 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ yarn-error.log* /src/server/model_datasets *.log src/server/venv/* -__pycache__/ \ No newline at end of file +__pycache__/ + +my_database.db \ No newline at end of file diff --git a/public/index.html b/public/index.html index aa069f2..16492b0 100755 --- a/public/index.html +++ b/public/index.html @@ -1,21 +1,18 @@ - - - - - - - - - - - React App - - - -
- - + diff --git a/src/App.js b/src/App.js index 091667d..b370670 100755 --- a/src/App.js +++ b/src/App.js @@ -1,24 +1,39 @@ -import '@mantine/core/styles.css'; +import "@mantine/core/styles.css"; -import './App.css'; -import { Dropzone } from './pages/components/Dropzone'; +import "./App.css"; +import { Dropzone } from "./pages/components/Dropzone"; -import { ActionIcon, AppShell, Box, Burger, Button, Group, LoadingOverlay, Progress, ScrollArea, Text, Tooltip } from '@mantine/core'; -import { useDisclosure, useHotkeys } from '@mantine/hooks'; -import { notifications } from '@mantine/notifications'; -import { IconChevronLeft, IconChevronRight, IconImageInPicture, IconRefreshDot, IconTrash } from '@tabler/icons-react'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { ImageDetect } from './pages/components/ImageDetect'; -import ImageLabel from './pages/components/ImageLabel'; -import { useImagesDetected } from './stores/use-images-detected'; -import { SaveButton } from './pages/components/SaveButton'; -import { generateClientID } from './ultils'; +import { ActionIcon, AppShell, Box, Burger, Button, Group, LoadingOverlay, Modal, Progress, ScrollArea, Tabs, Text, Tooltip } from "@mantine/core"; +import { useDisclosure, useHotkeys } from "@mantine/hooks"; +import { notifications } from "@mantine/notifications"; +import { + IconChevronLeft, + IconChevronRight, + IconEdit, + IconImageInPicture, + IconPlus, + IconRefreshDot, + IconReportAnalytics, + IconTrash, +} from "@tabler/icons-react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { ImageDetect } from "./pages/components/ImageDetect"; +import ImageLabel from "./pages/components/ImageLabel"; +import { useImagesDetected } from "./stores/use-images-detected"; +import { SaveButton } from "./pages/components/SaveButton"; +import { generateClientID } from "./ultils"; +import ModalLabel from "./pages/components/ModalLabel"; function App() { const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(); const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); const [clickData, setClickData] = useState(null); + const [listLabel, setListLabel] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [opened, { open, close }] = useDisclosure(false); + const [dataLabel, setDataLabel] = useState({}); + const [openConfirm, setOpenConfirm] = useState(false); const { images, clearImagesDetected, appendImageDetect, setImageDetected } = useImagesDetected(); @@ -27,13 +42,28 @@ function App() { const imageLabelRef = useRef(); const dropzoneRef = useRef(); + const fetchLabels = async () => { + try { + const response = await fetch(`${process.env.REACT_APP_API_URL}/labels`); + // const response = await fetch(`http://127.0.0.1:5000/labels`); + const result = await response.json(); + setListLabel(result); + } catch (error) { + console.log(error); + } + }; + + useEffect(() => { + fetchLabels(); + }, []); + const handleClear = () => { // const result = window.confirm('Are you want to clear ?'); - const ID_NOTI = 'handle-clear'; + const ID_NOTI = "handle-clear"; notifications.show({ id: ID_NOTI, - title: 'Are you want to clear ?', + title: "Are you want to clear ?", message: ( - { - handleNext(); - }} - currentData={clickData} - imageLabelRef={imageLabelRef} - /> - - - - - - - - - - - - - {images.length > 0 && - images.map((item, index) => { - return ( - { - console.log({ data }); - setClickData(data); - }} - key={`${item?.name || item?.image_name}_${index}`} - file={item} - onDelete={(data) => { - handleDeleteImage(data, index); - }} - /> - ); - })} + + + + + + + + + + + Detect + + + Config label + + - - {images.length <= 0 && ( - - No images to process + + + + { + handleNext(); + }} + currentData={clickData} + imageLabelRef={imageLabelRef} + /> + + + + + + + + + - )} - - {progress.total > 0 && ( - - - {progress.processFiles} / {progress.total} - - + + + {images.length > 0 && + images.map((item, index) => { + return ( + { + console.log({ data }); + setClickData(data); + }} + key={`${item?.name || item?.image_name}_${index}`} + file={item} + onDelete={(data) => { + handleDeleteImage(data, index); + }} + /> + ); + })} + + + {images.length <= 0 && ( + + No images to process + + )} + + + {progress.total > 0 && ( + + + {progress.processFiles} / {progress.total} + + + + )} - )} - - - - {clickData && } + + + {clickData && } - - + + + + + + + +
+ + {listLabel.length > 0 && + listLabel?.map((item, index) => { + return ( + + {item?.name} +
+ +
+
+ ); + })} +
+
+
+
+
+ + + { + close(); + setDataLabel({}); + }} + data={dataLabel} + onSave={() => { + close(); + setDataLabel({}); + setIsLoading(true); + handleSave(dataLabel); + setTimeout(() => { + setIsLoading(false); + }, 2000); + }} + setData={setDataLabel} + /> + + setOpenConfirm(false)} title={"Confirm"}> +
+
Do you want to run training?
+
Maybe you need to save some data to train!
+
+
+ + +
+
); diff --git a/src/pages/components/ImageLabel.js b/src/pages/components/ImageLabel.js index b71d3fb..44bb592 100755 --- a/src/pages/components/ImageLabel.js +++ b/src/pages/components/ImageLabel.js @@ -1,7 +1,7 @@ -import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from 'react'; -import { Box, LoadingOverlay } from '@mantine/core'; -import PointsBlur from './PointsBlur'; -import { generateImageUrl } from '../../ultils'; +import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from "react"; +import { Box, LoadingOverlay } from "@mantine/core"; +import PointsBlur from "./PointsBlur"; +import { generateImageUrl } from "../../ultils"; const ImageLabel = forwardRef(({ data }, ref) => { const [imageSrc, setImageSrc] = useState(null); @@ -10,7 +10,7 @@ const ImageLabel = forwardRef(({ data }, ref) => { const [reloading, setReloading] = useState(false); const [listLabel, setListLabel] = useState([]); const [infoImage, setInfoImage] = useState({}); - const [imageName, setImageName] = useState(''); + const [imageName, setImageName] = useState(""); // Expose blurredRegions cho component cha useImperativeHandle(ref, () => ({ @@ -33,18 +33,23 @@ const ImageLabel = forwardRef(({ data }, ref) => { alert(data.error); } else { if (data?.points) { - blurredRegions.current = data?.points?.map((pre) => [ - { x: pre.x1, y: pre.y1, label: pre.label }, - { x: pre.x2, y: pre.y1, label: pre.label }, - { x: pre.x2, y: pre.y2, label: pre.label }, - { x: pre.x1, y: pre.y2, label: pre.label }, - ]); + blurredRegions.current = data?.points?.map((pre) => { + const arrLabel = pre?.label?.split("."); + const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0]; + const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null; + return [ + { x: pre.x1, y: pre.y1, label: label, subLabel: subLabel }, + { x: pre.x2, y: pre.y1, label: label, subLabel: subLabel }, + { x: pre.x2, y: pre.y2, label: label, subLabel: subLabel }, + { x: pre.x1, y: pre.y2, label: label, subLabel: subLabel }, + ]; + }); } setListLabel( Object.entries(data?.labels).map(([id, name]) => ({ id: Number(id), name, - })), + })) ); setImageSrc(generateImageUrl(data?.image_path)); setImageName(data?.image_name); diff --git a/src/pages/components/ModalLabel.js b/src/pages/components/ModalLabel.js new file mode 100644 index 0000000..6f406e4 --- /dev/null +++ b/src/pages/components/ModalLabel.js @@ -0,0 +1,72 @@ +import { Modal, Button, TextInput } from "@mantine/core"; +import { useEffect, useState } from "react"; + +const ModalLabel = ({ opened, open, close, data, onSave, setData }) => { + const [openConfirm, setOpenConfirm] = useState(false); + const [isError, setIsError] = useState(false); + + useEffect(() => { + if (!opened) { + setIsError(false); + setOpenConfirm(false); + } + }, [opened]); + + return ( + <> + +
+ { + if (event.currentTarget.value) setIsError(false); + setData((pre) => ({ ...pre, name: event.currentTarget.value })); + }} + style={{ width: 300 }} + error={isError ? "Invalid label" : ""} + /> +
+
+ + +
+
+ + setOpenConfirm(false)} title={"Confirm"}> +
+
Do you want to add new label?
+
Label cannot delete after add!
+
+
+ + +
+
+ + ); +}; + +export default ModalLabel; diff --git a/src/pages/components/PointsBlur.js b/src/pages/components/PointsBlur.js index 2831ded..011cfd1 100755 --- a/src/pages/components/PointsBlur.js +++ b/src/pages/components/PointsBlur.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from "react"; const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, reloading, setReloading, setInfoImage, listLabel = [] }) => { const points = useRef([]); @@ -12,7 +12,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, useEffect(() => { const canvas = canvasRef.current; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); const image = new Image(); image.onload = () => { @@ -23,8 +23,12 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, setInfoImage({ width: image.width, height: image.height }); }; - image.crossOrigin = 'anonymous'; + image.crossOrigin = "anonymous"; image.src = imageSrc; + setInfo({}); + setIsOpen(false); + points.current = []; // Clear points after blurring + setReloading((pre) => !pre); }, [imageSrc]); useEffect(() => { @@ -39,7 +43,6 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, const handleCanvasClick = (event) => { if (!isImageLoaded.current) return; - if (isOpen) return; const { x, y } = getMousePos(canvas, event); // Check if a delete button is clicked @@ -49,14 +52,22 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20; if (Math.abs(buttonX - x) < 30 && Math.abs(buttonY - y) < 10) { // blurredRegions.current.splice(i, 1); + if (info?.index === i) { + setInfo({}); + setIsOpen(false); + return; + } setInfo({ index: i, value: blurredRegions.current[i] }); setIsOpen(true); redrawCanvas(i); return; } } - - points.current.push({ x, y, label: listLabel[0]?.name || 'cisco' }); + if (isOpen) return; + const arrLabel = (listLabel[0]?.name || ["logo.cisco"])?.split("."); + const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0]; + const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null; + points.current.push({ x, y, label: label, subLabel: subLabel }); drawRedPoint(canvas, x, y); if (points.current.length === 4) { @@ -67,11 +78,10 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, }; const handleCanvasMouseDown = (event) => { - if (isOpen) return; const { x, y } = getMousePos(canvas, event); - // Check if we clicked on an existing point for (let i = 0; i < blurredRegions.current.length; i++) { + // Check if we clicked on an existing point for (let j = 0; j < blurredRegions.current[i].length; j++) { const point = blurredRegions.current[i][j]; if (Math.abs(point.x - x) < 5 && Math.abs(point.y - y) < 5) { @@ -84,6 +94,25 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, // Check if we clicked inside a region const region = blurredRegions.current[i]; if (isPointInRegion(x, y, region)) { + // Check if a delete button is clicked + for (let i = 0; i < blurredRegions.current.length; i++) { + const region = blurredRegions.current[i]; + const buttonX = (region[0].x + region[1].x) / 2; + const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20; + if (Math.abs(buttonX - x) < 30 && Math.abs(buttonY - y) < 10) { + // blurredRegions.current.splice(i, 1); + if (info?.index === i) { + setInfo({}); + setIsOpen(false); + return; + } + setInfo({ index: i, value: blurredRegions.current[i] }); + setIsOpen(true); + redrawCanvas(i); + return; + } + } + if (isOpen) return; setMovingRegionIndex(i); setOffset({ x: region[0].x - x, y: region[0].y - y }); return; @@ -110,6 +139,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, x: x + offset.x, y: y + offset.y, label: blurredRegions.current[regionIndex][pointIndex]?.label, + subLabel: blurredRegions.current[regionIndex][pointIndex]?.subLabel, }; redrawCanvas(); } else if (movingRegionIndex !== null) { @@ -125,14 +155,14 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, } }; - canvas.addEventListener('mousedown', handleCanvasMouseDown); - canvas.addEventListener('mouseup', handleCanvasMouseUp); - canvas.addEventListener('mousemove', handleCanvasMouseMove); + canvas.addEventListener("mousedown", handleCanvasMouseDown); + canvas.addEventListener("mouseup", handleCanvasMouseUp); + canvas.addEventListener("mousemove", handleCanvasMouseMove); return () => { - canvas.removeEventListener('mousedown', handleCanvasMouseDown); - canvas.removeEventListener('mouseup', handleCanvasMouseUp); - canvas.removeEventListener('mousemove', handleCanvasMouseMove); + canvas.removeEventListener("mousedown", handleCanvasMouseDown); + canvas.removeEventListener("mouseup", handleCanvasMouseUp); + canvas.removeEventListener("mousemove", handleCanvasMouseMove); }; }, [draggingPointIndex, movingRegionIndex, offset, isOpen, info]); @@ -160,17 +190,17 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, }; const drawRedPoint = (canvas, x, y) => { - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); ctx.save(); ctx.beginPath(); ctx.arc(x, y, 5, 0, 2 * Math.PI); - ctx.fillStyle = 'red'; + ctx.fillStyle = "red"; ctx.fill(); ctx.restore(); }; const drawBorder = (canvas, points) => { - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); ctx.save(); ctx.beginPath(); points.forEach((point, index) => { @@ -182,7 +212,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, }); ctx.closePath(); ctx.lineWidth = 2; - ctx.strokeStyle = 'blue'; + ctx.strokeStyle = "blue"; ctx.stroke(); ctx.restore(); @@ -190,23 +220,26 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, }; const drawDeleteButton = (canvas, region, isSelected) => { - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); const buttonX = (region[0].x + region[1].x) / 2; const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20; ctx.save(); - ctx.fillStyle = isSelected ? 'rgb(196, 194, 61)' : 'rgb(2, 101, 182)'; - ctx.fillRect(buttonX - 30, buttonY - 10, 60, 20); - ctx.fillStyle = 'rgb(255, 255, 255)'; - ctx.font = '13px Arial'; - ctx.textAlign = 'center'; - ctx.fillText(region[0]?.label, buttonX, buttonY + 5); + ctx.fillStyle = isSelected ? "rgb(196, 194, 61)" : "rgb(2, 101, 182)"; + ctx.fillRect(buttonX - 30, buttonY - 10, Math.max(region[0]?.label?.length * 6 + 20, region[0]?.subLabel?.length * 5 + 10), 25); + ctx.fillStyle = "rgb(255, 255, 255)"; + ctx.font = "9px Arial"; + ctx.textAlign = "center"; + ctx.fillText(region[0]?.subLabel, buttonX - 10, buttonY); + ctx.font = "11px Arial"; + ctx.textAlign = "center"; + ctx.fillText(region[0]?.label, buttonX, buttonY + 10); ctx.restore(); }; const redrawCanvas = (index) => { const canvas = canvasRef.current; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); const image = new Image(); image.onload = () => { @@ -223,29 +256,39 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, }); }; - image.crossOrigin = 'anonymous'; + image.crossOrigin = "anonymous"; image.src = imageSrc; }; return ( -
+
{isOpen && ( -
+
- Select label + Select label
{listLabel?.map((el, i) => (
@@ -258,7 +301,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, setIsOpen(false); redrawCanvas(); }} - style={{ backgroundColor: '#ff0000', marginBottom: '10px', width: '150px', color: '#fff' }}> + style={{ backgroundColor: "#ff0000", marginBottom: "10px", width: "150px", color: "#fff" }}> Delete
diff --git a/src/pages/components/SaveButton.js b/src/pages/components/SaveButton.js index bf16461..53aa378 100755 --- a/src/pages/components/SaveButton.js +++ b/src/pages/components/SaveButton.js @@ -1,12 +1,12 @@ -import { Button, Tooltip, LoadingOverlay } from '@mantine/core'; -import { notifications } from '@mantine/notifications'; -import { IconImageInPicture } from '@tabler/icons-react'; -import { useState } from 'react'; -import { useImagesDetected } from '../../stores/use-images-detected'; -import { convertToBoundingBox } from '../../ultils'; -import { useHotkeys } from '@mantine/hooks'; +import { Button, Tooltip, LoadingOverlay } from "@mantine/core"; +import { notifications } from "@mantine/notifications"; +import { IconImageInPicture } from "@tabler/icons-react"; +import { useState } from "react"; +import { useImagesDetected } from "../../stores/use-images-detected"; +import { convertToBoundingBox } from "../../ultils"; +import { useHotkeys } from "@mantine/hooks"; -export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => { +export const SaveButton = ({ currentData, imageLabelRef, onSaved, disabled = false }) => { const [loading, setLoading] = useState(false); const { appendImageDetect, images } = useImagesDetected(); @@ -14,7 +14,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => { const handleSave = async ({ blurredRegions, infoImage, imageName, listLabel }) => { try { if (blurredRegions?.length > 0) { - let arrPoints = ''; + let arrPoints = ""; blurredRegions?.forEach((points) => { const img_w = infoImage?.width; const img_h = infoImage?.height; @@ -31,19 +31,21 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => { const height = (y_max - y_min) / img_h; // Get class ID - const class_id = listLabel.find((label) => label?.name === points[0].label)?.id || '0'; + const class_id = listLabel.find((label) => label?.name === points[0].label)?.id || "0"; const yolo_label = `${class_id} ${x_center.toFixed(6)} ${y_center.toFixed(6)} ${width.toFixed(6)} ${height.toFixed(6)}`; - arrPoints += yolo_label + '\n'; + arrPoints += yolo_label + "\n"; }); + const url = `${process.env.REACT_APP_API_URL}/save`; + // const url = `http://127.0.0.1:5000/save`; const formData = new FormData(); - formData.append('list', arrPoints); - formData.append('imageName', imageName); + formData.append("list", arrPoints); + formData.append("imageName", imageName); setLoading(true); const response = await fetch(url, { - method: 'POST', + method: "POST", body: formData, }); @@ -55,7 +57,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => { }); currentData.points = newPoints; - currentData['isSave'] = true; + currentData["isSave"] = true; const newImages = images.map((image) => { if (image.image_name === imageName) { @@ -72,9 +74,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => { appendImageDetect(newImages); notifications.show({ - title: 'Save Success', + title: "Save Success", message: `${imageName} save success`, - color: 'green', + color: "green", }); if (onSaved) { @@ -84,9 +86,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => { } } catch (error) { notifications.show({ - title: 'Save Error', - message: error?.message || 'Internal Server Error', - color: 'red', + title: "Save Error", + message: error?.message || "Internal Server Error", + color: "red", }); } finally { setLoading(false); @@ -101,11 +103,11 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => { handleSave(saveData); }; - useHotkeys([['ctrl+S', handleSubmit]]); + useHotkeys([["ctrl+S", handleSubmit]]); return ( - - diff --git a/src/server/main.py b/src/server/main.py index c46d942..c41dfdb 100755 --- a/src/server/main.py +++ b/src/server/main.py @@ -5,6 +5,7 @@ from routes.save import save_bp from routes.show import show_bp from routes.delete import delete_bp from routes.upload import upload_bp +from routes.label import config_label app = Flask(__name__) CORS(app) @@ -17,6 +18,7 @@ app.register_blueprint(save_bp) app.register_blueprint(show_bp) app.register_blueprint(delete_bp) app.register_blueprint(upload_bp) +app.register_blueprint(config_label) if __name__ == '__main__': app.run(debug=True) diff --git a/src/server/routes/detect.py b/src/server/routes/detect.py index 785b540..57ef367 100755 --- a/src/server/routes/detect.py +++ b/src/server/routes/detect.py @@ -9,6 +9,7 @@ import random import cv2 import json import time +import subprocess # Load mô hình YOLO model = YOLO(get_latest_model_including_today(trained_model_folder=TRAINED_MODEL_FOLDER)) @@ -139,6 +140,19 @@ def reload_model(): model = YOLO(new_model_path) return jsonify({"message": f"Model reloaded from {new_model_path}", "status": "true"}), 200 - +@detect_bp.route('/train', methods=['GET']) +def run_training(): + try: + # Run train.py as a subprocess + process = subprocess.Popen(["python", "train.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + if process.returncode == 0: + return jsonify({"message": "Training started successfully", "output": stdout.decode()}), 200 + else: + return jsonify({"error": "Training failed", "details": stderr.decode()}), 500 + + except Exception as e: + return jsonify({"error": str(e)}), 500 diff --git a/src/server/routes/label.py b/src/server/routes/label.py new file mode 100644 index 0000000..03b5eb4 --- /dev/null +++ b/src/server/routes/label.py @@ -0,0 +1,141 @@ +import datetime +from flask import Blueprint, request, jsonify, send_from_directory +import sqlite3 + +config_label = Blueprint('label', __name__) +# Connect to database (creates a new one if it doesn’t exist) +conn = sqlite3.connect("my_database.db") +# Create a cursor object to execute SQL commands +cursor = conn.cursor() + +# Connect to database +def get_db_connection(): + conn = sqlite3.connect("my_database.db") + conn.row_factory = sqlite3.Row # Enables dictionary-like access to rows + return conn + +# Create table (only call once at startup) +def create_table(): + conn = get_db_connection() + cursor = conn.cursor() + + # Check if the table exists + cursor.execute(""" + SELECT name FROM sqlite_master WHERE type='table' AND name='labels' + """) + table_exists = cursor.fetchone() + + if not table_exists: + # Create table if it doesn't exist + cursor.execute(""" + CREATE TABLE labels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + code INTEGER NOT NULL, + status TEXT DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Insert default values + default_values = [ + ("logo.cisco", 0), + ("barcode.SN", 1) + ] + cursor.executemany("INSERT INTO labels (name, code) VALUES (?, ?)", default_values) + + print("Table created and default values inserted.") + else: + print("Table already exists.") + + conn.commit() + conn.close() + +# Call create_table() when the script runs +create_table() + +# Route to add a new label +@config_label.route('/label/add', methods=['POST']) +def add_label(): + data = request.get_json() # Get JSON data from request + name = data.get("name") + + if not name: + return jsonify({"error": "Name is required"}), 400 + + conn = get_db_connection() + cursor = conn.cursor() + + # Get the last code value + cursor.execute("SELECT MAX(code) FROM labels") + last_code = cursor.fetchone()[0] + new_code = (last_code + 1) if last_code is not None else 1 # Start from 1 if table is empty + + # Insert new label + cursor.execute( + "INSERT INTO labels (name, code) VALUES (?, ?)", + (name, new_code) + ) + + conn.commit() + conn.close() + + return jsonify({"message": "Label added successfully", "name": name, "code": new_code, "success" : True}), 201 + +# Route to update a label +@config_label.route('/label/update', methods=['POST']) +def update_label(): + data = request.get_json() + name = data.get("name") + label_id = data.get("id") + + if not name or not label_id: + return jsonify({"error": "Name and id are required"}), 400 + + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute( + "UPDATE labels SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", + (name, label_id) + ) + + conn.commit() + conn.close() + + return jsonify({"message": "Label updated successfully", "success" : True}), 200 + +@config_label.route('/labels', methods=['GET']) +def get_labels(): + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute("SELECT * FROM labels WHERE status = 'active'") + labels = cursor.fetchall() + + conn.close() + + # Convert rows to list of dictionaries + label_list = [dict(row) for row in labels] + + return jsonify(label_list), 200 + +# Route to delete a label +@config_label.route('/delete/', methods=['GET']) +def delete_label(label_id): + if not label_id is None: + return jsonify({"error": "ID label are required"}), 400 + + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute( + "UPDATE labels SET status = 'delete' WHERE id = ?", + (label_id) + ) + + conn.commit() + conn.close() + + return jsonify({"message": "Label delete successfully"}), 200 \ No newline at end of file diff --git a/src/server/train.py b/src/server/train.py index 5d282c0..21b0a7b 100755 --- a/src/server/train.py +++ b/src/server/train.py @@ -7,13 +7,31 @@ from ultralytics import YOLO import yaml import requests import glob +import sqlite3 # 🗂️ Configure paths DATA_SPLIT_FOLDER = "model_datasets" IMAGE_EXTENSION = ".png" LOG_FILE = "training_logs.log" -class_names = ["cisco", "barcode"] +def get_label_names(): + conn = sqlite3.connect("my_database.db") + conn.row_factory = sqlite3.Row # Enables dictionary-like access to rows + # Create a cursor object to execute SQL commands + cursor = conn.cursor() + + # Fetch all names from the labels table + cursor.execute("SELECT name FROM labels") + labels = cursor.fetchall() + conn.close() + + # Convert fetched rows into a simple list of strings + label_names = [row["name"] for row in labels] + + return label_names + +class_names = get_label_names() + # 📅 Get today's and yesterday's date today = datetime.date.today() yesterday = today - datetime.timedelta(days=1) @@ -37,9 +55,9 @@ elif os.path.exists(image_folder_yesterday) and os.path.exists(label_folder_yest label_folder = label_folder_yesterday date_str = yesterday_str else: - print(image_folder) - print(label_folder) - raise Exception("⚠️ No valid image & label folder found in the last two days!") + # print(image_folder) + # print(label_folder) + raise Exception(" No valid image & label folder found in the last two days!") # 🏗️ Create dataset folders dataset_folder = os.path.join(DATA_SPLIT_FOLDER, date_str) @@ -78,7 +96,7 @@ def log_message(message: str): def get_latest_model(trained_model_folder: str, default_model: str = "train5/weights/best.pt"): """Tìm model gần nhất (trước ngày hôm nay), nếu không có thì dùng model mặc định.""" if not os.path.exists(trained_model_folder): - log_message(f"⚠️ Folder {trained_model_folder} does not exist. Using default model: {default_model}") + log_message(f" Folder {trained_model_folder} does not exist. Using default model: {default_model}") return default_model today_str = datetime.datetime.now().strftime("%Y-%m-%d") @@ -93,10 +111,10 @@ def get_latest_model(trained_model_folder: str, default_model: str = "train5/wei for folder in subfolders: model_path = os.path.join(trained_model_folder, folder, "weights", "best.pt") if os.path.exists(model_path): - log_message(f"✅ Found latest model: {model_path}") + log_message(f" Found latest model: {model_path}") return model_path - log_message(f"⚠️ No valid models found in {trained_model_folder}. Using default model: {default_model}") + log_message(f" No valid models found in {trained_model_folder}. Using default model: {default_model}") return default_model def create_dataset_yaml(dataset_folder: str, classes: list): @@ -122,10 +140,10 @@ def call_reload_model_api(base_url="http://localhost:5000"): try: response = requests.post(url, timeout=10) response_data = response.json() - log_message(f"✅ API Response: {response_data} \n") + log_message(f" API Response: {response_data} \n") return response_data except requests.RequestException as e: - log_message(f"❌ Error calling reload model API: {e} \n") + log_message(f" Error calling reload model API: {e} \n") return {"error": str(e)} def clear_images_source(): @@ -159,8 +177,8 @@ def move_files(file_list, dest_img_folder, dest_lbl_folder): copied_images += 1 copied_labels += 1 else: - log.write(f"[{datetime.datetime.now()}] ⚠️ Missing label for image: {img_file}\n") - shutil.copy(img_path, os.path.join(nolabel_img_folder, img_file)) + log.write(f"[{datetime.datetime.now()}] Missing label for image: {img_file}\n") + shutil.copy(img_path, os.path.join(nolable_img_folder, img_file)) @@ -171,10 +189,10 @@ def train_yolo_model(pretrained_model: str, dataset_folder: str,project_name: st dataset_yaml = os.path.join(dataset_folder, "data.yaml") if not os.path.exists(dataset_yaml): - raise FileNotFoundError(f"⚠️ Not found file {dataset_yaml}. Plases check datasets!") + raise FileNotFoundError(f" Not found file {dataset_yaml}. Plases check datasets!") if not os.path.exists(pretrained_model): - log_message(f"⚠️ Model not found {pretrained_model}. Start train with 'yolov8n.pt'.") + log_message(f" Model not found {pretrained_model}. Start train with 'yolov8n.pt'.") pretrained_model = "yolov8n.pt" # 🚀 Tạo model YOLOv8 và load model đã train trước đó @@ -196,7 +214,7 @@ def train_yolo_model(pretrained_model: str, dataset_folder: str,project_name: st ) with open(LOG_FILE, "a") as log: - log.write(f"\n[{datetime.datetime.now()}] ✅ Train completed\n") + log.write(f"\n[{datetime.datetime.now()}] Train completed\n") call_reload_model_api() clear_images_source() @@ -214,7 +232,7 @@ create_dataset_yaml(dataset_folder, class_names) # 🏁 Log summary with open(LOG_FILE, "a") as log: - log.write(f"\n[{datetime.datetime.now()}] ✅ Dataset split completed\n") + log.write(f"\n[{datetime.datetime.now()}] Dataset split completed\n") log.write(f"Source folder: {image_folder}\n") log.write(f"Dataset saved in: {dataset_folder}\n") log.write(f"Planned Train: {len(train_files)} images, Val: {len(val_files)} images\n") @@ -224,7 +242,7 @@ with open(LOG_FILE, "a") as log: if(train_copied_imgs <=0 or train_copied_lbls <= 0 or val_copied_imgs <=0 or val_copied_lbls <=0): with open(LOG_FILE, "a") as log: - log.write(f"\n[{datetime.datetime.now()}] ❌ Data not qualified\n") + log.write(f"\n[{datetime.datetime.now()}] Data not qualified\n") log.write("=" * 50 + "\n") else: train_yolo_model(pretrained_model = get_latest_model( diff --git a/src/ultils/index.js b/src/ultils/index.js index ae46a0e..14fac83 100755 --- a/src/ultils/index.js +++ b/src/ultils/index.js @@ -1,5 +1,6 @@ export const generateImageUrl = (image_path) => { return process.env.REACT_APP_API_URL + `/${image_path}`; + // return `http://127.0.0.1:5000/${image_path}`; }; export function convertToBoundingBox(points) { @@ -19,6 +20,6 @@ export const randomDelay = () => { export function generateClientID() { const id = Math.random().toString(36).substr(2, 9); - localStorage.setItem('client_id', id); + localStorage.setItem("client_id", id); return id; } diff --git a/src/workers/detect-worker.js b/src/workers/detect-worker.js index 52553eb..e577f02 100644 --- a/src/workers/detect-worker.js +++ b/src/workers/detect-worker.js @@ -4,21 +4,21 @@ self.onmessage = async function (e) { let processFiles = 0; - if (action === 'processImages') { + if (action === "processImages") { for (const file of files) { try { const formData = new FormData(); - formData.append('image', file, file.name); + formData.append("image", file, file.name); - const response = await fetch('/api/detect_image', { - method: 'POST', + const response = await fetch(`${process.env.REACT_APP_API_URL}/detect_image`, { + method: "POST", body: formData, }); const result = await response.json(); - self.postMessage({ status: 'success', result, total: files.length, processFiles: ++processFiles }); + self.postMessage({ status: "success", result, total: files.length, processFiles: ++processFiles }); } catch (error) { - self.postMessage({ status: 'error', error: error.message }); + self.postMessage({ status: "error", error: error.message }); } } } -- 2.39.2