///|
pub enum PipelineStage {
Normalize
Denoise
AdaptiveThreshold
Morphology
Edges
ConnectedComponents
Equalize
} derive(Debug, Eq)
///|
pub struct PipelineConfig {
threshold : Int
radius : Int
minimum_component_area : Int
stages : Array[PipelineStage]
} derive(Debug, Eq)
///|
pub fn PipelineConfig::new(
threshold? : Int = 128,
radius? : Int = 1,
minimum_component_area? : Int = 4,
) -> PipelineConfig {
{
threshold: clamp(threshold, low=0, high=255),
radius: max_i(0, radius),
minimum_component_area: max_i(0, minimum_component_area),
stages: [],
}
}
///|
pub fn PipelineConfig::add(
self : PipelineConfig,
stage : PipelineStage,
) -> Unit {
self.stages.push(stage)
}
///|
pub fn PipelineConfig::with_default_stages() -> PipelineConfig {
let config = PipelineConfig::new()
config.add(Normalize)
config.add(Denoise)
config.add(AdaptiveThreshold)
config.add(Morphology)
config.add(ConnectedComponents)
config.add(Edges)
config.add(Equalize)
config
}
///|
pub struct PipelineTrace {
stage : PipelineStage
width : Int
height : Int
checksum : UInt
mean : Int
} derive(Debug, Eq)
///|
pub fn PipelineTrace::from_image(
stage : PipelineStage,
image : GrayImage,
) -> PipelineTrace {
let summary = image.stats()
{
stage,
width: image.width,
height: image.height,
checksum: image_checksum(image),
mean: summary.mean_x1000 / 1000,
}
}
///|
pub struct PipelineResult {
image : GrayImage
traces : Array[PipelineTrace]
components : Array[Component]
valid : Bool
} derive(Debug, Eq)
///|
pub fn PipelineResult::new(
image~ : GrayImage,
traces~ : Array[PipelineTrace],
components~ : Array[Component],
) -> PipelineResult {
{
image,
traces,
components,
valid: validate_image(image, "pipeline.output").valid,
}
}
///|
pub fn run_pipeline(
image : GrayImage,
config : PipelineConfig,
) -> PipelineResult {
let mut current = image.copy()
let traces : Array[PipelineTrace] = []
let components : Array[Component] = []
for stage in config.stages {
match stage {
Normalize => current = current.multiply_contrast(11, 10)
Denoise => current = box_blur(current, config.radius)
AdaptiveThreshold =>
current = current.adaptive_threshold(config.radius, 0)
Morphology => current = current.close(config.radius)
Edges => current = current.edge_map(config.threshold)
ConnectedComponents => {
let mask = Mask::new(width=current.width, height=current.height)
for y in 0..= config.threshold {
mask.set(x, y, 1)
}
}
}
let found = connected_components(mask, 1)
for component in found {
if component.area >= config.minimum_component_area {
components.push(component)
}
}
current = components_to_image(components, image.size())
}
Equalize => current = current.equalize_histogram()
}
traces.push(PipelineTrace::from_image(stage, current))
}
PipelineResult::new(image=current, traces~, components~)
}
///|
fn components_to_image(components : Array[Component], size : Size) -> GrayImage {
let image = GrayImage::new(width=size.width, height=size.height)
for component in components {
for point in component.pixels {
image.set(point.x, point.y, Gray::white())
}
}
image
}
///|
pub fn PipelineResult::stage_count(self : PipelineResult) -> Int {
self.traces.length()
}
///|
pub fn PipelineResult::component_count(self : PipelineResult) -> Int {
self.components.length()
}
///|
pub fn PipelineResult::trace(
self : PipelineResult,
index : Int,
) -> PipelineTrace? {
if index < 0 || index >= self.traces.length() {
None
} else {
Some(self.traces[index])
}
}
///|
pub fn PipelineResult::to_json(self : PipelineResult) -> String {
let sb = StringBuilder()
sb.write_string("{\"valid\":\{self.valid},\"stages\":[")
for i, trace in self.traces {
if i > 0 {
sb.write_string(",")
}
sb.write_string(
"{\"stage\":\"\{pipeline_stage_name(trace.stage)}\",\"checksum\":\{trace.checksum},\"mean\":\{trace.mean}}",
)
}
sb.write_string("] ,\"components\":\{self.components.length()}}")
sb.to_string()
}
///|
pub fn pipeline_stage_name(stage : PipelineStage) -> String {
match stage {
Normalize => "normalize"
Denoise => "denoise"
AdaptiveThreshold => "adaptive-threshold"
Morphology => "morphology"
Edges => "edges"
ConnectedComponents => "connected-components"
Equalize => "equalize"
}
}
///|
pub struct BatchPipelineResult {
results : Array[PipelineResult]
total_pixels : Int
total_components : Int
} derive(Debug, Eq)
///|
pub fn run_pipeline_batch(
images : Array[GrayImage],
config : PipelineConfig,
) -> BatchPipelineResult {
let results : Array[PipelineResult] = []
let mut pixels = 0
let mut components = 0
for image in images {
let result = run_pipeline(image, config)
pixels += image.width * image.height
components += result.components.length()
results.push(result)
}
{ results, total_pixels: pixels, total_components: components }
}
///|
pub fn BatchPipelineResult::count(self : BatchPipelineResult) -> Int {
self.results.length()
}
///|
pub fn BatchPipelineResult::to_json(self : BatchPipelineResult) -> String {
"{\"images\":\{self.results.length()},\"pixels\":\{self.total_pixels},\"components\":\{self.total_components}}"
}
///|
pub fn scene_pipeline(
scene : SceneOutput,
config : PipelineConfig,
) -> PipelineResult {
run_pipeline(scene.image, config)
}
///|
pub fn validate_pipeline_config(config : PipelineConfig) -> ValidationReport {
let report = ValidationReport::new()
if config.threshold < 0 || config.threshold > 255 {
report.error("pipeline threshold must be in the 8-bit range")
}
if config.radius < 0 {
report.error("pipeline radius must not be negative")
}
if config.stages.length() == 0 {
report.warning("pipeline has no stages and returns a copy")
}
report
}
///|
pub fn pipeline_checksum(result : PipelineResult) -> UInt {
let mut checksum = image_checksum(result.image)
for trace in result.traces {
checksum = (checksum ^ trace.checksum) * 16777619
}
checksum
}