c# script support

python server support
This commit is contained in:
meelstorm
2025-08-25 17:02:33 +02:00
parent af8ee449a0
commit 07002dd875
22 changed files with 1710 additions and 95 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,103 @@
{
"runtime": "Net80",
"defaultVariables": null,
"documentGenerator": {
"fromDocument": {
"json": "",
"url": "http://localhost:8000/openapi.json",
"output": null,
"newLineBehavior": "Auto"
}
},
"codeGenerators": {
"openApiToCSharpClient": {
"clientBaseClass": null,
"configurationClass": null,
"generateClientClasses": true,
"suppressClientClassesOutput": false,
"generateClientInterfaces": false,
"suppressClientInterfacesOutput": false,
"clientBaseInterface": null,
"injectHttpClient": true,
"disposeHttpClient": true,
"protectedMethods": [],
"generateExceptionClasses": true,
"exceptionClass": "ApiException",
"wrapDtoExceptions": true,
"useHttpClientCreationMethod": false,
"httpClientType": "System.Net.Http.HttpClient",
"useHttpRequestMessageCreationMethod": false,
"useBaseUrl": true,
"generateBaseUrlProperty": true,
"generateSyncMethods": false,
"generatePrepareRequestAndProcessResponseAsAsyncMethods": false,
"exposeJsonSerializerSettings": false,
"clientClassAccessModifier": "public",
"typeAccessModifier": "public",
"propertySetterAccessModifier": "",
"generateNativeRecords": false,
"useRequiredKeyword": false,
"generateContractsOutput": false,
"contractsNamespace": null,
"contractsOutputFilePath": null,
"parameterDateTimeFormat": "s",
"parameterDateFormat": "yyyy-MM-dd",
"generateUpdateJsonSerializerSettingsMethod": true,
"useRequestAndResponseSerializationSettings": false,
"serializeTypeInformation": false,
"queryNullValue": "",
"className": "PythonModelAPI",
"operationGenerationMode": "SingleClientFromPathSegments",
"additionalNamespaceUsages": [],
"additionalContractNamespaceUsages": [],
"generateOptionalParameters": false,
"generateJsonMethods": false,
"enforceFlagEnums": false,
"parameterArrayType": "System.Collections.Generic.IEnumerable",
"parameterDictionaryType": "System.Collections.Generic.IDictionary",
"responseArrayType": "System.Collections.Generic.ICollection",
"responseDictionaryType": "System.Collections.Generic.IDictionary",
"wrapResponses": false,
"wrapResponseMethods": [],
"generateResponseClasses": true,
"responseClass": "SwaggerResponse",
"namespace": "Hawkeye.VisionBuilder.Workflow",
"requiredPropertiesMustBeDefined": true,
"dateType": "System.DateTimeOffset",
"jsonConverters": null,
"anyType": "object",
"dateTimeType": "System.DateTimeOffset",
"timeType": "System.TimeSpan",
"timeSpanType": "System.TimeSpan",
"arrayType": "System.Collections.Generic.ICollection",
"arrayInstanceType": "System.Collections.ObjectModel.Collection",
"dictionaryType": "System.Collections.Generic.IDictionary",
"dictionaryInstanceType": "System.Collections.Generic.Dictionary",
"arrayBaseType": "System.Collections.ObjectModel.Collection",
"dictionaryBaseType": "System.Collections.Generic.Dictionary",
"classStyle": "Poco",
"jsonLibrary": "NewtonsoftJson",
"jsonPolymorphicSerializationStyle": "NJsonSchema",
"generateDefaultValues": true,
"generateDataAnnotations": true,
"excludedTypeNames": [],
"excludedParameterNames": [],
"handleReferences": false,
"generateImmutableArrayProperties": false,
"generateImmutableDictionaryProperties": false,
"jsonSerializerSettingsTransformationMethod": null,
"inlineNamedArrays": false,
"inlineNamedDictionaries": false,
"inlineNamedTuples": true,
"inlineNamedAny": false,
"generateDtoTypes": true,
"generateOptionalPropertiesAsNullable": false,
"generateNullableReferenceTypes": false,
"templateDirectory": null,
"serviceHost": null,
"serviceSchemes": null,
"output": "PythonAPI.cs",
"newLineBehavior": "Auto"
}
}
}

View File

@@ -0,0 +1,107 @@
using static Community.CsharpSqlite.Sqlite3;
namespace Hawkeye.VisionBuilder.Workflow.DataTransfer;
public class CSharpDataTransferHTTP
{
private readonly PythonModelAPI _api;
private readonly HttpClient _client;
private readonly CancellationTokenSource _pingCts = new();
private readonly Thread _pingThread;
public CSharpDataTransferHTTP()
{
var handler = new SocketsHttpHandler
{
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(10),
MaxConnectionsPerServer = 100
};
_client = new HttpClient(handler)
{
// i've added this
DefaultRequestHeaders =
{
ExpectContinue = false
}
};
_api = new PythonModelAPI("http://localhost:8000",_client);
_pingThread = new Thread(PingLoop) { IsBackground = true };
_pingThread.Start();
}
private void PingLoop()
{
while (!_pingCts.Token.IsCancellationRequested)
{
try
{
_client.GetAsync("http://localhost:8000/").Wait();
}
catch
{
// Ignore errors, optionally log
}
Thread.Sleep(3000); // Ping every 3 seconds
}
}
public void LoadModel(string path, string modelName)
{
_api.LoadModelAsync(new LoadModelIn()
{
Name = modelName,
Path = Path.GetFileName(path)
}).Wait();
}
public void ActivateModel(string modelName)
{
_api.ActivateModelAsync(new ActivateModelIn(){Name = modelName}).Wait();
}
public int[] GetAcceptSize()
{
var response = _api.GetAcceptSizeAsync().Result;
return response.Accept_size.ToArray();
}
public int[] GetOutputSize()
{
var response = _api.GetOutputSizeAsync().Result;
return response.Accept_size.ToArray();
}
public byte[] PredictRaw(byte[] byteArray)
{
var response = _api.PredictRawAsync(new FileParameter(new MemoryStream(byteArray))).Result;
var stream = response.Stream;
// read the stream to byte array
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
var responseBytes = memoryStream.ToArray();
return responseBytes;
}
public byte[] Predict(byte[] byteArray)
{
var response = _api.PredictAsync(new FileParameter(new MemoryStream(byteArray))).Result;
var stream = response.Stream;
// read the stream to byte array
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
var responseBytes = memoryStream.ToArray();
return responseBytes;
}
}

View File

@@ -2,5 +2,26 @@
public class PythonModelProxyHTTP public class PythonModelProxyHTTP
{ {
private static bool _isInitialized = false;
private static UvicornScriptRunner _pythonScriptRunner;
private static CSharpDataTransferHTTP _cSharpDataTransferHTTP;
public static void Initialize()
{
if (_isInitialized) return;
var workDirAbsPath = Path.GetFullPath(@"..\Data\Models");
_pythonScriptRunner = new UvicornScriptRunner(workDirAbsPath);
_cSharpDataTransferHTTP = new CSharpDataTransferHTTP();
_pythonScriptRunner.Start();
Thread.Sleep(1000); // Wait for the server to start
}
public static CSharpDataTransferHTTP GetInterface()
{
if (!_isInitialized) Initialize();
return _cSharpDataTransferHTTP;
}
} }

View File

@@ -0,0 +1,63 @@
using System.Diagnostics;
namespace Hawkeye.VisionBuilder.Workflow.DataTransfer;
public class UvicornScriptRunner
{
private Process process;
private readonly string _workingDirectory;
public UvicornScriptRunner(string workingDirectory)
{
_workingDirectory = workingDirectory;
}
public static string ConvertToWslPath(string windowsPath)
{
if (string.IsNullOrWhiteSpace(windowsPath))
throw new ArgumentException("Path cannot be null or empty.", nameof(windowsPath));
// Replace backslashes with forward slashes
string wslPath = windowsPath.Replace('\\', '/');
// Extract the drive letter and convert it to WSL format
if (wslPath.Length > 1 && wslPath[1] == ':')
{
char driveLetter = char.ToLower(wslPath[0]);
wslPath = $"/mnt/{driveLetter}{wslPath.Substring(2)}";
}
return wslPath;
}
public void Start()
{
//startInfo.CreateNoWindow = true;
// create and start the process
process = new Process();
var convertedWd = ConvertToWslPath(_workingDirectory);
var arg =
$@"--distribution ubuntu --user root --cd ""{convertedWd}"" -- uvicorn PythonModelAPI:app";
process.StartInfo = new ProcessStartInfo("wsl", arg)
{
UseShellExecute = false
};
process.Start();
// on windows only
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
ChildProcessTracker.AddProcess(process);
}
public void Stop()
{
if (process != null && !process.HasExited)
{
process.Kill();
process.Dispose();
process = null;
}
}
}

View File

@@ -9,13 +9,29 @@
<Configurations>Debug;Release;CPU</Configurations> <Configurations>Debug;Release;CPU</Configurations>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="OpenAPIs\**" />
<EmbeddedResource Remove="OpenAPIs\**" />
<None Remove="OpenAPIs\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ILGPU" Version="1.5.1" /> <PackageReference Include="ILGPU" Version="1.5.1" />
<PackageReference Include="IronPython" Version="3.4.0" /> <PackageReference Include="IronPython" Version="3.4.0" />
<PackageReference Include="MessagePack" Version="2.5.108" /> <PackageReference Include="MessagePack" Version="2.5.108" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="7.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NetMQ" Version="4.0.1.11" /> <PackageReference Include="NetMQ" Version="4.0.1.11" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Ninject" Version="3.3.6" /> <PackageReference Include="Ninject" Version="3.3.6" />
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.18.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="OpenCvSharp4" Version="4.6.0.20220608" /> <PackageReference Include="OpenCvSharp4" Version="4.6.0.20220608" />
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.6.0.20220608" /> <PackageReference Include="OpenCvSharp4.Extensions" Version="4.6.0.20220608" />

View File

@@ -14,11 +14,11 @@ public abstract class ColorAIOperation:BaseOperation
public ColorAIOperation(string model_name) public ColorAIOperation(string model_name)
{ {
_modelName = model_name; _modelName = model_name;
_cSharpDataTransferMQRPC = PythonModelProxyRPC.GetInterface(); _cSharpDataTransferMQRPC = PythonModelProxyHTTP.GetInterface();
CanHaveProcessingError = false; CanHaveProcessingError = false;
} }
private readonly CSharpDataTransferMQRPC _cSharpDataTransferMQRPC; private readonly CSharpDataTransferHTTP _cSharpDataTransferMQRPC;
private int _imageWidth; private int _imageWidth;
private int _imageHeight; private int _imageHeight;
@@ -52,15 +52,16 @@ public abstract class ColorAIOperation:BaseOperation
this.SetError("Model file not found"); this.SetError("Model file not found");
return; return;
} }
_cSharpDataTransferMQRPC.TransferData<object>(new MethodCall("load_model", ModelFilePath.Path, _modelName));
_cSharpDataTransferMQRPC.LoadModel(ModelFilePath.Path, _modelName);
_initialized = true; _initialized = true;
} }
_cSharpDataTransferMQRPC.ActivateModel(_modelName);
_cSharpDataTransferMQRPC.TransferData<object>(new MethodCall("activate_model", _modelName)); var sizeEncoded = _cSharpDataTransferMQRPC.GetAcceptSize();
var sizeEncoded = _cSharpDataTransferMQRPC.TransferData<object[]>(new MethodCall("get_accept_size"));
var size = sizeEncoded.Select(Convert.ToInt32).ToArray(); var size = sizeEncoded.Select(Convert.ToInt32).ToArray();
_imageWidth = size[1]; _imageWidth = size[1];
_imageHeight = size[2]; _imageHeight = size[2];
@@ -82,22 +83,30 @@ public abstract class ColorAIOperation:BaseOperation
allowedFlags[i] = 1; allowedFlags[i] = 1;
} }
var resultEncoded = _cSharpDataTransferMQRPC.TransferData<object[]>(new MethodCall(IsRaw?"predict_raw": "predict", byteArray));
var result = resultEncoded.Select(Convert.ToByte).ToArray(); byte[] result;
if (IsRaw)
{
result = _cSharpDataTransferMQRPC.PredictRaw(byteArray);
}
else
{
result = _cSharpDataTransferMQRPC.Predict(byteArray);
}
byte[] resultColor; byte[] resultColor;
if (IsRaw) if (IsRaw)
{ {
resultColor = result.Chunk(25).Select(x => resultColor = result.Chunk(25).Select(x =>
{ {
return x.Select((x, i) => allowedFlags[i] == 1 ? (byte) (x) : (byte) 0).Max(); return x.Select((x, i) => allowedFlags[i] == 1 ? (byte) (x) : (byte) 0).Max();
}).ToArray(); }).ToArray();
//resultColor = result.Select(x => (byte)(x)).ToArray();
} }
else else
{ {
resultColor = result.Select(x => allowedFlags[x]==1? (byte)(x * 255) : (byte)0).ToArray(); resultColor = result.Select(x => allowedFlags[x]==1? (byte)(x * 255.0) : (byte)0).ToArray();
} }

View File

@@ -16,11 +16,11 @@ public class RawModelAIOperation: BaseOperation
public RawModelAIOperation() public RawModelAIOperation()
{ {
_cSharpDataTransferMQRPC = PythonModelProxyRPC.GetInterface(); _cSharpDataTransferMQRPC = PythonModelProxyHTTP.GetInterface();
CanHaveProcessingError = false; CanHaveProcessingError = false;
} }
private CSharpDataTransferMQRPC _cSharpDataTransferMQRPC; private CSharpDataTransferHTTP _cSharpDataTransferMQRPC;
private int _imageWidth; private int _imageWidth;
private int _imageHeight; private int _imageHeight;
@@ -66,34 +66,22 @@ public class RawModelAIOperation: BaseOperation
var absPath = Path.GetFullPath(Path.Combine(dir, ModelFilePath.Path)); var absPath = Path.GetFullPath(Path.Combine(dir, ModelFilePath.Path));
ModelFilePath.Path = absPath; ModelFilePath.Path = absPath;
} }
_cSharpDataTransferMQRPC.LoadModel(ModelFilePath.Path, _modelName);
_cSharpDataTransferMQRPC.TransferData<object>(new MethodCall("load_model", ModelFilePath.Path, _modelName));
_initialized = true; _initialized = true;
} }
_cSharpDataTransferMQRPC.ActivateModel(_modelName);
_cSharpDataTransferMQRPC.TransferData<object>(new MethodCall("activate_model", _modelName));
var sizeEncoded = _cSharpDataTransferMQRPC.TransferData<object[]>(new MethodCall("get_accept_size")); var sizeEncoded = _cSharpDataTransferMQRPC.GetAcceptSize();
var size = sizeEncoded.Select(Convert.ToInt32).ToArray(); var size = sizeEncoded.Select(Convert.ToInt32).ToArray();
_imageWidth = size[1]; _imageWidth = size[0];
_imageHeight = size[2]; _imageHeight = size[1];
var outputSize = _cSharpDataTransferMQRPC.TransferData<object[]>(new MethodCall("get_output_size")); var outputSize = _cSharpDataTransferMQRPC.GetOutputSize();
var outputSizeArray = outputSize.Select(Convert.ToInt32).ToArray(); var outputSizeArray = outputSize.Select(Convert.ToInt32).ToArray();
int classes;
if (outputSizeArray.Length<3)
{
classes=1;
}
else
{
classes = outputSizeArray[2];
}
var currentImage = context.ActiveImage; var currentImage = context.ActiveImage;
@@ -105,20 +93,11 @@ public class RawModelAIOperation: BaseOperation
var allowed = FilterClasses.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToInt32(x)).ToArray(); var allowed = FilterClasses.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToInt32(x)).ToArray();
var allowedFlags = new byte[128]; var result = _cSharpDataTransferMQRPC.PredictRaw(byteArray);
foreach (var i in allowed)
{
allowedFlags[i] = 1;
}
var resultEncoded = _cSharpDataTransferMQRPC.TransferData<object[]>(new MethodCall("predict_raw", byteArray));
var result = resultEncoded.Select(Convert.ToByte).ToArray();
byte[] resultColor; byte[] resultColor;
resultColor = result.Select(x => (byte)(x) ).ToArray(); resultColor = result.Select(x => (byte)(x) ).ToArray();
// black and white mask // black and white mask
var mask = new Mat(outputSizeArray[0], outputSizeArray[1], MatType.CV_8UC1, resultColor); var mask = new Mat(outputSizeArray[0], outputSizeArray[1], MatType.CV_8UC1, resultColor);

View File

@@ -59,6 +59,7 @@ public class YoloDetectionOperation:BaseOperation
} }
protected override void InterpretInternal(Context context) protected override void InterpretInternal(Context context)
{ {
CheckImageExists(context);
CheckColorful(context); CheckColorful(context);
try try
{ {

View File

@@ -0,0 +1,76 @@
using System.Reflection;
using Compunet.YoloV8;
using Hawkeye.VisionBuilder.Workflow.Datatypes;
using Hawkeye.VisionBuilder.Workflow.Operations.Attributes;
using Microsoft.Scripting.Runtime;
using OpenCvSharp;
namespace Hawkeye.VisionBuilder.Workflow.Operations.Scripts;
[Category("Script")]
public class CSharpProcessOperation:BaseOperation
{
private FilePath _scriptFilePath = new FilePath() { Format = "C# dll(*.dll)|*.dll" };
private bool _initialized;
private object _processor;
private MethodInfo? _method;
public FilePath ScriptFilePath
{
get => _scriptFilePath;
set
{
_scriptFilePath = value;
_initialized = false;
}
}
bool EnsureInitialized()
{
if (!_initialized)
{
if (!File.Exists(ScriptFilePath?.Path))
{
this.SetError("Script file not found");
return false;
}
var absPath = Path.GetFullPath(ScriptFilePath?.Path);
var loadContext = new PluginLoadContext(absPath);
var assembly = loadContext.LoadFromAssemblyPath(absPath);
// find class that has Process method
foreach (var type in assembly.GetTypes())
{
var method = type.GetMethod("ProcessImage", BindingFlags.Public | BindingFlags.Instance);
if (method != null)
{
_processor = Activator.CreateInstance(type);
_method = method;
break;
}
}
Label = Path.GetFileNameWithoutExtension(ScriptFilePath?.Path);
_initialized = true;
}
return true;
}
protected override void InterpretInternal(Context context)
{
CheckImageExists(context);
if (!EnsureInitialized()) return;
var image = context.ActiveImage.ImageData;
var result = (Mat)_method.Invoke(_processor, new []{ image });
context.ActiveImage = new HawkeyeImage()
{
ImageData = result
};
}
}

View File

@@ -0,0 +1,37 @@
using System.Reflection;
using System.Runtime.Loader;
namespace Hawkeye.VisionBuilder.Workflow.Operations.Scripts;
class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
}

View File

@@ -1,22 +1,43 @@
# app.py # app.py
from fastapi import FastAPI, UploadFile, File, Body from fastapi import FastAPI, Response, UploadFile, File, Body, HTTPException
from pydantic import BaseModel
from typing import List
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import numpy as np import numpy as np
from PIL import Image from PIL import Image
import io import io
import tensorflow as tf import tensorflow as tf
import time
app = FastAPI() app = FastAPI()
# ==== Schemas (show up in Swagger) ====
class PredictOut(BaseModel):
result: List[int]
model_config = {"json_schema_extra": {"examples": [{"result": [0,1,2,3]}]}}
class StatusOut(BaseModel):
status: str
message: str
class AcceptSizeOut(BaseModel):
status: str
accept_size: List[int]
class LoadModelIn(BaseModel):
path: str
name: str
class ActivateModelIn(BaseModel):
name: str
# ==== Service ====
class Service: class Service:
def __init__(self): def __init__(self):
self.model = None self.model = None
self.models={} self.models = {}
self.active_model = "default" self.active_model = "default"
def activate_model(self, name): def activate_model(self, name):
self.model = self.models[name] self.model = self.models[name]
self.active_model = name self.active_model = name
return 0 return 0
@@ -26,15 +47,13 @@ class Service:
return 0 return 0
def get_accept_size(self): def get_accept_size(self):
print(self.model.layers[0].input) print(self.model.input_shape)
return list(self.model.input_shape) # (B,H,W,C)
return list(self.model.input_shape)
def get_output_size(self): def get_output_size(self):
return list(self.model.layers[-1].output[0].shape[1:4]) return list(self.model.output_shape) # (B,H,W,C) or (B,H,W)
def predict(self, data: bytes) -> List[int]:
def predict(self, data: bytes):
target_size = self.get_accept_size() target_size = self.get_accept_size()
image = np.reshape( image = np.reshape(
np.frombuffer(data, dtype=np.uint8), np.frombuffer(data, dtype=np.uint8),
@@ -48,21 +67,25 @@ class Service:
else: else:
classed = tf.argmax(predictions[0], axis=2) classed = tf.argmax(predictions[0], axis=2)
return JSONResponse({"result": classed.numpy().reshape(-1).tolist()}) return classed.numpy().reshape(-1).tolist()
def predict_raw(self, data): def predict_raw(self, data: bytes) -> np.ndarray:
target_size = self.get_accept_size() target_size = self.get_accept_size()
image = np.reshape(np.frombuffer(data, dtype=np.uint8), (target_size[1], target_size[2], target_size[3])) / 255. image = np.reshape(
np.frombuffer(data, dtype=np.uint8),
(target_size[1], target_size[2], target_size[3])
) / 255.0
predictions = self.model(np.array([image])).numpy() predictions = self.model({"input_layer": np.array([image], dtype=np.float32)}).numpy()
if predictions[0].ndim==2: if predictions[0].ndim == 2:
classed = predictions[0][:,:,np.newaxis].clip(0,255) classed = predictions[0][:, :, np.newaxis].clip(0, 255)
return (classed).astype(np.uint8).reshape(-1).tolist() return classed.astype(np.uint8).reshape(-1).tolist()
else: else:
classed = predictions[0][:,:,:] classed = predictions[0][:, :, :]
return (classed*255).astype(np.uint8).reshape(-1).tolist() res = (classed * 255).astype(np.uint8).reshape(-1)
return np.asarray(res)
svc = Service() svc = Service()
@@ -75,33 +98,61 @@ def _prep_raw_bytes_for_predict(buf: bytes) -> bytes:
arr = np.asarray(img, dtype=np.uint8) # HxWx3 uint8 arr = np.asarray(img, dtype=np.uint8) # HxWx3 uint8
return arr.tobytes() return arr.tobytes()
@app.post("/predict") # ==== Routes ====
@app.post("/predict", response_model=bytes, summary="Predict (classified indices)")
async def predict(file: UploadFile = File(...)): async def predict(file: UploadFile = File(...)):
buf = await file.read() try:
raw = _prep_raw_bytes_for_predict(buf) buf = await file.read()
return svc.predict(raw) raw = _prep_raw_bytes_for_predict(buf)
result = svc.predict(raw)
return Response(content=result, media_type="application/octet-stream")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/predict_raw") @app.post("/predictRaw", response_model=bytes, summary="Predict (raw/uint8 flattened)")
async def predict_raw(file: UploadFile = File(...)): async def predict_raw(file: UploadFile = File(...)):
buf = await file.read()
raw = _prep_raw_bytes_for_predict(buf)
return svc.predict_raw(raw)
@app.post("/load_model")
async def load_model(path: str = Body(...), name: str = Body(...)):
try: try:
svc.load_model(path, name) start_time = time.time()
return JSONResponse({"status": "success", "message": f"Model {name} loaded successfully."}) buf = await file.read()
result = svc.predict_raw(buf)
elapsed_time = time.time() - start_time
print(f"Prediction took {elapsed_time:.2f} seconds")
return Response(content=result.tobytes(), media_type="application/octet-stream")
except Exception as e: except Exception as e:
return JSONResponse({"status": "error", "message": str(e)}, status_code=500) raise HTTPException(status_code=400, detail=str(e))
@app.post("/activate_model") @app.post("/loadModel", response_model=StatusOut, summary="Load a model from path")
async def activate_model(name: str = Body(...)): async def load_model(payload: LoadModelIn):
try: try:
svc.activate_model(name) svc.load_model(payload.path, payload.name)
return JSONResponse({"status": "success", "message": f"Model {name} activated successfully."}) return StatusOut(status="success", message=f"Model {payload.name} loaded successfully.")
except Exception as e: except Exception as e:
return JSONResponse({"status": "error", "message": str(e)}, status_code=500) raise HTTPException(status_code=500, detail=str(e))
@app.post("/activateModel", response_model=StatusOut, summary="Activate a loaded model")
async def activate_model(payload: ActivateModelIn):
try:
svc.activate_model(payload.name)
return StatusOut(status="success", message=f"Model {payload.name} activated successfully.")
except KeyError:
raise HTTPException(status_code=404, detail=f"Model {payload.name} not found.")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/getAcceptSize", response_model=AcceptSizeOut, summary="Get input tensor shape")
async def get_accept_size():
try:
size = svc.get_accept_size()[1:] # Exclude batch size
return AcceptSizeOut(status="success", accept_size=size)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/getOutputSize", response_model=AcceptSizeOut, summary="Get output tensor shape")
async def get_output_size():
try:
size = svc.get_output_size()[1:]
return AcceptSizeOut(status="success", accept_size=size)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -11,6 +11,7 @@
<OutputPath>.</OutputPath> <OutputPath>.</OutputPath>
<Name>PythonModelAPI</Name> <Name>PythonModelAPI</Name>
<RootNamespace>PythonModelAPI</RootNamespace> <RootNamespace>PythonModelAPI</RootNamespace>
<PublishUrl>D:\Inspectron\Hawkeye\code\VisionBuilder5\VisionBuilder.UI\Hawkeye.VisionBuilder\bin\Debug\Data\Models</PublishUrl>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>

Binary file not shown.

View File

@@ -1,7 +1,7 @@
using OpenCvSharp; using OpenCvSharp;
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
namespace VisionBuilder.UI.Common; namespace VisionBuilder.UI.Common.NullClasses;
public class NoLearning:ILearningTool public class NoLearning:ILearningTool
{ {

View File

@@ -0,0 +1,12 @@
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
namespace VisionBuilder.UI.Common.NullClasses;
public class NoRecipeCreation: IRecipeCreationTool
{
public bool Enabled { get; set; } = false;
public void CreateRecipe(string recipeName)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,7 @@
namespace VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
public interface IRecipeCreationTool
{
public bool Enabled { get; set; }
public void CreateRecipe(string recipeName);
}

View File

@@ -2,6 +2,7 @@
using Ninject; using Ninject;
using Ninject.Extensions.ChildKernel; using Ninject.Extensions.ChildKernel;
using System.ComponentModel; using System.ComponentModel;
using VisionBuilder.UI.Common.NullClasses;
using VisionBuilder.UI.Common.Plugins; using VisionBuilder.UI.Common.Plugins;
using VisionBuilder.UI.Common.ViewModel; using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
@@ -20,6 +21,7 @@ public static class VisionBuilder
mainKernel.Bind<InspectronSettings>().ToConstant(settings); mainKernel.Bind<InspectronSettings>().ToConstant(settings);
mainKernel.Bind<UIConfiguration, ISettings>().ToConstant(new UIConfiguration()); mainKernel.Bind<UIConfiguration, ISettings>().ToConstant(new UIConfiguration());
mainKernel.Bind<ILearningTool>().To<NoLearning>().InSingletonScope(); mainKernel.Bind<ILearningTool>().To<NoLearning>().InSingletonScope();
mainKernel.Bind<IRecipeCreationTool>().To<NoRecipeCreation>().InSingletonScope();
// --- // // --- //
mainKernel.Bind<MainWindowVM>().ToSelf().InSingletonScope(); mainKernel.Bind<MainWindowVM>().ToSelf().InSingletonScope();

View File

@@ -30,6 +30,7 @@
{ {
listView1 = new ListView(); listView1 = new ListView();
btnCancel = new MaterialSkin.Controls.MaterialRaisedButton(); btnCancel = new MaterialSkin.Controls.MaterialRaisedButton();
btnCreateNewRecipe = new MaterialSkin.Controls.MaterialRaisedButton();
SuspendLayout(); SuspendLayout();
// //
// listView1 // listView1
@@ -62,11 +63,30 @@
btnCancel.UseVisualStyleBackColor = true; btnCancel.UseVisualStyleBackColor = true;
btnCancel.Click += btnCancel_Click; btnCancel.Click += btnCancel_Click;
// //
// btnCreateNewRecipe
//
btnCreateNewRecipe.AutoSizeMode = AutoSizeMode.GrowAndShrink;
btnCreateNewRecipe.Depth = 0;
btnCreateNewRecipe.DrawBorder = true;
btnCreateNewRecipe.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold, GraphicsUnit.Point, 204);
btnCreateNewRecipe.Icon = null;
btnCreateNewRecipe.Location = new Point(8, 624);
btnCreateNewRecipe.MouseState = MaterialSkin.MouseState.HOVER;
btnCreateNewRecipe.Name = "btnCreateNewRecipe";
btnCreateNewRecipe.Primary = false;
btnCreateNewRecipe.Size = new Size(160, 48);
btnCreateNewRecipe.TabIndex = 6;
btnCreateNewRecipe.Text = "Create New Recipe";
btnCreateNewRecipe.UseVisualStyleBackColor = true;
btnCreateNewRecipe.Visible = false;
btnCreateNewRecipe.Click += btnCreateNewRecipe_Click;
//
// RecipeSelectionDialog // RecipeSelectionDialog
// //
AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(962, 688); ClientSize = new Size(962, 688);
Controls.Add(btnCreateNewRecipe);
Controls.Add(btnCancel); Controls.Add(btnCancel);
Controls.Add(listView1); Controls.Add(listView1);
MaximizeBox = false; MaximizeBox = false;
@@ -81,5 +101,6 @@
private ListView listView1; private ListView listView1;
private MaterialSkin.Controls.MaterialRaisedButton btnCancel; private MaterialSkin.Controls.MaterialRaisedButton btnCancel;
private MaterialSkin.Controls.MaterialRaisedButton btnCreateNewRecipe;
} }
} }

View File

@@ -1,22 +1,31 @@
using MaterialSkin.Controls; using MaterialSkin.Controls;
using OpenCvSharp.XImgProc; using OpenCvSharp.XImgProc;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MaterialSkin.Core.Controls;
using OpenCvSharp.Extensions; using OpenCvSharp.Extensions;
using VisionBuilder.UI.Common.ViewModel; using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.Common.ViewModel.Classes; using VisionBuilder.UI.Common.ViewModel.Classes;
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
namespace VisionBuilder.UI.Windows.Dialogs namespace VisionBuilder.UI.Windows.Dialogs
{ {
public partial class RecipeSelectionDialog : MaterialForm public partial class RecipeSelectionDialog : MaterialForm
{ {
private readonly RecipeSelectionVM _recipeSelectionVm; private readonly RecipeSelectionVM _recipeSelectionVm;
private readonly IRecipeCreationTool _recipeCreationTool;
public RecipeSelectionDialog(RecipeSelectionVM recipeSelectionVm) public RecipeSelectionDialog(RecipeSelectionVM recipeSelectionVm, IRecipeCreationTool recipeCreationTool)
{ {
_recipeSelectionVm = recipeSelectionVm; _recipeSelectionVm = recipeSelectionVm;
_recipeCreationTool = recipeCreationTool;
InitializeComponent(); InitializeComponent();
ReloadRecipes(); ReloadRecipes();
if (!_recipeCreationTool.Enabled)
{
btnCreateNewRecipe.Visible = true;
}
} }
public void ReloadRecipes() public void ReloadRecipes()
@@ -53,7 +62,7 @@ namespace VisionBuilder.UI.Windows.Dialogs
} }
else else
{ {
listView1.Items.Add(x.RecipeName, x.RecipeName, "plug").Tag=x; listView1.Items.Add(x.RecipeName, x.RecipeName, "plug").Tag = x;
} }
}); });
@@ -63,7 +72,7 @@ namespace VisionBuilder.UI.Windows.Dialogs
{ {
if (listView1.SelectedIndices.Count > 0) if (listView1.SelectedIndices.Count > 0)
{ {
_recipeSelectionVm.SelectedRecipe = (RecipeData)listView1.SelectedItems[0].Tag!; _recipeSelectionVm.SelectedRecipe = (RecipeData)listView1.SelectedItems[0].Tag!;
} }
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
} }
@@ -73,5 +82,13 @@ namespace VisionBuilder.UI.Windows.Dialogs
{ {
DialogResult = DialogResult.Cancel; DialogResult = DialogResult.Cancel;
} }
private void btnCreateNewRecipe_Click(object sender, EventArgs e)
{
if (MaterialInputBox.Prompt("Create new recipe", "Recipe name", out var recipeName)==DialogResult.OK)
{
_recipeCreationTool.CreateRecipe(recipeName);
}
}
} }
} }

View File

@@ -222,13 +222,13 @@ Global
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{B62D435C-3572-4B5B-A381-992345D343B0} = {F3414823-B70E-435E-B4EA-80ABF4371449}
{D5FD2E9D-DA4F-1343-47E0-FBC473A149BC} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} {D5FD2E9D-DA4F-1343-47E0-FBC473A149BC} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
{7697A266-A721-4D74-9C5C-0D4F2F6BBF68} = {F2406FBB-DFD3-4CBE-9644-A9FFC2FCBB71} {7697A266-A721-4D74-9C5C-0D4F2F6BBF68} = {F3414823-B70E-435E-B4EA-80ABF4371449}
{E286CE4C-B68A-94B6-F477-0DBF42358009} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} {E286CE4C-B68A-94B6-F477-0DBF42358009} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
{E8868ABD-E4D0-1B7E-494E-06FB1F7D1AF5} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} {E8868ABD-E4D0-1B7E-494E-06FB1F7D1AF5} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
{8B7FAFEE-4066-483C-9C9C-10D2D596206D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {8B7FAFEE-4066-483C-9C9C-10D2D596206D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{FC4698F6-C653-427F-9A42-22D07D466A14} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {FC4698F6-C653-427F-9A42-22D07D466A14} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{8F6BA34A-D070-372B-DD72-B74905460744} = {F3414823-B70E-435E-B4EA-80ABF4371449}
{4F937F88-70B7-A9E4-834A-D60B104686E0} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} {4F937F88-70B7-A9E4-834A-D60B104686E0} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
{0DAFEB64-E123-0974-B77C-3518AFD28D87} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} {0DAFEB64-E123-0974-B77C-3518AFD28D87} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
{7AACFEF4-794A-42A3-9A05-9B6B4ECD5B82} = {D05689E3-04C6-4E3B-ACA7-3F4507CED4CC} {7AACFEF4-794A-42A3-9A05-9B6B4ECD5B82} = {D05689E3-04C6-4E3B-ACA7-3F4507CED4CC}