///|
pub fn CaliperConfig::new(
start~ : Point,
finish~ : Point,
threshold~ : Double,
mode? : EdgeMode = Both,
expected_edges? : Int = 2,
unit? : String = "px",
) -> CaliperConfig {
{
start,
finish,
threshold: threshold.max(0.0),
mode,
expected_edges: expected_edges.max(1),
unit,
}
}
///|
pub fn EdgePolicy::new(
low_threshold~ : Double,
high_threshold~ : Double,
mode? : EdgeMode = Both,
min_strength? : Double = 0.0,
) -> EdgePolicy {
{
low_threshold: low_threshold.max(0.0),
high_threshold: high_threshold.max(low_threshold),
mode,
min_strength: min_strength.max(0.0),
}
}
///|
fn strongest_edge(edges : Array[EdgePoint]) -> EdgePoint? {
if edges.is_empty() {
None
} else {
let mut result = edges[0]
for edge in edges[1:] {
if edge.strength > result.strength {
result = edge
}
}
Some(result)
}
}
///|
pub fn summarize_profile(
samples : Array[ProfileSample],
threshold : Double,
mode? : EdgeMode = Both,
) -> ProfileSummary {
let edges = ProfileSample::detect_edges(samples, threshold, mode~)
let gradients = samples.map(fn(sample) { sample.gradient })
let noise = mean_absolute_deviation(gradients)
let strongest = strongest_edge(edges)
{
samples,
edges,
strongest_edge: strongest,
mean_gradient: mean_value(gradients),
noise,
signal_to_noise: match strongest {
Some(edge) =>
if noise <= 0.0000000001 {
edge.strength
} else {
edge.strength / noise
}
None => 0.0
},
}
}
///|
pub fn profile_quality(
profile : ProfileSummary,
minimum_signal_to_noise : Double,
) -> QualitySummary {
let notes : Array[String] = []
if profile.edges.is_empty() {
notes.push("no edge candidate")
}
if profile.signal_to_noise < minimum_signal_to_noise {
notes.push("edge signal below threshold")
}
if profile.noise > 0.25 {
notes.push("profile noise is high")
}
{
signal_to_noise: profile.signal_to_noise,
edge_count: profile.edges.length(),
confidence: clamp_double(
profile.signal_to_noise / (minimum_signal_to_noise.max(0.0001) * 2.0),
0.0,
1.0,
),
stable: !profile.edges.is_empty() &&
profile.signal_to_noise >= minimum_signal_to_noise,
notes,
}
}
///|
pub fn measure_caliper(
image : GrayImage,
config : CaliperConfig,
tolerance : Tolerance,
) -> PipelineResult {
let samples = edge_profile(image, config.start, config.finish)
let profile = summarize_profile(samples, config.threshold, mode=config.mode)
let quality = profile_quality(profile, 2.0)
let diagnostics : Array[PipelineDiagnostic] = []
let measurements : Array[Measurement] = []
if profile.edges.length() < config.expected_edges {
diagnostics.push({
stage: "edge-detection",
message: "expected edge count was not reached",
severity: Fail,
})
} else {
let width = edge_pair_width(
profile.edges,
Vector2::Vector2(
x=config.finish.x - config.start.x,
y=config.finish.y - config.start.y,
),
)
let measurement = analyze_gap("caliper", width, 1.0, tolerance)
measurements.push({ ..measurement, unit: config.unit })
}
{
measurements,
diagnostics,
accepted: !measurements.is_empty() && measurements[0].decision != Fail,
confidence: quality.confidence,
}
}
///|
pub fn measure_caliper_with_scale(
image : GrayImage,
config : CaliperConfig,
scale : Double,
tolerance : Tolerance,
) -> PipelineResult {
let result = measure_caliper(image, config, tolerance)
if result.measurements.is_empty() {
result
} else {
let measurement = result.measurements[0]
let scaled = {
..measurement,
value: measurement.value * scale,
deviation: measurement.value * scale - tolerance.nominal,
decision: tolerance.judge(measurement.value * scale),
}
{ ..result, measurements: [scaled], accepted: scaled.decision != Fail }
}
}
///|
pub fn pipeline_has_error(result : PipelineResult) -> Bool {
!result.diagnostics.is_empty() || !result.accepted
}
///|
pub fn pipeline_decision(result : PipelineResult) -> MeasurementDecision {
if result.measurements.is_empty() {
Fail
} else {
let mut decision = Pass
for measurement in result.measurements {
match measurement.decision {
Fail => decision = Fail
Warn => if decision == Pass { decision = Warn }
Pass => ()
}
}
decision
}
}
///|
pub fn pipeline_notes(result : PipelineResult) -> Array[String] {
result.diagnostics.map(fn(diagnostic) {
diagnostic.stage + ": " + diagnostic.message
})
}
///|
pub fn repeat_caliper(
image : GrayImage,
config : CaliperConfig,
tolerance : Tolerance,
repetitions : Int,
) -> StatsSummary {
let values : Array[Double] = []
for _ in 0.. Array[Point] {
profile.edges.map(fn(edge) { edge.point })
}
///|
pub fn profile_edge_strengths(profile : ProfileSummary) -> Array[Double] {
profile.edges.map(fn(edge) { edge.strength })
}
///|
pub fn edge_polarity_count(profile : ProfileSummary, polarity : Int) -> Int {
let mut result = 0
for edge in profile.edges {
if edge.polarity == polarity {
result += 1
}
}
result
}
///|
pub fn choose_edge_pair(
profile : ProfileSummary,
minimum_spacing : Double,
) -> Array[EdgePoint] {
if profile.edges.length() < 2 {
[]
} else {
let mut first = 0
let mut second = 1
let mut best = -1.0
for i in 0..= minimum_spacing {
profile.edges[i].strength + profile.edges[j].strength
} else {
-1.0
}
if score > best {
best = score
first = i
second = j
}
}
}
if best < 0.0 {
[]
} else {
[profile.edges[first], profile.edges[second]]
}
}
}
///|
pub fn edge_pair_midpoint(edges : Array[EdgePoint]) -> Point {
if edges.is_empty() {
Point::Point(x=0.0, y=0.0)
} else {
let mut x = 0.0
let mut y = 0.0
for edge in edges {
x += edge.point.x
y += edge.point.y
}
Point::Point(
x=x / Double::from_int(edges.length()),
y=y / Double::from_int(edges.length()),
)
}
}
///|
pub fn edge_pair_orientation(edges : Array[EdgePoint]) -> Double {
if edges.length() < 2 {
0.0
} else {
measure_angle(edges[0].point, edges[edges.length() - 1].point)
}
}
///|
pub fn profile_baseline(samples : Array[ProfileSample]) -> Double {
if samples.is_empty() {
0.0
} else {
(samples[0].value + samples[samples.length() - 1].value) / 2.0
}
}
///|
pub fn subtract_profile_baseline(
samples : Array[ProfileSample],
) -> Array[ProfileSample] {
let baseline = profile_baseline(samples)
samples.map(fn(sample) { { ..sample, value: sample.value - baseline } })
}
///|
pub fn profile_dynamic_range(samples : Array[ProfileSample]) -> Double {
let values = samples.map(fn(sample) { sample.value })
maximum_value(values) - minimum_value(values)
}
///|
pub fn profile_gradient_range(samples : Array[ProfileSample]) -> Double {
let gradients = samples.map(fn(sample) { sample.gradient })
maximum_value(gradients) - minimum_value(gradients)
}
///|
pub fn profile_is_monotonic(samples : Array[ProfileSample]) -> Bool {
if samples.length() < 2 {
true
} else {
let mut direction = 0
let mut valid = true
for sample in samples[1:] {
let current = if sample.gradient > 0.0 {
1
} else if sample.gradient < 0.0 {
-1
} else {
0
}
if direction == 0 {
direction = current
} else if current != 0 && current != direction {
valid = false
}
}
valid
}
}