// MoonBit Feature Forge - Core Pipeline Module
// Unified local feature extraction, ORB description, matching, and RANSAC filtering pipeline.
///|
pub(all) struct FeatureConfig {
fast : FastConfig
orb : OrbConfig
pyramid_levels : Int
match_k : Int
ratio : Double
use_ratio : Bool
enforce_unique : Bool
ransac_iters : Int
ransac_threshold : Double
seed : Int
}
///|
pub fn FeatureConfig::default() -> FeatureConfig {
{
fast: FastConfig::new(20, 9, true, 512, 3, 0.0),
orb: OrbConfig::default(),
pyramid_levels: 1,
match_k: 2,
ratio: 0.8,
use_ratio: true,
enforce_unique: true,
ransac_iters: 100,
ransac_threshold: 3.0,
seed: 42,
}
}
///|
pub(all) struct PipelineResult {
keypoints1 : Array[Keypoint]
keypoints2 : Array[Keypoint]
descriptors1 : Array[Descriptor]
descriptors2 : Array[Descriptor]
matches : Array[Match]
match_stats : MatchStats
affine : RansacResult
homography : HomographyRansacResult
}
///|
pub fn extract_features(
img : GrayImage,
fast_threshold : Int,
) -> (Array[Keypoint], Array[Descriptor]) {
let fast_config = FastConfig::new(fast_threshold, 9, true, 0, 3, 0.0)
let raw_kps = detect_fast_with_config(img, fast_config)
compute_orb_descriptors_with_config(img, raw_kps, OrbConfig::default())
}
///|
pub fn run_pipeline(
img1 : GrayImage,
img2 : GrayImage,
config : FeatureConfig,
) -> PipelineResult {
let (keypoints1, descriptors1) = extract_multiscale_features(
img1,
config.fast,
config.orb,
config.pyramid_levels,
)
let (keypoints2, descriptors2) = extract_multiscale_features(
img2,
config.fast,
config.orb,
config.pyramid_levels,
)
let policy = MatchPolicy::new(
config.match_k,
config.ratio,
config.use_ratio,
config.enforce_unique,
)
let matches = match_with_policy(descriptors1, descriptors2, policy)
let affine = ransac_affine(
matches,
keypoints1,
keypoints2,
config.ransac_iters,
config.ransac_threshold,
)
let homography = ransac_homography(
matches,
keypoints1,
keypoints2,
config.ransac_iters,
config.ransac_threshold,
config.seed,
)
{
keypoints1,
keypoints2,
descriptors1,
descriptors2,
matches,
match_stats: match_stats(matches),
affine,
homography,
}
}
///|
pub fn extract_multiscale_features(
img : GrayImage,
fast_config : FastConfig,
orb_config : OrbConfig,
level_count : Int,
) -> (Array[Keypoint], Array[Descriptor]) {
let keypoints : Array[Keypoint] = []
let descriptors : Array[Descriptor] = []
let pyramid = ImagePyramid::build(img, level_count)
let mut level = 0
while level < pyramid.level_count() {
let level_img = pyramid.level(level)
let detected = detect_fast_with_config(level_img, fast_config)
let (valid, level_descriptors) = compute_orb_descriptors_with_config(
level_img, detected, orb_config,
)
let mut index = 0
while index < valid.length() {
let level_kp = valid[index]
let original = pyramid.to_original(
FloatPoint2::new(
Double::from_int(level_kp.x),
Double::from_int(level_kp.y),
),
level,
)
let size = if level_kp.size <= 0 {
1
} else {
(Double::from_int(level_kp.size) / pyramid.scale(level)).to_int()
}
let mapped : Keypoint = {
x: original.x.to_int(),
y: original.y.to_int(),
response: level_kp.response,
angle: level_kp.angle,
octave: level,
size,
}
keypoints.push(mapped)
descriptors.push(level_descriptors[index])
index = index + 1
}
level = level + 1
}
(keypoints, descriptors)
}
///|
pub fn match_and_filter(
desc1 : Array[Descriptor],
desc2 : Array[Descriptor],
ratio : Double,
) -> Array[Match] {
let knn_matches = knn_match(desc1, desc2, 2)
ratio_test(knn_matches, ratio)
}
///|
pub fn full_feature_pipeline(
img1 : GrayImage,
img2 : GrayImage,
fast_threshold : Int,
ratio : Double,
ransac_threshold : Double,
) -> RansacResult {
let defaults = FeatureConfig::default()
let config : FeatureConfig = {
fast: FastConfig::new(fast_threshold, 9, true, 512, 3, 0.0),
orb: defaults.orb,
pyramid_levels: defaults.pyramid_levels,
match_k: defaults.match_k,
ratio,
use_ratio: defaults.use_ratio,
enforce_unique: defaults.enforce_unique,
ransac_iters: defaults.ransac_iters,
ransac_threshold,
seed: defaults.seed,
}
run_pipeline(img1, img2, config).affine
}