///|
pub fn sanitize_image(
image : GrayImage,
lower : Double,
upper : Double,
) -> GrayImage {
image_clamp_values(normalize_minmax(image), lower, upper)
}
///|
pub fn replace_non_finite_image(
image : GrayImage,
replacement : Double,
) -> GrayImage {
let result = image.copy()
for i in 0.. Bool,
) -> Int {
let mut result = 0
for value in image.pixels {
if predicate(value) {
result += 1
}
}
result
}
///|
pub fn image_fraction(
image : GrayImage,
predicate : (Double) -> Bool,
) -> Double {
if image.pixels.is_empty() {
0.0
} else {
Double::from_int(image_value_count(image, predicate)) /
Double::from_int(image.pixels.length())
}
}
///|
pub fn image_histogram_probability(
image : GrayImage,
bins : Int,
) -> Array[Double] {
let counts = histogram(image, bins).bins
let total = image.pixels.length()
if total == 0 {
counts.map(fn(_) { 0.0 })
} else {
counts.map(fn(value) { Double::from_int(value) / Double::from_int(total) })
}
}
///|
pub fn histogram_cumulative(histogram_value : Histogram) -> Array[Int] {
let result : Array[Int] = []
let mut total = 0
for value in histogram_value.bins {
total += value
result.push(total)
}
result
}
///|
pub fn histogram_quantile(histogram_value : Histogram, q : Double) -> Double {
let cumulative = histogram_cumulative(histogram_value)
if cumulative.is_empty() || cumulative[cumulative.length() - 1] == 0 {
histogram_value.minimum
} else {
let target = clamp_double(q, 0.0, 1.0) *
Double::from_int(cumulative[cumulative.length() - 1])
let mut index = 0
while index < cumulative.length() &&
Double::from_int(cumulative[index]) < target {
index += 1
}
histogram_value.minimum +
(histogram_value.maximum - histogram_value.minimum) *
Double::from_int(index) /
Double::from_int(cumulative.length())
}
}
///|
pub fn image_quality_flags(image : GrayImage) -> Array[String] {
let flags : Array[String] = []
if image.width < 16 || image.height < 16 {
flags.push("small-frame")
}
if saturation_fraction(image, 0.001, 0.999) > 0.1 {
flags.push("saturated")
}
if image_dynamic_range(image) < 0.05 {
flags.push("low-contrast")
}
if texture_noise(image) > 0.5 {
flags.push("noisy")
}
flags
}
///|
pub fn image_quality_score(image : GrayImage) -> Double {
let flags = image_quality_flags(image)
clamp_double(1.0 - Double::from_int(flags.length()) / 4.0, 0.0, 1.0)
}
///|
pub fn profile_quality_flags(profile : ProfileSummary) -> Array[String] {
let result : Array[String] = []
if profile.samples.length() < 2 {
result.push("short-profile")
}
if profile.edges.is_empty() {
result.push("no-edges")
}
if profile.noise > 0.2 {
result.push("noisy-profile")
}
if profile.signal_to_noise < 2.0 {
result.push("weak-edge")
}
result
}
///|
pub fn measurement_quality_flags(
measurement : Measurement,
tolerance : Tolerance,
) -> Array[String] {
let result : Array[String] = []
if measurement.decision == Warn {
result.push("near-limit")
}
if measurement.decision == Fail {
result.push("out-of-tolerance")
}
if measurement.unit != tolerance.unit {
result.push("unit-mismatch")
}
if !is_finite(measurement.value) {
result.push("non-finite")
}
result
}
///|
pub fn normalize_measurements(
measurements : Array[Measurement],
) -> Array[Measurement] {
let values = measurement_values(measurements)
let summary = describe_samples(values)
if summary.standard_deviation <= 0.0000001 {
measurements
} else {
measurements.map(fn(measurement) {
{
..measurement,
deviation: (measurement.value - summary.mean) /
summary.standard_deviation,
}
})
}
}
///|
pub fn measurements_within(
measurements : Array[Measurement],
lower : Double,
upper : Double,
) -> Array[Measurement] {
measurements.filter(fn(measurement) {
measurement.value >= lower && measurement.value <= upper
})
}
///|
pub fn measurement_bounds(measurements : Array[Measurement]) -> DoubleBounds {
{
min: minimum_value(measurement_values(measurements)),
max: maximum_value(measurement_values(measurements)),
}
}
///|
pub fn measurement_quantiles(
measurements : Array[Measurement],
) -> Array[Double] {
let values = measurement_values(measurements)
[
percentile_value(values, 0.05),
percentile_value(values, 0.5),
percentile_value(values, 0.95),
]
}
///|
pub fn quality_score_report(
image : GrayImage,
profile : ProfileSummary,
measurements : Array[Measurement],
) -> String {
let batch = BatchRun::new("quality")
for measurement in measurements {
batch.add(measurement)
}
let out = StringBuilder()
out.write_string("image=")
out.write_string(image_quality_score(image).to_string())
out.write_string("; profile=")
out.write_string(profile_quality(profile, 2.0).confidence.to_string())
out.write_string("; measurements=")
out.write_string(batch_quality(batch).to_string())
out.to_string()
}