/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.IO;
namespace Vision.VisualInspectionCell
{
///
/// Measures the bracket geometry from pixels instead of from fixture names.
///
internal sealed class FixtureImageAnalyzer
{
public IReadOnlyList Measure(string fixturePath)
{
if (string.IsNullOrWhiteSpace(fixturePath))
{
throw new ArgumentException("A fixture path is required.", nameof(fixturePath));
}
byte[] png = File.ReadAllBytes(fixturePath);
(byte[] rgb, int width, int height) = PngDecoder.Decode(png);
if (width != ImageWidth || height != ImageHeight)
{
throw new InvalidDataException($"Expected {ImageWidth}x{ImageHeight}, got {width}x{height}.");
}
int borePixels = CountDarkRunThrough(rgb, width, BoreCenterY, BoreCenterX);
PixelRun slot = FindDarkRunRightOf(rgb, width, SlotCenterY, minX: 400);
double bore = borePixels / ScalePixelsPerMillimetre;
double slotWidth = slot.Length / ScalePixelsPerMillimetre;
double edgeOffset = (BracketRightX - slot.StartX) / ScalePixelsPerMillimetre;
return
[
new MeasuredCharacteristic("BoreDiameter", bore, PixelPitchMillimetres),
new MeasuredCharacteristic("SlotWidth", slotWidth, PixelPitchMillimetres),
new MeasuredCharacteristic("EdgeOffset", edgeOffset, PixelPitchMillimetres)
];
}
private static int CountDarkRunThrough(byte[] rgb, int width, int y, int centerX)
{
int start = centerX;
while (start > 0 && IsDark(rgb, width, start - 1, y))
{
start--;
}
int end = centerX;
while (end + 1 < width && IsDark(rgb, width, end + 1, y))
{
end++;
}
return end - start + 1;
}
private static PixelRun FindDarkRunRightOf(byte[] rgb, int width, int y, int minX)
{
int bestStart = -1;
int bestLength = 0;
int x = minX;
while (x < BracketRightX)
{
while (x < BracketRightX && !IsDark(rgb, width, x, y))
{
x++;
}
int start = x;
while (x < BracketRightX && IsDark(rgb, width, x, y))
{
x++;
}
int length = x - start;
if (length > bestLength)
{
bestStart = start;
bestLength = length;
}
}
if (bestStart < 0)
{
throw new InvalidDataException("Could not find the slot in the fixture image.");
}
return new PixelRun(bestStart, bestLength);
}
private static bool IsDark(byte[] rgb, int width, int x, int y)
{
int offset = ((y * width) + x) * 3;
return rgb[offset] < 64 && rgb[offset + 1] < 64 && rgb[offset + 2] < 64;
}
private const int ImageWidth = 800;
private const int ImageHeight = 600;
private const int BoreCenterX = 290;
private const int BoreCenterY = 300;
private const int SlotCenterY = 300;
private const int BracketRightX = 650;
private const double ScalePixelsPerMillimetre = 10.0;
private const double PixelPitchMillimetres = 0.10;
private readonly record struct PixelRun(int StartX, int Length);
}
}