using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using HalconDotNet;
public sealed class PythonImageWorker : IDisposable
{
private readonly Process _process;
private readonly Stream _stdin;
private readonly Stream _stdout;
private int _frameId = 0;
private bool _disposed = false;
public PythonImageWorker(string pythonExePath, string workerPyPath)
{
var psi = new ProcessStartInfo
{
FileName = pythonExePath,
Arguments = $"\"{workerPyPath}\"",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
_process = new Process
{
StartInfo = psi,
EnableRaisingEvents = true
};
_process.Start();
_stdin = _process.StandardInput.BaseStream;
_stdout = _process.StandardOutput.BaseStream;
// stderrを読み続けないと、ログが溜まってPython側が詰まる可能性がある
_ = Task.Run(async () =>
{
try
{
while (!_process.HasExited)
{
string? line = await _process.StandardError.ReadLineAsync();
if (line == null)
break;
Debug.WriteLine("[Python stderr] " + line);
Console.Error.WriteLine("[Python stderr] " + line);
}
}
catch
{
// 終了時などは握りつぶす
}
});
}
/// <summary>
/// HALCONのMono8 HObject画像をPythonへ送り、
/// Pythonから返ってきたMono8画像をHObjectとして返す。
/// </summary>
public async Task<HObject> ProcessMono8Async(
HObject inputImage,
int timeoutMs = 5000,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
if (_process.HasExited)
throw new InvalidOperationException("Python worker has already exited.");
int frameId = Interlocked.Increment(ref _frameId);
// HObject -> byte[]
byte[] imageBytes = HObjectMono8ToBytes(inputImage, out int width, out int height);
var requestHeader = new PacketHeader
{
frame_id = frameId,
ok = true,
width = width,
height = height,
pixel_type = "mono8",
byte_length = imageBytes.Length,
message = "request"
};
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeoutMs);
// C# -> Python
await WritePacketAsync(_stdin, requestHeader, imageBytes, cts.Token);
// Python -> C#
var response = await ReadPacketAsync(_stdout, cts.Token);
if (!response.Header.ok)
{
throw new InvalidOperationException(
$"Python worker error: {response.Header.message}");
}
if (response.Header.pixel_type != "mono8")
{
throw new InvalidOperationException(
$"Unsupported response pixel_type: {response.Header.pixel_type}");
}
if (response.Header.width != width || response.Header.height != height)
{
throw new InvalidOperationException(
$"Response image size mismatch. " +
$"request={width}x{height}, response={response.Header.width}x{response.Header.height}");
}
int expectedLength = response.Header.width * response.Header.height;
if (response.ImageBytes.Length != expectedLength)
{
throw new InvalidOperationException(
$"Response byte length mismatch. expected={expectedLength}, actual={response.ImageBytes.Length}");
}
// byte[] -> HObject
HObject outputImage = BytesToHObjectMono8(
response.ImageBytes,
response.Header.width,
response.Header.height);
return outputImage;
}
private static byte[] HObjectMono8ToBytes(HObject image, out int width, out int height)
{
// HALCON画像のポインタを取得
HOperatorSet.GetImagePointer1(
image,
out HTuple pointer,
out HTuple type,
out HTuple widthTuple,
out HTuple heightTuple);
string halconType = type.S;
if (halconType != "byte")
{
throw new InvalidOperationException(
$"This sample supports only Mono8 HALCON image. Actual type: {halconType}");
}
width = widthTuple.I;
height = heightTuple.I;
int byteLength = checked(width * height);
byte[] bytes = new byte[byteLength];
IntPtr srcPtr = new IntPtr(pointer.L);
Marshal.Copy(srcPtr, bytes, 0, byteLength);
return bytes;
}
private static HObject BytesToHObjectMono8(byte[] bytes, int width, int height)
{
if (bytes.Length != width * height)
{
throw new ArgumentException(
$"Invalid byte length. expected={width * height}, actual={bytes.Length}");
}
HObject image;
// byte[]を一時的に固定してHALCONへ渡す
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
IntPtr ptr = handle.AddrOfPinnedObject();
HOperatorSet.GenImage1(
out image,
"byte",
width,
height,
ptr);
}
finally
{
handle.Free();
}
return image;
}
private static async Task WritePacketAsync(
Stream stream,
PacketHeader header,
byte[] imageBytes,
CancellationToken cancellationToken)
{
header.byte_length = imageBytes.Length;
byte[] headerBytes = JsonSerializer.SerializeToUtf8Bytes(header);
if (headerBytes.Length > int.MaxValue)
throw new InvalidOperationException("Header is too large.");
// little-endian 4byte
byte[] headerLengthBytes = BitConverter.GetBytes((uint)headerBytes.Length);
if (!BitConverter.IsLittleEndian)
Array.Reverse(headerLengthBytes);
await stream.WriteAsync(headerLengthBytes, 0, headerLengthBytes.Length, cancellationToken);
await stream.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken);
await stream.WriteAsync(imageBytes, 0, imageBytes.Length, cancellationToken);
await stream.FlushAsync(cancellationToken);
}
private static async Task<Packet> ReadPacketAsync(
Stream stream,
CancellationToken cancellationToken)
{
byte[] headerLengthBytes = await ReadExactAsync(stream, 4, cancellationToken);
if (!BitConverter.IsLittleEndian)
Array.Reverse(headerLengthBytes);
uint headerLength = BitConverter.ToUInt32(headerLengthBytes, 0);
if (headerLength == 0 || headerLength > 1024 * 1024)
{
throw new InvalidOperationException($"Invalid header length: {headerLength}");
}
byte[] headerBytes = await ReadExactAsync(stream, checked((int)headerLength), cancellationToken);
PacketHeader? header = JsonSerializer.Deserialize<PacketHeader>(headerBytes);
if (header == null)
throw new InvalidOperationException("Failed to deserialize response header.");
if (header.byte_length < 0)
throw new InvalidOperationException($"Invalid byte_length: {header.byte_length}");
byte[] imageBytes = await ReadExactAsync(stream, header.byte_length, cancellationToken);
return new Packet(header, imageBytes);
}
private static async Task<byte[]> ReadExactAsync(
Stream stream,
int size,
CancellationToken cancellationToken)
{
byte[] buffer = new byte[size];
int offset = 0;
while (offset < size)
{
int read = await stream.ReadAsync(
buffer,
offset,
size - offset,
cancellationToken);
if (read == 0)
throw new EndOfStreamException("Stream closed while reading packet.");
offset += read;
}
return buffer;
}
private void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(PythonImageWorker));
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
try
{
_stdin.Close();
}
catch
{
}
try
{
if (!_process.HasExited)
{
_process.Kill(entireProcessTree: true);
_process.WaitForExit(1000);
}
}
catch
{
}
_process.Dispose();
}
private sealed class PacketHeader
{
public int? frame_id { get; set; }
public bool ok { get; set; }
public int width { get; set; }
public int height { get; set; }
public string pixel_type { get; set; } = "mono8";
public int byte_length { get; set; }
public string message { get; set; } = "";
}
private sealed record Packet(PacketHeader Header, byte[] ImageBytes);
}
//使用例
using System;
using System.Threading.Tasks;
using HalconDotNet;
internal class Program
{
private static async Task Main()
{
// 環境に合わせて変更してください
string pythonExePath = "python";
string workerPyPath = @"C:\work\worker.py";
using var worker = new PythonImageWorker(pythonExePath, workerPyPath);
// --------------------------------------------
// 本来はここでカメラ撮像したHObjectを使う
// 例:
// HOperatorSet.GrabImage(out HObject inputImage, hv_AcqHandle);
// --------------------------------------------
HObject inputImage = CreateDummyMono8Image(640, 480);
HObject outputImage = await worker.ProcessMono8Async(inputImage);
Console.WriteLine("Python round-trip completed.");
// 結果画像を確認したい場合
HOperatorSet.WriteImage(outputImage, "png", 0, @"C:\work\result_from_python.png");
inputImage.Dispose();
outputImage.Dispose();
Console.WriteLine("Saved: C:\\work\\result_from_python.png");
}
private static HObject CreateDummyMono8Image(int width, int height)
{
byte[] bytes = new byte[width * height];
// 適当なグラデーション画像を作成
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
bytes[y * width + x] = (byte)(x % 256);
}
}
HObject image;
var handle = System.Runtime.InteropServices.GCHandle.Alloc(
bytes,
System.Runtime.InteropServices.GCHandleType.Pinned);
try
{
IntPtr ptr = handle.AddrOfPinnedObject();
HOperatorSet.GenImage1(
out image,
"byte",
width,
height,
ptr);
}
finally
{
handle.Free();
}
return image;
}
}
\\----------------
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading;
using HalconDotNet;
public sealed class PythonImageWorkerSync : IDisposable
{
private readonly Process _process;
private readonly Stream _stdin;
private readonly Stream _stdout;
private readonly object _lockObj = new object();
private Thread? _stderrThread;
private volatile bool _disposed = false;
private int _frameId = 0;
public PythonImageWorkerSync(string pythonExePath, string workerPyPath)
{
var psi = new ProcessStartInfo
{
FileName = pythonExePath,
Arguments = $"\"{workerPyPath}\"",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
_process = new Process
{
StartInfo = psi,
EnableRaisingEvents = true
};
bool started = _process.Start();
if (!started)
throw new InvalidOperationException("Failed to start Python worker.");
_stdin = _process.StandardInput.BaseStream;
_stdout = _process.StandardOutput.BaseStream;
StartStderrReadThread();
}
public HObject ProcessMono8(HObject inputImage)
{
ThrowIfDisposed();
if (_process.HasExited)
throw new InvalidOperationException("Python worker has already exited.");
lock (_lockObj)
{
int frameId = Interlocked.Increment(ref _frameId);
byte[] imageBytes = HObjectMono8ToBytes(
inputImage,
out int width,
out int height);
var requestHeader = new PacketHeader
{
frame_id = frameId,
ok = true,
width = width,
height = height,
pixel_type = "mono8",
byte_length = imageBytes.Length,
message = "request"
};
WritePacket(_stdin, requestHeader, imageBytes);
Packet response = ReadPacket(_stdout);
if (!response.Header.ok)
{
throw new InvalidOperationException(
$"Python worker error: {response.Header.message}");
}
if (response.Header.pixel_type != "mono8")
{
throw new InvalidOperationException(
$"Unsupported response pixel_type: {response.Header.pixel_type}");
}
if (response.Header.width != width || response.Header.height != height)
{
throw new InvalidOperationException(
$"Response image size mismatch. " +
$"request={width}x{height}, " +
$"response={response.Header.width}x{response.Header.height}");
}
int expectedLength = response.Header.width * response.Header.height;
if (response.ImageBytes.Length != expectedLength)
{
throw new InvalidOperationException(
$"Response byte length mismatch. " +
$"expected={expectedLength}, actual={response.ImageBytes.Length}");
}
HObject outputImage = BytesToHObjectMono8(
response.ImageBytes,
response.Header.width,
response.Header.height);
return outputImage;
}
}
private void StartStderrReadThread()
{
_stderrThread = new Thread(() =>
{
try
{
while (!_disposed && !_process.HasExited)
{
string? line = _process.StandardError.ReadLine();
if (line == null)
break;
Debug.WriteLine("[Python stderr] " + line);
Console.Error.WriteLine("[Python stderr] " + line);
}
}
catch
{
// 終了時の例外は握りつぶす
}
});
_stderrThread.IsBackground = true;
_stderrThread.Name = "Python stderr reader";
_stderrThread.Start();
}
private static byte[] HObjectMono8ToBytes(
HObject image,
out int width,
out int height)
{
HOperatorSet.GetImagePointer1(
image,
out HTuple pointer,
out HTuple type,
out HTuple widthTuple,
out HTuple heightTuple);
string halconType = type.S;
if (halconType != "byte")
{
throw new InvalidOperationException(
$"This sample supports only Mono8 HALCON image. Actual type: {halconType}");
}
width = widthTuple.I;
height = heightTuple.I;
int byteLength = checked(width * height);
byte[] bytes = new byte[byteLength];
IntPtr srcPtr = HTuplePointerToIntPtr(pointer);
Marshal.Copy(srcPtr, bytes, 0, byteLength);
return bytes;
}
private static HObject BytesToHObjectMono8(
byte[] bytes,
int width,
int height)
{
if (bytes.Length != width * height)
{
throw new ArgumentException(
$"Invalid byte length. expected={width * height}, actual={bytes.Length}");
}
HObject image;
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
IntPtr ptr = handle.AddrOfPinnedObject();
HOperatorSet.GenImage1(
out image,
"byte",
width,
height,
ptr);
}
finally
{
handle.Free();
}
return image;
}
private static IntPtr HTuplePointerToIntPtr(HTuple pointer)
{
if (IntPtr.Size == 8)
return new IntPtr(pointer.L);
return new IntPtr(pointer.I);
}
private static void WritePacket(
Stream stream,
PacketHeader header,
byte[] imageBytes)
{
header.byte_length = imageBytes.Length;
byte[] headerBytes = JsonSerializer.SerializeToUtf8Bytes(header);
if (headerBytes.Length <= 0)
throw new InvalidOperationException("Header is empty.");
byte[] headerLengthBytes = BitConverter.GetBytes((uint)headerBytes.Length);
if (!BitConverter.IsLittleEndian)
Array.Reverse(headerLengthBytes);
stream.Write(headerLengthBytes, 0, headerLengthBytes.Length);
stream.Write(headerBytes, 0, headerBytes.Length);
stream.Write(imageBytes, 0, imageBytes.Length);
stream.Flush();
}
private static Packet ReadPacket(Stream stream)
{
byte[] headerLengthBytes = ReadExact(stream, 4);
if (!BitConverter.IsLittleEndian)
Array.Reverse(headerLengthBytes);
uint headerLength = BitConverter.ToUInt32(headerLengthBytes, 0);
if (headerLength == 0 || headerLength > 1024 * 1024)
{
throw new InvalidOperationException(
$"Invalid response header length: {headerLength}");
}
byte[] headerBytes = ReadExact(stream, checked((int)headerLength));
PacketHeader? header = JsonSerializer.Deserialize<PacketHeader>(headerBytes);
if (header == null)
throw new InvalidOperationException("Failed to deserialize response header.");
if (header.byte_length < 0)
throw new InvalidOperationException($"Invalid byte_length: {header.byte_length}");
byte[] imageBytes = ReadExact(stream, header.byte_length);
return new Packet(header, imageBytes);
}
private static byte[] ReadExact(Stream stream, int size)
{
byte[] buffer = new byte[size];
int offset = 0;
while (offset < size)
{
int read = stream.Read(buffer, offset, size - offset);
if (read == 0)
throw new EndOfStreamException("Stream closed while reading packet.");
offset += read;
}
return buffer;
}
private void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(PythonImageWorkerSync));
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
try
{
_stdin.Close();
}
catch
{
}
try
{
if (!_process.HasExited)
{
_process.Kill(entireProcessTree: true);
_process.WaitForExit(1000);
}
}
catch
{
}
try
{
_process.Dispose();
}
catch
{
}
}
private sealed class PacketHeader
{
public int? frame_id { get; set; }
public bool ok { get; set; }
public int width { get; set; }
public int height { get; set; }
public string pixel_type { get; set; } = "mono8";
public int byte_length { get; set; }
public string message { get; set; } = "";
}
private sealed class Packet
{
public PacketHeader Header { get; }
public byte[] ImageBytes { get; }
public Packet(PacketHeader header, byte[] imageBytes)
{
Header = header;
ImageBytes = imageBytes;
}
}
}
//動作確認用
using System;
using System.Threading;
using HalconDotNet;
internal class Program
{
private static void Main()
{
string pythonExePath = "python";
string workerPyPath = @"C:\work\worker.py";
using var pythonWorker = new PythonImageWorkerSync(
pythonExePath,
workerPyPath);
using var processingThread = new ImageProcessingThread(
pythonWorker,
onProcessed: OnProcessed,
onError: OnError);
processingThread.Start();
try
{
// ============================================
// ダミー撮像ループ
// 実際にはここをGrabImageなどに置き換える
// ============================================
for (int i = 1; i <= 10; i++)
{
HObject grabbedImage = CreateDummyMono8Image(640, 480, i);
try
{
Console.WriteLine($"Enqueue frame {i}");
processingThread.EnqueueFrame(i, grabbedImage);
}
finally
{
// EnqueueFrame内部でCopyImageしているので、
// 撮像側の画像はここで破棄してよい
grabbedImage.Dispose();
}
Thread.Sleep(100);
}
// 撮像終了フラグ
Console.WriteLine("Capture finished.");
processingThread.SetCaptureFinished();
// 処理キューが空になるまで待つ
processingThread.Join();
Console.WriteLine("Processing thread finished.");
}
catch (Exception ex)
{
Console.WriteLine("Main error:");
Console.WriteLine(ex);
processingThread.RequestStop();
processingThread.Join();
}
}
private static void OnProcessed(int frameNo, HObject processedImage)
{
try
{
Console.WriteLine($"Processed frame {frameNo}");
string savePath = $@"C:\work\result_{frameNo:000}.png";
HOperatorSet.WriteImage(
processedImage,
"png",
0,
savePath);
Console.WriteLine($"Saved: {savePath}");
}
finally
{
// ImageProcessingThread側から所有権を受け取ったので、
// ここでDisposeする。
processedImage.Dispose();
}
}
private static void OnError(Exception ex)
{
Console.WriteLine("Processing error:");
Console.WriteLine(ex);
}
private static HObject CreateDummyMono8Image(
int width,
int height,
int frameNo)
{
byte[] bytes = new byte[width * height];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
bytes[y * width + x] = (byte)((x + frameNo * 10) % 256);
}
}
HObject image;
var handle = System.Runtime.InteropServices.GCHandle.Alloc(
bytes,
System.Runtime.InteropServices.GCHandleType.Pinned);
try
{
IntPtr ptr = handle.AddrOfPinnedObject();
HOperatorSet.GenImage1(
out image,
"byte",
width,
height,
ptr);
}
finally
{
handle.Free();
}
return image;
}
}