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
{
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>
</PropertyGroup>
<ItemGroup>
<Compile Remove="OpenAPIs\**" />
<EmbeddedResource Remove="OpenAPIs\**" />
<None Remove="OpenAPIs\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ILGPU" Version="1.5.1" />
<PackageReference Include="IronPython" Version="3.4.0" />
<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="Newtonsoft.Json" Version="13.0.1" />
<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.Extensions" Version="4.6.0.20220608" />

View File

@@ -14,11 +14,11 @@ public abstract class ColorAIOperation:BaseOperation
public ColorAIOperation(string model_name)
{
_modelName = model_name;
_cSharpDataTransferMQRPC = PythonModelProxyRPC.GetInterface();
_cSharpDataTransferMQRPC = PythonModelProxyHTTP.GetInterface();
CanHaveProcessingError = false;
}
private readonly CSharpDataTransferMQRPC _cSharpDataTransferMQRPC;
private readonly CSharpDataTransferHTTP _cSharpDataTransferMQRPC;
private int _imageWidth;
private int _imageHeight;
@@ -52,15 +52,16 @@ public abstract class ColorAIOperation:BaseOperation
this.SetError("Model file not found");
return;
}
_cSharpDataTransferMQRPC.TransferData<object>(new MethodCall("load_model", ModelFilePath.Path, _modelName));
_cSharpDataTransferMQRPC.LoadModel(ModelFilePath.Path, _modelName);
_initialized = true;
}
_cSharpDataTransferMQRPC.ActivateModel(_modelName);
var sizeEncoded = _cSharpDataTransferMQRPC.GetAcceptSize();
_cSharpDataTransferMQRPC.TransferData<object>(new MethodCall("activate_model", _modelName));
var sizeEncoded = _cSharpDataTransferMQRPC.TransferData<object[]>(new MethodCall("get_accept_size"));
var size = sizeEncoded.Select(Convert.ToInt32).ToArray();
_imageWidth = size[1];
_imageHeight = size[2];
@@ -82,22 +83,30 @@ public abstract class ColorAIOperation:BaseOperation
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;
if (IsRaw)
{
resultColor = result.Chunk(25).Select(x =>
{
return x.Select((x, i) => allowedFlags[i] == 1 ? (byte) (x) : (byte) 0).Max();
}).ToArray();
//resultColor = result.Select(x => (byte)(x)).ToArray();
}
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()
{
_cSharpDataTransferMQRPC = PythonModelProxyRPC.GetInterface();
_cSharpDataTransferMQRPC = PythonModelProxyHTTP.GetInterface();
CanHaveProcessingError = false;
}
private CSharpDataTransferMQRPC _cSharpDataTransferMQRPC;
private CSharpDataTransferHTTP _cSharpDataTransferMQRPC;
private int _imageWidth;
private int _imageHeight;
@@ -66,34 +66,22 @@ public class RawModelAIOperation: BaseOperation
var absPath = Path.GetFullPath(Path.Combine(dir, ModelFilePath.Path));
ModelFilePath.Path = absPath;
}
_cSharpDataTransferMQRPC.TransferData<object>(new MethodCall("load_model", ModelFilePath.Path, _modelName));
_cSharpDataTransferMQRPC.LoadModel(ModelFilePath.Path, _modelName);
_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();
_imageWidth = size[1];
_imageHeight = size[2];
_imageWidth = size[0];
_imageHeight = size[1];
var outputSize = _cSharpDataTransferMQRPC.TransferData<object[]>(new MethodCall("get_output_size"));
var outputSize = _cSharpDataTransferMQRPC.GetOutputSize();
var outputSizeArray = outputSize.Select(Convert.ToInt32).ToArray();
int classes;
if (outputSizeArray.Length<3)
{
classes=1;
}
else
{
classes = outputSizeArray[2];
}
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 allowedFlags = new byte[128];
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();
var result = _cSharpDataTransferMQRPC.PredictRaw(byteArray);
byte[] resultColor;
resultColor = result.Select(x => (byte)(x) ).ToArray();
// black and white mask
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)
{
CheckImageExists(context);
CheckColorful(context);
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;
}
}