Update Config label #2
|
|
@ -28,4 +28,6 @@ yarn-error.log*
|
||||||
/src/server/model_datasets
|
/src/server/model_datasets
|
||||||
*.log
|
*.log
|
||||||
src/server/venv/*
|
src/server/venv/*
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
|
my_database.db
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta name="description" content="Web site created using create-react-app" />
|
||||||
name="description"
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
content="Web site created using create-react-app"
|
<!--
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
-->
|
-->
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<!--
|
<!--
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
@ -24,12 +21,12 @@
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>React App</title>
|
<title>AI Tool</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!--
|
<!--
|
||||||
This HTML file is a template.
|
This HTML file is a template.
|
||||||
If you open it directly in the browser, you will see an empty page.
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
|
@ -39,5 +36,5 @@
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
-->
|
-->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
454
src/App.js
454
src/App.js
|
|
@ -1,24 +1,39 @@
|
||||||
import '@mantine/core/styles.css';
|
import "@mantine/core/styles.css";
|
||||||
|
|
||||||
import './App.css';
|
import "./App.css";
|
||||||
import { Dropzone } from './pages/components/Dropzone';
|
import { Dropzone } from "./pages/components/Dropzone";
|
||||||
|
|
||||||
import { ActionIcon, AppShell, Box, Burger, Button, Group, LoadingOverlay, Progress, ScrollArea, Text, Tooltip } from '@mantine/core';
|
import { ActionIcon, AppShell, Box, Burger, Button, Group, LoadingOverlay, Modal, Progress, ScrollArea, Tabs, Text, Tooltip } from "@mantine/core";
|
||||||
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
import { useDisclosure, useHotkeys } from "@mantine/hooks";
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from "@mantine/notifications";
|
||||||
import { IconChevronLeft, IconChevronRight, IconImageInPicture, IconRefreshDot, IconTrash } from '@tabler/icons-react';
|
import {
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
IconChevronLeft,
|
||||||
import { ImageDetect } from './pages/components/ImageDetect';
|
IconChevronRight,
|
||||||
import ImageLabel from './pages/components/ImageLabel';
|
IconEdit,
|
||||||
import { useImagesDetected } from './stores/use-images-detected';
|
IconImageInPicture,
|
||||||
import { SaveButton } from './pages/components/SaveButton';
|
IconPlus,
|
||||||
import { generateClientID } from './ultils';
|
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() {
|
function App() {
|
||||||
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
|
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
|
||||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
|
|
||||||
const [clickData, setClickData] = useState(null);
|
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();
|
const { images, clearImagesDetected, appendImageDetect, setImageDetected } = useImagesDetected();
|
||||||
|
|
||||||
|
|
@ -27,13 +42,28 @@ function App() {
|
||||||
const imageLabelRef = useRef();
|
const imageLabelRef = useRef();
|
||||||
const dropzoneRef = 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 handleClear = () => {
|
||||||
// const result = window.confirm('Are you want to clear ?');
|
// const result = window.confirm('Are you want to clear ?');
|
||||||
|
|
||||||
const ID_NOTI = 'handle-clear';
|
const ID_NOTI = "handle-clear";
|
||||||
notifications.show({
|
notifications.show({
|
||||||
id: ID_NOTI,
|
id: ID_NOTI,
|
||||||
title: 'Are you want to clear ?',
|
title: "Are you want to clear ?",
|
||||||
message: (
|
message: (
|
||||||
<Box className="flex items-center gap-2 justify-end">
|
<Box className="flex items-center gap-2 justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -55,11 +85,11 @@ function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteImage = (data, index) => {
|
const handleDeleteImage = (data, index) => {
|
||||||
const ID_NOTI = 'handle-delete-image';
|
const ID_NOTI = "handle-delete-image";
|
||||||
|
|
||||||
const deleteImage = async () => {
|
const deleteImage = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${process.env.REACT_APP_API_URL}/delete/${data.image_name}`, { method: 'DELETE' });
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/delete/${data.image_name}`, { method: "DELETE" });
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
|
|
@ -76,7 +106,7 @@ function App() {
|
||||||
|
|
||||||
notifications.show({
|
notifications.show({
|
||||||
id: ID_NOTI,
|
id: ID_NOTI,
|
||||||
title: 'Are you want to delete ?',
|
title: "Are you want to delete ?",
|
||||||
message: (
|
message: (
|
||||||
<Box className="flex items-center gap-2 justify-end">
|
<Box className="flex items-center gap-2 justify-end">
|
||||||
<Button onClick={deleteImage} color="red" size="xs">
|
<Button onClick={deleteImage} color="red" size="xs">
|
||||||
|
|
@ -132,10 +162,10 @@ function App() {
|
||||||
processFiles: 0,
|
processFiles: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const worker = new Worker(new URL('./workers/detect-worker.js', import.meta.url));
|
const worker = new Worker(new URL("./workers/detect-worker.js", import.meta.url));
|
||||||
|
|
||||||
worker.onmessage = (event) => {
|
worker.onmessage = (event) => {
|
||||||
if (event.data.status === 'success') {
|
if (event.data.status === "success") {
|
||||||
if (event.data?.result) {
|
if (event.data?.result) {
|
||||||
setImageDetected(event.data.result);
|
setImageDetected(event.data.result);
|
||||||
setProgress({
|
setProgress({
|
||||||
|
|
@ -151,11 +181,11 @@ function App() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('⚠️ Lỗi:', event.data.error);
|
console.error("⚠️ Lỗi:", event.data.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.postMessage({ action: 'processImages', files });
|
worker.postMessage({ action: "processImages", files });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrev = () => {
|
const handlePrev = () => {
|
||||||
|
|
@ -188,132 +218,294 @@ function App() {
|
||||||
return true;
|
return true;
|
||||||
}, [images, clickData]);
|
}, [images, clickData]);
|
||||||
|
|
||||||
|
const handleTraining = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setClickData(null);
|
||||||
|
clearImagesDetected();
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/train`);
|
||||||
|
// const response = await fetch(`http://127.0.0.1:5000/train`);
|
||||||
|
const result = await response.json();
|
||||||
|
if (result?.error) {
|
||||||
|
notifications.show({
|
||||||
|
title: "Failed",
|
||||||
|
message: result?.error,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
} else
|
||||||
|
notifications.show({
|
||||||
|
title: "Success",
|
||||||
|
message: `Training successfully!`,
|
||||||
|
color: "green",
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (value) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const url = `${process.env.REACT_APP_API_URL}/label/${value?.id ? "update" : "add"}`;
|
||||||
|
// const url = `http://127.0.0.1:5000/label/${value?.id ? "update" : "add"}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(value),
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result?.success) {
|
||||||
|
notifications.show({
|
||||||
|
title: "Failed",
|
||||||
|
message: result?.error,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
} else
|
||||||
|
notifications.show({
|
||||||
|
title: "Success",
|
||||||
|
message: value?.id ? `Update label successfully!` : `Add label successfully!`,
|
||||||
|
color: "green",
|
||||||
|
});
|
||||||
|
fetchLabels();
|
||||||
|
setIsLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useHotkeys([
|
useHotkeys([
|
||||||
['ArrowRight', handleNext],
|
["ArrowRight", handleNext],
|
||||||
['ArrowLeft', handlePrev],
|
["ArrowLeft", handlePrev],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: "relative" }}>
|
||||||
<AppShell
|
<AppShell
|
||||||
header={{ height: 60 }}
|
header={{ height: 60 }}
|
||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: 'sm',
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
||||||
}}
|
}}
|
||||||
padding="md">
|
padding="md">
|
||||||
<AppShell.Header>
|
<Tabs color="teal" defaultValue="detect">
|
||||||
<Box className="flex items-center justify-between h-full">
|
<AppShell.Header>
|
||||||
<Group h="100%" px="md">
|
<Box className="flex items-center justify-between h-full">
|
||||||
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
<Group h="100%" px="md">
|
||||||
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
|
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||||
</Group>
|
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
|
||||||
|
</Group>
|
||||||
<Box className="px-4 flex items-center gap-4 w-fit">
|
<Box className="px-4 flex items-center gap-4 w-fit">
|
||||||
<Button disabled={!clickData} onClick={handleClearSelect} leftSection={<IconRefreshDot size={14} />} color="orange">
|
<Tabs.List>
|
||||||
Reset select
|
<Tabs.Tab value="detect">
|
||||||
</Button>
|
<span style={{ fontSize: 20 }}>Detect</span>
|
||||||
<SaveButton
|
</Tabs.Tab>
|
||||||
onSaved={(data) => {
|
<Tabs.Tab value="config">
|
||||||
handleNext();
|
<span style={{ fontSize: 20 }}>Config label</span>
|
||||||
}}
|
</Tabs.Tab>
|
||||||
currentData={clickData}
|
</Tabs.List>
|
||||||
imageLabelRef={imageLabelRef}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</AppShell.Header>
|
|
||||||
<AppShell.Navbar p="md">
|
|
||||||
<Box className="flex flex-col justify-between gap-2 w-full h-full">
|
|
||||||
<Box className="flex items-center justify-between gap-4 mb-2">
|
|
||||||
<Button fullWidth disabled={!images.length} onClick={handleClear} leftSection={<IconTrash size={14} />} color="red">
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
<Button fullWidth disabled={images.length} onClick={handleAddImages} leftSection={<IconImageInPicture size={14} />}>
|
|
||||||
Add images
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<ScrollArea h={780} className="pb-5 flex-1">
|
|
||||||
<Box className="flex flex-col gap-3 w-full">
|
|
||||||
{images.length > 0 &&
|
|
||||||
images.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<ImageDetect
|
|
||||||
currentData={clickData}
|
|
||||||
onClick={(data) => {
|
|
||||||
console.log({ data });
|
|
||||||
setClickData(data);
|
|
||||||
}}
|
|
||||||
key={`${item?.name || item?.image_name}_${index}`}
|
|
||||||
file={item}
|
|
||||||
onDelete={(data) => {
|
|
||||||
handleDeleteImage(data, index);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box className="px-4 flex items-center gap-4 w-fit">
|
||||||
{images.length <= 0 && (
|
<Button disabled={isLoading} onClick={() => setOpenConfirm(true)} leftSection={<IconReportAnalytics size={14} />} color="#caa32c">
|
||||||
<Box className="flex items-center justify-center">
|
Training
|
||||||
<span>No images to process</span>
|
</Button>
|
||||||
|
<Button disabled={!clickData || isLoading} onClick={handleClearSelect} leftSection={<IconRefreshDot size={14} />} color="orange">
|
||||||
|
Reset select
|
||||||
|
</Button>
|
||||||
|
<SaveButton
|
||||||
|
disabled={isLoading}
|
||||||
|
onSaved={(data) => {
|
||||||
|
handleNext();
|
||||||
|
}}
|
||||||
|
currentData={clickData}
|
||||||
|
imageLabelRef={imageLabelRef}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</AppShell.Header>
|
||||||
|
<Tabs.Panel value="detect">
|
||||||
|
<AppShell.Navbar p="md">
|
||||||
|
<Box className="flex flex-col justify-between gap-2 w-full h-full">
|
||||||
|
<Box className="flex items-center justify-between gap-4 mb-2">
|
||||||
|
<Button fullWidth disabled={!images.length || isLoading} onClick={handleClear} leftSection={<IconTrash size={14} />} color="red">
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button fullWidth disabled={images.length || isLoading} onClick={handleAddImages} leftSection={<IconImageInPicture size={14} />}>
|
||||||
|
Add images
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
{progress.total > 0 && (
|
<ScrollArea h={780} className="pb-5 flex-1">
|
||||||
<Box className="flex flex-col items-center justify-end w-full">
|
<Box className="flex flex-col gap-3 w-full">
|
||||||
<Text>
|
{images.length > 0 &&
|
||||||
{progress.processFiles} / {progress.total}
|
images.map((item, index) => {
|
||||||
</Text>
|
return (
|
||||||
<Progress className="w-full" value={(progress.processFiles * 100) / progress.total} striped animated />
|
<ImageDetect
|
||||||
|
currentData={clickData}
|
||||||
|
onClick={(data) => {
|
||||||
|
console.log({ data });
|
||||||
|
setClickData(data);
|
||||||
|
}}
|
||||||
|
key={`${item?.name || item?.image_name}_${index}`}
|
||||||
|
file={item}
|
||||||
|
onDelete={(data) => {
|
||||||
|
handleDeleteImage(data, index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{images.length <= 0 && (
|
||||||
|
<Box className="flex items-center justify-center">
|
||||||
|
<span>No images to process</span>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{progress.total > 0 && (
|
||||||
|
<Box className="flex flex-col items-center justify-end w-full">
|
||||||
|
<Text>
|
||||||
|
{progress.processFiles} / {progress.total}
|
||||||
|
</Text>
|
||||||
|
<Progress className="w-full" value={(progress.processFiles * 100) / progress.total} striped animated />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
</AppShell.Navbar>
|
||||||
</Box>
|
<AppShell.Main style={{ position: "relative" }}>
|
||||||
</AppShell.Navbar>
|
{clickData && <ImageLabel ref={imageLabelRef} data={clickData} />}
|
||||||
<AppShell.Main style={{ position: 'relative' }}>
|
|
||||||
{clickData && <ImageLabel ref={imageLabelRef} data={clickData} />}
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
gap: '10px',
|
gap: "10px",
|
||||||
width: '100%',
|
width: "100%",
|
||||||
flexDirection: 'column',
|
flexDirection: "column",
|
||||||
}}>
|
}}>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
openRef={dropzoneRef}
|
openRef={dropzoneRef}
|
||||||
hidden={!clickData}
|
hidden={!clickData}
|
||||||
onErorrFiles={(errors) => {
|
onErorrFiles={(errors) => {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Invalid Images',
|
title: "Invalid Images",
|
||||||
message: `There ${errors.length === 1 ? 'is' : 'are'} ${errors.length} invalid image${
|
message: `There ${errors.length === 1 ? "is" : "are"} ${errors.length} invalid image${
|
||||||
errors.length > 1 ? 's' : ''
|
errors.length > 1 ? "s" : ""
|
||||||
}. Please re-check!`,
|
}. Please re-check!`,
|
||||||
color: 'red',
|
color: "red",
|
||||||
});
|
});
|
||||||
|
}}
|
||||||
|
onFilesChange={handleUpload}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="fixed bottom-5 right-5 flex items-center gap-4">
|
||||||
|
<Tooltip label={"Left (<)"}>
|
||||||
|
<ActionIcon disabled={!showPrev} onClick={handlePrev} size={"lg"}>
|
||||||
|
<IconChevronLeft size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={"Right (>)"}>
|
||||||
|
<ActionIcon disabled={!showNext} onClick={handleNext} size={"lg"}>
|
||||||
|
<IconChevronRight size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</AppShell.Main>
|
||||||
|
</Tabs.Panel>
|
||||||
|
<Tabs.Panel value="config">
|
||||||
|
<AppShell.Main style={{ position: "relative", paddingLeft: 0 }}>
|
||||||
|
<ScrollArea h={780} className="pb-5 flex-1">
|
||||||
|
<Box className="flex justify-end pe-4">
|
||||||
|
<Button disabled={isLoading} leftSection={<IconPlus size={14} />} color="#1a9b20" onClick={open}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<Box w={600} className="mt-4">
|
||||||
|
{listLabel.length > 0 &&
|
||||||
|
listLabel?.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<Box w={600} key={index} className="flex items-center justify-between mb-4">
|
||||||
|
<Text>{item?.name}</Text>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<Button
|
||||||
|
disabled={isLoading}
|
||||||
|
leftSection={<IconEdit size={14} />}
|
||||||
|
color="#0659a5"
|
||||||
|
onClick={() => {
|
||||||
|
open();
|
||||||
|
setDataLabel(item);
|
||||||
|
}}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</AppShell.Main>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
<LoadingOverlay
|
||||||
|
visible={isLoading}
|
||||||
|
zIndex={10}
|
||||||
|
overlayProps={{ radius: "sm", blur: 2 }}
|
||||||
|
loaderProps={{
|
||||||
|
size: "lg",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ModalLabel
|
||||||
|
opened={opened}
|
||||||
|
open={open}
|
||||||
|
close={() => {
|
||||||
|
close();
|
||||||
|
setDataLabel({});
|
||||||
|
}}
|
||||||
|
data={dataLabel}
|
||||||
|
onSave={() => {
|
||||||
|
close();
|
||||||
|
setDataLabel({});
|
||||||
|
setIsLoading(true);
|
||||||
|
handleSave(dataLabel);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 2000);
|
||||||
|
}}
|
||||||
|
setData={setDataLabel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal opened={openConfirm} onClose={() => setOpenConfirm(false)} title={"Confirm"}>
|
||||||
|
<div style={{ padding: 20 }} className="flex flex-col gap-4 justify-center items-center">
|
||||||
|
<div>Do you want to run training?</div>
|
||||||
|
<div>Maybe you need to save some data to train!</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: 20 }} className="flex justify-center gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setOpenConfirm(false);
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setOpenConfirm(false);
|
||||||
|
handleTraining();
|
||||||
}}
|
}}
|
||||||
onFilesChange={handleUpload}
|
color="#1a9b20">
|
||||||
/>
|
Confirm
|
||||||
</Box>
|
</Button>
|
||||||
|
</div>
|
||||||
<Box className="fixed bottom-5 right-5 flex items-center gap-4">
|
</Modal>
|
||||||
<Tooltip label={'Left (<)'}>
|
|
||||||
<ActionIcon disabled={!showPrev} onClick={handlePrev} size={'lg'}>
|
|
||||||
<IconChevronLeft size={20} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label={'Right (>)'}>
|
|
||||||
<ActionIcon disabled={!showNext} onClick={handleNext} size={'lg'}>
|
|
||||||
<IconChevronRight size={20} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</AppShell.Main>
|
|
||||||
</AppShell>
|
</AppShell>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from 'react';
|
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, memo } from "react";
|
||||||
import { Box, LoadingOverlay } from '@mantine/core';
|
import { Box, LoadingOverlay } from "@mantine/core";
|
||||||
import PointsBlur from './PointsBlur';
|
import PointsBlur from "./PointsBlur";
|
||||||
import { generateImageUrl } from '../../ultils';
|
import { generateImageUrl } from "../../ultils";
|
||||||
|
|
||||||
const ImageLabel = forwardRef(({ data }, ref) => {
|
const ImageLabel = forwardRef(({ data }, ref) => {
|
||||||
const [imageSrc, setImageSrc] = useState(null);
|
const [imageSrc, setImageSrc] = useState(null);
|
||||||
|
|
@ -10,7 +10,7 @@ const ImageLabel = forwardRef(({ data }, ref) => {
|
||||||
const [reloading, setReloading] = useState(false);
|
const [reloading, setReloading] = useState(false);
|
||||||
const [listLabel, setListLabel] = useState([]);
|
const [listLabel, setListLabel] = useState([]);
|
||||||
const [infoImage, setInfoImage] = useState({});
|
const [infoImage, setInfoImage] = useState({});
|
||||||
const [imageName, setImageName] = useState('');
|
const [imageName, setImageName] = useState("");
|
||||||
|
|
||||||
// Expose blurredRegions cho component cha
|
// Expose blurredRegions cho component cha
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
|
|
@ -33,18 +33,23 @@ const ImageLabel = forwardRef(({ data }, ref) => {
|
||||||
alert(data.error);
|
alert(data.error);
|
||||||
} else {
|
} else {
|
||||||
if (data?.points) {
|
if (data?.points) {
|
||||||
blurredRegions.current = data?.points?.map((pre) => [
|
blurredRegions.current = data?.points?.map((pre) => {
|
||||||
{ x: pre.x1, y: pre.y1, label: pre.label },
|
const arrLabel = pre?.label?.split(".");
|
||||||
{ x: pre.x2, y: pre.y1, label: pre.label },
|
const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0];
|
||||||
{ x: pre.x2, y: pre.y2, label: pre.label },
|
const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null;
|
||||||
{ x: pre.x1, y: pre.y2, label: pre.label },
|
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(
|
setListLabel(
|
||||||
Object.entries(data?.labels).map(([id, name]) => ({
|
Object.entries(data?.labels).map(([id, name]) => ({
|
||||||
id: Number(id),
|
id: Number(id),
|
||||||
name,
|
name,
|
||||||
})),
|
}))
|
||||||
);
|
);
|
||||||
setImageSrc(generateImageUrl(data?.image_path));
|
setImageSrc(generateImageUrl(data?.image_path));
|
||||||
setImageName(data?.image_name);
|
setImageName(data?.image_name);
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<Modal opened={opened} onClose={close} title={data?.id ? "Edit Label" : "Add Label"} centered>
|
||||||
|
<div style={{ padding: 20 }} className="flex justify-center">
|
||||||
|
<TextInput
|
||||||
|
label="Input label"
|
||||||
|
withAsterisk
|
||||||
|
placeholder="Input placeholder"
|
||||||
|
value={data?.name || ""}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (event.currentTarget.value) setIsError(false);
|
||||||
|
setData((pre) => ({ ...pre, name: event.currentTarget.value }));
|
||||||
|
}}
|
||||||
|
style={{ width: 300 }}
|
||||||
|
error={isError ? "Invalid label" : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: 20 }} className="flex justify-center gap-4">
|
||||||
|
<Button onClick={close}>Close</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (!data?.name) {
|
||||||
|
setIsError(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data?.id) {
|
||||||
|
onSave(data);
|
||||||
|
close();
|
||||||
|
} else setOpenConfirm(true);
|
||||||
|
}}
|
||||||
|
color="#1a9b20">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal opened={openConfirm} onClose={() => setOpenConfirm(false)} title={"Confirm"}>
|
||||||
|
<div style={{ padding: 20 }} className="flex flex-col gap-4 justify-center items-center">
|
||||||
|
<div>Do you want to add new label?</div>
|
||||||
|
<div style={{ color: "#f54e4e" }}>Label cannot delete after add!</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: 20 }} className="flex justify-center gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setOpenConfirm(false);
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onSave} color="#1a9b20">
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalLabel;
|
||||||
|
|
@ -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 PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded, reloading, setReloading, setInfoImage, listLabel = [] }) => {
|
||||||
const points = useRef([]);
|
const points = useRef([]);
|
||||||
|
|
@ -12,7 +12,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext("2d");
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
|
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
|
|
@ -23,8 +23,12 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
setInfoImage({ width: image.width, height: image.height });
|
setInfoImage({ width: image.width, height: image.height });
|
||||||
};
|
};
|
||||||
|
|
||||||
image.crossOrigin = 'anonymous';
|
image.crossOrigin = "anonymous";
|
||||||
image.src = imageSrc;
|
image.src = imageSrc;
|
||||||
|
setInfo({});
|
||||||
|
setIsOpen(false);
|
||||||
|
points.current = []; // Clear points after blurring
|
||||||
|
setReloading((pre) => !pre);
|
||||||
}, [imageSrc]);
|
}, [imageSrc]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -39,7 +43,6 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
|
|
||||||
const handleCanvasClick = (event) => {
|
const handleCanvasClick = (event) => {
|
||||||
if (!isImageLoaded.current) return;
|
if (!isImageLoaded.current) return;
|
||||||
if (isOpen) return;
|
|
||||||
const { x, y } = getMousePos(canvas, event);
|
const { x, y } = getMousePos(canvas, event);
|
||||||
|
|
||||||
// Check if a delete button is clicked
|
// 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;
|
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) {
|
if (Math.abs(buttonX - x) < 30 && Math.abs(buttonY - y) < 10) {
|
||||||
// blurredRegions.current.splice(i, 1);
|
// blurredRegions.current.splice(i, 1);
|
||||||
|
if (info?.index === i) {
|
||||||
|
setInfo({});
|
||||||
|
setIsOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setInfo({ index: i, value: blurredRegions.current[i] });
|
setInfo({ index: i, value: blurredRegions.current[i] });
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
redrawCanvas(i);
|
redrawCanvas(i);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isOpen) return;
|
||||||
points.current.push({ x, y, label: listLabel[0]?.name || 'cisco' });
|
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);
|
drawRedPoint(canvas, x, y);
|
||||||
|
|
||||||
if (points.current.length === 4) {
|
if (points.current.length === 4) {
|
||||||
|
|
@ -67,11 +78,10 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCanvasMouseDown = (event) => {
|
const handleCanvasMouseDown = (event) => {
|
||||||
if (isOpen) return;
|
|
||||||
const { x, y } = getMousePos(canvas, event);
|
const { x, y } = getMousePos(canvas, event);
|
||||||
|
|
||||||
// Check if we clicked on an existing point
|
|
||||||
for (let i = 0; i < blurredRegions.current.length; i++) {
|
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++) {
|
for (let j = 0; j < blurredRegions.current[i].length; j++) {
|
||||||
const point = blurredRegions.current[i][j];
|
const point = blurredRegions.current[i][j];
|
||||||
if (Math.abs(point.x - x) < 5 && Math.abs(point.y - y) < 5) {
|
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
|
// Check if we clicked inside a region
|
||||||
const region = blurredRegions.current[i];
|
const region = blurredRegions.current[i];
|
||||||
if (isPointInRegion(x, y, region)) {
|
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);
|
setMovingRegionIndex(i);
|
||||||
setOffset({ x: region[0].x - x, y: region[0].y - y });
|
setOffset({ x: region[0].x - x, y: region[0].y - y });
|
||||||
return;
|
return;
|
||||||
|
|
@ -110,6 +139,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
x: x + offset.x,
|
x: x + offset.x,
|
||||||
y: y + offset.y,
|
y: y + offset.y,
|
||||||
label: blurredRegions.current[regionIndex][pointIndex]?.label,
|
label: blurredRegions.current[regionIndex][pointIndex]?.label,
|
||||||
|
subLabel: blurredRegions.current[regionIndex][pointIndex]?.subLabel,
|
||||||
};
|
};
|
||||||
redrawCanvas();
|
redrawCanvas();
|
||||||
} else if (movingRegionIndex !== null) {
|
} else if (movingRegionIndex !== null) {
|
||||||
|
|
@ -125,14 +155,14 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
canvas.addEventListener('mousedown', handleCanvasMouseDown);
|
canvas.addEventListener("mousedown", handleCanvasMouseDown);
|
||||||
canvas.addEventListener('mouseup', handleCanvasMouseUp);
|
canvas.addEventListener("mouseup", handleCanvasMouseUp);
|
||||||
canvas.addEventListener('mousemove', handleCanvasMouseMove);
|
canvas.addEventListener("mousemove", handleCanvasMouseMove);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
canvas.removeEventListener('mousedown', handleCanvasMouseDown);
|
canvas.removeEventListener("mousedown", handleCanvasMouseDown);
|
||||||
canvas.removeEventListener('mouseup', handleCanvasMouseUp);
|
canvas.removeEventListener("mouseup", handleCanvasMouseUp);
|
||||||
canvas.removeEventListener('mousemove', handleCanvasMouseMove);
|
canvas.removeEventListener("mousemove", handleCanvasMouseMove);
|
||||||
};
|
};
|
||||||
}, [draggingPointIndex, movingRegionIndex, offset, isOpen, info]);
|
}, [draggingPointIndex, movingRegionIndex, offset, isOpen, info]);
|
||||||
|
|
||||||
|
|
@ -160,17 +190,17 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawRedPoint = (canvas, x, y) => {
|
const drawRedPoint = (canvas, x, y) => {
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext("2d");
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
||||||
ctx.fillStyle = 'red';
|
ctx.fillStyle = "red";
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawBorder = (canvas, points) => {
|
const drawBorder = (canvas, points) => {
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext("2d");
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
points.forEach((point, index) => {
|
points.forEach((point, index) => {
|
||||||
|
|
@ -182,7 +212,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
});
|
});
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.strokeStyle = 'blue';
|
ctx.strokeStyle = "blue";
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
|
|
@ -190,23 +220,26 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawDeleteButton = (canvas, region, isSelected) => {
|
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 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;
|
const buttonY = Math.min(region[0].y, region[1].y, region[2].y, region[3].y) - 20;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = isSelected ? 'rgb(196, 194, 61)' : 'rgb(2, 101, 182)';
|
ctx.fillStyle = isSelected ? "rgb(196, 194, 61)" : "rgb(2, 101, 182)";
|
||||||
ctx.fillRect(buttonX - 30, buttonY - 10, 60, 20);
|
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.fillStyle = "rgb(255, 255, 255)";
|
||||||
ctx.font = '13px Arial';
|
ctx.font = "9px Arial";
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = "center";
|
||||||
ctx.fillText(region[0]?.label, buttonX, buttonY + 5);
|
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();
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const redrawCanvas = (index) => {
|
const redrawCanvas = (index) => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext("2d");
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
|
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
|
|
@ -223,29 +256,39 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
image.crossOrigin = 'anonymous';
|
image.crossOrigin = "anonymous";
|
||||||
image.src = imageSrc;
|
image.src = imageSrc;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: "relative" }}>
|
||||||
<canvas ref={canvasRef} />
|
<canvas ref={canvasRef} />
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div style={{ backgroundColor: '#FFF', position: 'absolute', top: info?.value[0].y - 200, left: info?.value[0].x, zIndex: 9999 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#FFF",
|
||||||
|
position: "absolute",
|
||||||
|
top: info?.value[0].y - 30,
|
||||||
|
left: info?.value[0].x < 200 ? info?.value[0].x + 50 : info?.value[0].x - 170,
|
||||||
|
zIndex: 9999,
|
||||||
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ color: '#000', fontSize: '18px', fontWeight: 'bold' }}>Select label</span>
|
<span style={{ color: "#000", fontSize: "18px", fontWeight: "bold" }}>Select label</span>
|
||||||
</div>
|
</div>
|
||||||
{listLabel?.map((el, i) => (
|
{listLabel?.map((el, i) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newList = info?.value?.map((pre) => ({ ...pre, label: el?.name }));
|
const arrLabel = el?.name?.split(".");
|
||||||
|
const label = arrLabel?.length > 1 ? arrLabel[1] : arrLabel[0];
|
||||||
|
const subLabel = arrLabel?.length > 1 ? arrLabel[0] : null;
|
||||||
|
const newList = info?.value?.map((pre) => ({ ...pre, label: label, subLabel: subLabel }));
|
||||||
blurredRegions.current[info?.index] = newList;
|
blurredRegions.current[info?.index] = newList;
|
||||||
setInfo({});
|
setInfo({});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
redrawCanvas();
|
redrawCanvas();
|
||||||
}}
|
}}
|
||||||
style={{ backgroundColor: '#ccc', width: '150px', color: '#000' }}>
|
style={{ backgroundColor: "#ccc", width: "150px", color: "#000" }}>
|
||||||
{el?.name}
|
{el?.name}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -258,7 +301,7 @@ const PointBlurImageComponent = ({ imageSrc, blurredRegions, loaded, setLoaded,
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
redrawCanvas();
|
redrawCanvas();
|
||||||
}}
|
}}
|
||||||
style={{ backgroundColor: '#ff0000', marginBottom: '10px', width: '150px', color: '#fff' }}>
|
style={{ backgroundColor: "#ff0000", marginBottom: "10px", width: "150px", color: "#fff" }}>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Button, Tooltip, LoadingOverlay } from '@mantine/core';
|
import { Button, Tooltip, LoadingOverlay } from "@mantine/core";
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from "@mantine/notifications";
|
||||||
import { IconImageInPicture } from '@tabler/icons-react';
|
import { IconImageInPicture } from "@tabler/icons-react";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { useImagesDetected } from '../../stores/use-images-detected';
|
import { useImagesDetected } from "../../stores/use-images-detected";
|
||||||
import { convertToBoundingBox } from '../../ultils';
|
import { convertToBoundingBox } from "../../ultils";
|
||||||
import { useHotkeys } from '@mantine/hooks';
|
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 [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { appendImageDetect, images } = useImagesDetected();
|
const { appendImageDetect, images } = useImagesDetected();
|
||||||
|
|
@ -14,7 +14,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
|
||||||
const handleSave = async ({ blurredRegions, infoImage, imageName, listLabel }) => {
|
const handleSave = async ({ blurredRegions, infoImage, imageName, listLabel }) => {
|
||||||
try {
|
try {
|
||||||
if (blurredRegions?.length > 0) {
|
if (blurredRegions?.length > 0) {
|
||||||
let arrPoints = '';
|
let arrPoints = "";
|
||||||
blurredRegions?.forEach((points) => {
|
blurredRegions?.forEach((points) => {
|
||||||
const img_w = infoImage?.width;
|
const img_w = infoImage?.width;
|
||||||
const img_h = infoImage?.height;
|
const img_h = infoImage?.height;
|
||||||
|
|
@ -31,19 +31,21 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
|
||||||
const height = (y_max - y_min) / img_h;
|
const height = (y_max - y_min) / img_h;
|
||||||
|
|
||||||
// Get class ID
|
// 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)}`;
|
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 = `${process.env.REACT_APP_API_URL}/save`;
|
||||||
|
// const url = `http://127.0.0.1:5000/save`;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('list', arrPoints);
|
formData.append("list", arrPoints);
|
||||||
formData.append('imageName', imageName);
|
formData.append("imageName", imageName);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -55,7 +57,7 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
currentData.points = newPoints;
|
currentData.points = newPoints;
|
||||||
currentData['isSave'] = true;
|
currentData["isSave"] = true;
|
||||||
|
|
||||||
const newImages = images.map((image) => {
|
const newImages = images.map((image) => {
|
||||||
if (image.image_name === imageName) {
|
if (image.image_name === imageName) {
|
||||||
|
|
@ -72,9 +74,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
|
||||||
appendImageDetect(newImages);
|
appendImageDetect(newImages);
|
||||||
|
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Save Success',
|
title: "Save Success",
|
||||||
message: `${imageName} save success`,
|
message: `${imageName} save success`,
|
||||||
color: 'green',
|
color: "green",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onSaved) {
|
if (onSaved) {
|
||||||
|
|
@ -84,9 +86,9 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Save Error',
|
title: "Save Error",
|
||||||
message: error?.message || 'Internal Server Error',
|
message: error?.message || "Internal Server Error",
|
||||||
color: 'red',
|
color: "red",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -101,11 +103,11 @@ export const SaveButton = ({ currentData, imageLabelRef, onSaved }) => {
|
||||||
handleSave(saveData);
|
handleSave(saveData);
|
||||||
};
|
};
|
||||||
|
|
||||||
useHotkeys([['ctrl+S', handleSubmit]]);
|
useHotkeys([["ctrl+S", handleSubmit]]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={'Ctrl + S'}>
|
<Tooltip label={"Ctrl + S"}>
|
||||||
<Button className="relative ư-f" disabled={!currentData} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}>
|
<Button className="relative ư-f" disabled={!currentData || disabled} onClick={handleSubmit} leftSection={<IconImageInPicture size={14} />}>
|
||||||
Save
|
Save
|
||||||
<LoadingOverlay visible={loading} loaderProps={{ size: 14 }} />
|
<LoadingOverlay visible={loading} loaderProps={{ size: 14 }} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from routes.save import save_bp
|
||||||
from routes.show import show_bp
|
from routes.show import show_bp
|
||||||
from routes.delete import delete_bp
|
from routes.delete import delete_bp
|
||||||
from routes.upload import upload_bp
|
from routes.upload import upload_bp
|
||||||
|
from routes.label import config_label
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
@ -17,6 +18,7 @@ app.register_blueprint(save_bp)
|
||||||
app.register_blueprint(show_bp)
|
app.register_blueprint(show_bp)
|
||||||
app.register_blueprint(delete_bp)
|
app.register_blueprint(delete_bp)
|
||||||
app.register_blueprint(upload_bp)
|
app.register_blueprint(upload_bp)
|
||||||
|
app.register_blueprint(config_label)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import random
|
||||||
import cv2
|
import cv2
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import subprocess
|
||||||
|
|
||||||
# Load mô hình YOLO
|
# Load mô hình YOLO
|
||||||
model = YOLO(get_latest_model_including_today(trained_model_folder=TRAINED_MODEL_FOLDER))
|
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)
|
model = YOLO(new_model_path)
|
||||||
return jsonify({"message": f"Model reloaded from {new_model_path}", "status": "true"}), 200
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/<int:label_id>', 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
|
||||||
|
|
@ -7,13 +7,31 @@ from ultralytics import YOLO
|
||||||
import yaml
|
import yaml
|
||||||
import requests
|
import requests
|
||||||
import glob
|
import glob
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
# 🗂️ Configure paths
|
# 🗂️ Configure paths
|
||||||
DATA_SPLIT_FOLDER = "model_datasets"
|
DATA_SPLIT_FOLDER = "model_datasets"
|
||||||
IMAGE_EXTENSION = ".png"
|
IMAGE_EXTENSION = ".png"
|
||||||
LOG_FILE = "training_logs.log"
|
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
|
# 📅 Get today's and yesterday's date
|
||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
yesterday = today - datetime.timedelta(days=1)
|
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
|
label_folder = label_folder_yesterday
|
||||||
date_str = yesterday_str
|
date_str = yesterday_str
|
||||||
else:
|
else:
|
||||||
print(image_folder)
|
# print(image_folder)
|
||||||
print(label_folder)
|
# print(label_folder)
|
||||||
raise Exception("⚠️ No valid image & label folder found in the last two days!")
|
raise Exception(" No valid image & label folder found in the last two days!")
|
||||||
|
|
||||||
# 🏗️ Create dataset folders
|
# 🏗️ Create dataset folders
|
||||||
dataset_folder = os.path.join(DATA_SPLIT_FOLDER, date_str)
|
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"):
|
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."""
|
"""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):
|
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
|
return default_model
|
||||||
|
|
||||||
today_str = datetime.datetime.now().strftime("%Y-%m-%d")
|
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:
|
for folder in subfolders:
|
||||||
model_path = os.path.join(trained_model_folder, folder, "weights", "best.pt")
|
model_path = os.path.join(trained_model_folder, folder, "weights", "best.pt")
|
||||||
if os.path.exists(model_path):
|
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
|
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
|
return default_model
|
||||||
|
|
||||||
def create_dataset_yaml(dataset_folder: str, classes: list):
|
def create_dataset_yaml(dataset_folder: str, classes: list):
|
||||||
|
|
@ -122,10 +140,10 @@ def call_reload_model_api(base_url="http://localhost:5000"):
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, timeout=10)
|
response = requests.post(url, timeout=10)
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
log_message(f"✅ API Response: {response_data} \n")
|
log_message(f" API Response: {response_data} \n")
|
||||||
return response_data
|
return response_data
|
||||||
except requests.RequestException as e:
|
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)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
def clear_images_source():
|
def clear_images_source():
|
||||||
|
|
@ -159,8 +177,8 @@ def move_files(file_list, dest_img_folder, dest_lbl_folder):
|
||||||
copied_images += 1
|
copied_images += 1
|
||||||
copied_labels += 1
|
copied_labels += 1
|
||||||
else:
|
else:
|
||||||
log.write(f"[{datetime.datetime.now()}] ⚠️ Missing label for image: {img_file}\n")
|
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))
|
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")
|
dataset_yaml = os.path.join(dataset_folder, "data.yaml")
|
||||||
|
|
||||||
if not os.path.exists(dataset_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):
|
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"
|
pretrained_model = "yolov8n.pt"
|
||||||
|
|
||||||
# 🚀 Tạo model YOLOv8 và load model đã train trước đó
|
# 🚀 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:
|
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()
|
call_reload_model_api()
|
||||||
clear_images_source()
|
clear_images_source()
|
||||||
|
|
@ -214,7 +232,7 @@ create_dataset_yaml(dataset_folder, class_names)
|
||||||
|
|
||||||
# 🏁 Log summary
|
# 🏁 Log summary
|
||||||
with open(LOG_FILE, "a") as log:
|
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"Source folder: {image_folder}\n")
|
||||||
log.write(f"Dataset saved in: {dataset_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")
|
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):
|
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:
|
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")
|
log.write("=" * 50 + "\n")
|
||||||
else:
|
else:
|
||||||
train_yolo_model(pretrained_model = get_latest_model(
|
train_yolo_model(pretrained_model = get_latest_model(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export const generateImageUrl = (image_path) => {
|
export const generateImageUrl = (image_path) => {
|
||||||
return process.env.REACT_APP_API_URL + `/${image_path}`;
|
return process.env.REACT_APP_API_URL + `/${image_path}`;
|
||||||
|
// return `http://127.0.0.1:5000/${image_path}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function convertToBoundingBox(points) {
|
export function convertToBoundingBox(points) {
|
||||||
|
|
@ -19,6 +20,6 @@ export const randomDelay = () => {
|
||||||
|
|
||||||
export function generateClientID() {
|
export function generateClientID() {
|
||||||
const id = Math.random().toString(36).substr(2, 9);
|
const id = Math.random().toString(36).substr(2, 9);
|
||||||
localStorage.setItem('client_id', id);
|
localStorage.setItem("client_id", id);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,21 @@ self.onmessage = async function (e) {
|
||||||
|
|
||||||
let processFiles = 0;
|
let processFiles = 0;
|
||||||
|
|
||||||
if (action === 'processImages') {
|
if (action === "processImages") {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('image', file, file.name);
|
formData.append("image", file, file.name);
|
||||||
|
|
||||||
const response = await fetch('/api/detect_image', {
|
const response = await fetch(`${process.env.REACT_APP_API_URL}/detect_image`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
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) {
|
} catch (error) {
|
||||||
self.postMessage({ status: 'error', error: error.message });
|
self.postMessage({ status: "error", error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue