diff --git a/.vscode/settings.json b/.vscode/settings.json index 2793cf8..58a804b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "GLTF", "grau", "idontsudo", + "Pids", "raycaster", "skils", "typedataset", diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 5444c1a..0000000 --- a/flake.lock +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1733935512, - "narHash": "sha256-beDnPaHubnwcsbVPC3rIVtQM3QVFDMmc0dtfHCW5UrA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "d55cc4608dae1b42a0ef5c0cf701b501fc7bae58", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "master", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 8f0a918..0000000 --- a/flake.nix +++ /dev/null @@ -1,37 +0,0 @@ -{ - description = "Robossembler Development Environments on Nix"; - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs?ref=master"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem - (system: - let - pkgs = import nixpkgs { inherit system; overlays = []; }; - in - { - packages = { - frontend = pkgs.writeShellApplication { - name = "frontend"; - runtimeInputs = [ pkgs.nodejs ]; - text = '' - cd ui && npm i && npm run dev - ''; - }; - backend = pkgs.writeShellApplication { - name = "frontend"; - runtimeInputs = [ pkgs.nodejs ]; - text = '' - cd server && npm i && npm run dev - ''; - }; - }; - - devShells = { - default = pkgs.mkShell { packages = with pkgs; [ nodejs ]; }; - }; - } - ); -} diff --git a/p.json b/p.json new file mode 100644 index 0000000..0844c5e --- /dev/null +++ b/p.json @@ -0,0 +1,82 @@ +{ + "process": { + "type": "OBJECT_DETECTION", + "selectProcess": { + "value": { + "_id": "675db885429ef25f8d2efaa2", + "script": "ls -l -a", + "formBuilder": { + "result": "", + "context": "", + "form": [], + "output": "", + "type": "formBuilder" + }, + "type": "OBJECT_DETECTION", + "instanceName": "ls -l -a", + "name": "ls", + "isEnd": true, + "createDate": "1734195300981", + "card": "pose_estimate", + "path": "/Users/idontsudo/webservice/server/build/public//process/ls", + "instancePath": "/Users/idontsudo/webservice/server/build/public//process/ls/ls -l -a", + "project": { + "_id": "675eb125281cf9253681efa3", + "description": "e1wq", + "rootDir": "/Users/idontsudo/webservice/server/build/public/f49f8f47-5427-48aa-8aff-c5e7ae4e6efe", + "isActive": true, + "__v": 0 + }, + "__v": 0, + "lastProcessExecCommand": "ls -l -a --path /Users/idontsudo/webservice/server/build/public/process/ls/ls -l -a --form /Users/idontsudo/webservice/server/build/public/process/ls/ls -l -a/form.json", + "processStatus": "endError", + "lastProcessLogs": "ls: unrecognized option `--path'nnusage: ls [-@ABCFGHILOPRSTUWabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]n" + } + } + }, + "datasetObjects": { + "details": [] + }, + "typedataset": "ObjectDetection", + "models_randomization": { + "loc_range_low": [ + -1, + -1, + 0 + ], + "loc_range_high": [ + 1, + 1, + 2 + ] + }, + "scene": { + "objects": [], + "lights": [] + }, + "camera_position": { + "center_shell": [ + 0, + 0, + 0 + ], + "radius_range": [ + 1, + 1.4 + ], + "elevation_range": [ + 10, + 90 + ] + }, + "generation": { + "n_cam_pose": 5, + "n_sample_on_pose": 3, + "n_series": 100, + "image_format": "JPEG", + "image_size_wh": [ + 640, + 480 + ] + } +} \ No newline at end of file diff --git a/server/README.md b/server/README.md index cb0043a..6a3cc96 100644 --- a/server/README.md +++ b/server/README.md @@ -1 +1,14 @@ -Веб-сервис для отладки Robossembler Framework \ No newline at end of file +Веб-сервис для отладки Robossembler Framework + +### Миграция данных веб-сервиса + +Имеется ввиду перенос данных, хранящихся в БД MongoDB. + +Для этого вначале создаётся копия БД с именем `dev` в папке `my_copy`: +```sh +mongodump --host localhost --port 27017 --db dev --out my_copy +``` +Затем папка `my_copy` переносится в нужное место (например, на другой сервер). И запускается её восстановление в новом месте (копия БД с именем `dev`): +```sh +mongorestore --host localhost --port 27017 --db dev my_copy/dev +``` diff --git a/server/command.json b/server/command.json index 468f1fe..8762c7e 100644 --- a/server/command.json +++ b/server/command.json @@ -15,12 +15,12 @@ "checkCommand": null, "filter": null }, - "btBuilderProcess": { - "execCommand": "nix run github:nixos/nixpkgs#python312Packages.tensorboard -- --logdir ${dir_path}", + "btRuntimeProcess": { + "execCommand": "cd ~/robossembler-ws && source ./install/local_setup.bash; ros2 launch rbs_bt_executor rbs_bt_web.launch.py bt_path:=${bt_path}", "date": null, "status": null, "delay": 0, "checkCommand": null, "filter": null } -} \ No newline at end of file +} diff --git a/server/src/core/controllers/app.ts b/server/src/core/controllers/app.ts index 41a52e7..c1dca95 100644 --- a/server/src/core/controllers/app.ts +++ b/server/src/core/controllers/app.ts @@ -9,6 +9,9 @@ import { dirname } from "path"; import { CheckAndCreateStaticFilesFolderUseCase } from "../usecases/check_and_create_static_files_folder_usecase"; import { DataBaseConnectUseCase } from "../usecases/database_connect_usecase"; import { TypedEvent } from "../helpers/typed_event"; +import { CalculationInstanceDBModel } from "../../features/calculations_instance/models/calculations_instance_database_model"; +import * as fs from "fs"; +import { WriteFileSystemFileUseCase } from "../usecases/write_file_system_file_usecase"; export enum ServerStatus { init = "init", @@ -85,7 +88,25 @@ export class App extends TypedEvent { this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); this.app.use(express.static(App.staticFilesStoreDir())); + this.app.get('/logs', async (req, res) => { + const id = req.query.id; + if (id === undefined) { + return res.status(400).json('need req query id?=') + } + + const calculationInstanceDBModel = await CalculationInstanceDBModel.findById(id) + if (calculationInstanceDBModel === null) { + return res.status(400).json("calcultaion db model is null"); + } + const p = App.staticFilesStoreDir() + '/log.txt'; + + (await new WriteFileSystemFileUseCase().call(p, calculationInstanceDBModel.lastProcessLogs)).map(() => { + return res.sendFile(p); + }) + + + }) this.app.use( fileUpload({ createParentPath: true, diff --git a/server/src/core/controllers/http_controller.ts b/server/src/core/controllers/http_controller.ts index 5b61528..9542de1 100644 --- a/server/src/core/controllers/http_controller.ts +++ b/server/src/core/controllers/http_controller.ts @@ -65,12 +65,12 @@ interface ISubSetFeatureRouter { method: HttpMethodType; subUrl: string; fn: - | CallbackStrategyWithValidationModel - | CallbackStrategyWithEmpty - | CallbackStrategyWithIdQuery - | CallBackStrategyWithQueryPage - | CallbackStrategyWithFileUpload - | CallbackStrategyWithFilesUploads; + | CallbackStrategyWithValidationModel + | CallbackStrategyWithEmpty + | CallbackStrategyWithIdQuery + | CallBackStrategyWithQueryPage + | CallbackStrategyWithFileUpload + | CallbackStrategyWithFilesUploads; } abstract class ICoreHttpController { @@ -234,6 +234,7 @@ export class CoreHttpController implements ICoreHttpController { return res.json(ok); }, (err) => { + return res.status(400).json({ error: String(err) }); } ); diff --git a/server/src/core/models/skill_model.ts b/server/src/core/models/skill_model.ts index c22ab7d..57baf8c 100644 --- a/server/src/core/models/skill_model.ts +++ b/server/src/core/models/skill_model.ts @@ -18,7 +18,7 @@ export class TopicViewModel { export interface IParam { type: string; - dependency: Object; + dependency: object; } export interface Skill { SkillPackage: ISkillPackage; diff --git a/server/src/core/scenarios/create_intance_scenario.ts b/server/src/core/scenarios/create_intance_scenario.ts index 53b98fe..9df5453 100644 --- a/server/src/core/scenarios/create_intance_scenario.ts +++ b/server/src/core/scenarios/create_intance_scenario.ts @@ -7,9 +7,8 @@ export abstract class CreateInstanceScenario extends Callbac abstract validationModel: V; abstract databaseModel: any; call = async (model: V): ResponseBase => { - model.instancePath = `${model.path}/${model.instanceName}`; - console.log("INSTANCE PATh"); - console.log(model.instancePath) + model.instancePath = `${model.path.pathNormalize()}/${model.instanceName}`.pathNormalize(); + model.path = model.path.pathNormalize(); return (await new CreateFolderUseCase().call(model.instancePath)).map( async () => await new CreateDataBaseModelUseCase(this.databaseModel).call(model) ); diff --git a/server/src/core/scenarios/exec_process_scenario_v2.ts b/server/src/core/scenarios/exec_process_scenario_v2.ts new file mode 100644 index 0000000..cd846d4 --- /dev/null +++ b/server/src/core/scenarios/exec_process_scenario_v2.ts @@ -0,0 +1,72 @@ +import { Disposable, Listener, TypedEvent } from "../helpers/typed_event"; +import { GetRootDirUseCase } from "../usecases/get_root_dir_usecase" +import { exec } from 'child_process'; + +export const activeProcessPids: { [name: string]: { pid: number, status: ProcessStatus } } = {} + +export enum ProcessStatus { + run = 'run', + endError = 'endError', + endOk = 'endOk', + userDelete = 'userDelete', +} + +class ExecutorProgramServiceV2 extends TypedEvent<{ [name: string]: { pid: number, status: ProcessStatus } }> {} + +export const executorProgramServiceV2 = new ExecutorProgramServiceV2(); +class ProcessData { + log: string | undefined; + status: ProcessStatus; + constructor(log: string | undefined, status: ProcessStatus) { + + if (log !== undefined) { + this.log = log; + } + this.status = status; + + } +} +abstract class Watcher { +} +export abstract class ExecProcessWatcher extends TypedEvent implements Watcher { + logs: string[] = []; + status: ProcessStatus; + constructor() { + super(); + this.on((event) => { + if (event.log !== undefined) { + this.logs.push(event.log); + } + this.status = event.status; + if (event.status.isEqualMany([ProcessStatus.endError, ProcessStatus.endOk])) { + this.result() + } + }) + } + abstract result(): Promise + +} +export class ExecProcessScenarioV2 { + call = (command: string, watcher: ExecProcessWatcher, name?: string): void => { + const process = exec(command, { cwd: new GetRootDirUseCase().call() }); + if (process.pid) { + activeProcessPids[name ?? command] = { pid: process.pid, status: ProcessStatus.run }; + executorProgramServiceV2.emit(activeProcessPids); + } + process.stdout.on('data', (data) => watcher.emit(new ProcessData(data, ProcessStatus.run))) + process.stderr.on('data', (data) => watcher.emit(new ProcessData(data, ProcessStatus.run))); + process.on('close', (code) => { + if (code === 0) { + watcher.emit(new ProcessData(undefined, ProcessStatus.endOk)) + activeProcessPids[name ?? command] = { pid: process.pid ?? 0, status: ProcessStatus.endOk } + executorProgramServiceV2.emit(activeProcessPids); + } else { + watcher.emit(new ProcessData(undefined, ProcessStatus.endError)) + activeProcessPids[name ?? command] = { pid: process.pid ?? 0, status: ProcessStatus.endError }; + executorProgramServiceV2.emit(activeProcessPids); + } + }); + + process.on('error', (err) => watcher.emit(new ProcessData(err.message, ProcessStatus.endError))); + } +} \ No newline at end of file diff --git a/server/src/core/services/executor_program_service.ts b/server/src/core/services/executor_program_service.ts index 2f5dc7b..3b1a609 100644 --- a/server/src/core/services/executor_program_service.ts +++ b/server/src/core/services/executor_program_service.ts @@ -50,7 +50,6 @@ export class ExecutorProgramService }; worker.send(workerDataExec); worker.on("message", (e) => { - console.log(JSON.stringify(e)); const spawnError = SpawnError.isError(e); if (spawnError instanceof SpawnError) { diff --git a/server/src/core/usecases/exec_process_usecase.ts b/server/src/core/usecases/exec_process_usecase.ts index a6899dc..faa140e 100644 --- a/server/src/core/usecases/exec_process_usecase.ts +++ b/server/src/core/usecases/exec_process_usecase.ts @@ -7,6 +7,7 @@ import { ExecutorResult } from "../models/executor_result"; import { ExecutorProgramService } from "../services/executor_program_service"; export const executorProgramService = new ExecutorProgramService(""); + export class KillLastProcessUseCase extends CallbackStrategyWithEmpty { call = async (): Promise> => { executorProgramService.deleteWorker(); diff --git a/server/src/core/usecases/get_root_dir_usecase.ts b/server/src/core/usecases/get_root_dir_usecase.ts new file mode 100644 index 0000000..c134d6f --- /dev/null +++ b/server/src/core/usecases/get_root_dir_usecase.ts @@ -0,0 +1,5 @@ +import * as os from 'os'; + +export class GetRootDirUseCase { + call = (): string => os.homedir() +} \ No newline at end of file diff --git a/server/src/features/behavior_trees/behavior_trees_presentation.ts b/server/src/features/behavior_trees/behavior_trees_presentation.ts index 1ea112a..cff6a73 100644 --- a/server/src/features/behavior_trees/behavior_trees_presentation.ts +++ b/server/src/features/behavior_trees/behavior_trees_presentation.ts @@ -8,6 +8,9 @@ import { SaveBtScenario as FillBtScenario } from "./domain/save_bt_scenario"; import { GetCameraUseCase } from "./domain/get_cameras_usecase"; import { GetRobotsUseCase } from "./domain/get_robots_usecase"; import { GetTopicsUseCase } from "./domain/get_topics_usecase"; +import { DeleteBehaviorTreeScenario } from "./domain/delete_behavior_tree_scenario"; + + export class BehaviorTreesPresentation extends CrudController { constructor() { @@ -43,6 +46,11 @@ export class BehaviorTreesPresentation extends CrudController ( + await new SearchOneDataBaseModelUseCase(ProjectDBModel).call({ isActive: true }, "no active projects") + ).map(async (model) => (await (new DeleteRecursiveFolderUseCase().call(`${model.rootDir}/${StaticFilesProject.behaviorTrees}`))).map(async () => (await new DeleteDataBaseModelUseCase(BehaviorTreeDBModel).call(id)))); +} \ No newline at end of file diff --git a/server/src/features/behavior_trees/models/behavior_tree_validation_model.ts b/server/src/features/behavior_trees/models/behavior_tree_validation_model.ts index e729b67..06f103c 100644 --- a/server/src/features/behavior_trees/models/behavior_tree_validation_model.ts +++ b/server/src/features/behavior_trees/models/behavior_tree_validation_model.ts @@ -2,12 +2,13 @@ import { IsMongoId, IsNumber, IsString } from "class-validator"; import { IBehaviorTreeModel } from "./behavior_tree_database_model"; export class BehaviorTreeValidationModel implements IBehaviorTreeModel { + _id: string; @IsString() public name: string; @IsMongoId() - public project:string; - public skills:any; - public xml:string; + public project: string; + public skills: any; + public xml: string; @IsNumber() public unixTime: number } diff --git a/server/src/features/calculations_instance/calculations_instance_presentation.ts b/server/src/features/calculations_instance/calculations_instance_presentation.ts index 42f8793..d89766d 100644 --- a/server/src/features/calculations_instance/calculations_instance_presentation.ts +++ b/server/src/features/calculations_instance/calculations_instance_presentation.ts @@ -4,6 +4,8 @@ import { CalculationInstanceValidationModel } from "./models/calculations_instan import { CalculationInstanceDBModel } from "./models/calculations_instance_database_model"; import { CreateCalculationInstanceScenario } from "./domain/create_calculation_instance_scenario"; import { DeleteCalculationsInstanceScenario } from "./domain/delete_calculations_instance_scenario"; +import { GetAllEndCalculationsInstanceActiveProjectScenarios } from "./domain/get_all_end_calculations_instance_active_project_scenarios"; + export class CalculationsInstancesPresentation extends CrudController< CalculationInstanceValidationModel, @@ -18,7 +20,12 @@ export class CalculationsInstancesPresentation extends CrudController< super.delete(new DeleteCalculationsInstanceScenario().call); super.post(new CreateCalculationInstanceScenario().call); - + this.subRoutes.push({ + method: "POST", + subUrl: "get/all/end/calculations", + //@ts-expect-error + fn: new GetAllEndCalculationsInstanceActiveProjectScenarios(), + }) this.subRoutes.push({ method: "GET", subUrl: "exec", diff --git a/server/src/features/calculations_instance/domain/exec_calculations_instance_process_scenario.ts b/server/src/features/calculations_instance/domain/exec_calculations_instance_process_scenario.ts index e98afc1..ec28885 100644 --- a/server/src/features/calculations_instance/domain/exec_calculations_instance_process_scenario.ts +++ b/server/src/features/calculations_instance/domain/exec_calculations_instance_process_scenario.ts @@ -7,7 +7,21 @@ import { ProcessWatcherAndDatabaseUpdateService } from "../../datasets/domain/cr import { CalculationInstanceDBModel, ICalculationInstance } from "../models/calculations_instance_database_model"; import { Result } from "../../../core/helpers/result"; import { CreateFileUseCase } from "../../../core/usecases/create_file_usecase"; +import { ExecProcessScenarioV2, ExecProcessWatcher } from "../../../core/scenarios/exec_process_scenario_v2"; +class ExecProcess extends ExecProcessWatcher { + id: string; + constructor(id: string) { + super() + this.id = id; + } + result = async (): Promise => (await new ReadByIdDataBaseModelUseCase(CalculationInstanceDBModel).call(this.id)).map(async (model) => { + model.lastProcessLogs = this.logs.join('\n') + model.processStatus = this.status; + // @ts-ignore + await model.save() + }); +} export class ExecCalculationInstanceProcessScenario extends CallbackStrategyWithIdQuery { idValidationExpression = new MongoIdValidation(); call = async (id: string): ResponseBase => @@ -17,19 +31,15 @@ export class ExecCalculationInstanceProcessScenario extends CallbackStrategyWith return (await new IsHaveActiveProcessUseCase().call()).map(async () => { const execCommand = `${model.script } --path ${model.instancePath.pathNormalize()} --form ${fileOutPath}`.replace("\n", ""); - + await new CreateFileUseCase().call(fileOutPath, Buffer.from(JSON.stringify(model.formBuilder))); await CalculationInstanceDBModel.findById(id).updateOne({ processStatus: "RUN", lastProcessExecCommand: execCommand, }); - new ExecProcessUseCase().call( - // @ts-expect-error - `${model.project.rootDir}/`, - execCommand, - id, - new ProcessWatcherAndDatabaseUpdateService(id as unknown as ObjectId, CalculationInstanceDBModel) - ); + + new ExecProcessScenarioV2().call(execCommand, new ExecProcess(model._id)) + return Result.ok("OK"); }); } diff --git a/server/src/features/calculations_instance/domain/get_all_end_calculations_instance_active_project_scenarios.ts b/server/src/features/calculations_instance/domain/get_all_end_calculations_instance_active_project_scenarios.ts new file mode 100644 index 0000000..b9eb5f3 --- /dev/null +++ b/server/src/features/calculations_instance/domain/get_all_end_calculations_instance_active_project_scenarios.ts @@ -0,0 +1,20 @@ +import { IsString } from "class-validator"; +import { CallbackStrategyWithValidationModel, ResponseBase } from "../../../core/controllers/http_controller"; +import { SearchManyDataBaseModelUseCase } from "../../../core/usecases/search_many_database_model_usecase"; +import { GetActiveProjectIdScenario } from "../../projects/domain/get_active_project_id_scenario"; +import { ICalculationInstance, CalculationInstanceDBModel } from "../models/calculations_instance_database_model"; + + +export class CalculationInstanceType { + @IsString() + type: string; +} + +export class GetAllEndCalculationsInstanceActiveProjectScenarios extends CallbackStrategyWithValidationModel { + validationModel: CalculationInstanceType = new CalculationInstanceType(); + call = async (model: CalculationInstanceType): ResponseBase => (await new GetActiveProjectIdScenario().call()).map( + async (activeProjectModel) => await new SearchManyDataBaseModelUseCase(CalculationInstanceDBModel).call({ project: activeProjectModel.id, isEnd: true, type: model.type }), + ); + + +} diff --git a/server/src/features/calculations_instance/domain/log_to_text_proces_usecase.ts b/server/src/features/calculations_instance/domain/log_to_text_proces_usecase.ts new file mode 100644 index 0000000..bed8aa0 --- /dev/null +++ b/server/src/features/calculations_instance/domain/log_to_text_proces_usecase.ts @@ -0,0 +1,11 @@ +import { CallbackStrategyWithIdQuery, ResponseBase } from "../../../core/controllers/http_controller"; +import { ReadByIdDataBaseModelUseCase } from "../../../core/usecases/read_database_model_usecase"; +import { CoreValidation } from "../../../core/validations/core_validation"; +import { MongoIdValidation } from "../../../core/validations/mongo_id_validation"; +import { ICalculationInstance, CalculationInstanceDBModel } from "../models/calculations_instance_database_model"; + +// export class LogToProcessUseCase extends CallbackStrategyWithIdQuery { +// idValidationExpression: CoreValidation = new MongoIdValidation(); +// call = async (id: string): ResponseBase => (await new ReadByIdDataBaseModelUseCase(CalculationInstanceDBModel).call(id)).map((model) => ); + +// } \ No newline at end of file diff --git a/server/src/features/calculations_instance/models/calculations_instance_database_model.ts b/server/src/features/calculations_instance/models/calculations_instance_database_model.ts index 43e13d9..43a091d 100644 --- a/server/src/features/calculations_instance/models/calculations_instance_database_model.ts +++ b/server/src/features/calculations_instance/models/calculations_instance_database_model.ts @@ -3,6 +3,7 @@ import { FormBuilderValidationModel } from "../../datasets/models/dataset_valida import { IProjectModel, projectSchema } from "../../projects/models/project_model_database_model"; export interface ICalculationInstance { + _id: string; script: string; instancePath: string; formBuilder: FormBuilderValidationModel; @@ -61,6 +62,7 @@ export const CalculationInstanceSchema = new Schema({ instancePath: { type: String, }, + project: { type: Schema.Types.ObjectId, ref: projectSchema, diff --git a/server/src/features/calculations_instance/models/calculations_instance_validation_model.ts b/server/src/features/calculations_instance/models/calculations_instance_validation_model.ts index 46b993c..eb8e6e2 100644 --- a/server/src/features/calculations_instance/models/calculations_instance_validation_model.ts +++ b/server/src/features/calculations_instance/models/calculations_instance_validation_model.ts @@ -4,6 +4,7 @@ import { FormBuilderValidationModel } from "../../datasets/models/dataset_valida import { IProjectModel } from "../../projects/models/project_model_database_model"; export class CalculationInstanceValidationModel implements ICalculationInstance { + _id: string; @IsNotEmpty() @IsString() instanceName: string; diff --git a/server/src/features/datasets/models/dataset_validation_model.ts b/server/src/features/datasets/models/dataset_validation_model.ts index 5486ace..abd6e84 100644 --- a/server/src/features/datasets/models/dataset_validation_model.ts +++ b/server/src/features/datasets/models/dataset_validation_model.ts @@ -15,6 +15,7 @@ export enum ProcessStatus { END = "END", ERROR = "ERROR", NEW = "NEW", + } export interface IDatasetModel { _id?: string; diff --git a/server/src/features/digital_twins_instance/domain/exec_instance_scenario.ts b/server/src/features/digital_twins_instance/domain/exec_instance_scenario.ts index f4ec799..5171e07 100644 --- a/server/src/features/digital_twins_instance/domain/exec_instance_scenario.ts +++ b/server/src/features/digital_twins_instance/domain/exec_instance_scenario.ts @@ -21,8 +21,7 @@ export class ExecInstanceScenario extends CallbackStrategyWithIdQuery { await new ReadByIdDataBaseModelUseCase(DigitalTwinsInstanceDatabaseModel).call(id) ).map( (document) => ( - console.log('DOCUMeNT PATH'), - console.log(document.instancePath.pathNormalize()), + new ExecProcessUseCase().call( document.instancePath, `python3 $GET_INTERFACES --path ${document.instancePath.pathNormalize()} --package '${JSON.stringify( diff --git a/server/src/features/projects/domain/get_active_project_id_scenario.ts b/server/src/features/projects/domain/get_active_project_id_scenario.ts index 0e06a58..74267b3 100644 --- a/server/src/features/projects/domain/get_active_project_id_scenario.ts +++ b/server/src/features/projects/domain/get_active_project_id_scenario.ts @@ -4,9 +4,8 @@ import { SearchOneDataBaseModelUseCase } from "../../../core/usecases/search_dat import { IProjectModel, ProjectDBModel } from "../models/project_model_database_model"; export class GetActiveProjectIdScenario extends CallbackStrategyWithEmpty { - async call(): Promise> { - return ( - await new SearchOneDataBaseModelUseCase(ProjectDBModel).call({ isActive: true }, "no active projects") - ).map((model) => Result.ok({ id: model._id })); - } + call = async (): Promise> => ( + await new SearchOneDataBaseModelUseCase(ProjectDBModel).call({ isActive: true }, "no active projects") + ).map((model) => Result.ok({ id: model._id })); } + diff --git a/server/src/features/projects/domain/robossembler_assets_network_mapper_scenario.ts b/server/src/features/projects/domain/robossembler_assets_network_mapper_scenario.ts index 8b24b30..368b00b 100644 --- a/server/src/features/projects/domain/robossembler_assets_network_mapper_scenario.ts +++ b/server/src/features/projects/domain/robossembler_assets_network_mapper_scenario.ts @@ -35,7 +35,6 @@ export class RobossemblerAssetsNetworkMapperScenario extends CallbackStrategyWit "/assets/"; model.map((el) => { const assetLibsAddress = assetAddress + "/libs/objects/" + el.name; - console.log(assetLibsAddress); el.stlUrl = `${assetAddress}${el.part_path}`; el.glUrl = `${assetLibsAddress}.glb`; el.daeUrl = `${assetLibsAddress}.dae`; diff --git a/server/src/features/runtime/domain/exec_bt_builder_usecase.ts b/server/src/features/runtime/domain/exec_bt_builder_usecase.ts index 66ea474..dd243ec 100644 --- a/server/src/features/runtime/domain/exec_bt_builder_usecase.ts +++ b/server/src/features/runtime/domain/exec_bt_builder_usecase.ts @@ -1,7 +1,53 @@ -import { CallbackStrategyWithEmpty, ResponseBase } from "../../../core/controllers/http_controller"; +import { + CallbackStrategyWithIdQuery, + CallbackStrategyWithValidationModel, + ResponseBase, +} from "../../../core/controllers/http_controller"; +import { StaticFilesProject } from "../../../core/models/static_files"; +import { + ExecProcessScenarioV2, + ExecProcessWatcher, + activeProcessPids, +} from "../../../core/scenarios/exec_process_scenario_v2"; +import { ReadByIdDataBaseModelScenario } from "../../../core/scenarios/read_by_id_database_model_scenario"; +import { SearchOneDataBaseModelUseCase } from "../../../core/usecases/search_database_model_usecase"; +import { CoreValidation } from "../../../core/validations/core_validation"; +import { MongoIdValidation } from "../../../core/validations/mongo_id_validation"; +import { BehaviorTreeDBModel } from "../../behavior_trees/models/behavior_tree_database_model"; +import { BehaviorTreeValidationModel } from "../../behavior_trees/models/behavior_tree_validation_model"; +import { IProjectModel, ProjectDBModel } from "../../projects/models/project_model_database_model"; +import { GetCommandScenario } from "./get_command_scenario"; -export class ExecBtBuilderUseCase extends CallbackStrategyWithEmpty { - call(): ResponseBase { - throw new Error("Method not implemented."); +class ProcessWatcher extends ExecProcessWatcher { + async result(): Promise { + console.log(this); } } + +export enum Proceed { + EXEC_BT = "EXEC_BT", +} +export class ExecBtScenario extends CallbackStrategyWithIdQuery { + idValidationExpression: CoreValidation = new MongoIdValidation(); + call = async (id: string): ResponseBase => + (await new ReadByIdDataBaseModelScenario(BehaviorTreeDBModel).call(id)).map( + async (behaviorTreeValidationModel) => + ( + await new SearchOneDataBaseModelUseCase(ProjectDBModel).call( + { isActive: true }, + "no active projects" + ) + ).map(async (model) => + (await new GetCommandScenario().call("btRuntimeProcess")).map((execProcessData) => + new ExecProcessScenarioV2().call( + execProcessData.execCommand.replace( + "${bt_path}", + `${model.rootDir}/${StaticFilesProject.behaviorTrees}/${behaviorTreeValidationModel.name}/bt.xml` + ), + new ProcessWatcher(), + Proceed.EXEC_BT + ) + ) + ) + ); +} diff --git a/server/src/features/runtime/domain/get_run_time_statuses_usecase.ts b/server/src/features/runtime/domain/get_run_time_statuses_usecase.ts new file mode 100644 index 0000000..2967c31 --- /dev/null +++ b/server/src/features/runtime/domain/get_run_time_statuses_usecase.ts @@ -0,0 +1,7 @@ +import { CallbackStrategyWithEmpty, ResponseBase } from "../../../core/controllers/http_controller"; +import { Result } from "../../../core/helpers/result"; +import { activeProcessPids } from "../../../core/scenarios/exec_process_scenario_v2"; + +export class GetRunTimeStatuses extends CallbackStrategyWithEmpty { + call = async (): ResponseBase => Result.ok(activeProcessPids); +} \ No newline at end of file diff --git a/server/src/features/runtime/domain/get_simulation_state_usecase.ts b/server/src/features/runtime/domain/get_simulation_state_usecase.ts deleted file mode 100644 index 4658739..0000000 --- a/server/src/features/runtime/domain/get_simulation_state_usecase.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { CallbackStrategyWithEmpty, ResponseBase } from "../../../core/controllers/http_controller"; -import { Result } from "../../../core/helpers/result"; -import { SpawnProcessUseCase } from "../../../core/usecases/exec_process_usecase"; -import { GetCommandScenario } from "./get_command_scenario"; -import { ProcessWatcher } from "../service/process_watcher"; -import { App } from "../../../core/controllers/app"; - -export class GetSimulationStateScenario extends CallbackStrategyWithEmpty { - call = async (): ResponseBase => Result.ok(""); -} diff --git a/server/src/features/runtime/domain/stop_process_usecase.ts b/server/src/features/runtime/domain/stop_process_usecase.ts new file mode 100644 index 0000000..4e52d90 --- /dev/null +++ b/server/src/features/runtime/domain/stop_process_usecase.ts @@ -0,0 +1,51 @@ +import { IsString } from "class-validator"; +import { CallbackStrategyWithValidationModel, ResponseBase } from "../../../core/controllers/http_controller"; +import { ProcessStatus, activeProcessPids } from "../../../core/scenarios/exec_process_scenario_v2"; +import { Result } from "../../../core/helpers/result"; +import { GetRootDirUseCase } from "../../../core/usecases/get_root_dir_usecase"; +import { exec } from 'child_process'; + + +export class StopProcessModel { + @IsString() + pid: string +} + +export class StopProcessUseCase extends CallbackStrategyWithValidationModel { + validationModel: StopProcessModel = new StopProcessModel(); + call = async (model: StopProcessModel): ResponseBase => { + try { + if (activeProcessPids[model.pid] === undefined) { + return Result.error('missing pid'); + } + const processKillStatus = await new Promise((resolve, reject) => { + try { + exec(`kill ${activeProcessPids[model.pid].pid}`, { cwd: new GetRootDirUseCase().call() }, (error, stdout, stderr) => { + if (error) { + reject(`Ошибка: ${stderr}`); return; + } + if (stderr) { + reject(`Ошибка: ${stderr}`); + return; + } + + resolve('Process kill') + + }); + } catch (e) { + resolve('Process kill') + } + + }) + if (processKillStatus == 'Process kill') { + activeProcessPids[model.pid].status = ProcessStatus.userDelete; + } + return Result.ok(processKillStatus); + } catch (error) { + return Result.ok(error) + } + + + } + +} \ No newline at end of file diff --git a/server/src/features/runtime/model/command.ts b/server/src/features/runtime/model/command.ts index 863d14e..3334f20 100644 --- a/server/src/features/runtime/model/command.ts +++ b/server/src/features/runtime/model/command.ts @@ -11,4 +11,4 @@ export interface ExecProcess { delay: number; checkCommand: null; filter: null; -} +} \ No newline at end of file diff --git a/server/src/features/runtime/runtime_presentation.ts b/server/src/features/runtime/runtime_presentation.ts index 335deee..9804b39 100644 --- a/server/src/features/runtime/runtime_presentation.ts +++ b/server/src/features/runtime/runtime_presentation.ts @@ -1,42 +1,19 @@ -import { CrudController } from "../../core/controllers/crud_controller"; + import { ExecRunTimeCommandValidationModel } from "./model/run_time_validation_model"; -import { ExecRuntimeDatabaseModel } from "./model/run_time_database_model"; -import { CoreHttpController, SubRouter, HttpMethodType, CallbackStrategyWithIdQuery, ResponseBase } from "../../core/controllers/http_controller"; -import { ExecBtBuilderUseCase } from "./domain/exec_bt_builder_usecase"; -import { ExecSimulationUseCase } from "./domain/exec_simulation_usecase"; -import { GetBtBuilderStateUseCase } from "./domain/get_bt_builder_status_usecase"; -import { GetSimulationStateScenario } from "./domain/get_simulation_state_usecase"; -import { MongoIdValidation } from "../../core/validations/mongo_id_validation"; -import { CoreValidation } from "../../core/validations/core_validation"; -import { ReadByIdDataBaseModelUseCase } from "../../core/usecases/read_by_id_database_model_usecase"; -import { ICalculationInstance, CalculationInstanceDBModel } from "../calculations_instance/models/calculations_instance_database_model"; -import { Result } from "../../core/helpers/result"; -import { SpawnProcessUseCase } from "../../core/usecases/exec_process_usecase"; -import { ProcessWatcher } from "./service/process_watcher"; +import { CoreHttpController, SubRouter, CallbackStrategyWithIdQuery } from "../../core/controllers/http_controller"; +import { ExecBtScenario } from "./domain/exec_bt_builder_usecase"; +import { GetRunTimeStatuses } from "./domain/get_run_time_statuses_usecase"; +import { StopProcessUseCase } from "./domain/stop_process_usecase"; -class ExecAnalyzeScenario extends CallbackStrategyWithIdQuery { - idValidationExpression: CoreValidation = new MongoIdValidation() - call = async (id: string) => - (await new ReadByIdDataBaseModelUseCase(CalculationInstanceDBModel).call(id)).map(async (model) => - (await new SpawnProcessUseCase().call('/Users/idontsudo/webservice', `nix run github:nixos/nixpkgs#python312Packages.tensorboard -- --logdir ${model.instancePath}`, "", new ProcessWatcher())).map(() => Result.ok('ok'),), - ) -} - -export class RunTimePresentation extends CrudController { +export class RunTimePresentation extends CoreHttpController { constructor() { super({ url: "run_time", }); - this.subRoutes.push(new SubRouter("POST", "/exec/bt/builder", new ExecBtBuilderUseCase())); - this.subRoutes.push(new SubRouter("POST", "/get/bt/builder/state", new GetBtBuilderStateUseCase())); - this.subRoutes.push(new SubRouter("POST", "/get/simulator/state", new GetSimulationStateScenario())); - this.subRoutes.push(new SubRouter('POST', "exec/analyze", new ExecAnalyzeScenario())) - this.subRoutes.push({ - method: "POST", - subUrl: "/exec/simulation/", - fn: new ExecSimulationUseCase(), - }); + this.subRoutes.push(new SubRouter("POST", "exec/bt", new ExecBtScenario())); + this.subRoutes.push(new SubRouter("GET", "status", new GetRunTimeStatuses())); + this.subRoutes.push(new SubRouter("POST", "kill", new StopProcessUseCase())) } } diff --git a/server/src/main.ts b/server/src/main.ts index 31d168e..d88e058 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -3,11 +3,11 @@ import { App } from "./core/controllers/app"; import { SocketSubscriber } from "./core/controllers/socket_controller"; import { extensions } from "./core/extensions/extensions"; import { httpRoutes } from "./core/controllers/routes"; -import { SpawnProcessUseCase, executorProgramService } from "./core/usecases/exec_process_usecase"; -import { ProcessWatcher } from "./features/runtime/service/process_watcher"; +import { executorProgramService } from "./core/usecases/exec_process_usecase"; +import { executorProgramServiceV2 } from "./core/scenarios/exec_process_scenario_v2"; extensions(); -const socketSubscribers = [new SocketSubscriber(executorProgramService, "realtime")]; +const socketSubscribers = [new SocketSubscriber(executorProgramService, "realtime"), new SocketSubscriber(executorProgramServiceV2, 'realtimeV2',)]; new App(httpRoutes, socketSubscribers).listen(); diff --git a/ui/src/core/extensions/array.ts b/ui/src/core/extensions/array.ts index 33ec3d8..2c4cf2a 100644 --- a/ui/src/core/extensions/array.ts +++ b/ui/src/core/extensions/array.ts @@ -1,7 +1,14 @@ +/* eslint-disable no-extend-native */ import { Result } from "../helper/result"; /* eslint-disable @typescript-eslint/no-this-alias */ export const ArrayExtensions = () => { + if (Array.prototype.plus === undefined) { + Array.prototype.plus = function (array) { + array.forEach((el) => this.push(el)) + return this; + } + } if ([].indexOfR === undefined) { Array.prototype.indexOfR = function (element) { if (this.indexOf(element) === -1) { @@ -10,6 +17,11 @@ export const ArrayExtensions = () => { return Result.ok(this); }; } + if ([].whereOne === undefined) { + Array.prototype.whereOne = function (predicate) { + return this.filter(predicate).atR(0); + } + } if ([].atR === undefined) { Array.prototype.atR = function (index) { if (index === undefined) { diff --git a/ui/src/core/extensions/extensions.ts b/ui/src/core/extensions/extensions.ts index 5afcbed..2eb2ff7 100644 --- a/ui/src/core/extensions/extensions.ts +++ b/ui/src/core/extensions/extensions.ts @@ -30,6 +30,8 @@ declare global { someR(predicate: (value: T) => boolean): Result>; updateAll(value: Partial): Array; atR(index: number | undefined): Result; + whereOne(predicate: (value: T) => boolean): Result; + plus(array: any[]): Array } interface Date { formatDate(): string; diff --git a/ui/src/core/model/form_builder_validation_model.tsx b/ui/src/core/model/form_builder_validation_model.tsx index f858f40..4f1837b 100644 --- a/ui/src/core/model/form_builder_validation_model.tsx +++ b/ui/src/core/model/form_builder_validation_model.tsx @@ -1,23 +1,15 @@ import { IsNotEmpty, IsString } from "class-validator"; import { BehaviorTreeBuilderStore } from "../../features/behavior_tree_builder/presentation/behavior_tree_builder_store"; -import { - datasetFormMockContext, - datasetFormMockResult, - defaultFormValue, -} from "../../features/dataset/dataset_model"; +import { datasetFormMockContext, datasetFormMockResult, defaultFormValue } from "../../features/dataset/dataset_model"; import { DependencyViewModel } from "./skill_model"; import { ValidationModel } from "./validation_model"; import { FormType } from "./form"; import makeAutoObservable from "mobx-store-inheritance"; -export class FormBuilderValidationModel - extends ValidationModel - implements DependencyViewModel -{ +export class FormBuilderValidationModel extends ValidationModel implements DependencyViewModel { @IsNotEmpty() @IsString() public result: string; - @IsNotEmpty() @IsString() public context: string; public form: string[]; @@ -36,22 +28,26 @@ export class FormBuilderValidationModel formBuilderValidationModel.context.isEmpty() && formBuilderValidationModel.result.isEmpty() && formBuilderValidationModel.form.isEmpty(); - static test = () => - new FormBuilderValidationModel(ffContext, ff1Result, [], ""); + static test = () => new FormBuilderValidationModel(ffContext, ff1Result, [], ""); static datasetEmpty = () => - new FormBuilderValidationModel( - datasetFormMockContext, - datasetFormMockResult, - [], - defaultFormValue - ); + new FormBuilderValidationModel(datasetFormMockContext, datasetFormMockResult, [], defaultFormValue); static empty = () => new FormBuilderValidationModel("", "", [], ""); - static emptyTest = () => - new FormBuilderValidationModel(``, ``, [], defaultFormValue); - static creteDataSetTest = () => - new FormBuilderValidationModel(``, scene, [], ""); - static emptySimple = () => - new FormBuilderValidationModel("", simpleFormBuilder, [], ""); + static emptyTest = () => new FormBuilderValidationModel(``, ``, [], defaultFormValue); + static creteDataSetTest = () => new FormBuilderValidationModel(``, scene, [], ""); + static emptySimple = () => new FormBuilderValidationModel("", simpleFormBuilder, [], ""); + static eee = () => + new FormBuilderValidationModel( + ``, + `{ + "robot_name": \${ROBOT_NAME:string:rbs_arm}, + "pose": { + "position": { "x": \${X:number:0.1}, "y": \${Y:number:0.1}, "z": \${Z:number:0.7} }, + "orientation": { "x": \${X:number:0.1}, "y": \${Y:number:0.1}, "z": \${Z:number:0.7} } + } + }`, + [], + "" + ); static vision = () => new FormBuilderValidationModel( ` @@ -96,6 +92,7 @@ export class FormBuilderValidationModel };`, ` { + "process":\${:OBJECT:{"type": "OBJECT_DETECTION"}, "datasetObjects":\${:OBJECT:{"details": []}, "typedataset": \${typedataset:Enum:ObjectDetection}, "models_randomization":{ @@ -142,3 +139,6 @@ export const ff1Result = `{ "empty":\${NAME:string:default}, "params": \${ITEM:Array:[]} }`; +// { +// "process":\${:OBJECT:{"type": "OBJECT_DETECTION"} +// } \ No newline at end of file diff --git a/ui/src/core/model/skill_model.ts b/ui/src/core/model/skill_model.ts index d9e3a40..cbc311d 100644 --- a/ui/src/core/model/skill_model.ts +++ b/ui/src/core/model/skill_model.ts @@ -36,7 +36,7 @@ export interface IWeightsDependency { export interface IDeviceDependency { sid: string; } -export interface IDependency {} +export interface IDependency { } export interface IParam { isFilled: boolean; type: string; @@ -276,7 +276,7 @@ export class SkillModel extends ValidationModel implements ISkill { } export class SkillDependency implements IDependency { - constructor(public skills: ISkillDependency[]) {} + constructor(public skills: ISkillDependency[]) { } static empty() { return new SkillDependency([]); } @@ -424,24 +424,27 @@ export class Skills { .flat(1) .filter((el) => el !== ""); - getDependencyBySkillLabelAndType = (skillType: string, sid: string): DependencyViewModel => - this.skills - .reduce((acc, skill) => { - if (skill.sid?.isEqual(sid)) { - skill.BTAction.map((action) => { - action.param.map((param) => { - if (param.type.isEqualR(skillType)) { - acc.push(param?.dependency ?? DependencyViewModel.empty()); - } - return param; - }); - return action; - }); - } + getDependencyBySkillLabelAndType = (skillType: string, sid: string): DependencyViewModel => this.skills + .reduce((acc, skill) => { + if (skill.sid?.isEqual(sid)) { + skill.BTAction.map((action) => { + action.param.map((param) => { - return acc; - }, []) - .at(0) ?? DependencyViewModel.empty(); + if (param.type.isEqual(skillType)) { + + + acc.push(param?.dependency ?? DependencyViewModel.empty()); + } + + return param; + }); + return action; + }); + } + + return acc; + }, []) + .at(0) ?? DependencyViewModel.empty() static isEmpty(model: Skills): Result { if (model.skills.isEmpty()) { return Result.error(undefined); diff --git a/ui/src/core/model/validation_model.ts b/ui/src/core/model/validation_model.ts index 7e41b5e..f28d353 100644 --- a/ui/src/core/model/validation_model.ts +++ b/ui/src/core/model/validation_model.ts @@ -1,3 +1,4 @@ +import { message } from "antd"; import { Result } from "../helper/result"; import { validate, ValidationError } from "class-validator"; @@ -26,4 +27,12 @@ export class ValidationModel { return Result.ok(this as unknown as T); } }; + validMessage = async(): Promise> => { + const result = await this.valid(); + + if (result.isFailure()) { + message.error(result.error); + } + return result; + } } diff --git a/ui/src/core/repository/core_http_repository.ts b/ui/src/core/repository/core_http_repository.ts index 48eb520..37ed2bb 100644 --- a/ui/src/core/repository/core_http_repository.ts +++ b/ui/src/core/repository/core_http_repository.ts @@ -132,7 +132,7 @@ export class CoreHttpRepository extends HttpRepository { getAssetsActiveProject = async (): Promise> => { return this._jsonRequest(HttpMethod.GET, "/projects/assets"); }; - + getSceneAsset = (id: string) => this._jsonToClassInstanceRequest(HttpMethod.GET, `/scenes/by_id?id=${id}`, SceneAsset) as Promise< Result @@ -141,5 +141,6 @@ export class CoreHttpRepository extends HttpRepository { return this._jsonRequest(HttpMethod.GET, "/projects/get/active/project/id"); } getAllScenes = () => this._jsonRequest(HttpMethod.GET, "/scenes"); + getAllTopics = () => this._jsonRequest(HttpMethod.GET, `/topics`); + } - \ No newline at end of file diff --git a/ui/src/core/repository/core_socket_repository.ts b/ui/src/core/repository/core_socket_repository.ts index db8d3eb..acd0e32 100644 --- a/ui/src/core/repository/core_socket_repository.ts +++ b/ui/src/core/repository/core_socket_repository.ts @@ -1,23 +1,27 @@ import { Socket, io } from "socket.io-client"; import { Result } from "../helper/result"; import { TypedEvent } from "../helper/typed_event"; +import { RuntimeModel } from "../../features/behavior_tree_builder/presentation/ui/actions/runtime_actions"; export class SocketRepository extends TypedEvent { serverURL = "ws://localhost:4001"; socket: Socket | undefined; - - async connect():Promise> { + + async connect(): Promise> { const socket = io(this.serverURL); - + this.socket = socket; socket.connect(); - socket.on('realtime', (d) =>{ + socketCoreInstances.forEach((el) => { + socket.on(el.event, (event) => el.emit(event)) + }) + socket.on('realtime', (d) => { this.emit({ - event:"realtime", - payload:d + event: "realtime", + payload: d }) }) - if(socket.connected){ + if (socket.connected) { return Result.ok(true) } return Result.error(false) @@ -25,4 +29,15 @@ export class SocketRepository extends TypedEvent { } -export const socketRepository = new SocketRepository() \ No newline at end of file +export const socketRepository = new SocketRepository() + + +export abstract class SocketCore extends TypedEvent { + abstract event: string + +} +export class RunTimeSocketRepository extends SocketCore { + event = 'realtimeV2'; +} +export const runTimeSocketRepository = new RunTimeSocketRepository(); +const socketCoreInstances: SocketCore[] = [runTimeSocketRepository] \ No newline at end of file diff --git a/ui/src/core/store/base_store.ts b/ui/src/core/store/base_store.ts index b9562f6..d9eafaf 100644 --- a/ui/src/core/store/base_store.ts +++ b/ui/src/core/store/base_store.ts @@ -17,6 +17,7 @@ interface IMessage { errorMessage?: string; } export abstract class UiLoader { + navigate?: NavigateFunction; isLoading = false; async httpHelper(callBack: Promise>) { this.isLoading = true; @@ -75,7 +76,7 @@ export abstract class UiErrorState extends UiLoader { console.log(error); }; abstract init(navigate?: NavigateFunction): Promise; - dispose() {} + dispose() { } errors: UiBaseError[] = []; } @@ -126,7 +127,17 @@ export abstract class FormState extends UiErrorState { loadClassInstance = (instance: ClassConstructor, viewModel: V) => { this.viewModel = plainToInstance(instance, viewModel); }; -} - + isModalOpen: boolean = false; + modalShow = () => { + this.isModalOpen = true; + }; + + modalClickOk = () => { + this.isModalOpen = false; + }; + + modalCancel = () => { + this.isModalOpen = false; + }; +} - \ No newline at end of file diff --git a/ui/src/core/ui/drawer/drawer.tsx b/ui/src/core/ui/drawer/drawer.tsx index d825d59..14f9f5d 100644 --- a/ui/src/core/ui/drawer/drawer.tsx +++ b/ui/src/core/ui/drawer/drawer.tsx @@ -7,20 +7,20 @@ export const DrawerV2: React.FC<{ title?: string; onClose: () => void; children: React.ReactNode; -}> = ({ isOpen, onClose, children, title }) => { + width?: number; +}> = ({ isOpen, onClose, children, title, width }) => { return (
diff --git a/ui/src/core/ui/form_builder/form_builder.tsx b/ui/src/core/ui/form_builder/form_builder.tsx index 7376f86..22abe72 100644 --- a/ui/src/core/ui/form_builder/form_builder.tsx +++ b/ui/src/core/ui/form_builder/form_builder.tsx @@ -1,9 +1,5 @@ import * as React from "react"; -import { - FormViewModel, - InputBuilderViewModel, - InputType, -} from "./form_view_model"; +import { FormViewModel, InputBuilderViewModel, InputType } from "./form_view_model"; import { observer } from "mobx-react-lite"; import { FormBuilderStore } from "./form_builder_store"; import { CoreSelect } from "../select/select"; @@ -13,219 +9,162 @@ import { CoreText, CoreTextType } from "../text/text"; import { getFormBuilderComponents } from "./forms/form_builder_components"; import { FormBuilderValidationModel } from "../../model/form_builder_validation_model"; -export interface IFormBuilder { - formBuilder: FormBuilderValidationModel; - onChange: (change: FormBuilderValidationModel) => void; -} +export const FormBuilder = observer( + (props: { formBuilder: FormBuilderValidationModel; onChange: (change: FormBuilderValidationModel) => void }) => { + const [store] = React.useState(() => new FormBuilderStore()); -export const FormBuilder = observer((props: IFormBuilder) => { - const [store] = React.useState(() => new FormBuilderStore()); + React.useEffect(() => { + store.init(props.formBuilder.context, props.formBuilder.result); + if (props.formBuilder.form.isNotEmpty()) { + store.formViewModel = new FormViewModel( + props.formBuilder.form.map((el) => InputBuilderViewModel.fromJSON(el)), + props.formBuilder.result, + props.formBuilder.context + ); + props.formBuilder.form.map((el) => InputBuilderViewModel.fromJSON(el)); + } + store.changerForm.on((event) => { + if (event) props.onChange(event); + }); + }, []); - React.useEffect(() => { - store.init(props.formBuilder.context, props.formBuilder.result); - if (props.formBuilder.form.isNotEmpty()) { - store.formViewModel = new FormViewModel( - props.formBuilder.form.map((el) => InputBuilderViewModel.fromJSON(el)), - props.formBuilder.result, - props.formBuilder.context - ); - props.formBuilder.form.map((el) => InputBuilderViewModel.fromJSON(el)); - } - store.changerForm.on((event) => { - if (event) props.onChange(event); - }); - }, []); + return ( +
+ {store.isError ? ( + <>Error + ) : ( +
+ {store.formViewModel?.inputs?.map((element, index) => { + if (element.type?.isEqual(InputType.ENUM)) { + const values = element.values as string[]; + return ( + store.changeTotalValue(element.id, value)} + label={element.name} + style={{ margin: 20 }} + /> + ); + } + if (element.type?.isEqual(InputType.ARRAY)) { + return ( +
+
{ + store.open(element.id); + }} + > + + +
- return ( -
- {store.isError ? ( - <>Error - ) : ( -
- {store.formViewModel?.inputs?.map((element, index) => { - if (element.type?.isEqual(InputType.ENUM)) { - const values = element.values as string[]; - return ( - - store.changeTotalValue(element.id, value) - } - label={element.name} - style={{ margin: 20 }} - /> - ); - } - if (element.type?.isEqual(InputType.ARRAY)) { - return ( -
-
{ - store.open(element.id); - }} - > - - -
+ {element.isOpen ? ( +
+ {element.totalValue instanceof Array + ? element.totalValue?.map((subArray, index) => { + return ( +
+
+ + store.deleteTotalValueSubItem(element.id, index)} + /> +
- {element.isOpen ? ( -
- {element.totalValue instanceof Array - ? element.totalValue?.map((subArray, index) => { - return ( -
-
- - - store.deleteTotalValueSubItem( - element.id, - index - ) - } - /> -
- - {subArray.map( - ( - subSubArrayItem: InputBuilderViewModel, - subIndex: number - ) => { - if ( - subSubArrayItem.type.isEqual( - InputType.ENUM - ) - ) { + {subArray.map((subSubArrayItem: InputBuilderViewModel, subIndex: number) => { + if (subSubArrayItem.type.isEqual(InputType.ENUM)) { return ( <> String(el) - ) ?? [] - } - value={ - subSubArrayItem.totalValue ?? - subSubArrayItem.defaultValue - } - onChange={(value) => console.log(subSubArrayItem.id) - } + items={subSubArrayItem.values?.map((el) => String(el)) ?? []} + value={subSubArrayItem.totalValue ?? subSubArrayItem.defaultValue} + onChange={(value) => console.log(subSubArrayItem.id)} label={element.name} style={{ margin: 5 }} /> ); } - if ( - subSubArrayItem.type.isEqualMany([ - InputType.NUMBER, - InputType.STRING, - ]) - ) + if (subSubArrayItem.type.isEqualMany([InputType.NUMBER, InputType.STRING])) return (
- store.changeTotalSubValue( - element.id, - subIndex, - e, - index - ) - } + onChange={(e) => store.changeTotalSubValue(element.id, subIndex, e, index)} validation={ - subSubArrayItem.type.isEqual( - InputType.NUMBER - ) + subSubArrayItem.type.isEqual(InputType.NUMBER) ? (el) => Number().isValid(el) : undefined } error="только числа" - value={ - subSubArrayItem.totalValue ?? - subSubArrayItem.defaultValue - } + value={subSubArrayItem.totalValue ?? subSubArrayItem.defaultValue} label={subSubArrayItem.name} />
); return <>Error; - } - )} -
- ); - }) - : null} -
- ) : null} -
- ); - } + })} +
+ ); + }) + : null} +
+ ) : null} +
+ ); + } - if (element.type?.isEqualMany([InputType.NUMBER, InputType.STRING])) - return ( -
- Number().isValid(el) - : undefined - } - onChange={(e) => { - store.changeTotalValue(element.id, e); - }} - value={element.totalValue ?? element.defaultValue} - error="только числа" - label={element.name} - style={{ margin: 20 }} - /> -
- ); - if (element.type?.isEqual(InputType.OBJECT)) - return ( - <> - {getFormBuilderComponents( - element.name - .replace(">", "") - .replace("<", "") - .replace("/", ""), - element.totalValue ?? element.defaultValue, - (text) => store.changeTotalValue(element.id, text) - ).fold( - (s) => ( - <>{s} - ), - (error) => ( - <>{error} - ) - )} - - ); + if (element.type?.isEqualMany([InputType.NUMBER, InputType.STRING])) + return ( +
+ Number().isValid(el) : undefined} + onChange={(e) => { + store.changeTotalValue(element.id, e); + }} + value={element.totalValue ?? element.defaultValue} + error="только числа" + label={element.name} + style={{ margin: 20 }} + /> +
+ ); + if (element.type?.isEqual(InputType.OBJECT)) + return ( + <> + {getFormBuilderComponents( + element.name.replace(">", "").replace("<", "").replace("/", ""), + element.totalValue ?? element.defaultValue, + (text) => store.changeTotalValue(element.id, text) + ).fold( + (s) => ( + <>{s} + ), + (error) => ( + <>{error} + ) + )} + + ); return
Error {String(element)}
; - })} -
- )} -
- ); -}); + })} +
+ )} +
+ ); + } +); diff --git a/ui/src/core/ui/form_builder/form_view_model.ts b/ui/src/core/ui/form_builder/form_view_model.ts index ec5e61b..a542e55 100644 --- a/ui/src/core/ui/form_builder/form_view_model.ts +++ b/ui/src/core/ui/form_builder/form_view_model.ts @@ -95,7 +95,7 @@ export class FormViewModel { .replace(/[^\x00-\x7F]/g, "") .replaceAll("\n", "") .replaceAll("\\", "") - // .replaceAll("/", "") + // .replaceAll("/", "") ); } catch (error) { console.log("ERROR: FormViewModel json() " + result); @@ -190,8 +190,9 @@ export class FormViewModel { }); return result as unknown as string; } - static fromString(result: string, context: string): Result { + static fromString(result: string = '', context: string = ''): Result { try { + if (result.isEmpty() && context.isEmpty()) { return Result.error(undefined); } diff --git a/ui/src/core/ui/form_builder/forms/form_builder_components.tsx b/ui/src/core/ui/form_builder/forms/form_builder_components.tsx index ec30844..1e40957 100644 --- a/ui/src/core/ui/form_builder/forms/form_builder_components.tsx +++ b/ui/src/core/ui/form_builder/forms/form_builder_components.tsx @@ -1,10 +1,10 @@ import { Result } from "../../../helper/result"; -import { SelectDatasetScreen } from "./select_dataset/presentation/select_dataset_screen"; +import { SelectProcess } from "./select_dataset/presentation/select_process"; import { SelectDetail } from "./select_detail/presentation/select_detail_screen"; export enum FormBuilderComponents { SelectDetail = "SelectDetail", - SelectDataset = "SelectDataset", + SelectProcess = "SelectProcess", } export interface IFormBuilderComponentsProps { dependency: T; @@ -18,8 +18,8 @@ export const getFormBuilderComponents = ( if (name.isEqual(FormBuilderComponents.SelectDetail)) { return Result.ok(); } - if (name.isEqual(FormBuilderComponents.SelectDataset)) { - return Result.ok(); + if (name.isEqual(FormBuilderComponents.SelectProcess)) { + return Result.ok(); } return Result.error(name); }; diff --git a/ui/src/core/ui/form_builder/forms/select_dataset/data/select_dataset_repository.tsx b/ui/src/core/ui/form_builder/forms/select_dataset/data/select_dataset_repository.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/ui/src/core/ui/form_builder/forms/select_dataset/data/select_process_repository.ts b/ui/src/core/ui/form_builder/forms/select_dataset/data/select_process_repository.ts new file mode 100644 index 0000000..b511865 --- /dev/null +++ b/ui/src/core/ui/form_builder/forms/select_dataset/data/select_process_repository.ts @@ -0,0 +1,5 @@ +import { CoreHttpRepository, HttpMethod } from "../../../../../repository/core_http_repository"; + +export class SelectProcessRepository extends CoreHttpRepository { + getAllProcessByType = (type: string) => this._jsonRequest(HttpMethod.POST, '/calculations/instances/get/all/end/calculations', { type: type }) +} \ No newline at end of file diff --git a/ui/src/core/ui/form_builder/forms/select_dataset/model/select_dataset_model.ts b/ui/src/core/ui/form_builder/forms/select_dataset/model/select_dataset_model.ts deleted file mode 100644 index e69de29..0000000 diff --git a/ui/src/core/ui/form_builder/forms/select_dataset/model/select_process_model.ts b/ui/src/core/ui/form_builder/forms/select_dataset/model/select_process_model.ts new file mode 100644 index 0000000..f6e8ca1 --- /dev/null +++ b/ui/src/core/ui/form_builder/forms/select_dataset/model/select_process_model.ts @@ -0,0 +1,18 @@ +import { IsObject, IsString } from "class-validator"; +import { ValidationModel } from "../../../../../model/validation_model"; +import { CalculationModel } from "../../../../../../features/calculation_instance/model/calculation_model"; + + +export class SelectProcessModel extends ValidationModel { + @IsString() + type: string = ''; + @IsObject() + selectProcess?: SelectProcess; +} + + + +export interface SelectProcess { + value: CalculationModel; +} + \ No newline at end of file diff --git a/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_dataset_screen.tsx b/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_dataset_screen.tsx deleted file mode 100644 index 31d57a6..0000000 --- a/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_dataset_screen.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { observer } from "mobx-react-lite"; -import { IFormBuilderComponentsProps } from "../../form_builder_components"; - -export const SelectDatasetScreen = observer((props:IFormBuilderComponentsProps) => { - return <>SELECT DATASET; -}); diff --git a/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_dataset_store.ts b/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_dataset_store.ts deleted file mode 100644 index e69de29..0000000 diff --git a/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_process.tsx b/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_process.tsx new file mode 100644 index 0000000..68a80ca --- /dev/null +++ b/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_process.tsx @@ -0,0 +1,48 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +import { IFormBuilderComponentsProps } from "../../form_builder_components"; +import { useStore } from "../../../../../helper/use_store"; +import { SelectProcessStore } from "./select_process_store"; +import { useEffect } from "react"; +import { SelectProcessModel } from "../model/select_process_model"; +import { Loader } from "../../../../loader/loader"; +import { CoreSelect } from "../../../../select/select"; +import { message } from "antd"; +import { CalculationModel } from "../../../../../../features/calculation_instance/model/calculation_model"; + +export const SelectProcess = observer((props: IFormBuilderComponentsProps) => { + const [store] = useState(new SelectProcessStore()); + useEffect(() => { + if (typeof props.dependency === "string") { + store.loadClassInstance(SelectProcessModel, JSON.parse(props.dependency)); + } else { + store.loadClassInstance(SelectProcessModel, props.dependency); + } + + store.init(); + }, []); + return ( +
+ {store.isLoading ? ( + + ) : ( +
+ el.instanceName)} + value={store.viewModel?.selectProcess?.value.instanceName ?? ""} + label={`Процесс тип ${store?.viewModel?.type ?? ""}`} + onChange={async (value: string, index: number) => { + store.updateForm({ + selectProcess: { value: store.calculationInstances.at(index) ?? CalculationModel.empty() }, + }); + (await store.viewModel.valid()).fold( + (model) => props.onChange(model), + (error) => message.error(error) + ); + }} + /> +
+ )} +
+ ); +}); diff --git a/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_process_store.ts b/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_process_store.ts new file mode 100644 index 0000000..fefca2e --- /dev/null +++ b/ui/src/core/ui/form_builder/forms/select_dataset/presentation/select_process_store.ts @@ -0,0 +1,26 @@ +import { NavigateFunction } from "react-router-dom"; +import { FormState } from "../../../../../store/base_store"; +import { SelectProcessModel } from "../model/select_process_model"; +import { SelectProcessRepository } from "../data/select_process_repository"; +import { CalculationModel } from "../../../../../../features/calculation_instance/model/calculation_model"; +import makeAutoObservable from "mobx-store-inheritance"; + +export class SelectProcessStore extends FormState { + selectProcessRepository = new SelectProcessRepository(); + viewModel: SelectProcessModel; + calculationInstances: CalculationModel[] = []; + constructor() { + super(); + makeAutoObservable(this); + } + async init(navigate?: NavigateFunction | undefined): Promise { + await this.mapOk('calculationInstances', this.selectProcessRepository.getAllProcessByType(this.viewModel.type)); + + this.calculationInstances = this.calculationInstances.map((el) => { + // @ts-ignore + delete el['formBuilder']; + return el; + }) + + } +} \ No newline at end of file diff --git a/ui/src/core/ui/form_builder/forms/select_detail/presentation/select_detail_screen.tsx b/ui/src/core/ui/form_builder/forms/select_detail/presentation/select_detail_screen.tsx index 5e0aa28..145c670 100644 --- a/ui/src/core/ui/form_builder/forms/select_detail/presentation/select_detail_screen.tsx +++ b/ui/src/core/ui/form_builder/forms/select_detail/presentation/select_detail_screen.tsx @@ -1,21 +1,18 @@ -// @ts-nocheck import React from "react"; import { IFormBuilderComponentsProps } from "../../form_builder_components"; import { observer } from "mobx-react-lite"; import { ListItem } from "./ui/list_item"; import { SelectDetailStore } from "./select_detail_store"; import { SelectDetailViewModel } from "../model/select_details_model"; -import { plainToInstance } from "class-transformer"; export const SelectDetail = observer((props: IFormBuilderComponentsProps) => { const [store] = React.useState(() => new SelectDetailStore()); React.useEffect(() => { - console.log(props.dependency.details); store.viewModel = new SelectDetailViewModel(props.dependency.details); store.isLoading = false; store.init(); }, []); - + return (
{store.isLoading ? ( diff --git a/ui/src/core/ui/form_builder/forms/select_detail/presentation/ui/list_item.tsx b/ui/src/core/ui/form_builder/forms/select_detail/presentation/ui/list_item.tsx index 7263d7a..0690fbe 100644 --- a/ui/src/core/ui/form_builder/forms/select_detail/presentation/ui/list_item.tsx +++ b/ui/src/core/ui/form_builder/forms/select_detail/presentation/ui/list_item.tsx @@ -16,7 +16,6 @@ export const ListItem = (props: IListItemProps) => { backgroundColor: "rgba(254, 247, 255, 1)", border: "1px #6750a4 solid", width: "100%", - height: 110, display: "flex", justifyContent: "space-between", alignItems: "center", diff --git a/ui/src/core/ui/form_builder/test.tsx b/ui/src/core/ui/form_builder/test.tsx index 5074df0..b9735d7 100644 --- a/ui/src/core/ui/form_builder/test.tsx +++ b/ui/src/core/ui/form_builder/test.tsx @@ -9,7 +9,7 @@ import { FormBuilder } from "./form_builder"; import makeAutoObservable from "mobx-store-inheritance"; class FormBuilderTextStore extends ModalStore { - viewModel = FormBuilderValidationModel.empty(); + viewModel = FormBuilderValidationModel.eee(); constructor() { super(); makeAutoObservable(this); @@ -21,6 +21,7 @@ export const FormBuildTest = observer(() => { return (
+ (store.viewModel.result = text)} /> (store.viewModel.context = text)} /> (store.isModalOpen = true)} /> @@ -33,15 +34,16 @@ export const FormBuildTest = observer(() => { onCancel={() => { store.isModalOpen = false; }} - > - { - console.log(e.output); - // console.log(JSON.stringify(e.output)) - }} - /> - + > +
+ { + console.log(e) + // console.log(e.output); + console.log(JSON.stringify(e.output)) + }} + />
); }); diff --git a/ui/src/core/ui/icons/icons.tsx b/ui/src/core/ui/icons/icons.tsx index 33b571b..823e680 100644 --- a/ui/src/core/ui/icons/icons.tsx +++ b/ui/src/core/ui/icons/icons.tsx @@ -931,6 +931,18 @@ const getIconSvg = ( /> ); + + case "3points": + return Result.ok( + + + + ); case "Move": return Result.ok( diff --git a/ui/src/core/ui/input/input.tsx b/ui/src/core/ui/input/input.tsx index 1a78a4a..a2af4d9 100644 --- a/ui/src/core/ui/input/input.tsx +++ b/ui/src/core/ui/input/input.tsx @@ -16,7 +16,6 @@ interface IInputProps extends IStyle { type?: CoreInputType; trim?: boolean; styleContentEditable?: React.CSSProperties; - isFormBuilder?: boolean; } export const CoreInput = (props: IInputProps) => { @@ -29,15 +28,6 @@ export const CoreInput = (props: IInputProps) => { setAppendInnerText(false); } }, [ref, value, isAppendInnerText, setAppendInnerText, props]); - // React.useEffect(() => { - // if (props.isFormBuilder) { - // if (ref.current && props.value) { - // ref.current.innerText = value; - // setValue(props.value); - // console.log(props.value); - // } - // } - // }, [props.value]); const isSmall = props.type !== undefined && props.type.isEqual(CoreInputType.small); return ( diff --git a/ui/src/core/ui/input/input_v2.tsx b/ui/src/core/ui/input/input_v2.tsx index 30bfc20..72e04aa 100644 --- a/ui/src/core/ui/input/input_v2.tsx +++ b/ui/src/core/ui/input/input_v2.tsx @@ -2,15 +2,16 @@ import { themeStore } from "../../.."; import { Icon } from "../icons/icons"; import { CoreText, CoreTextType, FontType } from "../text/text"; -interface InputV2Props { +export const InputV2: React.FC<{ + style?: React.CSSProperties; label: string; value?: string; trim?: boolean; + validation?: (value: string) => boolean; + error?: string; height?: number; onChange?: (text: string) => void; - -} -export const InputV2: React.FC = ({ label, height, value, onChange, trim }) => { +}> = ({ label, height, value, onChange, trim }) => { return (
-
{rightChild}
-
+
*/}
{children}
diff --git a/ui/src/core/ui/popover/popover.tsx b/ui/src/core/ui/popover/popover.tsx new file mode 100644 index 0000000..55b6901 --- /dev/null +++ b/ui/src/core/ui/popover/popover.tsx @@ -0,0 +1,46 @@ +import React, { useEffect, useRef, useState } from "react"; +import styled from "styled-components"; + +interface PopoverProps { + content: React.ReactNode; + children: React.ReactNode; +} + +const PopoverV2: React.FC = ({ content, children }) => { + const [visible, setVisible] = useState(false); + const ref = useRef(null); + const togglePopover = () => { + setVisible((prev) => !prev); + }; + + return ( +
togglePopover()} + > + {children} +
+ {content} +
+
+ ); +}; +// visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')}; +// opacity: ${({ visible }) => (visible ? 1 : 0)}; +// transition: opacity 0.2s ease, visibility 0.2s ease; +export default PopoverV2; diff --git a/ui/src/core/ui/select/select.tsx b/ui/src/core/ui/select/select.tsx index 017c16f..47bff79 100644 --- a/ui/src/core/ui/select/select.tsx +++ b/ui/src/core/ui/select/select.tsx @@ -6,7 +6,7 @@ interface ICoreSelectProps extends IStyle { items: string[]; value: string; label: string; - onChange: (value: string) => void; + onChange: (value: string, index: number) => void; } export const CoreSelect = (props: ICoreSelectProps) => { const ref = React.useRef(null); @@ -52,7 +52,7 @@ export const CoreSelect = (props: ICoreSelectProps) => { key={i} onClick={() => { setValue(el); - props.onChange(el); + props.onChange(el, i); }} style={{ backgroundColor: "rgba(230, 224, 233, 1)", diff --git a/ui/src/features/behavior_tree_builder/data/behavior_tree_builder_http_repository.ts b/ui/src/features/behavior_tree_builder/data/behavior_tree_builder_http_repository.ts index 0da5653..9c6e264 100644 --- a/ui/src/features/behavior_tree_builder/data/behavior_tree_builder_http_repository.ts +++ b/ui/src/features/behavior_tree_builder/data/behavior_tree_builder_http_repository.ts @@ -21,7 +21,6 @@ export class BehaviorTreeBuilderHttpRepository extends CoreHttpRepository { `${this.featureApi}/by_id?id=${id}`, BehaviorTreeModel ) as unknown as Promise>; - deleteBt = (id: string) => this._jsonRequest(HttpMethod.DELETE, `${this.featureApi}?id=${id}`); editBt = async (model: BehaviorTreeModel) => { await this._jsonRequest(HttpMethod.POST, `${this.featureApi}/fill/tree`, model); model.__v = undefined diff --git a/ui/src/features/behavior_tree_builder/presentation/behavior_tree_builder_screen.tsx b/ui/src/features/behavior_tree_builder/presentation/behavior_tree_builder_screen.tsx index 5a890c6..67f44a6 100644 --- a/ui/src/features/behavior_tree_builder/presentation/behavior_tree_builder_screen.tsx +++ b/ui/src/features/behavior_tree_builder/presentation/behavior_tree_builder_screen.tsx @@ -4,20 +4,15 @@ import { createEditor } from "./ui/editor/editor"; import { SkillTree } from "./ui/skill_tree/skill_tree"; import { BehaviorTreeBuilderStore, DrawerState } from "./behavior_tree_builder_store"; import { observer } from "mobx-react-lite"; -import { match } from "ts-pattern"; import { Icon } from "../../../core/ui/icons/icons"; import { CoreText, CoreTextType } from "../../../core/ui/text/text"; import { useNavigate, useParams } from "react-router-dom"; import { IForms, forms } from "./ui/forms/forms"; -import { ButtonV2, ButtonV2Type } from "../../../core/ui/button/button_v2"; -import { CoreCard } from "../../../core/ui/card/card"; import { themeStore } from "../../.."; -import { CoreModal } from "../../../core/ui/modal/modal"; -import { InputV2 } from "../../../core/ui/input/input_v2"; -import { SelectV2 } from "../../../core/ui/select/select_v2"; import { MainPageV2 } from "../../../core/ui/pages/main_page_v2"; import { DrawerV2 } from "../../../core/ui/drawer/drawer"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; +import PopoverV2 from "../../../core/ui/popover/popover"; export const behaviorTreeBuilderScreenPath = "behavior/tree/screen/path"; @@ -54,7 +49,9 @@ export const BehaviorTreeBuilderScreen = observer(() => { if (ref.current) { // @ts-expect-error const domReact: DOMReact = ref.current.getBoundingClientRect(); - store.dragZoneSetOffset(0, domReact.y, domReact.width, domReact.height); + + // УБЕРИ + 300 + store.dragZoneSetOffset(0, domReact.y, domReact.width + 300, domReact.height); } }, [ref.current]); @@ -66,54 +63,36 @@ export const BehaviorTreeBuilderScreen = observer(() => { }, []); return ( -
- {/* {}} text="Запуск" textColor={themeStore.theme.black} /> -
- {}} - text="Стоп" - type={ButtonV2Type.empty} - textColor={themeStore.theme.greenWhite} - /> -
*/} - {store.isNeedSaveBtn ? ( -
- {store.isNeedSaveBtn ? ( - store.onClickSaveBehaviorTree()} type="Floppy" /> - ) : undefined} -
- ) : ( - <> - )} -
- - } style={{ position: "absolute", height: "100%", overflow: "hidden" }} bgColor={themeStore.theme.black} children={ <> <> +
+
+
+ store.onClickSaveBehaviorTree()} type="Floppy" /> +
+
+
{ }} /> - -
-
- + {store.panels.map((el, index) => ( + <> + +
+ + } + >
+ + store.panelResize(size, index)} + > +
+ } + content={ +
+ {store.panelActions.map((el, i) => ( + el.action(index)} /> + ))} +
+ } + /> +
{el.name}
+
+ {el.body} +
+ + ))}
@@ -172,8 +186,17 @@ export const BehaviorTreeBuilderScreen = observer(() => { title={store.titleDrawer} onClose={() => store.editDrawer(DrawerState.editThreadBehaviorTree, false)} isOpen={store.drawers.find((el) => el.name === DrawerState.editThreadBehaviorTree)?.status} + width={window.innerWidth / 2} > -
+
{store.skillTemplates?.getForms(store.selected ?? "").map((formType, index) => forms( @@ -186,11 +209,13 @@ export const BehaviorTreeBuilderScreen = observer(() => { ) .rFind((form) => form.name.isEqual(formType)) .fold( - (s) => ( -
- {s.component} -
- ), + (s) => { + return ( +
+ {s.component} +
+ ); + }, () => (
Error: Unknown form type {formType} diff --git a/ui/src/features/behavior_tree_builder/presentation/behavior_tree_builder_store.tsx b/ui/src/features/behavior_tree_builder/presentation/behavior_tree_builder_store.tsx index 126d256..f3658d3 100644 --- a/ui/src/features/behavior_tree_builder/presentation/behavior_tree_builder_store.tsx +++ b/ui/src/features/behavior_tree_builder/presentation/behavior_tree_builder_store.tsx @@ -26,6 +26,8 @@ import { BehaviorTreeModel } from "../model/behavior_tree_model"; import { PrimitiveViewModel, SystemPrimitive } from "../model/primitive_view_model"; import { SceneAsset } from "../../../core/model/scene_asset"; import { themeStore } from "../../.."; +import { RunTimeActions } from "./ui/actions/runtime_actions"; +import { ITopicModel } from "../../topics/topic_view_model"; interface I2DArea { x: number; @@ -37,8 +39,31 @@ interface I2DArea { export enum DrawerState { editThreadBehaviorTree = "Редактирование", } +interface IActionPanel { + name: string; + selectIsClosePopover?: boolean; + action: (index: number) => void; +} + +export class PanelBody { + body?: React.ReactNode; + size: number = 0; + name: string = "выберите тип панели"; + constructor(body: React.ReactNode | undefined, name: string | undefined, size: number | undefined) { + makeAutoObservable(this); + + if (name) this.name = name; + if (body) this.body = body; + if (size) this.size = size; + } +} export class BehaviorTreeBuilderStore extends UiDrawerFormState { + panelActions: IActionPanel[] = [ + { name: "Добавить панель", action: () => this.addNewPanel() }, + { name: "Убрать панель", action: (index) => this.removePanel(index) }, + { name: "Runtime", action: (index) => this.changePanel(index, "Runtime", ) }, + ]; sceneAsset?: SceneAsset; viewModel: BehaviorTreeViewModel = BehaviorTreeViewModel.empty(); behaviorTreeModel: BehaviorTreeModel = BehaviorTreeModel.empty(); @@ -51,15 +76,15 @@ export class BehaviorTreeBuilderStore extends UiDrawerFormState; areaPlugin?: AreaPlugin; nodeUpdateObserver?: NodeRerenderObserver; primitiveViewModel: PrimitiveViewModel; + topics: ITopicModel[]; skillTree: ISkillView = { name: "", children: [ @@ -70,7 +95,12 @@ export class BehaviorTreeBuilderStore extends UiDrawerFormState this.panels.push(new PanelBody(undefined, undefined, undefined)); + removePanel = (index: number) => + this.panels.length !== 1 + ? (this.panels = this.panels.filter((_, i) => i !== index)) + : message.error("должна быть хоть одна панель"); constructor() { super(DrawerState); makeAutoObservable(this); @@ -86,7 +116,7 @@ export class BehaviorTreeBuilderStore extends UiDrawerFormState this.filledOutTemplates.topicsStack; + getAllTopics = () => this.filledOutTemplates.topicsStack.plus(this.topics); errorHandingStrategy = (_: CoreError) => {}; dragEnd = (e: EventTarget) => { @@ -102,6 +132,13 @@ export class BehaviorTreeBuilderStore extends UiDrawerFormState { const drawPoint = { x: x, y: y, w: 1, h: 1 }; + + console.log( + drawPoint.x < this.area!.x + this.area!.w && + drawPoint.x + drawPoint.w > this.area!.x && + drawPoint.y < this.area!.y + this.area!.h && + drawPoint.y + drawPoint.h > this.area!.y + ); if ( drawPoint.x < this.area!.x + this.area!.w && drawPoint.x + drawPoint.w > this.area!.x && @@ -115,7 +152,7 @@ export class BehaviorTreeBuilderStore extends UiDrawerFormState { @@ -179,7 +216,7 @@ export class BehaviorTreeBuilderStore extends UiDrawerFormState { @@ -207,7 +244,6 @@ export class BehaviorTreeBuilderStore extends UiDrawerFormState { - console.log(xml); this.behaviorTreeModel.skills = this.filledOutTemplates; this.behaviorTreeModel.scene = NodeBehaviorTree.fromReteScene( this.editor as NodeEditor, @@ -345,4 +381,11 @@ export class BehaviorTreeBuilderStore extends UiDrawerFormState {}; + changePanel = (index: number, name: string, body?: React.ReactNode) => { + this.panels = this.panels.replacePropIndex({ name: name, body: body }, index); + }; + + panelResize = (size: number, index: number) => { + this.panels.replacePropIndex({ size: size }, index); + }; } diff --git a/ui/src/features/behavior_tree_builder/presentation/ui/actions/runtime_actions.tsx b/ui/src/features/behavior_tree_builder/presentation/ui/actions/runtime_actions.tsx new file mode 100644 index 0000000..ea5ecde --- /dev/null +++ b/ui/src/features/behavior_tree_builder/presentation/ui/actions/runtime_actions.tsx @@ -0,0 +1,94 @@ +import { NavigateFunction, useParams } from "react-router-dom"; +import { CoreHttpRepository, HttpMethod } from "../../../../../core/repository/core_http_repository"; +import { CoreError, UiErrorState } from "../../../../../core/store/base_store"; +import { ProcessStatus } from "../../../../dataset/dataset_model"; +import makeAutoObservable from "mobx-store-inheritance"; +import { useStore } from "../../../../../core/helper/use_store"; +import { Loader } from "../../../../../core/ui/loader/loader"; +import { useEffect } from "react"; +import { + RunTimeSocketRepository, + runTimeSocketRepository, +} from "../../../../../core/repository/core_socket_repository"; +import { observer } from "mobx-react-lite"; +interface IPid { + pid: string; +} +export interface RuntimeModel { + [name: string]: { pid: number; status: String }; +} + +export class RunTimeHttpRepository extends CoreHttpRepository { + feature = "/run_time"; + stop = (model: IPid) => this._jsonRequest(HttpMethod.POST, this.feature + "/kill", model); + getStatuses = () => this._jsonRequest(HttpMethod.GET, this.feature + "/status"); + execBt = (id: string) => this._jsonRequest(HttpMethod.POST, this.feature + `/exec/bt?id=${id}`); +} +// export class RunTimeSocketRepository extends SockeCore {} +export class RunTimeStore extends UiErrorState { + runTimeHttpRepository: RunTimeHttpRepository = new RunTimeHttpRepository(); + runTimeSocketRepository: RunTimeSocketRepository = runTimeSocketRepository; + pageId: string; + runTime?: RuntimeModel = {}; + constructor() { + super(); + makeAutoObservable(this); + runTimeSocketRepository.on((event) => (console.log(event), (this.runTime = event))); + } + async init(navigate?: NavigateFunction | undefined): Promise { + this.mapOk("runTime", this.runTimeHttpRepository.getStatuses()); + this.navigate = navigate; + } + + executeBt = () => this.runTimeHttpRepository.execBt(this.pageId); + initParam = (id: string) => (this.pageId = id); + isExecuteBt = (): boolean => { + if (this.runTime === undefined) { + return false; + } + if ( + this.runTime["EXEC_BT"] !== undefined && + this.runTime["EXEC_BT"]["status"] !== undefined && + this.runTime["EXEC_BT"]["status"] === "endOk" + ) { + return false; + } + return true; + }; +} +export const RunTimeActions = observer(() => { + const store = useStore(RunTimeStore); + const params = useParams(); + useEffect(() => { + store.initParam(params.id as string); + }, []); + return ( +
+ {store.isLoading ? ( + + ) : ( + <> + {store.isExecuteBt() ? ( + <> +
store.executeBt()} + > + Дерево запущено +
+ + ) : ( + <> +
store.executeBt()} + > + Запустить дерево +
+ + )} + + )} +
+ ); +}); diff --git a/ui/src/features/behavior_tree_builder/presentation/ui/forms/form_builder/form_builder_form.tsx b/ui/src/features/behavior_tree_builder/presentation/ui/forms/form_builder/form_builder_form.tsx index 7c5d2a5..dfffe63 100644 --- a/ui/src/features/behavior_tree_builder/presentation/ui/forms/form_builder/form_builder_form.tsx +++ b/ui/src/features/behavior_tree_builder/presentation/ui/forms/form_builder/form_builder_form.tsx @@ -19,7 +19,7 @@ export const FormBuilderForm = observer((props: IPropsForm +
FormBuilder
{store.isBtScreen ? (
@@ -27,7 +27,6 @@ export const FormBuilderForm = observer((props: IPropsForm { store.viewModel = form; - console.log(form); }} />
@@ -81,7 +80,7 @@ export const FormBuilderForm = observer((props: IPropsForm - {}} /> + (console.log(form), props.onChange(form))} /> )} diff --git a/ui/src/features/behavior_tree_manager/behavior_tree_manager_repository.ts b/ui/src/features/behavior_tree_manager/behavior_tree_manager_repository.ts index b99cbf2..a64f732 100644 --- a/ui/src/features/behavior_tree_manager/behavior_tree_manager_repository.ts +++ b/ui/src/features/behavior_tree_manager/behavior_tree_manager_repository.ts @@ -4,7 +4,7 @@ import { BehaviorTreeViewModel } from "../behavior_tree_builder/model/behavior_t export class BehaviorTreeManagerHttpRepository extends CoreHttpRepository { featureApi = `/behavior/trees`; - deleteBt = (id: string) => this._jsonRequest(HttpMethod.DELETE, `${this.featureApi}?id=${id}`); + deleteBt = (id: string) => this._jsonRequest(HttpMethod.POST, `${this.featureApi}/delete/bt?id=${id}`); saveNewBt = async (model: BehaviorTreeViewModel) => this._jsonRequest(HttpMethod.POST, this.featureApi, model); getAllBtInstances = async () => this._jsonRequest(HttpMethod.GET, this.featureApi); } diff --git a/ui/src/features/behavior_tree_manager/behavior_tree_manager_screen.tsx b/ui/src/features/behavior_tree_manager/behavior_tree_manager_screen.tsx index fe8d179..482bbc2 100644 --- a/ui/src/features/behavior_tree_manager/behavior_tree_manager_screen.tsx +++ b/ui/src/features/behavior_tree_manager/behavior_tree_manager_screen.tsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react-lite"; import { BehaviorTreeManagerStore } from "./behavior_tree_manager_store"; -import React from "react"; +import React, { useEffect } from "react"; import { useStore } from "../../core/helper/use_store"; import { ButtonV2, ButtonV2Type } from "../../core/ui/button/button_v2"; import { CoreCard } from "../../core/ui/card/card"; @@ -16,13 +16,13 @@ export const BehaviorTreeManagerScreenPath = "/behavior/tree/manager"; export const BehaviorTreeManagerScreen = observer(() => { const store = useStore(BehaviorTreeManagerStore); - + useEffect(() => {}, []); return ( <> -
+
} @@ -65,9 +65,13 @@ export const BehaviorTreeManagerScreen = observer(() => { }} />
- store.updateForm({ name: text })} /> + store.updateForm({ name: text })} />
- store.updateForm({ description: text })} /> + store.updateForm({ description: text })} + />
({ name: el.name, value: el._id })) ?? []} diff --git a/ui/src/features/behavior_tree_manager/behavior_tree_manager_store.ts b/ui/src/features/behavior_tree_manager/behavior_tree_manager_store.ts index f19fc5b..4d2b069 100644 --- a/ui/src/features/behavior_tree_manager/behavior_tree_manager_store.ts +++ b/ui/src/features/behavior_tree_manager/behavior_tree_manager_store.ts @@ -12,7 +12,6 @@ export enum DrawerState { } export class BehaviorTreeManagerStore extends UiDrawerFormState { viewModel: BehaviorTreeViewModel = BehaviorTreeViewModel.empty(); - navigate?: NavigateFunction; btTreeModels: BehaviorTreeModel[] = []; scenes?: SceneModel[]; behaviorTreeManagerHttpRepository = new BehaviorTreeManagerHttpRepository(); diff --git a/ui/src/features/calculation_instance/data/calculation_http_repository.ts b/ui/src/features/calculation_instance/data/calculation_http_repository.ts index 90e4695..2b60a10 100644 --- a/ui/src/features/calculation_instance/data/calculation_http_repository.ts +++ b/ui/src/features/calculation_instance/data/calculation_http_repository.ts @@ -14,7 +14,13 @@ export interface ISkils { } export class CalculationHttpRepository extends CoreHttpRepository { - + async getLogs(id: string) { + + await this._request(HttpMethod.GET, `/logs?id=${id}`) + window.location.href = 'http://localhost:4001/log.txt'; + + } + featureApi = `/calculations/instances`; subFeatureApi = `/calculations/template`; diff --git a/ui/src/features/calculation_instance/model/calculation_model.ts b/ui/src/features/calculation_instance/model/calculation_model.ts index 01e1519..9fd6c2d 100644 --- a/ui/src/features/calculation_instance/model/calculation_model.ts +++ b/ui/src/features/calculation_instance/model/calculation_model.ts @@ -4,7 +4,9 @@ import { FormBuilderValidationModel } from "../../../core/model/form_builder_val export enum ModelMachineLearningTypes { OBJECT_DETECTION = "OBJECT_DETECTION", - POSE_ESTIMATE = "POSE_ESTIMATE", + POSE_ESTIMATION = "POSE_ESTIMATION", + BOP_DATASET = "BOP_DATASET", + WEIGHTS = "WEIGHTS", } export class CalculationModel extends ValidationModel { diff --git a/ui/src/features/calculation_instance/presentation/calculation_instance_screen.tsx b/ui/src/features/calculation_instance/presentation/calculation_instance_screen.tsx index fc9dec4..7a56975 100644 --- a/ui/src/features/calculation_instance/presentation/calculation_instance_screen.tsx +++ b/ui/src/features/calculation_instance/presentation/calculation_instance_screen.tsx @@ -28,7 +28,6 @@ export const CalculationInstanceScreenPath = "/calculation"; export const CalculationInstanceScreen = observer(() => { const store = useStore(CalculationInstanceStore); - return ( <> { () => store.makeEditProcess(el), () => store.deleteInstance(el._id ?? ""), () => store.execSkill(el._id ?? ""), - () => store.execSkill(el._id ?? "") + () => store.execSkill(el._id ?? ""), + () => store.changeProcessStatus(el._id ?? ""), + () => store.getTxtLog(el._id ?? "") )} ); @@ -94,7 +95,10 @@ export const CalculationInstanceScreen = observer(() => {
store.updateForm({ formBuilder: formBuilder })} + onChange={(formBuilder) => { + console.log(formBuilder); + store.updateForm({ formBuilder: formBuilder }); + }} />
@@ -134,7 +138,7 @@ export const CalculationInstanceScreen = observer(() => { store.updateForm({ type: text })} /> { label={"Тип карточки"} onChange={(text: string) => store.updateForm({ card: text })} /> - store.updateForm({ name: text })} /> - store.updateForm({ script: text })} /> + store.updateForm({ name: text.replaceAll("\n", "") })} + /> + store.updateForm({ script: text.replaceAll("\n", "") })} + /> (store.viewModel.formBuilder.result = text)} @@ -206,7 +218,7 @@ export const CalculationInstanceScreen = observer(() => { store.isModalOpen = false; }} > - {}} /> + {}} /> ); diff --git a/ui/src/features/calculation_instance/presentation/calculation_instance_store.tsx b/ui/src/features/calculation_instance/presentation/calculation_instance_store.tsx index 1ca399a..bb68c7e 100644 --- a/ui/src/features/calculation_instance/presentation/calculation_instance_store.tsx +++ b/ui/src/features/calculation_instance/presentation/calculation_instance_store.tsx @@ -5,7 +5,7 @@ import { Drawer, UiDrawerFormState } from "../../../core/store/base_store"; import { CalculationHttpRepository } from "../data/calculation_http_repository"; import { message } from "antd"; import { UUID } from "../../all_projects/data/project_http_repository"; -import { CalculationModel } from "../model/calculation_model"; +import { CalculationModel as calculationModel } from "../model/calculation_model"; import { ProcessUpdate, CalculationSocketRepository } from "../data/calculation_socket_repository"; import { match } from "ts-pattern"; import { plainToInstance } from "class-transformer"; @@ -20,16 +20,17 @@ export enum StoreTypes { empty = "empty", } -export class CalculationInstanceStore extends UiDrawerFormState { +export class CalculationInstanceStore extends UiDrawerFormState { + getTxtLog = (id: string) => this.calculationHttpRepository.getLogs(id); calculationHttpRepository: CalculationHttpRepository = new CalculationHttpRepository(); calculationSocketRepository: CalculationSocketRepository = new CalculationSocketRepository(); activeProjectId?: UUID; storeType: StoreTypes = StoreTypes.empty; - viewModel: CalculationModel = CalculationModel.empty(); - modelTemplate?: CalculationModel[]; - editProcess?: CalculationModel; - calculationInstances?: CalculationModel[]; - selectTemplate?: CalculationModel; + viewModel: calculationModel = calculationModel.empty(); + modelTemplate?: calculationModel[]; + editProcess?: calculationModel; + calculationInstances?: calculationModel[]; + selectTemplate?: calculationModel; titleDrawer: string = DrawersSkill.NEW_SKILL; drawers: Drawer[]; isModalOpen: boolean = false; @@ -41,7 +42,7 @@ export class CalculationInstanceStore extends UiDrawerFormState { + setSelectTemplate = (el: calculationModel) => { this.selectTemplate = el; - const instance = plainToInstance(CalculationModel, el); + const instance = plainToInstance(calculationModel, el); instance.instanceName = this?.viewModel.instanceName; this.viewModel = instance; }; @@ -84,7 +85,7 @@ export class CalculationInstanceStore extends UiDrawerFormState {}) .with(StoreTypes.newType, async () => - (await this.viewModel.valid()).fold( + (await this.viewModel.valid()).fold( async (model) => { model.project = this.activeProjectId?.id; @@ -99,7 +100,7 @@ export class CalculationInstanceStore extends UiDrawerFormState { - (await this.viewModel.valid()).fold( + (await this.viewModel.valid()).fold( async (model) => { delete model._id; model.project = this.activeProjectId?.id; @@ -128,20 +129,30 @@ export class CalculationInstanceStore extends UiDrawerFormState { this.editDrawer(DrawersSkill.EDIT_SKILL, false); - (await this.viewModel.valid()).fold( - async (model) => await this.calculationHttpRepository.editCalculation(model), + (await this.viewModel.valid()).fold( + async (model) => (await this.calculationHttpRepository.editCalculation(model), await this.init()), async (err) => message.error(err) ); }; - makeEditProcess = (el: CalculationModel) => { + makeEditProcess = (el: calculationModel) => { this.editProcess = el; - this.loadClassInstance(CalculationModel, el); + this.loadClassInstance(calculationModel, el); this.editDrawer(DrawersSkill.EDIT_SKILL, true); }; - deleteTemplate = async (el: CalculationModel) => { + deleteTemplate = async (el: calculationModel) => { await this.messageHttp(this.calculationHttpRepository.deleteTemplate(el._id ?? ""), { successMessage: "Удален", }); await this.mapOk("modelTemplate", this.calculationHttpRepository.getAllTemplates()); }; + changeProcessStatus = async (id: string) => + this! + .calculationInstances!.whereOne((el) => el._id === id) + .map( + async (calculationModel) => ( + (calculationModel.isEnd = !calculationModel.isEnd), + await this.calculationHttpRepository.editCalculation(calculationModel), + await this.init(undefined) + ) + ); } diff --git a/ui/src/features/calculation_instance/presentation/ui/cards/get_model_card.tsx b/ui/src/features/calculation_instance/presentation/ui/cards/get_model_card.tsx index 901029d..c0fdb2c 100644 --- a/ui/src/features/calculation_instance/presentation/ui/cards/get_model_card.tsx +++ b/ui/src/features/calculation_instance/presentation/ui/cards/get_model_card.tsx @@ -1,6 +1,6 @@ import { match } from "ts-pattern"; import { PoseEstimateCard } from "./pose_estimate_card/model_card"; -import { Dropdown, MenuProps, message } from "antd"; +import { Dropdown, MenuProps, } from "antd"; import { CoreText, CoreTextType } from "../../../../../core/ui/text/text"; import { IMenuItem } from "../../../../dataset/card_dataset"; import { Icon } from "../../../../../core/ui/icons/icons"; @@ -15,7 +15,9 @@ export const getModelCard = ( onEdit: Function, onDelete: Function, onPlay: Function, - onPause: Function + onPause: Function, + onChangeProcessIsEnd: Function, + onLog: Function ) => { const menu: IMenuItem[] = [ { @@ -26,6 +28,10 @@ export const getModelCard = ( onClick: () => onDelete(), name: "Удалить", }, + { + onClick: () => onChangeProcessIsEnd(), + name: calculationModel.isEnd ? "Вернуть на доработку" : "Завершить", + }, ]; const items: MenuProps["items"] = menu.map((el, index) => { @@ -63,7 +69,8 @@ export const getModelCard = ( - window.prompt("Copy to clipboard: Ctrl+C, Enter", calculationModel.lastProcessLogs ?? "Not found logs") + // window.prompt("Copy to clipboard: Ctrl+C, Enter", calculationModel.lastProcessLogs ?? "Not found logs") + onLog() } /> this._jsonRequest(HttpMethod.POST, `/run_time/exec/analyze?id=${id}`); } export class PoseEstimateStore extends UiErrorState { - navigate?: NavigateFunction; poseEstimateRepository = new PoseEstimateRepository(); constructor() { super(); diff --git a/ui/src/features/skills/skills_screen.tsx b/ui/src/features/skills/skills_screen.tsx index 894fc56..216604e 100644 --- a/ui/src/features/skills/skills_screen.tsx +++ b/ui/src/features/skills/skills_screen.tsx @@ -12,7 +12,6 @@ import { useStore } from "../../core/helper/use_store"; import { FormBuilder } from "../../core/ui/form_builder/form_builder"; import { ButtonV2 } from "../../core/ui/button/button_v2"; import { Result } from "../../core/helper/result"; -import { plainToInstance } from "class-transformer"; export const isValidJson = (json: any): Result => { try { @@ -288,3 +287,4 @@ export const SkillsScreen = observer(() => { ); }); + \ No newline at end of file diff --git a/ui/src/features/topics/topic_view_model.ts b/ui/src/features/topics/topic_view_model.ts index 44c8bc2..74551db 100644 --- a/ui/src/features/topics/topic_view_model.ts +++ b/ui/src/features/topics/topic_view_model.ts @@ -21,6 +21,7 @@ export class TopicViewModel extends ValidationModel { export interface ITopicModel { digitalTwinId?: string; sid?: string; + _id?: string; name: string; type: string; } diff --git a/ui/src/features/topics/topics_repository.ts b/ui/src/features/topics/topics_repository.ts index 852926b..e06412c 100644 --- a/ui/src/features/topics/topics_repository.ts +++ b/ui/src/features/topics/topics_repository.ts @@ -1,6 +1,9 @@ -import { HttpMethod, HttpRepository } from "../../core/repository/core_http_repository"; +import { CoreHttpRepository, HttpMethod, HttpRepository } from "../../core/repository/core_http_repository"; +import { TopicViewModel } from "./topic_view_model"; -export class TopicsHttpRepository extends HttpRepository { +export class TopicsHttpRepository extends CoreHttpRepository { + deleteTopic = (id: any) => this._jsonRequest(HttpMethod.DELETE, `${this.featureApi}?id=${id}`); featureApi = "/topics"; - getAllTopics = () => this._jsonRequest(HttpMethod.GET, this.featureApi); + createTopics = (model: TopicViewModel) => this._jsonRequest(HttpMethod.POST, this.featureApi, model); + } diff --git a/ui/src/features/topics/topics_screen.tsx b/ui/src/features/topics/topics_screen.tsx index 2fb82c8..f61fc7b 100644 --- a/ui/src/features/topics/topics_screen.tsx +++ b/ui/src/features/topics/topics_screen.tsx @@ -2,6 +2,11 @@ import React from "react"; import { observer } from "mobx-react-lite"; import { TopicsStore } from "./topics_store"; import { useStore } from "../../core/helper/use_store"; +import { CoreButton } from "../../core/ui/button/button"; +import { CoreModal } from "../../core/ui/modal/modal"; +import { InputV2 } from "../../core/ui/input/input_v2"; +import { CoreText, CoreTextType } from "../../core/ui/text/text"; +import { CoreInput } from "../../core/ui/input/input"; export const TopicsScreenPath = "/topics"; @@ -10,12 +15,43 @@ export const TopicsScreen = observer(() => { return ( <> +
+ store.modalShow()} /> + store.openTopicModal()} /> +
{store.topics?.map((el) => (
+ store.deleteTopic(el._id ?? "")} /> + window.prompt("Copy to clipboard: Ctrl+C, Enter", JSON.stringify(el))} + /> +
{el.name}
{el.type}
))} + store.modalCancel()} + children={ +
+ store.updateForm({ type: text })} /> + store.updateForm({ name: text })} /> + store.createTopic()} /> +
+ } + /> + store.cancelImportTopicModal()} + children={ +
+ store.importTopic(text)} label="import" /> +
+ } + /> ); }); diff --git a/ui/src/features/topics/topics_store.ts b/ui/src/features/topics/topics_store.ts index eb680b1..d9e4157 100644 --- a/ui/src/features/topics/topics_store.ts +++ b/ui/src/features/topics/topics_store.ts @@ -6,14 +6,35 @@ import { NavigateFunction } from "react-router-dom"; import { TopicsHttpRepository } from "./topics_repository"; export class TopicsStore extends FormState { + importTopic(text: string): void { + this.loadClassInstance(TopicModel, JSON.parse(text)) + + this.createTopic() + } + + viewModel: TopicModel = TopicModel.empty(); topics?: ITopicModel[]; topicsHttpRepository: TopicsHttpRepository = new TopicsHttpRepository(); + importTopicModal = false; constructor() { super(); makeAutoObservable(this); } init = async (navigate?: NavigateFunction | undefined) => { + this.modalCancel(); + this.importTopicModal = false; await this.mapOk("topics", this.topicsHttpRepository.getAllTopics()); }; + cancelImportTopicModal = () => { + this.importTopicModal = false; + } + openTopicModal = () => { + this.importTopicModal = true; + } + deleteTopic = async (id: string) => (await (this.topicsHttpRepository.deleteTopic(id))).map(() => this.init()); + createTopic = async () => + (await this.viewModel.validMessage()).map(async (model) => + (await this.topicsHttpRepository.createTopics(model)).map(() => this.init()) + ); } diff --git a/ui/src/index.tsx b/ui/src/index.tsx index a8d3fea..a2b72f7 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -7,6 +7,7 @@ import { RouterProvider } from "react-router-dom"; import { router } from "./core/routers/routers"; import { configure } from "mobx"; import { ThemeStore } from "./core/store/theme_store"; +import { FormBuildTest } from "./core/ui/form_builder/test"; configure({ enforceActions: "never", @@ -22,5 +23,6 @@ root.render( + {/* */} ); diff --git a/web_p/camera_info_topic.json b/web_p/camera_info_topic.json new file mode 100644 index 0000000..f7006fd --- /dev/null +++ b/web_p/camera_info_topic.json @@ -0,0 +1,6 @@ +{ + "_id": "67b43b59ba78351d1a11d74a", + "name": "/rgbd_camera/camera_info", + "type": "sensor_msgs/msg/CameraInfo", + "__v": 0 +} \ No newline at end of file diff --git a/web_p/image_topic.json b/web_p/image_topic.json new file mode 100644 index 0000000..b8a7e1a --- /dev/null +++ b/web_p/image_topic.json @@ -0,0 +1,6 @@ +{ + "_id": "67b43b20ba78351d1a11d747", + "name": "/rgbd_camera/image", + "type": "sensor_msgs/msg/Image", + "__v": 0 +} \ No newline at end of file diff --git a/web_p/models_dope.py b/web_p/models_dope.py new file mode 100755 index 0000000..0c89004 --- /dev/null +++ b/web_p/models_dope.py @@ -0,0 +1,196 @@ +""" +NVIDIA from jtremblay@gmail.com +""" + +# Networks +import torch +import torch +import torch.nn as nn +import torch.nn.parallel +import torch.utils.data +import torchvision.models as models + + +class DopeNetwork(nn.Module): + def __init__( + self, + pretrained=False, + numBeliefMap=9, + numAffinity=16, + stop_at_stage=6, # number of stages to process (if less than total number of stages) + ): + super(DopeNetwork, self).__init__() + + self.stop_at_stage = stop_at_stage + + vgg_full = models.vgg19(pretrained=False).features + self.vgg = nn.Sequential() + for i_layer in range(24): + self.vgg.add_module(str(i_layer), vgg_full[i_layer]) + + # Add some layers + i_layer = 23 + self.vgg.add_module( + str(i_layer), nn.Conv2d(512, 256, kernel_size=3, stride=1, padding=1) + ) + self.vgg.add_module(str(i_layer + 1), nn.ReLU(inplace=True)) + self.vgg.add_module( + str(i_layer + 2), nn.Conv2d(256, 128, kernel_size=3, stride=1, padding=1) + ) + self.vgg.add_module(str(i_layer + 3), nn.ReLU(inplace=True)) + + # print('---Belief------------------------------------------------') + # _2 are the belief map stages + self.m1_2 = DopeNetwork.create_stage(128, numBeliefMap, True) + self.m2_2 = DopeNetwork.create_stage( + 128 + numBeliefMap + numAffinity, numBeliefMap, False + ) + self.m3_2 = DopeNetwork.create_stage( + 128 + numBeliefMap + numAffinity, numBeliefMap, False + ) + self.m4_2 = DopeNetwork.create_stage( + 128 + numBeliefMap + numAffinity, numBeliefMap, False + ) + self.m5_2 = DopeNetwork.create_stage( + 128 + numBeliefMap + numAffinity, numBeliefMap, False + ) + self.m6_2 = DopeNetwork.create_stage( + 128 + numBeliefMap + numAffinity, numBeliefMap, False + ) + + # print('---Affinity----------------------------------------------') + # _1 are the affinity map stages + self.m1_1 = DopeNetwork.create_stage(128, numAffinity, True) + self.m2_1 = DopeNetwork.create_stage( + 128 + numBeliefMap + numAffinity, numAffinity, False + ) + self.m3_1 = DopeNetwork.create_stage( + 128 + numBeliefMap + numAffinity, numAffinity, False + ) + self.m4_1 = DopeNetwork.create_stage( + 128 + numBeliefMap + numAffinity, numAffinity, False + ) + self.m5_1 = DopeNetwork.create_stage( + 128 + numBeliefMap + numAffinity, numAffinity, False + ) + self.m6_1 = DopeNetwork.create_stage( + 128 + numBeliefMap + numAffinity, numAffinity, False + ) + + def forward(self, x): + """Runs inference on the neural network""" + + out1 = self.vgg(x) + + out1_2 = self.m1_2(out1) + out1_1 = self.m1_1(out1) + + if self.stop_at_stage == 1: + return [out1_2], [out1_1] + + out2 = torch.cat([out1_2, out1_1, out1], 1) + out2_2 = self.m2_2(out2) + out2_1 = self.m2_1(out2) + + if self.stop_at_stage == 2: + return [out1_2, out2_2], [out1_1, out2_1] + + out3 = torch.cat([out2_2, out2_1, out1], 1) + out3_2 = self.m3_2(out3) + out3_1 = self.m3_1(out3) + + if self.stop_at_stage == 3: + return [out1_2, out2_2, out3_2], [out1_1, out2_1, out3_1] + + out4 = torch.cat([out3_2, out3_1, out1], 1) + out4_2 = self.m4_2(out4) + out4_1 = self.m4_1(out4) + + if self.stop_at_stage == 4: + return [out1_2, out2_2, out3_2, out4_2], [out1_1, out2_1, out3_1, out4_1] + + out5 = torch.cat([out4_2, out4_1, out1], 1) + out5_2 = self.m5_2(out5) + out5_1 = self.m5_1(out5) + + if self.stop_at_stage == 5: + return [out1_2, out2_2, out3_2, out4_2, out5_2], [ + out1_1, + out2_1, + out3_1, + out4_1, + out5_1, + ] + + out6 = torch.cat([out5_2, out5_1, out1], 1) + out6_2 = self.m6_2(out6) + out6_1 = self.m6_1(out6) + + return [out1_2, out2_2, out3_2, out4_2, out5_2, out6_2], [ + out1_1, + out2_1, + out3_1, + out4_1, + out5_1, + out6_1, + ] + + @staticmethod + def create_stage(in_channels, out_channels, first=False): + """Create the neural network layers for a single stage.""" + + model = nn.Sequential() + mid_channels = 128 + if first: + padding = 1 + kernel = 3 + count = 6 + final_channels = 512 + else: + padding = 3 + kernel = 7 + count = 10 + final_channels = mid_channels + + # First convolution + model.add_module( + "0", + nn.Conv2d( + in_channels, mid_channels, kernel_size=kernel, stride=1, padding=padding + ), + ) + + # Middle convolutions + i = 1 + while i < count - 1: + model.add_module(str(i), nn.ReLU(inplace=True)) + i += 1 + model.add_module( + str(i), + nn.Conv2d( + mid_channels, + mid_channels, + kernel_size=kernel, + stride=1, + padding=padding, + ), + ) + i += 1 + + # Penultimate convolution + model.add_module(str(i), nn.ReLU(inplace=True)) + i += 1 + model.add_module( + str(i), nn.Conv2d(mid_channels, final_channels, kernel_size=1, stride=1) + ) + i += 1 + + # Last convolution + model.add_module(str(i), nn.ReLU(inplace=True)) + i += 1 + model.add_module( + str(i), nn.Conv2d(final_channels, out_channels, kernel_size=1, stride=1) + ) + i += 1 + + return model diff --git a/web_p/od_skill.json b/web_p/od_skill.json new file mode 100644 index 0000000..34d2e50 --- /dev/null +++ b/web_p/od_skill.json @@ -0,0 +1,87 @@ +{ + "_id": "67b33b15f3412f3530fdb337", + "bgColor": "rgba(5, 26, 39, 1)", + "borderColor": "rgba(25, 130, 196, 1)", + "SkillPackage": { "name": "Robossembler", "version": "1", "format": "1.0" }, + "Module": { "node_name": "lc_yolo", "name": "ObjectDetection", "description": "Object detection skill with YOLOv8" }, + "BTAction": [ + { + "name": "odConfigure", + "type": "run", + "param": [ + { + "type": "topic", + "dependency": { + "type": "topic", + "topicType": "sensor_msgs/msg/CameraInfo" + }, + "isFilled": false + }, + { + "type": "topic", + "dependency": { + "type": "topic", + "topicType": "sensor_msgs/msg/Image" + }, + "isFilled": false + }, + { + "type": "formBuilder", + "dependency": { + "result": "{\"process\":\\${:OBJECT:{\"type\":\"WEIGHTS\"},\"object_name\":\\${object_name:string:}}", + "context": "", + "form": [], + "output": "", + "type": "formBuilder" + }, + "isFilled": false + } + ], + "typeAction": "ACTION" + }, + { + "name": "odStop", + "type": "stop", + "param": [], + "typeAction": "ACTION" + }, + { + "name": "isDetectionRun", + "type": "if", + "param": [], + "typeAction": "CONDITION" + }, + { + "name": "isDetection", + "type": "if", + "param": [], + "typeAction": "CONDITION" + } + ], + "topicsOut": [ + { + "name": "lc_yolo/object_detection", + "type": "rbs_skill_interfaces/msg/BoundBox" + }, + { + "name": "lc_yolo/detect_image", + "type": "sensor_msgs/msg/Image" + } + ], + "Launch": { + "executable": "od_yolo_lc.py", + "package": "rbss_objectdetection" + }, + "Settings": { + "result": "{\"params\": \\${ITEM:Array:[]}}", + "context": "type ITEM = {\"name\": \\${NAME:string:default},\"value\": \\${VALUE:string:default}};", + "form": [ + "{\"name\":\"ITEM\",\"type\":\"Array\",\"defaultValue\":\"[]\",\"values\":[{\"name\":\"NAME\",\"type\":\"string\",\"defaultValue\":\"default\",\"isOpen\":false,\"id\":\"fa8b442a-101b-448b-b5fd-55ad64f8e578\"},{\"name\":\"VALUE\",\"type\":\"string\",\"defaultValue\":\"default\",\"isOpen\":false,\"id\":\"c59b86d8-a54b-42da-b2bf-5fdfeff7f711\"}],\"totalValue\":[],\"isOpen\":true,\"subType\":\"ITEM\",\"id\":\"8be7af67-5860-4a3f-bdab-09c75a53bff7\"}" + ], + "output": { + "params": [] + }, + "type": "formBuilder" + }, + "__v": 0 +} \ No newline at end of file diff --git a/web_p/rbs_train2.py b/web_p/rbs_train2.py new file mode 100644 index 0000000..f72257d --- /dev/null +++ b/web_p/rbs_train2.py @@ -0,0 +1,64 @@ +""" + rbs_train2 + Общая задача: web-service pipeline + Реализуемая функция: обучение нейросетевой модели по заданному BOP-датасету + + python3 $PYTHON_EDUCATION --path /home/user/webservice/server/build/public/process/proc/inst_proc \ + --form /home/user/webservice/server/build/public/process/proc/inst_proc/form.json + + 28.01.2025 @shalenikol release 0.1 + 17.02.2025 @shalenikol release 0.2 addon_dir +""" +import argparse +import os +import json +from train_Yolo import train_YoloV8 +from train_Dope import train_Dope_i + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--path", required=True, help="Output path for weights") + parser.add_argument("--form", required=True, help="Json-file with training parameters") + args = parser.parse_args() + + if not os.path.isdir(args.path): + print(f"Invalid output path '{args.path}'") + exit(-1) + wname = os.path.basename(args.path) + outpath = os.path.dirname(args.path) + + if not os.path.isfile(args.form): + print(f"Error: no such file '{args.form}'") + exit(-2) + with open(args.form, "r") as f: + j_data = f.read() + try: + cfg = json.loads(j_data) + except json.JSONDecodeError as e: + print(f"JSon error: {e}") + exit(-3) + + cfg = cfg["output"] # edited params + dataset_params = cfg["process"]["selectProcess"]["value"] + dataset_type = dataset_params["type"] + if dataset_type != "BOP_DATASET": + print(f"Error: Invalid dataset type '{dataset_type}'") + exit(-4) + dataset_name = dataset_params["instanceName"] + dataset_path = dataset_params["path"] + dataset_path = dataset_path.replace("//", "/") # !!! TODO !!! Nikita + + epoch = cfg["n_epoch"] + pretrain = (cfg["pretrain"] == "True") #False + ttype = cfg["typeWeight"] #"ObjectDetection" + + addon_dir = "" + if "addon" in cfg: + addon = cfg["addon"].strip() + if addon and os.path.isdir(addon): + addon_dir = addon + + if ttype == "ObjectDetection": + train_YoloV8(dataset_path, wname, dataset_name, outpath, epoch, pretrain, addon_dir) + else: + train_Dope_i(dataset_path, wname, dataset_name, outpath, epoch, pretrain) diff --git a/web_p/renderBOPdataset2.py b/web_p/renderBOPdataset2.py index 61cdf90..3a037fd 100755 --- a/web_p/renderBOPdataset2.py +++ b/web_p/renderBOPdataset2.py @@ -8,6 +8,7 @@ import blenderproc as bproc 02.05.2024 @shalenikol release 0.1 02.07.2024 @shalenikol release 0.2 28.10.2024 @shalenikol release 0.3 + 28.02.2025 @shalenikol release 0.4 blenderproc 2.8.0 + blender 4.2.1 LTS """ import numpy as np import argparse @@ -16,17 +17,32 @@ import os import shutil import json from pathlib import Path +import time + +########################### +# !!! чтобы избежать ошибки в версии 2.8.0 +# free(): invalid pointer +# при вызове bproc.writer.write_bop +import pyrender +from pyrender.platforms import egl +########################### + +start_time = time.time() # Запоминаем время начала import bpy VHACD_PATH = "blenderproc_resources/vhacd" DIR_MODELS = "models" -DIR_MESH = "assets/libs/objects/" +# DIR_MESH = "assets/libs/objects/" FILE_LOG_SCENE = "res.txt" FILE_RBS_INFO = "rbs_info.json" FILE_GT_COCO = "scene_gt_coco.json" -EXT_MODELS = ".fbx" +FILE_PARAMS = "form.json" +PROCEDURAL_TEXTURE = "texture_path" # key in randomization params: for texture types (Noise Textures), (Procedural Patterns) or (Tileable Textures) +EXT_MODELS = ".fbx" # for scene objects (floor ...) +DETAIL_KEY = "daeUrl" # "fbx" # key in dict 'Detail' for mesh path of model TEXTURE_TMPL = "*.jpg" +TEXTURE_IMAGE_TYPES = ["Base Color", "Metallic", "Normal", "Roughness", "Specular IOR Level"] Not_Categories_Name = True # наименование категории в COCO-аннотации отсутствует @@ -57,19 +73,50 @@ def convert2relative(height, width, bbox): y += h/2 return x/width, y/height, w/width, h/height +def convert_seconds(total_seconds): + hours = int(total_seconds // 3600) + minutes = int((total_seconds % 3600) // 60) + seconds = int(total_seconds % 60) + return f"{hours:02}:{minutes:02}:{seconds:02}" + def render() -> int: + res_dir = rnd_par.output_dir + log_dir = os.path.dirname(res_dir) + # copy file with randomization params + file_params = os.path.join(res_dir, FILE_PARAMS) + if os.path.isfile(file_params): + shutil.copy2(file_params, log_dir) + + if os.path.isdir(res_dir): + shutil.rmtree(res_dir) + i = 0 for obj in all_meshs: # Make the object actively participate in the physics simulation obj.enable_rigidbody(active=True, collision_shape="COMPOUND") # Also use convex decomposition as collision shapes obj.build_convex_decomposition_collision_shape(VHACD_PATH) + + # # это для procedural texture, но пока не правильно + # fn = (os.path.splitext(rnd_par.models.filenames[i]))[0] + ".jpg" # файл с текстурой + # if os.path.isfile(fn): + # material = bproc.material.create_material_from_texture(fn, material_name="texture_model"+str(i)) + # # Применяем текстуру к материалу + # obj.replace_materials(material) + tex = rnd_par.models.textures[i] # описание текстур + if tex["is"]: + mat = bproc.material.create("m"+str(i)) + for x in tex["t_images"]: + key = list(x.keys())[0] + mat.set_principled_shader_value(key, bpy.data.images.load(filepath=x[key])) + obj.replace_materials(mat) + i += 1 # print(f"{i} : {obj.get_name()}") objs = all_meshs + rnd_par.scene.objs - log_txt = os.path.join(os.path.dirname(rnd_par.output_dir), FILE_LOG_SCENE) + log_txt = os.path.join(log_dir, FILE_LOG_SCENE) with open(log_txt, "w") as fh: for i,o in enumerate(objs): loc = o.get_location() @@ -91,23 +138,21 @@ def render() -> int: rnd_par.image_size_wh[1], lens_unit="FOV") + # Enable transparency so the background becomes transparent + bproc.renderer.set_output_format(enable_transparency=True) # ??? # add segmentation masks (per class and per instance) bproc.renderer.enable_segmentation_output(map_by=["category_id", "instance", "name"]) # activate depth rendering bproc.renderer.enable_depth_output(activate_antialiasing=False) - # res_dir = os.path.join(rnd_par.output_dir, rnd_par.ds_name) - res_dir = rnd_par.output_dir - if os.path.isdir(res_dir): - shutil.rmtree(res_dir) # Цикл рендеринга # Do multiple times: Position the shapenet objects using the physics simulator and render X images with random camera poses for r in range(rnd_par.n_series): print(f"********** Series : {r+1}") - is_texture = True if "texture_path" in rnd_par.models_randomization else False + is_texture = True if PROCEDURAL_TEXTURE in rnd_par.models_randomization else False if is_texture: - val = rnd_par.models_randomization["texture_path"] + val = rnd_par.models_randomization[PROCEDURAL_TEXTURE] l_texture = _get_list_texture(val) image = bpy.data.images.load(filepath=str(l_texture[r % len(l_texture)])) # один случайный объект в кадре / все заданные объекты @@ -125,16 +170,32 @@ def render() -> int: for i,o in enumerate(rnd_par.scene.objs): # объекты сцены rnd_mat = rnd_par.scene.obj_data[i]["material_randomization"] + + # if PROCEDURAL_TEXTURE in rnd_mat: # путь к текстурам (*.jpg) + # mat = bproc.material.create("m"+str(i)) + # # for x in tex["t_images"]: + # # key = list(x.keys())[0] + # val = rnd_mat[PROCEDURAL_TEXTURE] + # val = _get_list_texture(val) + # image = bpy.data.images.load(filepath=str(random.choice(val))) + # mat.set_principled_shader_value("Base Color", image) + # o.replace_materials(mat) + mats = o.get_materials() #[0] for mat in mats: + + # with open(log_txt, "a") as fh: + # fh.write("************* mat\n") + # fh.write(f"{mat}\n") + val = rnd_mat["specular"] - mat.set_principled_shader_value("Specular", random.uniform(val[0], val[1])) + mat.set_principled_shader_value("Specular IOR Level", random.uniform(val[0], val[1])) # для Blender < 4.2 было "Specular" val = rnd_mat["roughness"] mat.set_principled_shader_value("Roughness", random.uniform(val[0], val[1])) val = rnd_mat["metallic"] mat.set_principled_shader_value("Metallic", random.uniform(val[0], val[1])) - if "texture_path" in rnd_mat: # путь к текстурам (*.jpg) - val = rnd_mat["texture_path"] + if PROCEDURAL_TEXTURE in rnd_mat: # путь к текстурам (*.jpg) + val = rnd_mat[PROCEDURAL_TEXTURE] val = _get_list_texture(val) image = bpy.data.images.load(filepath=str(random.choice(val))) mat.set_principled_shader_value("Base Color", image) @@ -156,7 +217,7 @@ def render() -> int: # Define a function that samples 6-DoF poses def sample_pose(obj: bproc.types.MeshObject): obj.set_location(np.random.uniform(rnd_par.loc_range_low, rnd_par.loc_range_high)) #[-1, -1, 0], [1, 1, 2])) - obj.set_rotation_euler(bproc.sampler.uniformSO3()) + obj.set_rotation_euler(bproc.sampler.uniformSO3(around_x=rnd_par.around_x, around_y=rnd_par.around_y, around_z=rnd_par.around_z)) # Sample the poses of all shapenet objects above the ground without any collisions in-between bproc.object.sample_poses(meshs, @@ -232,7 +293,12 @@ def render() -> int: rec["name"] = objn rec["model"] = os.path.join(DIR_MODELS, os.path.split(rnd_par.models.filenames[i])[1]) # путь относительный t = [obj.get_bound_box(local_coords=True).tolist() for obj in all_meshs if obj.get_name() == objn] - rec["cuboid"] = t[0] + if len(t) > 0: + rec["cuboid"] = t[0] + else: # object name does not match file name + rec["Error"] = "!!! object name does not match file name: cuboid is zero" + rec["cuboid"] = np.zeros((8, 3)).tolist() + data.append(rec) shutil.copy2(rnd_par.models.filenames[i], models_dir) f = (os.path.splitext(rnd_par.models.filenames[i]))[0] + ".mtl" # файл материала @@ -283,9 +349,37 @@ def render() -> int: if Not_Categories_Name: explore(res_dir) + + end_time = time.time() # время окончания + execution_time = end_time - start_time # время выполнения + with open(log_txt, "a") as fh: + fh.write("*****************\n") + fh.write(f"Время выполнения: {convert_seconds(execution_time)}\n") return 0 # success -def _get_models(par, data) -> int: +def set_texture_model(name: str, textures: list, model_d) -> None: + """ + textures заполняется массивом текстур вида: + [{"is": True, "t_images": [{"Base Color":"/path/to/shkaf_d.png"}, {"Normal":"/path/to/shkaf_n.png"}] }, ... ] + """ + d = {"is": False} + if "models" in model_d: + for model in model_d["models"]: + if model["name"] == name: + path = model["texture_dir"].strip() + if path: + t_images = [] + for x in TEXTURE_IMAGE_TYPES: + if x in model: + rel_path = model[x].strip() + if rel_path: + t_images.append({x: os.path.join(path, rel_path)}) + if len(t_images): + d["is"] = True + d["t_images"] = t_images + textures.append(d) + +def _get_models(par, data, models_data) -> int: global all_meshs par.models = lambda: None @@ -294,13 +388,14 @@ def _get_models(par, data) -> int: return 0 # no models # загрузим объекты - par.models.names = [] # obj_names - par.models.filenames = [] # obj_filenames + par.models.names = [] + par.models.filenames = [] + par.models.textures = [] i = 1 for f in data: nam = f["name"] par.models.names.append(nam) - ff = f["fbx"] # _get_path_model(nam) + ff = f[DETAIL_KEY] # _get_path_model(nam) par.models.filenames.append(ff) if not os.path.isfile(ff): print(f"Error: no such file '{ff}'") @@ -311,6 +406,7 @@ def _get_models(par, data) -> int: obj = bproc.loader.load_obj(ff) all_meshs += obj obj[0].set_cp("category_id", i) # начиная с 1 + set_texture_model(nam, par.models.textures, models_data) i += 1 return par.models.n_item @@ -370,8 +466,6 @@ if __name__ == "__main__": print(f"JSon error: {e}") exit(-2) - # output_dir = args.path - ds_cfg = cfg["output"] # dataset config generation = ds_cfg["generation"] cam_pos = ds_cfg["camera_position"] @@ -399,11 +493,14 @@ if __name__ == "__main__": rnd_par.models_randomization = models_randomization rnd_par.loc_range_low = models_randomization["loc_range_low"] rnd_par.loc_range_high = models_randomization["loc_range_high"] + rnd_par.around_x = (models_randomization["around_x"] == "True") + rnd_par.around_y = (models_randomization["around_y"] == "True") + rnd_par.around_z = (models_randomization["around_z"] == "True") bproc.init() all_meshs = [] - if _get_models(rnd_par, rnd_par.dataset_objs) <= 0: + if _get_models(rnd_par, rnd_par.dataset_objs, models_randomization) <= 0: print("Error: no models in config") exit(-4) if _get_scene(rnd_par, ds_cfg["scene"]) <= 0: diff --git a/web_p/train/F1_curve.png b/web_p/train/F1_curve.png deleted file mode 100644 index 1a4e9ca..0000000 Binary files a/web_p/train/F1_curve.png and /dev/null differ diff --git a/web_p/train/PR_curve.png b/web_p/train/PR_curve.png deleted file mode 100644 index c961c64..0000000 Binary files a/web_p/train/PR_curve.png and /dev/null differ diff --git a/web_p/train/P_curve.png b/web_p/train/P_curve.png deleted file mode 100644 index b62292b..0000000 Binary files a/web_p/train/P_curve.png and /dev/null differ diff --git a/web_p/train/R_curve.png b/web_p/train/R_curve.png deleted file mode 100644 index 757d47e..0000000 Binary files a/web_p/train/R_curve.png and /dev/null differ diff --git a/web_p/train/args.yaml b/web_p/train/args.yaml deleted file mode 100644 index f69a613..0000000 --- a/web_p/train/args.yaml +++ /dev/null @@ -1,105 +0,0 @@ -task: detect -mode: train -model: /home/shalenikol/fork_work/webservice/server/build/public/4c4f3909-74b0-4206-aec1-fc4acd3a1081/weights/od_w01/yolov8n.pt -data: /home/shalenikol/fork_work/webservice/server/build/public/4c4f3909-74b0-4206-aec1-fc4acd3a1081/weights/od_w01/rbs_train.yaml -epochs: 33 -time: null -patience: 50 -batch: 16 -imgsz: 640 -save: true -save_period: -1 -cache: false -device: null -workers: 8 -project: /home/shalenikol/fork_work/webservice/server/build/public/4c4f3909-74b0-4206-aec1-fc4acd3a1081/weights/od_w01 -name: train -exist_ok: false -pretrained: true -optimizer: auto -verbose: true -seed: 0 -deterministic: true -single_cls: false -rect: false -cos_lr: false -close_mosaic: 10 -resume: false -amp: true -fraction: 1.0 -profile: false -freeze: null -multi_scale: false -overlap_mask: true -mask_ratio: 4 -dropout: 0.0 -val: true -split: val -save_json: false -save_hybrid: false -conf: null -iou: 0.7 -max_det: 300 -half: false -dnn: false -plots: true -source: null -vid_stride: 1 -stream_buffer: false -visualize: false -augment: false -agnostic_nms: false -classes: null -retina_masks: false -embed: null -show: false -save_frames: false -save_txt: false -save_conf: false -save_crop: false -show_labels: true -show_conf: true -show_boxes: true -line_width: null -format: torchscript -keras: false -optimize: false -int8: false -dynamic: false -simplify: false -opset: null -workspace: 4 -nms: false -lr0: 0.01 -lrf: 0.01 -momentum: 0.937 -weight_decay: 0.0005 -warmup_epochs: 3.0 -warmup_momentum: 0.8 -warmup_bias_lr: 0.1 -box: 7.5 -cls: 0.5 -dfl: 1.5 -pose: 12.0 -kobj: 1.0 -label_smoothing: 0.0 -nbs: 64 -hsv_h: 0.015 -hsv_s: 0.7 -hsv_v: 0.4 -degrees: 0.0 -translate: 0.1 -scale: 0.5 -shear: 0.0 -perspective: 0.0 -flipud: 0.0 -fliplr: 0.5 -mosaic: 1.0 -mixup: 0.0 -copy_paste: 0.0 -auto_augment: randaugment -erasing: 0.4 -crop_fraction: 1.0 -cfg: null -tracker: botsort.yaml -save_dir: /home/shalenikol/fork_work/webservice/server/build/public/4c4f3909-74b0-4206-aec1-fc4acd3a1081/weights/od_w01/train diff --git a/web_p/train/confusion_matrix.png b/web_p/train/confusion_matrix.png deleted file mode 100644 index 24a7fb8..0000000 Binary files a/web_p/train/confusion_matrix.png and /dev/null differ diff --git a/web_p/train/confusion_matrix_normalized.png b/web_p/train/confusion_matrix_normalized.png deleted file mode 100644 index 29fc07a..0000000 Binary files a/web_p/train/confusion_matrix_normalized.png and /dev/null differ diff --git a/web_p/train/events.out.tfevents.1732122141.shalenikol-desktop.109110.0 b/web_p/train/events.out.tfevents.1732122141.shalenikol-desktop.109110.0 deleted file mode 100644 index 825cfe6..0000000 Binary files a/web_p/train/events.out.tfevents.1732122141.shalenikol-desktop.109110.0 and /dev/null differ diff --git a/web_p/train/labels.jpg b/web_p/train/labels.jpg deleted file mode 100644 index b9fb066..0000000 Binary files a/web_p/train/labels.jpg and /dev/null differ diff --git a/web_p/train/labels_correlogram.jpg b/web_p/train/labels_correlogram.jpg deleted file mode 100644 index e634e7b..0000000 Binary files a/web_p/train/labels_correlogram.jpg and /dev/null differ diff --git a/web_p/train/results.csv b/web_p/train/results.csv deleted file mode 100644 index b6231f9..0000000 --- a/web_p/train/results.csv +++ /dev/null @@ -1,34 +0,0 @@ - epoch, train/box_loss, train/cls_loss, train/dfl_loss, metrics/precision(B), metrics/recall(B), metrics/mAP50(B), metrics/mAP50-95(B), val/box_loss, val/cls_loss, val/dfl_loss, lr/pg0, lr/pg1, lr/pg2 - 1, 0.62674, 1.281, 0.92555, 0.99239, 0.99448, 0.99323, 0.90966, 0.40212, 0.8264, 0.80447, 0.00066247, 0.00066247, 0.00066247 - 2, 0.60996, 0.71899, 0.93387, 0.9945, 0.99945, 0.99484, 0.91551, 0.43253, 0.60301, 0.8228, 0.0012893, 0.0012893, 0.0012893 - 3, 0.58648, 0.54879, 0.92909, 1, 0.98871, 0.99494, 0.9213, 0.40211, 0.39327, 0.81593, 0.0018761, 0.0018761, 0.0018761 - 4, 0.58195, 0.48301, 0.92375, 0.99087, 0.9337, 0.97172, 0.89393, 0.41614, 0.46785, 0.82069, 0.00182, 0.00182, 0.00182 - 5, 0.56201, 0.44926, 0.92381, 0.99447, 0.99385, 0.99494, 0.94951, 0.34807, 0.32406, 0.8013, 0.00182, 0.00182, 0.00182 - 6, 0.52696, 0.40581, 0.9068, 0.95813, 0.98343, 0.99281, 0.94494, 0.33023, 0.48053, 0.79401, 0.00176, 0.00176, 0.00176 - 7, 0.51017, 0.3952, 0.90752, 0.99889, 1, 0.995, 0.95388, 0.3192, 0.33973, 0.7992, 0.0017, 0.0017, 0.0017 - 8, 0.50772, 0.37889, 0.90238, 0.98351, 0.98842, 0.98581, 0.94918, 0.30154, 0.28504, 0.79667, 0.00164, 0.00164, 0.00164 - 9, 0.47737, 0.3576, 0.89251, 0.99946, 0.99448, 0.995, 0.97205, 0.28135, 0.23642, 0.79101, 0.00158, 0.00158, 0.00158 - 10, 0.46587, 0.34547, 0.89324, 0.99948, 1, 0.995, 0.96897, 0.28021, 0.28522, 0.78694, 0.00152, 0.00152, 0.00152 - 11, 0.45881, 0.33452, 0.89055, 0.99954, 1, 0.995, 0.97012, 0.26364, 0.21443, 0.7813, 0.00146, 0.00146, 0.00146 - 12, 0.44939, 0.32887, 0.89206, 0.9996, 1, 0.995, 0.98382, 0.24486, 0.20614, 0.78109, 0.0014, 0.0014, 0.0014 - 13, 0.44388, 0.32289, 0.88796, 0.99932, 1, 0.995, 0.97195, 0.27681, 0.21443, 0.77933, 0.00134, 0.00134, 0.00134 - 14, 0.43847, 0.31282, 0.88496, 0.99965, 1, 0.995, 0.98019, 0.25014, 0.20255, 0.7775, 0.00128, 0.00128, 0.00128 - 15, 0.41585, 0.30067, 0.8774, 0.99943, 1, 0.995, 0.97609, 0.25842, 0.21239, 0.78006, 0.00122, 0.00122, 0.00122 - 16, 0.41436, 0.29784, 0.87488, 0.99964, 1, 0.995, 0.97823, 0.25499, 0.19837, 0.78004, 0.00116, 0.00116, 0.00116 - 17, 0.414, 0.29771, 0.87575, 0.99943, 1, 0.995, 0.98746, 0.2251, 0.203, 0.77468, 0.0011, 0.0011, 0.0011 - 18, 0.39273, 0.29075, 0.86927, 0.99445, 1, 0.995, 0.98597, 0.22693, 0.19648, 0.77208, 0.00104, 0.00104, 0.00104 - 19, 0.40052, 0.28802, 0.87804, 0.99958, 1, 0.995, 0.98541, 0.22268, 0.18749, 0.77233, 0.00098, 0.00098, 0.00098 - 20, 0.38066, 0.27951, 0.86666, 0.99969, 1, 0.995, 0.98901, 0.20959, 0.1775, 0.7697, 0.00092, 0.00092, 0.00092 - 21, 0.38115, 0.27813, 0.8658, 0.99964, 1, 0.995, 0.98895, 0.20699, 0.1779, 0.77073, 0.00086, 0.00086, 0.00086 - 22, 0.37441, 0.27094, 0.87121, 0.99965, 1, 0.995, 0.98975, 0.20138, 0.17235, 0.76785, 0.0008, 0.0008, 0.0008 - 23, 0.36808, 0.26148, 0.86426, 0.99965, 1, 0.995, 0.98829, 0.19861, 0.1628, 0.76706, 0.00074, 0.00074, 0.00074 - 24, 0.25547, 0.199, 0.77555, 0.99955, 1, 0.995, 0.98791, 0.21853, 0.18063, 0.76972, 0.00068, 0.00068, 0.00068 - 25, 0.24799, 0.1969, 0.78404, 0.99958, 1, 0.995, 0.98812, 0.23069, 0.18178, 0.76985, 0.00062, 0.00062, 0.00062 - 26, 0.24232, 0.1915, 0.78022, 0.99968, 1, 0.995, 0.99024, 0.20883, 0.16788, 0.76752, 0.00056, 0.00056, 0.00056 - 27, 0.23288, 0.1839, 0.77463, 0.99968, 1, 0.995, 0.99151, 0.2026, 0.16501, 0.76809, 0.0005, 0.0005, 0.0005 - 28, 0.23066, 0.18012, 0.77547, 0.99961, 1, 0.995, 0.98912, 0.19388, 0.1534, 0.76246, 0.00044, 0.00044, 0.00044 - 29, 0.22286, 0.17062, 0.77932, 0.9997, 1, 0.995, 0.99039, 0.20566, 0.14978, 0.76601, 0.00038, 0.00038, 0.00038 - 30, 0.21427, 0.16357, 0.77529, 0.9997, 1, 0.995, 0.99215, 0.18345, 0.14148, 0.76206, 0.00032, 0.00032, 0.00032 - 31, 0.20895, 0.16067, 0.77189, 0.9997, 1, 0.995, 0.99187, 0.17027, 0.13746, 0.76124, 0.00026, 0.00026, 0.00026 - 32, 0.20248, 0.15421, 0.77526, 0.9997, 1, 0.995, 0.99246, 0.17229, 0.13828, 0.76056, 0.0002, 0.0002, 0.0002 - 33, 0.19494, 0.15005, 0.76361, 0.99971, 1, 0.995, 0.99302, 0.16442, 0.12543, 0.76043, 0.00014, 0.00014, 0.00014 diff --git a/web_p/train/results.png b/web_p/train/results.png deleted file mode 100644 index 1978bab..0000000 Binary files a/web_p/train/results.png and /dev/null differ diff --git a/web_p/train/train_batch0.jpg b/web_p/train/train_batch0.jpg deleted file mode 100644 index 4cb50d7..0000000 Binary files a/web_p/train/train_batch0.jpg and /dev/null differ diff --git a/web_p/train/train_batch1.jpg b/web_p/train/train_batch1.jpg deleted file mode 100644 index 5a20720..0000000 Binary files a/web_p/train/train_batch1.jpg and /dev/null differ diff --git a/web_p/train/train_batch2.jpg b/web_p/train/train_batch2.jpg deleted file mode 100644 index 0ccd9f0..0000000 Binary files a/web_p/train/train_batch2.jpg and /dev/null differ diff --git a/web_p/train/train_batch3657.jpg b/web_p/train/train_batch3657.jpg deleted file mode 100644 index 0f1eac8..0000000 Binary files a/web_p/train/train_batch3657.jpg and /dev/null differ diff --git a/web_p/train/train_batch3658.jpg b/web_p/train/train_batch3658.jpg deleted file mode 100644 index 615376e..0000000 Binary files a/web_p/train/train_batch3658.jpg and /dev/null differ diff --git a/web_p/train/train_batch3659.jpg b/web_p/train/train_batch3659.jpg deleted file mode 100644 index 0248e7a..0000000 Binary files a/web_p/train/train_batch3659.jpg and /dev/null differ diff --git a/web_p/train/val_batch0_labels.jpg b/web_p/train/val_batch0_labels.jpg deleted file mode 100644 index bd751a0..0000000 Binary files a/web_p/train/val_batch0_labels.jpg and /dev/null differ diff --git a/web_p/train/val_batch0_pred.jpg b/web_p/train/val_batch0_pred.jpg deleted file mode 100644 index aac07e4..0000000 Binary files a/web_p/train/val_batch0_pred.jpg and /dev/null differ diff --git a/web_p/train/val_batch1_labels.jpg b/web_p/train/val_batch1_labels.jpg deleted file mode 100644 index 23b8ca3..0000000 Binary files a/web_p/train/val_batch1_labels.jpg and /dev/null differ diff --git a/web_p/train/val_batch1_pred.jpg b/web_p/train/val_batch1_pred.jpg deleted file mode 100644 index b71a650..0000000 Binary files a/web_p/train/val_batch1_pred.jpg and /dev/null differ diff --git a/web_p/train/val_batch2_labels.jpg b/web_p/train/val_batch2_labels.jpg deleted file mode 100644 index 353ceea..0000000 Binary files a/web_p/train/val_batch2_labels.jpg and /dev/null differ diff --git a/web_p/train/val_batch2_pred.jpg b/web_p/train/val_batch2_pred.jpg deleted file mode 100644 index ce2e32f..0000000 Binary files a/web_p/train/val_batch2_pred.jpg and /dev/null differ diff --git a/web_p/train/weights/best.pt b/web_p/train/weights/best.pt deleted file mode 100644 index 943e696..0000000 Binary files a/web_p/train/weights/best.pt and /dev/null differ diff --git a/web_p/train/weights/last.pt b/web_p/train/weights/last.pt deleted file mode 100644 index d48d34d..0000000 Binary files a/web_p/train/weights/last.pt and /dev/null differ diff --git a/web_p/train_Yolo.py b/web_p/train_Yolo.py index 1eaf7a0..6d77902 100644 --- a/web_p/train_Yolo.py +++ b/web_p/train_Yolo.py @@ -7,6 +7,8 @@ --name test123 --datasetName ds213 --outpath /Users/idontsudo/webservice/server/build/public/7065d6b6-c8a3-48c5-9679-bb8f3a690296/weights 27.04.2024 @shalenikol release 0.1 + 20.11.2024 @shalenikol release 0.2 parser.add_argument("--addon", default="", help="Folder with add-on for dataset") + 20.02.2025 @shalenikol release 0.2.1 add_on_dataset : fix """ import os import shutil @@ -14,9 +16,13 @@ import json import yaml from ultralytics import YOLO +# from ultralytics import settings # from ultralytics.utils.metrics import DetMetrics +# import torch +# import torch.profiler +# import torch.utils.data -FILE_BASEMODEL = "yolov8n.pt" +FILE_BASEMODEL = "yolov8s.pt" #"yolov8n.pt" FILE_RBS_INFO = "rbs_info.json" FILE_RBS_TRAIN = "rbs_train.yaml" FILE_GT_COCO = "scene_gt_coco.json" @@ -27,6 +33,7 @@ DIR_ROOT_DS = "datasets" DIR_COCO_DS = "rbs_coco" DIR_RGB_DS = "images" DIR_LABELS_DS = "labels" +LABELS_EXT = ".txt" SZ_SERIES = 15 # number of train images per validation images @@ -40,6 +47,50 @@ def convert2relative(height, width, bbox): y += h/2 return x/width, y/height, w/width, h/height +def add_on_dataset(source_dir, target_dir) -> dict: + global nn_image, f1, f2 + # Получаем список файлов в исходной директории + files = sorted(os.listdir(source_dir)) + + # Словарь для отслеживания порядковых номеров для каждого имени файла + file_nn = {} + + for file in files: + if os.path.isdir(os.path.join(source_dir, file)): + continue + # Получаем имя файла и его расширение + file_name, file_extension = os.path.splitext(file) + + # Запоминаем порядковый номер для данного имени файла + if file_name in file_nn: + nn = file_nn[file_name] + else: # new file name + nn = nn_image # текущий номер + file_nn[file_name] = nn_image + nn_image += 1 + + # Создаем новое имя файла + new_file_name = f"{nn:06}{file_extension}" + if file_extension == LABELS_EXT: + new_file_path = os.path.join(target_dir, DIR_LABELS_DS) + else: + new_file_path = os.path.join(target_dir, DIR_RGB_DS) + + line = os.path.join("./", DIR_RGB_DS, new_file_name) + "\n" + if nn % SZ_SERIES == 0: + f2.write(line) + else: + f1.write(line) + + # Полные пути к старому и новому файлам + old_file_path = os.path.join(source_dir, file) + new_file_path = os.path.join(new_file_path, new_file_name) + + # Копируем файл + shutil.copy2(old_file_path, new_file_path) + + return file_nn + def gt_parse(path: str, out_dir: str): global nn_image, f1, f2 with open(os.path.join(path, FILE_GT_COCO), "r") as fh: @@ -67,12 +118,12 @@ def gt_parse(path: str, out_dir: str): # формат: fh.write(f"{cat_id-1} {rel[0]} {rel[1]} {rel[2]} {rel[3]}\n") # category from 0 - nn_image += 1 line = os.path.join("./", DIR_RGB_DS, f + ext) + "\n" if nn_image % SZ_SERIES == 0: f2.write(line) else: f1.write(line) + nn_image += 1 def explore(path: str, res_dir: str): if not os.path.isdir(path): @@ -88,7 +139,7 @@ def explore(path: str, res_dir: str): else: explore(path_entry, res_dir) -def BOP2Yolo_dataset(dpath: str, out_dir: str, lname: list) -> str: +def BOP2Yolo_dataset(dpath: str, out_dir: str, lname: list, addon:str) -> str: """ Convert BOP-dataset to YOLO format for train """ cfg_yaml = os.path.join(out_dir, FILE_RBS_TRAIN) p = os.path.join(out_dir, DIR_ROOT_DS, DIR_COCO_DS) @@ -116,12 +167,14 @@ def BOP2Yolo_dataset(dpath: str, out_dir: str, lname: list) -> str: f1 = open(os.path.join(res_dir, FILE_L_TRAIN), "w") f2 = open(os.path.join(res_dir, FILE_L_VAL), "w") explore(dpath, res_dir) + if addon: + add_on_dataset(addon, res_dir) f1.close() f2.close() return out_dir -def train_YoloV8(path:str, wname:str, dname:str, outpath:str, epochs:int, pretrain: bool): +def train_YoloV8(path:str, wname:str, dname:str, outpath:str, epochs:int, pretrain: bool, addon: str): """ Main procedure for train YOLOv8 model """ if not os.path.isdir(outpath): print(f"Invalid output path '{outpath}'") @@ -151,21 +204,33 @@ def train_YoloV8(path:str, wname:str, dname:str, outpath:str, epochs:int, pretra # список имён объектов list_name = list(map(lambda x: x["name"], y)) - dpath = BOP2Yolo_dataset(ds_path, out_dir, list_name) + dpath = BOP2Yolo_dataset(ds_path, out_dir, list_name, addon) if len(dpath) == 0: print(f"Error in convert dataset '{ds_path}' to '{outpath}'") exit(-4) model_path = os.path.join(dpath, FILE_BASEMODEL) model = YOLO(model_path) - results = model.train(data=os.path.join(dpath, FILE_RBS_TRAIN), epochs=epochs, project=out_dir) + + # # Update settings + # settings.update({"profile": True}) + + # prof = torch.profiler.profile( + # schedule=torch.profiler.schedule(wait=1, warmup=1, active=3, repeat=1), + # on_trace_ready=torch.profiler.tensorboard_trace_handler('./log/resnet18'), + # record_shapes=True, + # with_stack=True) + # prof.start() + results = model.train(data=os.path.join(dpath, FILE_RBS_TRAIN), epochs=epochs, project=out_dir) #, log_dir="runs/train") + # prof.stop() + wf = os.path.join(results.save_dir, FILE_TRAIN_RES) if not os.path.isfile(wf): print(f"Error in train: no result file '{wf}'") exit(-5) shutil.copy2(wf, os.path.join(dpath, wname + ".pt")) - shutil.rmtree(results.save_dir) + # shutil.rmtree(results.save_dir) if __name__ == "__main__": import argparse @@ -176,6 +241,7 @@ if __name__ == "__main__": parser.add_argument("--outpath", default="weights", help="Output path for weights") parser.add_argument("--epoch", default=3, type=int, help="How many training epochs") parser.add_argument('--pretrain', action="store_true", help="Use pretraining") + parser.add_argument("--addon", default="", help="Folder with add-on for dataset") args = parser.parse_args() - train_YoloV8(args.path, args.name, args.datasetName, args.outpath, args.epoch, args.pretrain) + train_YoloV8(args.path, args.name, args.datasetName, args.outpath, args.epoch, args.pretrain, args.addon) diff --git a/web_p/utils_dope.py b/web_p/utils_dope.py new file mode 100755 index 0000000..55ab058 --- /dev/null +++ b/web_p/utils_dope.py @@ -0,0 +1,967 @@ +""" +NVIDIA from jtremblay@gmail.com +""" +import numpy as np +import torch + +import os + +import torch +import torch.nn as nn +import torch.nn.parallel + +import torch.utils.data + +import torchvision.transforms as transforms + +import torch.utils.data as data +import glob +import os +import boto3 +import io + +from PIL import Image +from PIL import ImageDraw +from PIL import ImageEnhance + +from math import acos +from math import sqrt +from math import pi + +from os.path import exists, basename +import json +from os.path import join + +import albumentations as A + + +def default_loader(path): + return Image.open(path).convert("RGB") + + +def length(v): + return sqrt(v[0] ** 2 + v[1] ** 2) + + +def dot_product(v, w): + return v[0] * w[0] + v[1] * w[1] + + +def normalize(v): + norm = np.linalg.norm(v, ord=1) + if norm == 0: + norm = np.finfo(v.dtype).eps + return v / norm + + +def determinant(v, w): + return v[0] * w[1] - v[1] * w[0] + + +def inner_angle(v, w): + cosx = dot_product(v, w) / (length(v) * length(w)) + rad = acos(cosx) # in radians + return rad * 180 / pi # returns degrees + + +def py_ang(A, B=(1, 0)): + inner = inner_angle(A, B) + det = determinant(A, B) + if ( + det < 0 + ): # this is a property of the det. If the det < 0 then B is clockwise of A + return inner + else: # if the det > 0 then A is immediately clockwise of B + return 360 - inner + + +import colorsys, math + + +def append_dot(extensions): + res = [] + + for ext in extensions: + if not ext.startswith("."): + res.append(f".{ext}") + else: + res.append(ext) + + return res + + +def loadimages(root, extensions=["png"]): + imgs = [] + extensions = append_dot(extensions) + + def add_json_files( + path, + ): + for ext in extensions: + for file in os.listdir(path): + imgpath = os.path.join(path, file) + if ( + imgpath.endswith(ext) + and exists(imgpath) + and exists(imgpath.replace(ext, ".json")) + ): + imgs.append( + ( + imgpath, + imgpath.replace(path, "").replace("/", ""), + imgpath.replace(ext, ".json"), + ) + ) + + def explore(path): + if not os.path.isdir(path): + return + folders = [ + os.path.join(path, o) + for o in os.listdir(path) + if os.path.isdir(os.path.join(path, o)) + ] + + for path_entry in folders: + explore(path_entry) + + add_json_files(path) + + explore(root) + + return imgs + + +def loadweights(root): + if root.endswith(".pth") and os.path.isfile(root): + return [root] + else: + weights = [ + os.path.join(root, f) + for f in os.listdir(root) + if os.path.isfile(os.path.join(root, f)) and f.endswith(".pth") + ] + + weights.sort() + return weights + + +def loadimages_inference(root, extensions): + imgs, imgsname = [], [] + extensions = append_dot(extensions) + + def add_imgs( + path, + ): + for ext in extensions: + for file in os.listdir(path): + imgpath = os.path.join(path, file) + if imgpath.endswith(ext) and exists(imgpath): + imgs.append(imgpath) + imgsname.append(imgpath.replace(root, "")) + + def explore(path): + if not os.path.isdir(path): + return + folders = [ + os.path.join(path, o) + for o in os.listdir(path) + if os.path.isdir(os.path.join(path, o)) + ] + + for path_entry in folders: + explore(path_entry) + + add_imgs(path) + + explore(root) + + return imgs, imgsname + + +class CleanVisiiDopeLoader(data.Dataset): + def __init__( + self, + path_dataset, + objects=None, + sigma=1, + output_size=400, + extensions=["png"], + debug=False, + use_s3=False, + buckets=[], + endpoint_url=None, + ): + ################### + self.path_dataset = path_dataset + self.objects_interest = objects + self.sigma = sigma + self.output_size = output_size + self.extensions = append_dot(extensions) + self.debug = debug + ################### + + self.imgs = [] + self.s3_buckets = {} + self.use_s3 = use_s3 + + if self.use_s3: + self.session = boto3.Session() + self.s3 = self.session.resource( + service_name="s3", endpoint_url=endpoint_url + ) + + for bucket_name in buckets: + try: + self.s3_buckets[bucket_name] = self.s3.Bucket(bucket_name) + except Exception as e: + print( + f"Error trying to load bucket {bucket_name} for training data:", + e, + ) + + for bucket in self.s3_buckets: + bucket_objects = [ + str(obj.key) for obj in self.s3_buckets[bucket].objects.all() + ] + + jsons = set([json for json in bucket_objects if json.endswith(".json")]) + imgs = [ + img + for img in bucket_objects + if img.endswith(tuple(self.extensions)) + ] + + for ext in self.extensions: + for img in imgs: + # Only add images that have a ground truth file + if img.endswith(ext) and img.replace(ext, ".json") in jsons: + # (img key, bucket name, json key) + self.imgs.append((img, bucket, img.replace(ext, ".json"))) + + else: + for path_look in path_dataset: + self.imgs += loadimages(path_look, extensions=self.extensions) + + # np.random.shuffle(self.imgs) + print("Number of Training Images:", len(self.imgs)) + print(self.imgs) + + if debug: + print("Debuging will be save in debug/") + if os.path.isdir("debug"): + print(f'folder {"debug"}/ exists') + else: + os.mkdir("debug") + print(f'created folder {"debug"}/') + + def __len__(self): + return len(self.imgs) + + def __getitem__(self, index): + + # load the data + if self.use_s3: + img_key, bucket, json_key = self.imgs[index] + mem_img = io.BytesIO() + + object_img = self.s3_buckets[bucket].Object(img_key) + object_img.download_fileobj(mem_img) + + img = np.array(Image.open(mem_img).convert("RGB")) + + object_json = self.s3_buckets[bucket].Object(json_key) + data_json = json.load(object_json.get()["Body"]) + + img_name = img_key[:-3] + + else: + path_img, img_name, path_json = self.imgs[index] + + # load the image + img = np.array(Image.open(path_img).convert("RGB")) + + # load the json file + with open(path_json) as f: + data_json = json.load(f) + + all_projected_cuboid_keypoints = [] + + # load the projected cuboid keypoints + for obj in data_json["objects"]: + if ( + self.objects_interest is not None + and not obj["class"] in self.objects_interest + ): + continue + # load the projected_cuboid_keypoints + # 06.02.2024 @shalenikol + # if obj["visibility_image"] > 0: + if obj["visibility"] > 0: + projected_cuboid_keypoints = obj["projected_cuboid"] + # FAT dataset only has 8 corners for 'projected_cuboid' + if len(projected_cuboid_keypoints) == 8: + projected_cuboid_keypoints.append(obj["projected_cuboid_centroid"]) + else: + projected_cuboid_keypoints = [ + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + ] + all_projected_cuboid_keypoints.append(projected_cuboid_keypoints) + + if len(all_projected_cuboid_keypoints) == 0: + all_projected_cuboid_keypoints = [ + [ + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + [-100, -100], + ] + ] + + # flatten the keypoints + flatten_projected_cuboid = [] + for obj in all_projected_cuboid_keypoints: + for p in obj: + flatten_projected_cuboid.append(p) + + ####### + if self.debug: + img_to_save = Image.fromarray(img) + draw = ImageDraw.Draw(img_to_save) + + for ip, p in enumerate(flatten_projected_cuboid): + draw.ellipse( + (int(p[0]) - 2, int(p[1]) - 2, int(p[0]) + 2, int(p[1]) + 2), + fill="green", + ) + + img_to_save.save(f"debug/{img_name.replace('.png','_original.png')}") + ####### + + # data augmentation + transform = A.Compose( + [ + A.RandomCrop(width=400, height=400), + A.Rotate(limit=180), + A.RandomBrightnessContrast( + brightness_limit=0.2, contrast_limit=0.15, p=1 + ), + A.GaussNoise(p=1), + ], + keypoint_params=A.KeypointParams(format="xy", remove_invisible=False), + ) + transformed = transform(image=img, keypoints=flatten_projected_cuboid) + img_transformed = transformed["image"] + flatten_projected_cuboid_transformed = transformed["keypoints"] + + ####### + + # transform to the final output + if not self.output_size == 400: + transform = A.Compose( + [ + A.Resize(width=self.output_size, height=self.output_size), + ], + keypoint_params=A.KeypointParams(format="xy", remove_invisible=False), + ) + transformed = transform( + image=img_transformed, keypoints=flatten_projected_cuboid_transformed + ) + img_transformed_output_size = transformed["image"] + flatten_projected_cuboid_transformed_output_size = transformed["keypoints"] + + else: + img_transformed_output_size = img_transformed + flatten_projected_cuboid_transformed_output_size = ( + flatten_projected_cuboid_transformed + ) + + ####### + if self.debug: + img_transformed_saving = Image.fromarray(img_transformed) + + draw = ImageDraw.Draw(img_transformed_saving) + + for ip, p in enumerate(flatten_projected_cuboid_transformed): + draw.ellipse( + (int(p[0]) - 2, int(p[1]) - 2, int(p[0]) + 2, int(p[1]) + 2), + fill="green", + ) + + img_transformed_saving.save( + f"debug/{img_name.replace('.png','_transformed.png')}" + ) + ####### + + # update the keypoints list + # obj x keypoint_id x (x,y) + i_all = 0 + for i_obj, obj in enumerate(all_projected_cuboid_keypoints): + for i_p, point in enumerate(obj): + all_projected_cuboid_keypoints[i_obj][ + i_p + ] = flatten_projected_cuboid_transformed_output_size[i_all] + i_all += 1 + + # generate the belief maps + beliefs = CreateBeliefMap( + size=int(self.output_size), + pointsBelief=all_projected_cuboid_keypoints, + sigma=self.sigma, + nbpoints=9, + save=False, + ) + beliefs = torch.from_numpy(np.array(beliefs)) + # generate affinity fields with centroid. + affinities = GenerateMapAffinity( + size=int(self.output_size), + nb_vertex=8, + pointsInterest=all_projected_cuboid_keypoints, + objects_centroid=np.array(all_projected_cuboid_keypoints)[:, -1].tolist(), + scale=1, + ) + + # prepare for the image tensors + normalize_tensor = transforms.Compose( + [ + transforms.ToTensor(), + transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)), + ] + ) + to_tensor = transforms.Compose( + [ + transforms.ToTensor(), + ] + ) + img_tensor = normalize_tensor(Image.fromarray(img_transformed)) + img_original = to_tensor(img_transformed) + + ######## + if self.debug: + imgs = VisualizeBeliefMap(beliefs) + img, grid = save_image( + imgs, + f"debug/{img_name.replace('.png','_beliefs.png')}", + mean=0, + std=1, + nrow=3, + save=True, + ) + imgs = VisualizeAffinityMap(affinities) + save_image( + imgs, + f"debug/{img_name.replace('.png','_affinities.png')}", + mean=0, + std=1, + nrow=3, + save=True, + ) + ######## + img_tensor[torch.isnan(img_tensor)] = 0 + affinities[torch.isnan(affinities)] = 0 + beliefs[torch.isnan(beliefs)] = 0 + + img_tensor[torch.isinf(img_tensor)] = 0 + affinities[torch.isinf(affinities)] = 0 + beliefs[torch.isinf(beliefs)] = 0 + + return { + "img": img_tensor, + "affinities": torch.clamp(affinities, -1, 1), + "beliefs": torch.clamp(beliefs, 0, 1), + "file_name": img_name, + "img_original": img_original, + } + + +def VisualizeAffinityMap( + tensor, + # tensor of (len(keypoints)*2)xwxh + threshold_norm_vector=0.4, + # how long does the vector has to be to be drawn + points=None, + # list of points to draw in white on top of the image + factor=1.0, + # by how much the image was reduced, scale factor + translation=(0, 0) + # by how much the points were moved + # return len(keypoints)x3xwxh # stack of images +): + images = torch.zeros(tensor.shape[0] // 2, 3, tensor.shape[1], tensor.shape[2]) + for i_image in range(0, tensor.shape[0], 2): # could be read as i_keypoint + + indices = ( + torch.abs(tensor[i_image, :, :]) + torch.abs(tensor[i_image + 1, :, :]) + > threshold_norm_vector + ).nonzero() + + for indice in indices: + + i, j = indice + + angle_vector = np.array([tensor[i_image, i, j], tensor[i_image + 1, i, j]]) + if length(angle_vector) > threshold_norm_vector: + angle = py_ang(angle_vector) + c = colorsys.hsv_to_rgb(angle / 360, 1, 1) + else: + c = [0, 0, 0] + for i_c in range(3): + images[i_image // 2, i_c, i, j] = c[i_c] + if not points is None: + point = points[i_image // 2] + + print( + int(point[1] * factor + translation[1]), + int(point[0] * factor + translation[0]), + ) + images[ + i_image // 2, + :, + int(point[1] * factor + translation[1]) + - 1 : int(point[1] * factor + translation[1]) + + 1, + int(point[0] * factor + translation[0]) + - 1 : int(point[0] * factor + translation[0]) + + 1, + ] = 1 + + return images + + +def VisualizeBeliefMap( + tensor, + # tensor of len(keypoints)xwxh + points=None, + # list of points to draw on top of the image + factor=1.0, + # by how much the image was reduced, scale factor + translation=(0, 0) + # by how much the points were moved + # return len(keypoints)x3xwxh # stack of images in torch tensor +): + images = torch.zeros(tensor.shape[0], 3, tensor.shape[1], tensor.shape[2]) + for i_image in range(0, tensor.shape[0]): # could be read as i_keypoint + + belief = tensor[i_image].clone() + belief -= float(torch.min(belief).item()) + belief /= float(torch.max(belief).item()) + + belief = torch.clamp(belief, 0, 1) + belief = torch.cat( + [belief.unsqueeze(0), belief.unsqueeze(0), belief.unsqueeze(0)] + ).unsqueeze(0) + + images[i_image] = belief + + return images + + +def GenerateMapAffinity( + size, nb_vertex, pointsInterest, objects_centroid, scale, save=False +): + # Apply the downscale right now, so the vectors are correct. + + img_affinity = Image.new("RGB", (int(size / scale), int(size / scale)), "black") + # create the empty tensors + totensor = transforms.Compose([transforms.ToTensor()]) + + affinities = [] + for i_points in range(nb_vertex): + affinities.append(torch.zeros(2, int(size / scale), int(size / scale))) + + for i_pointsImage in range(len(pointsInterest)): + pointsImage = pointsInterest[i_pointsImage] + center = objects_centroid[i_pointsImage] + for i_points in range(nb_vertex): + point = pointsImage[i_points] + + affinity_pair, img_affinity = getAfinityCenter( + int(size / scale), + int(size / scale), + tuple((np.array(pointsImage[i_points]) / scale).tolist()), + tuple((np.array(center) / scale).tolist()), + img_affinity=img_affinity, + radius=1, + ) + + affinities[i_points] = (affinities[i_points] + affinity_pair) / 2 + + # Normalizing + v = affinities[i_points].numpy() + + xvec = v[0] + yvec = v[1] + + norms = np.sqrt(xvec * xvec + yvec * yvec) + nonzero = norms > 0 + + xvec[nonzero] /= norms[nonzero] + yvec[nonzero] /= norms[nonzero] + + affinities[i_points] = torch.from_numpy(np.concatenate([[xvec], [yvec]])) + affinities = torch.cat(affinities, 0) + + return affinities + + +def getAfinityCenter( + width, height, point, center, radius=7, tensor=None, img_affinity=None +): + """ + Create the affinity map + """ + if tensor is None: + tensor = torch.zeros(2, height, width).float() + + # create the canvas for the afinity output + imgAffinity = Image.new("RGB", (width, height), "black") + totensor = transforms.Compose([transforms.ToTensor()]) + draw = ImageDraw.Draw(imgAffinity) + r1 = radius + p = point + draw.ellipse((p[0] - r1, p[1] - r1, p[0] + r1, p[1] + r1), (255, 255, 255)) + + del draw + + # compute the array to add the afinity + array = (np.array(imgAffinity) / 255)[:, :, 0] + + angle_vector = np.array(center) - np.array(point) + angle_vector = normalize(angle_vector) + affinity = np.concatenate([[array * angle_vector[0]], [array * angle_vector[1]]]) + + if not img_affinity is None: + # find the angle vector + if length(angle_vector) > 0: + angle = py_ang(angle_vector) + else: + angle = 0 + c = np.array(colorsys.hsv_to_rgb(angle / 360, 1, 1)) * 255 + draw = ImageDraw.Draw(img_affinity) + draw.ellipse( + (p[0] - r1, p[1] - r1, p[0] + r1, p[1] + r1), + fill=(int(c[0]), int(c[1]), int(c[2])), + ) + del draw + re = torch.from_numpy(affinity).float() + tensor + return re, img_affinity + + +def CreateBeliefMap(size, pointsBelief, nbpoints, sigma=16, save=False): + # Create the belief maps in the points + beliefsImg = [] + for numb_point in range(nbpoints): + array = np.zeros([size, size]) + out = np.zeros([size, size]) + + for point in pointsBelief: + p = [point[numb_point][1], point[numb_point][0]] + w = int(sigma * 2) + if p[0] - w >= 0 and p[0] + w < size and p[1] - w >= 0 and p[1] + w < size: + for i in range(int(p[0]) - w, int(p[0]) + w + 1): + for j in range(int(p[1]) - w, int(p[1]) + w + 1): + + # if there is already a point there. + array[i, j] = max( + np.exp( + -( + ((i - p[0]) ** 2 + (j - p[1]) ** 2) + / (2 * (sigma**2)) + ) + ), + array[i, j], + ) + + beliefsImg.append(array.copy()) + + if save: + stack = np.stack([array, array, array], axis=0).transpose(2, 1, 0) + imgBelief = Image.fromarray((stack * 255).astype("uint8")) + imgBelief.save("debug/{}.png".format(numb_point)) + return beliefsImg + + +def crop(img, i, j, h, w): + """Crop the given PIL.Image. + Args: + img (PIL.Image): Image to be cropped. + i: Upper pixel coordinate. + j: Left pixel coordinate. + h: Height of the cropped image. + w: Width of the cropped image. + Returns: + PIL.Image: Cropped image. + """ + return img.crop((j, i, j + w, i + h)) + + +class AddRandomContrast(object): + """ + Apply some random image filters from PIL + """ + + def __init__(self, sigma=0.1): + self.sigma = sigma + + def __call__(self, im): + + contrast = ImageEnhance.Contrast(im) + + im = contrast.enhance(np.random.normal(1, self.sigma)) + + return im + + +class AddRandomBrightness(object): + """ + Apply some random image filters from PIL + """ + + def __init__(self, sigma=0.1): + self.sigma = sigma + + def __call__(self, im): + + contrast = ImageEnhance.Brightness(im) + im = contrast.enhance(np.random.normal(1, self.sigma)) + return im + + +class AddNoise(object): + """Given mean: (R, G, B) and std: (R, G, B), + will normalize each channel of the torch.*Tensor, i.e. + channel = (channel - mean) / std + """ + + def __init__(self, std=0.1): + self.std = std + + def __call__(self, tensor): + # TODO: make efficient + t = torch.FloatTensor(tensor.size()).normal_(0, self.std) + + t = tensor.add(t) + t = torch.clamp(t, -1, 1) # this is expansive + return t + + +irange = range + + +def make_grid( + tensor, + nrow=8, + padding=2, + normalize=False, + range=None, + scale_each=False, + pad_value=0, +): + """Make a grid of images. + Args: + tensor (Tensor or list): 4D mini-batch Tensor of shape (B x C x H x W) + or a list of images all of the same size. + nrow (int, optional): Number of images displayed in each row of the grid. + The Final grid size is (B / nrow, nrow). Default is 8. + padding (int, optional): amount of padding. Default is 2. + normalize (bool, optional): If True, shift the image to the range (0, 1), + by subtracting the minimum and dividing by the maximum pixel value. + range (tuple, optional): tuple (min, max) where min and max are numbers, + then these numbers are used to normalize the image. By default, min and max + are computed from the tensor. + scale_each (bool, optional): If True, scale each image in the batch of + images separately rather than the (min, max) over all images. + pad_value (float, optional): Value for the padded pixels. + Example: + See this notebook `here `_ + """ + if not ( + torch.is_tensor(tensor) + or (isinstance(tensor, list) and all(torch.is_tensor(t) for t in tensor)) + ): + raise TypeError( + "tensor or list of tensors expected, got {}".format(type(tensor)) + ) + + # if list of tensors, convert to a 4D mini-batch Tensor + if isinstance(tensor, list): + tensor = torch.stack(tensor, dim=0) + + if tensor.dim() == 2: # single image H x W + tensor = tensor.view(1, tensor.size(0), tensor.size(1)) + if tensor.dim() == 3: # single image + if tensor.size(0) == 1: # if single-channel, convert to 3-channel + tensor = torch.cat((tensor, tensor, tensor), 0) + tensor = tensor.view(1, tensor.size(0), tensor.size(1), tensor.size(2)) + + if tensor.dim() == 4 and tensor.size(1) == 1: # single-channel images + tensor = torch.cat((tensor, tensor, tensor), 1) + + if normalize is True: + tensor = tensor.clone() # avoid modifying tensor in-place + if range is not None: + assert isinstance( + range, tuple + ), "range has to be a tuple (min, max) if specified. min and max are numbers" + + def norm_ip(img, min, max): + img.clamp_(min=min, max=max) + img.add_(-min).div_(max - min + 1e-5) + + def norm_range(t, range): + if range is not None: + norm_ip(t, range[0], range[1]) + else: + norm_ip(t, float(t.min()), float(t.max())) + + if scale_each is True: + for t in tensor: # loop over mini-batch dimension + norm_range(t, range) + else: + norm_range(tensor, range) + + if tensor.size(0) == 1: + return tensor.squeeze() + + # make the mini-batch of images into a grid + nmaps = tensor.size(0) + xmaps = min(nrow, nmaps) + ymaps = int(math.ceil(float(nmaps) / xmaps)) + height, width = int(tensor.size(2) + padding), int(tensor.size(3) + padding) + grid = tensor.new(3, height * ymaps + padding, width * xmaps + padding).fill_( + pad_value + ) + k = 0 + for y in irange(ymaps): + for x in irange(xmaps): + if k >= nmaps: + break + grid.narrow(1, y * height + padding, height - padding).narrow( + 2, x * width + padding, width - padding + ).copy_(tensor[k]) + k = k + 1 + return grid + + +def save_image(tensor, filename, nrow=4, padding=2, mean=None, std=None, save=True): + """ + Saves a given Tensor into an image file. + If given a mini-batch tensor, will save the tensor as a grid of images. + """ + from PIL import Image + + tensor = tensor.cpu() + grid = make_grid(tensor, nrow=nrow, padding=10, pad_value=1) + if not mean is None: + # ndarr = grid.mul(std).add(mean).mul(255).byte().transpose(0,2).transpose(0,1).numpy() + ndarr = ( + grid.mul(std) + .add(mean) + .mul(255) + .byte() + .transpose(0, 2) + .transpose(0, 1) + .numpy() + ) + else: + ndarr = ( + grid.mul(0.5) + .add(0.5) + .mul(255) + .byte() + .transpose(0, 2) + .transpose(0, 1) + .numpy() + ) + im = Image.fromarray(ndarr) + if save is True: + im.save(filename) + return im, grid + + +from PIL import ImageDraw, Image, ImageFont +import json + + +class Draw(object): + """Drawing helper class to visualize the neural network output""" + + def __init__(self, im): + """ + :param im: The image to draw in. + """ + self.draw = ImageDraw.Draw(im) + self.width = im.size[0] + + def draw_line(self, point1, point2, line_color, line_width=2): + """Draws line on image""" + if point1 is not None and point2 is not None: + self.draw.line([point1, point2], fill=line_color, width=line_width) + + def draw_dot(self, point, point_color, point_radius): + """Draws dot (filled circle) on image""" + if point is not None: + xy = [ + point[0] - point_radius, + point[1] - point_radius, + point[0] + point_radius, + point[1] + point_radius, + ] + self.draw.ellipse(xy, fill=point_color, outline=point_color) + + def draw_text(self, point, text, text_color): + """Draws text on image""" + if point is not None: + self.draw.text(point, text, fill=text_color, font=ImageFont.truetype("misc/arial.ttf", self.width // 50)) + + def draw_cube(self, points, color=(0, 255, 0)): + """ + Draws cube with a thick solid line across + the front top edge and an X on the top face. + """ + # draw front + self.draw_line(points[0], points[1], color) + self.draw_line(points[1], points[2], color) + self.draw_line(points[3], points[2], color) + self.draw_line(points[3], points[0], color) + + # draw back + self.draw_line(points[4], points[5], color) + self.draw_line(points[6], points[5], color) + self.draw_line(points[6], points[7], color) + self.draw_line(points[4], points[7], color) + + # draw sides + self.draw_line(points[0], points[4], color) + self.draw_line(points[7], points[3], color) + self.draw_line(points[5], points[1], color) + self.draw_line(points[2], points[6], color) + + # draw dots + self.draw_dot(points[0], point_color=color, point_radius=4) + self.draw_dot(points[1], point_color=color, point_radius=4) + + # draw x on the top + self.draw_line(points[0], points[5], color) + self.draw_line(points[1], points[4], color) + + # Draw center + self.draw_dot(points[8], point_color=color, point_radius=6) + + for i in range(9): + self.draw_text(points[i], str(i), (255, 0, 0)) + +