///|
pub fn calibrate_from_grid(
source_grid : Array[Point],
target_grid : Array[Point],
) -> CalibrationFit {
calibration_fit(source_grid, target_grid)
}
///|
pub fn calibrate_from_rectangle(
source : Bounds,
target_width : Double,
target_height : Double,
) -> AffineTransform {
let width = Double::from_int((source.max_x - source.min_x).max(1))
let height = Double::from_int((source.max_y - source.min_y).max(1))
AffineTransform::scaling(sx=target_width / width, sy=target_height / height)
}
///|
pub fn calibration_transform_at(
fit : CalibrationFit,
_point : Point,
) -> AffineTransform {
fit.transform
}
///|
pub fn calibration_apply_to_components(
fit : CalibrationFit,
components : Array[Component],
) -> Array[Component] {
components.map(fn(component) {
let transformed_bounds = polygon_bounds([
fit.transform.apply(
Point::Point(
x=Double::from_int(component.bounds.min_x),
y=Double::from_int(component.bounds.min_y),
),
),
fit.transform.apply(
Point::Point(
x=Double::from_int(component.bounds.max_x),
y=Double::from_int(component.bounds.max_y),
),
),
])
{
..component,
bounds: transformed_bounds,
centroid: fit.transform.apply(component.centroid),
area: (Double::from_int(component.area) *
fit.transform.determinant().abs())
.round()
.to_int(),
perimeter: component.perimeter * calibration_scale(fit).x,
}
})
}
///|
pub fn calibration_apply_to_measurement(
fit : CalibrationFit,
measurement : Measurement,
) -> Measurement {
let scale = (calibration_scale(fit).x + calibration_scale(fit).y) / 2.0
{
..measurement,
value: measurement.value * scale,
deviation: measurement.deviation * scale,
}
}
///|
pub fn calibration_apply_to_batch(
fit : CalibrationFit,
batch : BatchRun,
) -> BatchRun {
let result = BatchRun::new(batch.id + "-calibrated")
for measurement in batch.records {
result.add(calibration_apply_to_measurement(fit, measurement))
}
result
}
///|
pub fn calibration_residual_plot(
fit : CalibrationFit,
width : Int,
height : Int,
) -> GrayImage {
let image = GrayImage(width=width.max(0), height=height.max(0), fill=0.0)
if fit.residuals.is_empty() {
image
} else {
let maximum = maximum_value(fit.residuals).max(0.0000001)
for i in 0.. Double {
let scale = calibration_scale(fit)
(scale.x + scale.y) / 2.0
}
///|
pub fn calibration_anisotropy(fit : CalibrationFit) -> Double {
let scale = calibration_scale(fit)
if scale.x.max(scale.y) <= 0.0000001 {
0.0
} else {
(scale.x - scale.y).abs() / scale.x.max(scale.y)
}
}
///|
pub fn calibration_rotation(fit : CalibrationFit) -> Double {
@math.atan2(fit.transform.c, fit.transform.a)
}
///|
pub fn calibration_validate_grid(
source : Array[Point],
target : Array[Point],
maximum_error : Double,
) -> Array[PipelineDiagnostic] {
let result : Array[PipelineDiagnostic] = []
if source.length() != target.length() || source.length() < 3 {
result.push(
diagnostic_error(
"calibration", "at least three paired points are required",
),
)
} else {
let fit = calibration_fit(source, target)
if fit.rms_error <= maximum_error {
result.push(diagnostic_ok("calibration", "grid residual is within limit"))
} else {
result.push(
diagnostic_error("calibration", "grid residual exceeds limit"),
)
}
}
result
}
///|
pub fn calibration_reference_distance(
fit : CalibrationFit,
first : Point,
second : Point,
) -> Double {
calibrated_distance(fit.transform, first, second)
}
///|
pub fn calibration_scale_report(fit : CalibrationFit) -> String {
let scale = calibration_scale(fit)
"scale_x=" +
scale.x.to_string() +
"; scale_y=" +
scale.y.to_string() +
"; rotation=" +
calibration_rotation(fit).to_string()
}
///|
pub fn calibration_roundtrip_error(
fit : AffineTransform,
points : Array[Point],
) -> Double {
let inverse = fit.inverse()
let mut maximum = 0.0
for point in points {
maximum = maximum.max(distance(inverse.apply(fit.apply(point)), point))
}
maximum
}
///|
pub fn calibration_translate_origin(
fit : CalibrationFit,
origin : Point,
) -> AffineTransform {
AffineTransform::translation(dx=-origin.x, dy=-origin.y).compose(
fit.transform,
)
}