///|
pub fn edge_quality(
samples : Array[ProfileSample],
threshold : Double,
) -> QualitySummary {
let profile = summarize_profile(samples, threshold)
profile_quality(profile, 2.0)
}
///|
pub fn contrast_quality(image : GrayImage, bounds : Bounds) -> Double {
let crop = crop_image(image, bounds)
let values = crop.pixels
if values.is_empty() {
0.0
} else {
(maximum_value(values) - minimum_value(values)) /
(variance_value(values).sqrt() + 0.0000001)
}
}
///|
pub fn texture_noise(image : GrayImage) -> Double {
let horizontal : Array[Double] = []
let vertical : Array[Double] = []
for y in 0.. Double {
let gradient = gradient_magnitude(image)
mean_value(gradient.pixels)
}
///|
pub fn sharpness_score(image : GrayImage) -> Double {
let gradient = gradient_magnitude(image)
variance_value(gradient.pixels)
}
///|
pub fn illumination_uniformity(image : GrayImage) -> Double {
let summary = describe_samples(image.pixels)
if summary.mean.abs() <= 0.0000001 {
0.0
} else {
1.0 - summary.standard_deviation / summary.mean.abs()
}
}
///|
pub fn saturation_fraction(
image : GrayImage,
lower : Double,
upper : Double,
) -> Double {
if image.pixels.is_empty() {
0.0
} else {
let saturated = image.pixels
.filter(fn(value) { value <= lower || value >= upper })
.length()
Double::from_int(saturated) / Double::from_int(image.pixels.length())
}
}
///|
pub fn measurement_confidence(
measurement : Measurement,
tolerance : Tolerance,
quality : Double,
) -> Double {
let span = (tolerance.upper - tolerance.lower).abs()
let deviation = if span <= 0.0 {
1.0
} else {
(measurement.value - tolerance.nominal).abs() / span
}
clamp_double((1.0 - deviation) * quality, 0.0, 1.0)
}
///|
pub fn decision_penalty(decision : MeasurementDecision) -> Double {
match decision {
Pass => 0.0
Warn => 0.25
Fail => 1.0
}
}
///|
pub fn batch_quality(batch : BatchRun) -> Double {
let summary = batch.summary()
if summary.total == 0 {
0.0
} else {
let mut penalty = 0.0
for measurement in batch.records {
penalty += decision_penalty(measurement.decision)
}
clamp_double(1.0 - penalty / Double::from_int(summary.total), 0.0, 1.0)
}
}
///|
pub fn quality_diagnostics(
image : GrayImage,
profile : ProfileSummary,
) -> Array[DiagnosticEvent] {
let result : Array[DiagnosticEvent] = []
result.push({
code: "FOCUS",
stage: "image",
detail: "mean gradient magnitude",
value: focus_score(image),
})
result.push({
code: "SHARPNESS",
stage: "image",
detail: "gradient variance",
value: sharpness_score(image),
})
result.push({
code: "SNR",
stage: "profile",
detail: "strongest edge to MAD",
value: profile.signal_to_noise,
})
result.push({
code: "NOISE",
stage: "profile",
detail: "median absolute deviation",
value: profile.noise,
})
result
}
///|
pub fn quality_summary_text(summary : QualitySummary) -> String {
let out = StringBuilder()
out.write_string("signal_to_noise=")
out.write_string(summary.signal_to_noise.to_string())
out.write_string("; edge_count=")
out.write_string(summary.edge_count.to_string())
out.write_string("; confidence=")
out.write_string(summary.confidence.to_string())
out.write_string("; stable=")
out.write_string(summary.stable.to_string())
out.to_string()
}
///|
pub fn stability_from_repetitions(
values : Array[Double],
maximum_deviation : Double,
) -> Bool {
mean_absolute_deviation(values) <= maximum_deviation.max(0.0)
}
///|
pub fn outlier_flags(values : Array[Double], z_limit : Double) -> Array[Bool] {
let summary = describe_samples(values)
let result : Array[Bool] = []
for value in values {
let z = if summary.standard_deviation <= 0.0000001 {
0.0
} else {
(value - summary.mean).abs() / summary.standard_deviation
}
result.push(z > z_limit.max(0.0))
}
result
}
///|
pub fn reject_outliers(
values : Array[Double],
z_limit : Double,
) -> Array[Double] {
let flags = outlier_flags(values, z_limit)
let result : Array[Double] = []
for i in 0.. DoubleBounds {
let summary = describe_samples(values)
let margin = if summary.count == 0 {
0.0
} else {
z_score.abs() *
summary.standard_deviation /
Double::from_int(summary.count).sqrt()
}
{ min: summary.mean - margin, max: summary.mean + margin }
}
///|
pub fn gauge_risk_score(
profile : ProfileSummary,
tolerance : Tolerance,
measurement : Double,
) -> Double {
let distance_to_limit = (tolerance.upper - measurement)
.abs()
.min((measurement - tolerance.lower).abs())
let signal_penalty = if profile.signal_to_noise <= 0.0 {
1.0
} else {
1.0 / (1.0 + profile.signal_to_noise)
}
clamp_double(signal_penalty + 1.0 / (1.0 + distance_to_limit), 0.0, 2.0)
}
///|
pub fn quality_gate(
summary : QualitySummary,
minimum_confidence : Double,
) -> Bool {
summary.stable &&
summary.confidence >= minimum_confidence.clamp(min=0.0, max=1.0)
}
///|
pub fn normalized_quality(
value : Double,
lower : Double,
upper : Double,
) -> Double {
if upper <= lower {
0.0
} else {
clamp_double((value - lower) / (upper - lower), 0.0, 1.0)
}
}
///|
pub fn compare_quality(first : QualitySummary, second : QualitySummary) -> Int {
if first.confidence > second.confidence {
1
} else if first.confidence < second.confidence {
-1
} else {
0
}
}