imageproc-0.25.0/.cargo/config.toml000064400000000000000000000001041046102023000151640ustar 00000000000000[target.wasm32-unknown-unknown] runner = 'wasm-bindgen-test-runner' imageproc-0.25.0/.cargo_vcs_info.json0000644000000001360000000000100130660ustar { "git": { "sha1": "44bbf5e7efd8d9de41ffafc2be2a0696cfecfb4b" }, "path_in_vcs": "" }imageproc-0.25.0/.gitignore000064400000000000000000000003211046102023000136420ustar 00000000000000.DS_Store *~ *# *.o *.so *.swp *.dylib *.dSYM *.dll *.rlib *.dummy *.exe *-test /bin/main /bin/test-internal /bin/test-external /doc/ /target/ /build/ /.rust/ /.idea/ rusti.sh Cargo.lock proptest-regressions imageproc-0.25.0/CHANGELOG.md000064400000000000000000000053561046102023000135000ustar 00000000000000# Change Log ## [0.25.0] - 2024-05-19 New features: * Added functions `template_matching::match_template_with_mask` and `template_matching::match_template_with_mask_parallel` to support masked templates in template matching. * Added `L2` variant to the `distance_transform::Norm` enum used to specify the distance function in `distance_transfrom::distance_transform` and several functions in the `morphology` module. * Added function `filter::laplacian_filter` using a 3x3 approximation to the Laplacian kernel. * Added function `stats::min_max()` which reports the min and max intensities in an image for each channel. * Added support for grayscale morphology operators: `grayscale_(dilate|erode|open|close)`. Breaking changes: * Added `ThresholdType` parameter to `contrast::threshold{_mut}` to allow configuration of thresholding behaviour. To match the behaviour of `threshold(image, thresh)` from `imageproc 0.24`, use `threshold(image, thresh, ThresholdType::Binary)`. * Changed the signature of `contrast::stretch_contrast{_mut}` to make the output intensity range configurable. To match the behaviour of `stretch_contrast(image, lower, upper)` from `imageproc 0.24`, use `stretch_contrast(image, lower, upper, 0u8, 255u8)`. * Changed input parameter to `convex_hull` from `&[Point]` to `impl Into>>`. * Removed dependency on `conv` crate and removed function `math::cast`. This replaces `ValueInto` trait bounds with `Into` in many function signatures. Bug fixes: * Fix panic when drawing large ellipses. * Fix `BresenhamLineIter` panic when using non-integer endpoints. * Fix text rendering for overlapping glyphs, e.g. Arabic. * Fix Gaussian blur by normalising kernel values. ## [0.24.0] - 2024-03-16 New features: * Added BRIEF descriptors * Added draw_antialiased_polygon * Added draw_hollow_polygon, draw_hollow_polygon_mut * Added contour_area * Added match_template_parallel * Made Contour clonable * Re-export image crate and add image/default as default feature Performance improvements: * Faster interpolate_nearest * Faster find_contours_with_threshold * Faster approximate_polygon_do * Faster rotating_calipers Bug fixes: * Stop window::display_image consuming 100% CPU on Linux Breaking changes: * Migrate text rendering from rusttype to ab_glyph * Updated depenedencies * Increased MSRV to 1.70 ## [0.23.0] - 2022-04-10 ... ## [0.6.1] - 2016-12-28 - Fixed bug in draw_line_segment_mut when line extends outside of image bounds. - Generalised connected_components to handle arbitrary equatable pixel types. - Added support for drawing hollow and filled circles. - Added support for drawing anti-aliased lines, and convex polygons. - Added adaptive_threshold function. ## [0.6.0] - 2016-05-07 No change log kept for this or earlier versions. imageproc-0.25.0/Cargo.lock0000644000001174420000000000100110520ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "ab_glyph" version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", ] [[package]] name = "ab_glyph_rasterizer" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "aligned-vec" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" [[package]] name = "anyhow" version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "approx" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" dependencies = [ "num-traits", ] [[package]] name = "arbitrary" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" [[package]] name = "arg_enum_proc_macro" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", "syn 2.0.52", ] [[package]] name = "arrayvec" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "assert_approx_eq" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "av1-grain" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" dependencies = [ "anyhow", "arrayvec", "log", "nom", "num-rational", "v_frame", ] [[package]] name = "avif-serialize" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" dependencies = [ "arrayvec", ] [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bit_field" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bitstream-io" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06c9989a51171e2e81038ab168b6ae22886fe9ded214430dbb4f41c28cf176da" [[package]] name = "built" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d17f4d6e4dc36d1a02fbedc2753a096848e7c1b0772f7654eab8e2c927dd53" [[package]] name = "bumpalo" version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytemuck" version = "1.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", ] [[package]] name = "cfg-expr" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" dependencies = [ "smallvec", "target-lexicon", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cmake" version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" dependencies = [ "cc", ] [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "console_error_panic_hook" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ "cfg-if", "wasm-bindgen", ] [[package]] name = "crc32fast" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-deque" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crunchy" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "either" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "env_logger" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" dependencies = [ "log", "regex", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys", ] [[package]] name = "exr" version = "1.72.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" dependencies = [ "bit_field", "flume", "half", "lebe", "miniz_oxide", "rayon-core", "smallvec", "zune-inflate", ] [[package]] name = "fastrand" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fdeflate" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" dependencies = [ "simd-adler32", ] [[package]] name = "flate2" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "flume" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "spin", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "gif" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" dependencies = [ "color_quant", "weezl", ] [[package]] name = "half" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" dependencies = [ "cfg-if", "crunchy", ] [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "image" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9b4f005360d32e9325029b38ba47ebd7a56f3316df09249368939562d518645" dependencies = [ "bytemuck", "byteorder", "color_quant", "exr", "gif", "image-webp", "num-traits", "png", "qoi", "ravif", "rayon", "rgb", "tiff", "zune-core", "zune-jpeg", ] [[package]] name = "image-webp" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba6107a25f04af48ceeb4093eebc9b405ee5a1813a0bab5ecf1805d3eabb3337" dependencies = [ "byteorder", "thiserror", ] [[package]] name = "imageproc" version = "0.25.0" dependencies = [ "ab_glyph", "approx", "assert_approx_eq", "getrandom", "image", "itertools", "katexit", "nalgebra", "num", "proptest", "quickcheck", "rand", "rand_distr", "rayon", "sdl2", "wasm-bindgen-test", ] [[package]] name = "imgref" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" [[package]] name = "indexmap" version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "interpolate_name" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", "syn 2.0.52", ] [[package]] name = "itertools" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "jobserver" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] [[package]] name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "katexit" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb1304c448ce2c207c2298a34bc476ce7ae47f63c23fa2b498583b26be9bc88c" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lebe" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libfuzzer-sys" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" dependencies = [ "arbitrary", "cc", "once_cell", ] [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "loop9" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" dependencies = [ "imgref", ] [[package]] name = "matrixmultiply" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" dependencies = [ "autocfg", "rawpointer", ] [[package]] name = "maybe-rayon" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", "rayon", ] [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", "simd-adler32", ] [[package]] name = "nalgebra" version = "0.32.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4541eb06dce09c0241ebbaab7102f0a01a0c8994afed2e5d0d66775016e25ac2" dependencies = [ "approx", "matrixmultiply", "num-complex", "num-rational", "num-traits", "simba", "typenum", ] [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "noop_proc_macro" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "num" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" dependencies = [ "num-bigint", "num-complex", "num-integer", "num-iter", "num-rational", "num-traits", ] [[package]] name = "num-bigint" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-complex" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" dependencies = [ "num-traits", ] [[package]] name = "num-derive" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", "syn 2.0.52", ] [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-iter" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", "num-bigint", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "owned_ttf_parser" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" dependencies = [ "ttf-parser", ] [[package]] name = "paste" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "png" version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", "syn 2.0.52", ] [[package]] name = "proptest" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", "bitflags 2.5.0", "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", "unarray", ] [[package]] name = "qoi" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" dependencies = [ "bytemuck", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-error" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quickcheck" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "env_logger", "log", "rand", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rand_distr" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", "rand", ] [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ "rand_core", ] [[package]] name = "rav1e" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" dependencies = [ "arbitrary", "arg_enum_proc_macro", "arrayvec", "av1-grain", "bitstream-io", "built", "cfg-if", "interpolate_name", "itertools", "libc", "libfuzzer-sys", "log", "maybe-rayon", "new_debug_unreachable", "noop_proc_macro", "num-derive", "num-traits", "once_cell", "paste", "profiling", "rand", "rand_chacha", "simd_helpers", "system-deps", "thiserror", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234" dependencies = [ "avif-serialize", "imgref", "loop9", "quick-error 2.0.1", "rav1e", "rayon", "rgb", ] [[package]] name = "rawpointer" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "regex" version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rgb" version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" dependencies = [ "bytemuck", ] [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "rusty-fork" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", "quick-error 1.2.3", "tempfile", "wait-timeout", ] [[package]] name = "safe_arch" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" dependencies = [ "bytemuck", ] [[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdl2" version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8356b2697d1ead5a34f40bcc3c5d3620205fe0c7be0a14656223bfeec0258891" dependencies = [ "bitflags 1.3.2", "lazy_static", "libc", "sdl2-sys", ] [[package]] name = "sdl2-sys" version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bcacfdd45d539fb5785049feb0038a63931aa896c7763a2a12e125ec58bd29" dependencies = [ "cfg-if", "cmake", "libc", "version-compare", ] [[package]] name = "serde" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", "syn 2.0.52", ] [[package]] name = "serde_spanned" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] [[package]] name = "simba" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" dependencies = [ "approx", "num-complex", "num-traits", "paste", "wide", ] [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simd_helpers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" dependencies = [ "quote", ] [[package]] name = "smallvec" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ "lock_api", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "system-deps" version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8e9199467bcbc77c6a13cc6e32a6af21721ab8c96aa0261856c4fda5a4433f0" dependencies = [ "cfg-expr", "heck", "pkg-config", "toml", "version-compare", ] [[package]] name = "target-lexicon" version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys", ] [[package]] name = "thiserror" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", "syn 2.0.52", ] [[package]] name = "tiff" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" dependencies = [ "flate2", "jpeg-decoder", "weezl", ] [[package]] name = "toml" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "ttf-parser" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "v_frame" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" dependencies = [ "aligned-vec", "num-traits", "wasm-bindgen", ] [[package]] name = "version-compare" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.52", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-bindgen-test" version = "0.3.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" dependencies = [ "console_error_panic_hook", "js-sys", "scoped-tls", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", ] [[package]] name = "wasm-bindgen-test-macro" version = "0.3.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", "syn 2.0.52", ] [[package]] name = "web-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "weezl" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "wide" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89beec544f246e679fc25490e3f8e08003bc4bf612068f325120dad4cea02c1c" dependencies = [ "bytemuck", "safe_arch", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] name = "windows_i686_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" dependencies = [ "memchr", ] [[package]] name = "zune-core" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-inflate" version = "0.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" dependencies = [ "simd-adler32", ] [[package]] name = "zune-jpeg" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" dependencies = [ "zune-core", ] imageproc-0.25.0/Cargo.toml0000644000000041770000000000100110750ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.70.0" name = "imageproc" version = "0.25.0" authors = ["theotherphil"] exclude = [ ".github/*", "examples/*", "tests/*", ] description = "Image processing operations" homepage = "https://github.com/image-rs/imageproc" readme = "README.md" license = "MIT" repository = "https://github.com/image-rs/imageproc.git" [package.metadata.docs.rs] features = [ "property-testing", "katexit", ] [profile.bench] opt-level = 3 lto = false codegen-units = 1 debug = 2 debug-assertions = false rpath = false [profile.release] opt-level = 3 debug = 2 [dependencies.ab_glyph] version = "0.2.23" [dependencies.approx] version = "0.5" [dependencies.image] version = "0.25.0" default-features = false [dependencies.itertools] version = "0.12" [dependencies.katexit] version = "0.1.4" optional = true [dependencies.nalgebra] version = "0.32" features = ["std"] default-features = false [dependencies.num] version = "0.4.1" [dependencies.quickcheck] version = "1.0.3" optional = true [dependencies.rand] version = "0.8.5" [dependencies.rand_distr] version = "0.4.3" [dependencies.rayon] version = "1.8.0" optional = true [dependencies.sdl2] version = "0.36" features = ["bundled"] optional = true default-features = false [dev-dependencies.assert_approx_eq] version = "1.1.0" [dev-dependencies.proptest] version = "1.4.0" [dev-dependencies.quickcheck] version = "1.0.3" [dev-dependencies.wasm-bindgen-test] version = "0.3.38" [features] default = [ "rayon", "image/default", ] display-window = ["sdl2"] property-testing = ["quickcheck"] [target."cfg(target_arch = \"wasm32\")".dependencies.getrandom] version = "0.2" features = ["js"] imageproc-0.25.0/Cargo.toml.orig000064400000000000000000000030561046102023000145510ustar 00000000000000[package] name = "imageproc" version = "0.25.0" authors = ["theotherphil"] # note: when changed, also update `msrv` in `.github/workflows/check.yml` rust-version = "1.70.0" edition = "2021" license = "MIT" description = "Image processing operations" readme = "README.md" repository = "https://github.com/image-rs/imageproc.git" homepage = "https://github.com/image-rs/imageproc" exclude = [ ".github/*", "examples/*", "tests/*" ] [features] default = [ "rayon", "image/default" ] property-testing = [ "quickcheck" ] display-window = ["sdl2"] [dependencies] ab_glyph = "0.2.23" approx = "0.5" image = { version = "0.25.0", default-features = false } itertools = "0.12" nalgebra = { version = "0.32", default-features = false, features = ["std"] } num = "0.4.1" rand = "0.8.5" rand_distr = "0.4.3" rayon = { version = "1.8.0", optional = true } quickcheck = { version = "1.0.3", optional = true } sdl2 = { version = "0.36", optional = true, default-features = false, features = ["bundled"] } katexit = { version = "0.1.4", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } [dev-dependencies] assert_approx_eq = "1.1.0" proptest = "1.4.0" quickcheck = "1.0.3" wasm-bindgen-test = "0.3.38" [package.metadata.docs.rs] # See https://github.com/image-rs/imageproc/issues/358 # all-features = true features = [ "property-testing", "katexit" ] [profile.release] opt-level = 3 debug = true [profile.bench] opt-level = 3 debug = true rpath = false lto = false debug-assertions = false codegen-units = 1 imageproc-0.25.0/LICENSE000064400000000000000000000020731046102023000126650ustar 00000000000000The MIT License (MIT) Copyright (c) 2015 PistonDevelopers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. imageproc-0.25.0/README.md000064400000000000000000000067251046102023000131470ustar 00000000000000imageproc ==== [![crates.io](https://img.shields.io/crates/v/imageproc.svg)](https://crates.io/crates/imageproc) [![doc-badge]][doc-link] [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/image-rs/imageproc/blob/master/LICENSE) [![Dependency status](https://deps.rs/repo/github/image-rs/imageproc/status.svg)](https://deps.rs/repo/github/image-rs/imageproc) An image processing library, based on the [image](https://github.com/image-rs/image) library. There may initially be overlap between the functions in this library and those in `image::imageops`. This is very much a work in progress. If you have ideas for things that could be done better, or new features you'd like to see, then please create issues for them. Nothing's set in stone. [API documentation][doc-link] [doc-badge]: https://docs.rs/imageproc/badge.svg [doc-link]: https://docs.rs/imageproc # Goals A performant, well-tested, well-documented library with a consistent API, suitable for use as the basis of computer vision applications or graphics editors. # Non-goals Maximum genericity over image storages or formats, or support for higher-dimensional images. Full blown computer vision applications (e.g. face recognition or image registration) probably also belong elsewhere, but the line's a bit blurred here (e.g. is image in-painting an image processing task or a computer vision task?). However, worrying about how to structure the code can probably wait until we have more code to structure... # Crate Features Imageproc is built with these features enabled by default: - `rayon` enables multithreading for certain operations (e.g., geometric transformations) via [rayon](https://github.com/rayon-rs/rayon) Optionally, the following dependencies can be enabled: - `property-testing` exposes helper types and methods to enable property testing via [quickcheck](https://github.com/BurntSushi/quickcheck) - `display-window` enables the displaying of images (using `imageproc::window`) with [sdl2](https://github.com/Rust-SDL2/rust-sdl2) # How to contribute All pull requests are welcome. Some specific areas that would be great to get some help with are: * New features! If you're planning on adding some new functions or modules, please create an issue with a name along the lines of "Add [feature name]" and assign it to yourself (or comment on the issue that you're planning on doing it). This way we'll not have multiple people working on the same functionality. * Performance - profiling current code, documenting or fixing performance problems, adding benchmarks, comparisons to other libraries. * Testing - more unit tests and regression tests. Some more property-based testing would be particularly nice. * APIs - are the current APIs hard to use or inconsistent? Some open questions: Should we return Result types more often? How should functions indicate acceptable input image dimensions? Should we use enum arguments or have lots of similarly named functions? What's the best way to get concise code while still allowing control over allocations? * Documentation - particularly more example code showing what's currently possible. Pretty pictures in this README. * Feature requests - are there any functions you'd like to see added? Is the library currently unsuitable for your use case for some reason? ## Documentation This crate uses `katexit` to render equations in the documentation. To open the documentation locally with `katexit` enabled, use ```sh cargo doc --open --features=katexit ``` imageproc-0.25.0/src/binary_descriptors/brief.rs000064400000000000000000000312711046102023000200130ustar 00000000000000//! Structs and functions for finding and computing BRIEF descriptors as //! described in [Calonder, et. al. (2010)]. /// /// [Calonder, et. al. (2010)]: https://www.cs.ubc.ca/~lowe/525/papers/calonder_eccv10.pdf use image::{GenericImageView, GrayImage, ImageBuffer, Luma}; use rand_distr::{Distribution, Normal}; use crate::{corners::Corner, integral_image::integral_image, point::Point}; use super::{ constants::{BRIEF_PATCH_DIAMETER, BRIEF_PATCH_RADIUS}, BinaryDescriptor, }; /// BRIEF descriptor as described in [Calonder, et. al. (2010)]. /// /// [Calonder, et. al. (2010)]: https://www.cs.ubc.ca/~lowe/525/papers/calonder_eccv10.pdf #[derive(Clone, PartialEq)] pub struct BriefDescriptor { /// Results of the pairwise pixel intensity tests that comprise this BRIEF /// descriptor. pub bits: Vec, /// Pixel location and corner score of the keypoint associated with this /// BRIEF descriptor. pub corner: Corner, } impl BinaryDescriptor for BriefDescriptor { fn get_size(&self) -> u32 { (self.bits.len() * 128) as u32 } fn hamming_distance(&self, other: &Self) -> u32 { assert_eq!(self.get_size(), other.get_size()); self.bits .iter() .zip(other.bits.iter()) .fold(0, |acc, x| acc + (x.0 ^ x.1).count_ones()) } fn get_bit_subset(&self, bits: &[u32]) -> u128 { assert!( bits.len() <= 128, "Can't extract more than 128 bits (found {})", bits.len() ); let mut subset = 0; for b in bits { subset <<= 1; // isolate the bit at index b subset += (self.bits[(b / 128) as usize] >> (b % 128)) % 2; } subset } fn position(&self) -> Point { self.corner.into() } } /// Collection of two points that a BRIEF descriptor uses to generate its bits. #[derive(Debug, Clone, Copy, PartialEq)] pub struct TestPair { /// The first point in the pair. pub p0: Point, /// The second point in the pair. pub p1: Point, } fn local_pixel_average( integral_image: &ImageBuffer, Vec>, x: u32, y: u32, radius: u32, ) -> u8 { if radius == 0 { return 0; } let y_min = if y < radius { 0 } else { y - radius }; let x_min = if x < radius { 0 } else { x - radius }; let y_max = u32::min(y + radius + 1, integral_image.height() - 1); let x_max = u32::min(x + radius + 1, integral_image.width() - 1); let pixel_area = (y_max - y_min) * (x_max - x_min); if pixel_area == 0 { return 0; } // UNSAFETY JUSTIFICATION // // Benefit // // Removing the unsafe pixel accesses in this function increases the // runtimes for bench_brief_fixed_test_pairs_1000_keypoints, // bench_brief_random_test_pairs_1000_keypoints, and // bench_rotated_brief_1000_keypoints by about 40%, 30%, and 30%, // respectively. // // Correctness // // The values of x_min, x_max, y_min, and y_max are all bounded between zero // and the integral image dimensions by the checks at the top of this // function. let (bottom_right, top_left, top_right, bottom_left) = unsafe { ( integral_image.unsafe_get_pixel(x_max, y_max)[0], integral_image.unsafe_get_pixel(x_min, y_min)[0], integral_image.unsafe_get_pixel(x_max, y_min)[0], integral_image.unsafe_get_pixel(x_min, y_max)[0], ) }; let total_intensity = bottom_right + top_left - top_right - bottom_left; (total_intensity / pixel_area) as u8 } pub(crate) fn brief_impl( integral_image: &ImageBuffer, Vec>, keypoints: &[Point], test_pairs: &[TestPair], length: usize, ) -> Result, String> { if length % 128 != 0 { return Err(format!( "BRIEF descriptor length must be a multiple of 128 bits (found {})", length )); } if length != test_pairs.len() { return Err(format!( "BRIEF descriptor length must be equal to the number of test pairs ({} != {})", length, test_pairs.len() )); } let mut descriptors: Vec = Vec::with_capacity(keypoints.len()); let (width, height) = (integral_image.width(), integral_image.height()); for keypoint in keypoints { // if the keypoint is too close to the edge, return an error if keypoint.x <= BRIEF_PATCH_RADIUS || keypoint.x + BRIEF_PATCH_RADIUS >= width || keypoint.y <= BRIEF_PATCH_RADIUS || keypoint.y + BRIEF_PATCH_RADIUS >= height { return Err(format!( "Found keypoint within {} px of image edge: ({}, {})", BRIEF_PATCH_RADIUS + 1, keypoint.x, keypoint.y )); } let patch_top_left = Point { x: keypoint.x - BRIEF_PATCH_RADIUS, y: keypoint.y - BRIEF_PATCH_RADIUS, }; let mut descriptor = BriefDescriptor { bits: Vec::with_capacity(length / 128), corner: Corner { x: keypoint.x, y: keypoint.y, score: 0., }, }; let mut descriptor_chunk = 0u128; // for each test pair, compare the pixels within the patch at those points for (idx, test_pair) in test_pairs.iter().enumerate() { // if we've entered a new chunk, then save the previous one if idx != 0 && idx % 128 == 0 { descriptor.bits.push(descriptor_chunk); descriptor_chunk = 0; } let p0 = Point { x: test_pair.p0.x + patch_top_left.x + 1, y: test_pair.p0.y + patch_top_left.y + 1, }; let p1 = Point { x: test_pair.p1.x + patch_top_left.x + 1, y: test_pair.p1.y + patch_top_left.y + 1, }; // if p0 < p1, then record true for this test; otherwise, record false descriptor_chunk += (local_pixel_average(integral_image, p0.x, p0.y, 2) < local_pixel_average(integral_image, p1.x, p1.y, 2)) as u128; descriptor_chunk <<= 1; } // save the final chunk too descriptor.bits.push(descriptor_chunk); descriptors.push(descriptor); } Ok(descriptors) } /// Generates BRIEF descriptors for small patches around keypoints in an image. /// /// Returns a tuple containing a vector of `BriefDescriptor` and a vector of /// `TestPair`. All returned descriptors are based on the same `TestPair` set. /// Patches are 31x31 pixels, so keypoints must be at least 17 pixels from any /// edge. If any keypoints are too close to an edge, returns an error. /// /// `length` must be a multiple of 128 bits. Returns an error otherwise. /// /// If `override_test_pairs` is `Some`, then those test pairs are used, and none /// are generated. Use this when you already have test pairs from another run /// and want to compare the descriptors later. /// /// If `override_test_pairs` is `None`, then `TestPair`s are generated according /// to an isotropic Gaussian. /// /// [Calonder, et. al. (2010)] used Gaussian smoothing to decrease the effects of noise in the /// patches. This is slow, even with a box filter approximation. For maximum /// performance, the average intensities of sub-patches of radius 5 around the /// test points are computed and used instead of the intensities of the test /// points themselves. This is much faster because the averages come from /// integral images. Calonder suggests that this approach may be faster, and /// [Rublee et. al. (2012)][rublee] use this approach to quickly compute ORB /// descriptors. /// /// [rublee]: http://www.gwylab.com/download/ORB_2012.pdf /// [Calonder, et. al. (2010)]: https://www.cs.ubc.ca/~lowe/525/papers/calonder_eccv10.pdf pub fn brief( image: &GrayImage, keypoints: &[Point], length: usize, override_test_pairs: Option<&Vec>, ) -> Result<(Vec, Vec), String> { // if we have test pairs already, use them; otherwise, generate some let test_pairs = if let Some(t) = override_test_pairs { t.clone() } else { // generate a set of test pairs within a 31x31 grid with a Gaussian bias (sigma = 6.6) let test_pair_distribution = Normal::new(BRIEF_PATCH_RADIUS as f32 + 1.0, 6.6).unwrap(); let mut rng = rand::thread_rng(); let mut test_pairs: Vec = Vec::with_capacity(length); while test_pairs.len() < length { let (x0, y0, x1, y1) = ( test_pair_distribution.sample(&mut rng) as u32, test_pair_distribution.sample(&mut rng) as u32, test_pair_distribution.sample(&mut rng) as u32, test_pair_distribution.sample(&mut rng) as u32, ); if x0 < BRIEF_PATCH_DIAMETER && y0 < BRIEF_PATCH_DIAMETER && x1 < BRIEF_PATCH_DIAMETER && y1 < BRIEF_PATCH_DIAMETER { test_pairs.push(TestPair { p0: Point::new(x0, y0), p1: Point::new(x1, y1), }); } } test_pairs.clone() }; let integral_image = integral_image(image); let descriptors = brief_impl(&integral_image, keypoints, &test_pairs, length)?; // return the descriptors for all the keypoints and the test pairs used Ok((descriptors, test_pairs)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_compute_hamming_distance() { let d1 = BriefDescriptor { bits: vec![ 0xe41749023c71b74df05b57165519a180, 0x9c06c422620e05d01105618cb3a2dcf1, ], corner: Corner::new(0, 0, 0.), }; let d2 = BriefDescriptor { bits: vec![ 0x8af22bb0596edc267c6f72cf425ebe1a, 0xc1f6291520e474e8fa114e15420413d1, ], corner: Corner::new(0, 0, 0.), }; assert_eq!(d1.hamming_distance(&d2), 134); } #[test] fn test_get_bit_subset() { let d = BriefDescriptor { bits: vec![ 0xdbe3de5bd950adf3d730034f9e4a55f7, 0xf275f00f6243892a18ffefd0499996ee, ], corner: Corner::new(0, 0, 0.), }; let bits = vec![ 226, 38, 212, 210, 60, 205, 68, 184, 47, 105, 152, 169, 11, 39, 76, 217, 183, 113, 189, 251, 37, 181, 62, 28, 148, 92, 251, 77, 222, 148, 56, 142, ]; assert_eq!(d.get_bit_subset(&bits), 0b11001010011100011100011111011110); } #[test] fn test_local_pixel_average() { let image = gray_image!( 186, 106, 86, 22, 191, 10, 204, 217; 37, 188, 82, 28, 99, 110, 166, 202; 36, 97, 176, 54, 141, 42, 44, 40; 248, 163, 218, 204, 117, 121, 151, 135; 138, 100, 77, 115, 93, 246, 204, 163; 123, 1, 104, 97, 67, 208, 0, 116; 5, 237, 254, 171, 172, 165, 50, 39; 92, 31, 238, 88, 44, 67, 140, 255 ); let integral_image: ImageBuffer, Vec> = integral_image(&image); assert_eq!(local_pixel_average(&integral_image, 3, 3, 2), 117); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::utils::gray_bench_image; use rand::Rng; use test::{black_box, Bencher}; #[bench] #[ignore] fn bench_brief_random_test_pairs_1000_keypoints(b: &mut Bencher) { let image = gray_bench_image(640, 480); let mut rng = rand::thread_rng(); let keypoints = (0..1000) .map(|_| { Point::new( rng.gen_range(24..image.width() - 24), rng.gen_range(24..image.height() - 24), ) }) .collect::>>(); b.iter(|| { black_box(brief(&image, &keypoints, 256, None)).unwrap(); }) } #[bench] #[ignore] fn bench_brief_fixed_test_pairs_1000_keypoints(b: &mut Bencher) { let image = gray_bench_image(640, 480); let mut rng = rand::thread_rng(); let keypoints = (0..1000) .map(|_| { Point::new( rng.gen_range(24..image.width() - 24), rng.gen_range(24..image.height() - 24), ) }) .collect::>>(); let (_, test_pairs) = brief(&image, &keypoints, 256, None).unwrap(); b.iter(|| { black_box(brief(&image, &keypoints, 256, Some(&test_pairs))).unwrap(); }) } } imageproc-0.25.0/src/binary_descriptors/constants.rs000064400000000000000000000001521046102023000207320ustar 00000000000000pub const BRIEF_PATCH_RADIUS: u32 = 15; pub const BRIEF_PATCH_DIAMETER: u32 = BRIEF_PATCH_RADIUS * 2 + 1; imageproc-0.25.0/src/binary_descriptors/mod.rs000064400000000000000000000135751046102023000175120ustar 00000000000000//! Functions for generating and comparing compact binary patch descriptors. use rand::{rngs::StdRng, Rng, SeedableRng}; use std::collections::HashMap; use crate::point::Point; pub mod brief; mod constants; /// A feature descriptor whose value is given by a string of bits. pub trait BinaryDescriptor { /// Returns the length of the descriptor in bits. Typical values are 128, /// 256, and 512. Will always be a multiple of 128. fn get_size(&self) -> u32; /// Returns the number of bits that are different between the two descriptors. /// /// Panics if the two descriptors have unequal lengths. The descriptors /// should have been computed using the same set of test pairs, otherwise /// comparing them has no meaning. fn hamming_distance(&self, other: &Self) -> u32; /// Given a set of bit indices, returns those bits from the descriptor as a /// single concatenated value. /// /// Panics if `bits.len() > 128`. fn get_bit_subset(&self, bits: &[u32]) -> u128; /// Returns the pixel location of this binary descriptor in its associated /// image. fn position(&self) -> Point; } /// For each descriptor in `d1`, find the descriptor in `d2` with the minimum /// Hamming distance below `threshold`. If no such descriptor exists in `d2`, /// the descriptor in `d1` is left unmatched. /// /// Descriptors in `d2` may be matched with more than one descriptor in `d1`. /// /// Uses [locality-sensitive hashing][lsh] (LSH) for efficient matching. The /// number of tables is fixed at three, but the hash length is proportional to /// the log of the size of the largest input array. /// /// Returns a vector of references describing the matched pairs. The first /// reference is to a descriptor in `d1`, and the second reference is to an /// index into `d2`. /// /// [lsh]: /// https://en.wikipedia.org/wiki/Locality_sensitive_hashing#Bit_sampling_for_Hamming_distance pub fn match_binary_descriptors<'a, T: BinaryDescriptor>( d1: &'a [T], d2: &'a [T], threshold: u32, seed: Option, ) -> Vec<(&'a T, &'a T)> { // early return if either input is empty if d1.is_empty() || d2.is_empty() { return Vec::new(); } let mut rng = if let Some(s) = seed { StdRng::seed_from_u64(s) } else { StdRng::from_entropy() }; // locality-sensitive hashing (LSH) // this algorithm is log(d2.len()) but linear in d1.len(), so swap the inputs if needed let (queries, database, swapped) = if d1.len() > d2.len() { (d2, d1, true) } else { (d1, d2, false) }; // build l hash tables by selecting k random bits from each descriptor let l = 3; // k grows as the log of the database size // this keeps bucket size roughly constant let k = (database.len() as f32).log2() as i32; let mut hash_tables = Vec::with_capacity(l); for _ in 0..l { // choose k random bits (not necessarily unique) let bits = (0..k) .map(|_| rng.gen_range(0..queries[0].get_size())) .collect::>(); let mut new_hashmap = HashMap::>::with_capacity(database.len()); // compute the hash of each descriptor in the database and store its index // there will be collisions --- we want that to happen for d in database.iter() { let hash = d.get_bit_subset(&bits); if let Some(v) = new_hashmap.get_mut(&hash) { v.push(d); } else { new_hashmap.insert(hash, vec![d]); } } hash_tables.push((bits, new_hashmap)); } // find the hash buckets corresponding to each query descriptor // then check all bucket members to find the (probable) best match let mut matches = Vec::with_capacity(queries.len()); for query in queries.iter() { // find all buckets for the query descriptor let mut candidates = Vec::with_capacity(l); for (bits, table) in hash_tables.iter() { let query_hash = query.get_bit_subset(bits); if let Some(m) = table.get(&query_hash) { for new_candidate in m.clone() { candidates.push(new_candidate); } } } // perform linear scan to find the best match let mut best_score = u32::MAX; let mut best_candidate = None; for c in candidates { let distance = query.hamming_distance(c); if distance < best_score { best_score = distance; best_candidate = Some(c); } } // ignore the match if it's beyond our threshold if best_score < threshold { if swapped { matches.push((best_candidate.unwrap(), query)); } else { matches.push((query, best_candidate.unwrap())); } } } matches } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::{binary_descriptors::brief::brief, utils::gray_bench_image}; use test::{black_box, Bencher}; #[bench] #[ignore] fn bench_matcher_1000_keypoints_each(b: &mut Bencher) { let image = gray_bench_image(640, 480); let mut rng = rand::thread_rng(); let keypoints = (0..1000) .map(|_| { Point::new( rng.gen_range(20..image.width() - 20), rng.gen_range(20..image.height() - 20), ) }) .collect::>>(); let (first_descriptors, test_pairs) = brief(&image, &keypoints, 256, None).unwrap(); let (second_descriptors, _) = brief(&image, &keypoints, 256, Some(&test_pairs)).unwrap(); b.iter(|| { black_box(match_binary_descriptors( &first_descriptors, &second_descriptors, 24, Some(0xc0), )); }); } } imageproc-0.25.0/src/contours.rs000064400000000000000000000361741046102023000147020ustar 00000000000000//! Functions for finding border contours within binary images. use crate::point::Point; use image::GrayImage; use num::{cast, Num, NumCast}; use std::collections::VecDeque; /// Whether a border of a foreground region borders an enclosing background region or a contained background region. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum BorderType { /// A border between a foreground region and the background region enclosing it. /// All points in the border lie within the foreground region. Outer, /// A border between a foreground region and a background region contained within it. /// All points in the border lie within the foreground region. Hole, } /// A border of an 8-connected foreground region. #[derive(Debug, Clone)] pub struct Contour { /// The points in the border. pub points: Vec>, /// Whether this is an outer border or a hole border. pub border_type: BorderType, /// Calls to `find_contours` and `find_contours_with_threshold` return a `Vec` of all borders /// in an image. This field provides the index for the parent of the current border in that `Vec`. pub parent: Option, } impl Contour { /// Construct a contour. pub fn new(points: Vec>, border_type: BorderType, parent: Option) -> Self { Contour { points, border_type, parent, } } } /// Finds all borders of foreground regions in an image. All non-zero pixels are /// treated as belonging to the foreground. /// /// Based on the algorithm proposed by Suzuki and Abe: Topological Structural /// Analysis of Digitized Binary Images by Border Following. pub fn find_contours(image: &GrayImage) -> Vec> where T: Num + NumCast + Copy + PartialEq + Eq, { find_contours_with_threshold(image, 0) } /// Finds all borders of foreground regions in an image. All pixels with intensity strictly greater /// than `threshold` are treated as belonging to the foreground. /// /// Based on the algorithm proposed by Suzuki and Abe: Topological Structural /// Analysis of Digitized Binary Images by Border Following. pub fn find_contours_with_threshold(image: &GrayImage, threshold: u8) -> Vec> where T: Num + NumCast + Copy + PartialEq + Eq, { let width = image.width() as usize; let height = image.height() as usize; let mut image_values = vec![0i32; height * width]; let at = |x, y| x + width * y; let get_position_if_non_zero_pixel = |image: &[i32], curr: Point| { let (x, y) = (curr.x, curr.y); let in_bounds = x > -1 && x < width as i32 && y > -1 && y < height as i32; if in_bounds && image[at(x as usize, y as usize)] != 0 { Some(Point::new(x as usize, y as usize)) } else { None } }; for y in 0..height { for x in 0..width { if image.get_pixel(x as u32, y as u32).0[0] > threshold { image_values[at(x, y)] = 1; } } } let mut diffs = VecDeque::from(vec![ Point::new(-1, 0), // w Point::new(-1, -1), // nw Point::new(0, -1), // n Point::new(1, -1), // ne Point::new(1, 0), // e Point::new(1, 1), // se Point::new(0, 1), // s Point::new(-1, 1), // sw ]); let mut contours: Vec> = Vec::new(); let mut curr_border_num = 1; for y in 0..height { let mut parent_border_num = 1; for x in 0..width { if image_values[at(x, y)] == 0 { continue; } if let Some((adj, border_type)) = if image_values[at(x, y)] == 1 && x > 0 && image_values[at(x - 1, y)] == 0 { Some((Point::new(x - 1, y), BorderType::Outer)) } else if image_values[at(x, y)] > 0 && x + 1 < width && image_values[at(x + 1, y)] == 0 { if image_values[at(x, y)] > 1 { parent_border_num = image_values[at(x, y)] as usize; } Some((Point::new(x + 1, y), BorderType::Hole)) } else { None } { curr_border_num += 1; let parent = if parent_border_num > 1 { let parent_index = parent_border_num - 2; let parent_contour = &contours[parent_index]; if (border_type == BorderType::Outer) ^ (parent_contour.border_type == BorderType::Outer) { Some(parent_index) } else { parent_contour.parent } } else { None }; let mut contour_points = Vec::new(); let curr = Point::new(x, y); rotate_to_value(&mut diffs, adj.to_i32() - curr.to_i32()); if let Some(pos1) = diffs.iter().find_map(|diff| { get_position_if_non_zero_pixel(&image_values, curr.to_i32() + *diff) }) { let mut pos2 = pos1; let mut pos3 = curr; loop { contour_points .push(Point::new(cast(pos3.x).unwrap(), cast(pos3.y).unwrap())); rotate_to_value(&mut diffs, pos2.to_i32() - pos3.to_i32()); let pos4 = diffs .iter() .rev() // counter-clockwise .find_map(|diff| { get_position_if_non_zero_pixel(&image_values, pos3.to_i32() + *diff) }) .unwrap(); let mut is_right_edge = false; for diff in diffs.iter().rev() { if *diff == (pos4.to_i32() - pos3.to_i32()) { break; } if *diff == Point::new(1, 0) { is_right_edge = true; break; } } if pos3.x + 1 == width || is_right_edge { image_values[at(pos3.x, pos3.y)] = -curr_border_num; } else if image_values[at(pos3.x, pos3.y)] == 1 { image_values[at(pos3.x, pos3.y)] = curr_border_num; } if pos4 == curr && pos3 == pos1 { break; } pos2 = pos3; pos3 = pos4; } } else { contour_points.push(Point::new(cast(x).unwrap(), cast(y).unwrap())); image_values[at(x, y)] = -curr_border_num; } contours.push(Contour::new(contour_points, border_type, parent)); } if image_values[at(x, y)] != 1 { parent_border_num = image_values[at(x, y)].unsigned_abs() as usize; } } } contours } fn rotate_to_value(values: &mut VecDeque, value: T) { let rotate_pos = values.iter().position(|x| *x == value).unwrap(); values.rotate_left(rotate_pos); } #[cfg(test)] mod tests { use super::*; use crate::point::Point; // Checks that a contour has the expected border type and parent, and // that it contains each of a given set of points. fn check_contour( contour: &Contour, expected_border_type: BorderType, expected_parent: Option, required_points: &[Point], ) { for point in required_points { assert!(contour.points.contains(point)); } assert_eq!(contour.border_type, expected_border_type); assert_eq!(contour.parent, expected_parent); } #[cfg_attr(miri, ignore = "slow")] #[test] fn test_contours_structured() { use crate::drawing::draw_polygon_mut; use image::Luma; let white = Luma([255u8]); let black = Luma([0u8]); let mut image = GrayImage::from_pixel(300, 300, black); // border 1 (outer) draw_polygon_mut( &mut image, &[ Point::new(20, 20), Point::new(280, 20), Point::new(280, 280), Point::new(20, 280), ], white, ); // border 2 (hole) draw_polygon_mut( &mut image, &[ Point::new(40, 40), Point::new(260, 40), Point::new(260, 260), Point::new(40, 260), ], black, ); // border 3 (outer) draw_polygon_mut( &mut image, &[ Point::new(60, 60), Point::new(240, 60), Point::new(240, 240), Point::new(60, 240), ], white, ); // border 4 (hole) draw_polygon_mut( &mut image, &[ Point::new(80, 80), Point::new(220, 80), Point::new(220, 220), Point::new(80, 220), ], black, ); // rectangle in the corner (outer) draw_polygon_mut( &mut image, &[ Point::new(290, 290), Point::new(300, 290), Point::new(300, 300), Point::new(290, 300), ], white, ); let contours = find_contours::(&image); assert_eq!(contours.len(), 5); // border 1 check_contour( &contours[0], BorderType::Outer, None, &[ Point::new(20, 20), Point::new(280, 20), Point::new(280, 280), Point::new(20, 280), ], ); // border 2 check_contour( &contours[1], BorderType::Hole, Some(0), &[ Point::new(39, 40), Point::new(261, 40), Point::new(261, 260), Point::new(39, 260), ], ); // border 3 check_contour( &contours[2], BorderType::Outer, Some(1), &[ Point::new(60, 60), Point::new(240, 60), Point::new(240, 240), Point::new(60, 220), ], ); // border 4 check_contour( &contours[3], BorderType::Hole, Some(2), &[ Point::new(79, 80), Point::new(221, 80), Point::new(221, 220), Point::new(79, 220), ], ); // rectangle in the corner check_contour( &contours[4], BorderType::Outer, None, &[ Point::new(290, 290), Point::new(299, 290), Point::new(299, 299), Point::new(290, 299), ], ); } #[test] fn find_contours_basic_test() { use crate::definitions::HasWhite; use crate::drawing::draw_polygon_mut; use image::Luma; let mut image = GrayImage::new(15, 20); draw_polygon_mut( &mut image, &[Point::new(5, 5), Point::new(11, 5)], Luma::white(), ); draw_polygon_mut( &mut image, &[Point::new(11, 5), Point::new(11, 9)], Luma::white(), ); draw_polygon_mut( &mut image, &[Point::new(11, 9), Point::new(5, 9)], Luma::white(), ); draw_polygon_mut( &mut image, &[Point::new(5, 5), Point::new(5, 9)], Luma::white(), ); draw_polygon_mut( &mut image, &[Point::new(8, 5), Point::new(8, 9)], Luma::white(), ); *image.get_pixel_mut(13, 6) = Luma::white(); let contours = find_contours::(&image); assert_eq!(contours.len(), 4); check_contour( &contours[0], BorderType::Outer, None, &[ Point::new(5, 5), Point::new(11, 5), Point::new(5, 9), Point::new(11, 9), ], ); assert!(!contours[0].points.contains(&Point::new(13, 6))); check_contour( &contours[1], BorderType::Hole, Some(0), &[ Point::new(5, 6), Point::new(8, 6), Point::new(6, 9), Point::new(8, 8), ], ); assert!(!contours[1].points.contains(&Point::new(10, 5))); assert!(!contours[1].points.contains(&Point::new(10, 9))); assert!(!contours[1].points.contains(&Point::new(13, 6))); check_contour( &contours[2], BorderType::Hole, Some(0), &[ Point::new(8, 6), Point::new(10, 5), Point::new(8, 8), Point::new(10, 9), ], ); assert!(!contours[2].points.contains(&Point::new(6, 9))); assert!(!contours[2].points.contains(&Point::new(5, 6))); assert!(!contours[2].points.contains(&Point::new(13, 6))); assert_eq!(contours[3].border_type, BorderType::Outer); assert_eq!(contours[3].points, [Point::new(13, 6)]); assert_eq!(contours[3].parent, None); } #[cfg_attr(miri, ignore = "slow")] #[test] fn get_contours_approx_points() { use crate::drawing::draw_polygon_mut; use image::{GrayImage, Luma}; let mut image = GrayImage::from_pixel(300, 300, Luma([0])); let white = Luma([255]); let star = vec![ Point::new(100, 20), Point::new(120, 35), Point::new(140, 30), Point::new(115, 45), Point::new(130, 60), Point::new(100, 50), Point::new(80, 55), Point::new(90, 40), Point::new(60, 25), Point::new(90, 35), ]; draw_polygon_mut(&mut image, &star, white); let contours = find_contours::(&image); let c1_approx = crate::geometry::approximate_polygon_dp( &contours[0].points, crate::geometry::arc_length(&contours[0].points, true) * 0.01, true, ); assert_eq!( c1_approx, vec![ Point::new(100, 20), Point::new(90, 35), Point::new(60, 25), Point::new(90, 40), Point::new(80, 55), Point::new(101, 50), Point::new(130, 60), Point::new(115, 45), Point::new(140, 30), Point::new(120, 35) ] ); } } imageproc-0.25.0/src/contrast.rs000064400000000000000000000527561046102023000146670ustar 00000000000000//! Functions for manipulating the contrast of images. use std::cmp::{max, min}; use image::{GrayImage, ImageBuffer, Luma}; #[cfg(feature = "rayon")] use rayon::prelude::*; use crate::definitions::{HasBlack, HasWhite}; use crate::integral_image::{integral_image, sum_image_pixels}; use crate::map::map_subpixels_mut; use crate::stats::{cumulative_histogram, histogram}; /// Applies an adaptive threshold to an image. /// /// This algorithm compares each pixel's brightness with the average brightness of the pixels /// in the (2 * `block_radius` + 1) square block centered on it. If the pixel is at least as bright /// as the threshold then it will have a value of 255 in the output image, otherwise 0. pub fn adaptive_threshold(image: &GrayImage, block_radius: u32) -> GrayImage { assert!(block_radius > 0); let integral = integral_image::<_, u32>(image); let mut out = ImageBuffer::from_pixel(image.width(), image.height(), Luma::black()); for y in 0..image.height() { for x in 0..image.width() { let current_pixel = image.get_pixel(x, y); // Traverse all neighbors in (2 * block_radius + 1) x (2 * block_radius + 1) let (y_low, y_high) = ( max(0, y as i32 - (block_radius as i32)) as u32, min(image.height() - 1, y + block_radius), ); let (x_low, x_high) = ( max(0, x as i32 - (block_radius as i32)) as u32, min(image.width() - 1, x + block_radius), ); // Number of pixels in the block, adjusted for edge cases. let w = (y_high - y_low + 1) * (x_high - x_low + 1); let mean = sum_image_pixels(&integral, x_low, y_low, x_high, y_high)[0] / w; if current_pixel[0] as u32 >= mean as u32 { out.put_pixel(x, y, Luma::white()); } } } out } /// Returns the [Otsu threshold level] of an 8bpp image. /// /// [Otsu threshold level]: https://en.wikipedia.org/wiki/Otsu%27s_method pub fn otsu_level(image: &GrayImage) -> u8 { let hist = histogram(image); let (width, height) = image.dimensions(); let total_weight = width * height; // Sum of all pixel intensities, to use when calculating means. let total_pixel_sum = hist.channels[0] .iter() .enumerate() .fold(0f64, |sum, (t, h)| sum + (t as u32 * h) as f64); // Sum of all pixel intensities in the background class. let mut background_pixel_sum = 0f64; // The weight of a class (background or foreground) is // the number of pixels which belong to that class at // the current threshold. let mut background_weight = 0u32; let mut foreground_weight; let mut largest_variance = 0f64; let mut best_threshold = 0u8; for (threshold, hist_count) in hist.channels[0].iter().enumerate() { background_weight += hist_count; if background_weight == 0 { continue; }; foreground_weight = total_weight - background_weight; if foreground_weight == 0 { break; }; background_pixel_sum += (threshold as u32 * hist_count) as f64; let foreground_pixel_sum = total_pixel_sum - background_pixel_sum; let background_mean = background_pixel_sum / (background_weight as f64); let foreground_mean = foreground_pixel_sum / (foreground_weight as f64); let mean_diff_squared = (background_mean - foreground_mean).powi(2); let intra_class_variance = (background_weight as f64) * (foreground_weight as f64) * mean_diff_squared; if intra_class_variance > largest_variance { largest_variance = intra_class_variance; best_threshold = threshold as u8; } } best_threshold } /// Options for how to treat the threshold value in [`threshold`] and [`threshold_mut`]. pub enum ThresholdType { /// `dst(x,y) = if src(x,y) > threshold { 255 } else { 0 }` Binary, /// `dst(x,y) = if src(x,y) > threshold { 0 } else { 255 }` BinaryInverted, /// `dst(x,y) = if src(x,y) > threshold { threshold } else { src(x,y) }` Truncate, /// `dst(x,y) = if src(x,y) > threshold { src(x,y) } else { 0 }` ToZero, /// `dst(x,y) = if src(x,y) > threshold { 0 } else { src(x,y) }` ToZeroInverted, } /// Applies a threshold to each pixel in a grayscale image. The action taken depends on `threshold_type` - see [`ThresholdType`]. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::contrast::{threshold, ThresholdType}; /// /// let image = gray_image!( /// 10, 80, 20; /// 50, 90, 70); /// /// // Binary threshold /// let threshold_binary = gray_image!( /// 0, 255, 0; /// 0, 255, 255); /// /// assert_pixels_eq!( /// threshold(&image, 50, ThresholdType::Binary), /// threshold_binary); /// /// // Inverted binary threshold /// let threshold_binary_inverted = gray_image!( /// 255, 0, 255; /// 255, 0, 0); /// /// assert_pixels_eq!( /// threshold(&image, 50, ThresholdType::BinaryInverted), /// threshold_binary_inverted); /// /// // Truncate /// let threshold_truncate = gray_image!( /// 10, 50, 20; /// 50, 50, 50); /// /// assert_pixels_eq!( /// threshold(&image, 50, ThresholdType::Truncate), /// threshold_truncate); /// /// // To zero /// let threshold_to_zero = gray_image!( /// 10, 0, 20; /// 50, 0, 0); /// /// assert_pixels_eq!( /// threshold(&image, 50, ThresholdType::ToZero), /// threshold_to_zero); /// /// // To zero inverted /// let threshold_to_zero_inverted = gray_image!( /// 0, 80, 0; /// 0, 90, 70); /// /// assert_pixels_eq!( /// threshold(&image, 50, ThresholdType::ToZeroInverted), /// threshold_to_zero_inverted); /// # } /// ``` pub fn threshold(image: &GrayImage, threshold: u8, threshold_type: ThresholdType) -> GrayImage { let mut out = image.clone(); threshold_mut(&mut out, threshold, threshold_type); out } /// Applies a threshold to each pixel in a grayscale image. The action taken depends on `threshold_type` - see [`ThresholdType`]. /// /// See [`threshold`] for a list of examples covering each `ThresholdType`. /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::contrast::{threshold_mut, ThresholdType}; /// /// let mut image = gray_image!( /// 10, 80, 20; /// 50, 90, 70); /// /// threshold_mut(&mut image, 50, ThresholdType::Binary); /// /// let result = gray_image!( /// 0, 255, 0; /// 0, 255, 255); /// /// assert_pixels_eq!(image, result); /// # } /// ``` pub fn threshold_mut(image: &mut GrayImage, threshold: u8, threshold_type: ThresholdType) { match threshold_type { ThresholdType::Binary => { for p in image.iter_mut() { *p = if *p > threshold { 255 } else { 0 }; } } ThresholdType::BinaryInverted => { for p in image.iter_mut() { *p = if *p > threshold { 0 } else { 255 }; } } ThresholdType::Truncate => { for p in image.iter_mut() { *p = if *p > threshold { threshold } else { *p }; } } ThresholdType::ToZero => { for p in image.iter_mut() { *p = if *p > threshold { 0 } else { *p }; } } ThresholdType::ToZeroInverted => { for p in image.iter_mut() { *p = if *p > threshold { *p } else { 0 }; } } } } /// Equalises the histogram of an 8bpp grayscale image in place. See also /// [histogram equalization (wikipedia)](https://en.wikipedia.org/wiki/Histogram_equalization). pub fn equalize_histogram_mut(image: &mut GrayImage) { let hist = cumulative_histogram(image).channels[0]; let total = hist[255] as f32; #[cfg(feature = "rayon")] let iter = image.par_iter_mut(); #[cfg(not(feature = "rayon"))] let iter = image.iter_mut(); iter.for_each(|p| { // JUSTIFICATION // Benefit // Using checked indexing here makes this function take 1.1x longer, as measured // by bench_equalize_histogram_mut // Correctness // Each channel of CumulativeChannelHistogram has length 256, and a GrayImage has 8 bits per pixel let fraction = unsafe { *hist.get_unchecked(*p as usize) as f32 / total }; *p = (f32::min(255f32, 255f32 * fraction)) as u8; }); } /// Equalises the histogram of an 8bpp grayscale image. See also /// [histogram equalization (wikipedia)](https://en.wikipedia.org/wiki/Histogram_equalization). pub fn equalize_histogram(image: &GrayImage) -> GrayImage { let mut out = image.clone(); equalize_histogram_mut(&mut out); out } /// Stretches the contrast in an image, linearly mapping intensities in `(input_lower, input_upper)` to `(output_lower, output_upper)` and saturating /// values outside this input range. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::contrast::stretch_contrast; /// /// let image = gray_image!( /// 0, 20, 50; /// 80, 100, 255); /// /// let lower = 20; /// let upper = 100; /// /// // Pixel intensities between 20 and 100 are linearly /// // scaled so that 20 is mapped to 0 and 100 is mapped to 255. /// // Pixel intensities less than 20 are sent to 0 and pixel /// // intensities greater than 100 are sent to 255. /// let stretched = stretch_contrast(&image, lower, upper, 0u8, 255u8); /// /// let expected = gray_image!( /// 0, 0, 95; /// 191, 255, 255); /// /// assert_pixels_eq!(stretched, expected); /// # } /// ``` /// /// # Panics /// If `input_lower >= input_upper` or `output_lower > output_upper`. pub fn stretch_contrast( image: &GrayImage, input_lower: u8, input_upper: u8, output_lower: u8, output_upper: u8, ) -> GrayImage { let mut out = image.clone(); stretch_contrast_mut( &mut out, input_lower, input_upper, output_lower, output_upper, ); out } /// Stretches the contrast in an image, linearly mapping intensities in `(input_lower, input_upper)` to `(output_lower, output_upper)` and saturating /// values outside this input range. /// /// See the [`stretch_contrast`](fn.stretch_contrast.html) documentation for more. /// /// # Panics /// If `input_lower >= input_upper` or `output_lower > output_upper`. pub fn stretch_contrast_mut( image: &mut GrayImage, input_min: u8, input_max: u8, output_min: u8, output_max: u8, ) { assert!( input_min < input_max, "input_min must be smaller than input_max" ); assert!( output_min <= output_max, "output_min must be smaller or equal to output_max" ); let input_min: u16 = input_min.into(); let input_max: u16 = input_max.into(); let output_min: u16 = output_min.into(); let output_max: u16 = output_max.into(); let input_width = input_max - input_min; let output_width = output_max - output_min; let f = |p: u8| { let p: u16 = p.into(); let output = if p <= input_min { output_min } else if p >= input_max { output_max } else { (((p - input_min) * output_width) / input_width) + output_min }; output as u8 }; map_subpixels_mut(image, f); } /// Adjusts contrast of an 8bpp grayscale image in place so that its /// histogram is as close as possible to that of the target image. pub fn match_histogram_mut(image: &mut GrayImage, target: &GrayImage) { let image_histc = cumulative_histogram(image).channels[0]; let target_histc = cumulative_histogram(target).channels[0]; let lut = histogram_lut(&image_histc, &target_histc); for p in image.iter_mut() { *p = lut[*p as usize] as u8; } } /// Adjusts contrast of an 8bpp grayscale image so that its /// histogram is as close as possible to that of the target image. pub fn match_histogram(image: &GrayImage, target: &GrayImage) -> GrayImage { let mut out = image.clone(); match_histogram_mut(&mut out, target); out } /// `l = histogram_lut(s, t)` is chosen so that `target_histc[l[i]] / sum(target_histc)` /// is as close as possible to `source_histc[i] / sum(source_histc)`. fn histogram_lut(source_histc: &[u32; 256], target_histc: &[u32; 256]) -> [usize; 256] { let source_total = source_histc[255] as f32; let target_total = target_histc[255] as f32; let mut lut = [0usize; 256]; let mut y = 0usize; let mut prev_target_fraction = 0f32; for s in 0..256 { let source_fraction = source_histc[s] as f32 / source_total; let mut target_fraction = target_histc[y] as f32 / target_total; while source_fraction > target_fraction && y < 255 { y += 1; prev_target_fraction = target_fraction; target_fraction = target_histc[y] as f32 / target_total; } if y == 0 { lut[s] = y; } else { let prev_dist = f32::abs(prev_target_fraction - source_fraction); let dist = f32::abs(target_fraction - source_fraction); if prev_dist < dist { lut[s] = y - 1; } else { lut[s] = y; } } } lut } #[cfg(test)] mod tests { use super::*; use crate::definitions::{HasBlack, HasWhite}; use image::{GrayImage, Luma}; #[test] fn adaptive_threshold_constant() { let image = GrayImage::from_pixel(3, 3, Luma([100u8])); let binary = adaptive_threshold(&image, 1); let expected = GrayImage::from_pixel(3, 3, Luma::white()); assert_pixels_eq!(expected, binary); } #[test] fn adaptive_threshold_one_darker_pixel() { for y in 0..3 { for x in 0..3 { let mut image = GrayImage::from_pixel(3, 3, Luma([200u8])); image.put_pixel(x, y, Luma([100u8])); let binary = adaptive_threshold(&image, 1); // All except the dark pixel have brightness >= their local mean let mut expected = GrayImage::from_pixel(3, 3, Luma::white()); expected.put_pixel(x, y, Luma::black()); assert_pixels_eq!(binary, expected); } } } #[test] fn adaptive_threshold_one_lighter_pixel() { for y in 0..5 { for x in 0..5 { let mut image = GrayImage::from_pixel(5, 5, Luma([100u8])); image.put_pixel(x, y, Luma([200u8])); let binary = adaptive_threshold(&image, 1); for yb in 0..5 { for xb in 0..5 { let output_intensity = binary.get_pixel(xb, yb)[0]; let is_light_pixel = xb == x && yb == y; let local_mean_includes_light_pixel = (yb as i32 - y as i32).abs() <= 1 && (xb as i32 - x as i32).abs() <= 1; if is_light_pixel { assert_eq!(output_intensity, 255); } else if local_mean_includes_light_pixel { assert_eq!(output_intensity, 0); } else { assert_eq!(output_intensity, 255); } } } } } } #[test] fn test_histogram_lut_source_and_target_equal() { let mut histc = [0u32; 256]; for i in 1..histc.len() { histc[i] = 2 * i as u32; } let lut = histogram_lut(&histc, &histc); let expected = (0..256).collect::>(); assert_eq!(&lut[0..256], &expected[0..256]); } #[test] fn test_histogram_lut_gradient_to_step_contrast() { let mut grad_histc = [0u32; 256]; for i in 0..grad_histc.len() { grad_histc[i] = i as u32; } let mut step_histc = [0u32; 256]; for i in 30..130 { step_histc[i] = 100; } for i in 130..256 { step_histc[i] = 200; } let lut = histogram_lut(&grad_histc, &step_histc); let mut expected = [0usize; 256]; // No black pixels in either image expected[0] = 0; for i in 1..64 { expected[i] = 29; } for i in 64..128 { expected[i] = 30; } for i in 128..192 { expected[i] = 129; } for i in 192..256 { expected[i] = 130; } assert_eq!(&lut[0..256], &expected[0..256]); } fn constant_image(width: u32, height: u32, intensity: u8) -> GrayImage { GrayImage::from_pixel(width, height, Luma([intensity])) } #[test] fn test_otsu_constant() { // Variance is 0 at any threshold, and we // only increase the current threshold if we // see a strictly greater variance assert_eq!(otsu_level(&constant_image(10, 10, 0)), 0); assert_eq!(otsu_level(&constant_image(10, 10, 128)), 0); assert_eq!(otsu_level(&constant_image(10, 10, 255)), 0); } #[test] fn test_otsu_level_gradient() { let contents = (0u8..26u8).map(|x| x * 10u8).collect(); let image = GrayImage::from_raw(26, 1, contents).unwrap(); let level = otsu_level(&image); assert_eq!(level, 120); } #[test] fn test_threshold_0_image_0() { let expected = 0u8; let actual = threshold(&constant_image(10, 10, 0), 0, ThresholdType::Binary); assert_pixels_eq!(actual, constant_image(10, 10, expected)); } #[test] fn test_threshold_0_image_1() { let expected = 255u8; let actual = threshold(&constant_image(10, 10, 1), 0, ThresholdType::Binary); assert_pixels_eq!(actual, constant_image(10, 10, expected)); } #[test] fn test_threshold_threshold_255_image_255() { let expected = 0u8; let actual = threshold(&constant_image(10, 10, 255), 255, ThresholdType::Binary); assert_pixels_eq!(actual, constant_image(10, 10, expected)); } #[test] fn test_threshold() { let original_contents = (0u8..26u8).map(|x| x * 10u8).collect(); let original = GrayImage::from_raw(26, 1, original_contents).unwrap(); let expected_contents = vec![0u8; 13].into_iter().chain(vec![255u8; 13]).collect(); let expected = GrayImage::from_raw(26, 1, expected_contents).unwrap(); let actual = threshold(&original, 125u8, ThresholdType::Binary); assert_pixels_eq!(expected, actual); } #[test] fn test_stretch_contrast() { let input = gray_image!(1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 255); let expected = gray_image!(10u8, 10, 10, 11, 11, 12, 12, 13, 13, 13, 52, 120); assert_pixels_eq!(stretch_contrast(&input, 1, 255, 10, 120), expected); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::utils::gray_bench_image; use image::{GrayImage, Luma}; use test::{black_box, Bencher}; #[bench] fn bench_adaptive_threshold(b: &mut Bencher) { let image = gray_bench_image(200, 200); let block_radius = 10; b.iter(|| { let thresholded = adaptive_threshold(&image, block_radius); black_box(thresholded); }); } #[bench] fn bench_match_histogram(b: &mut Bencher) { let target = GrayImage::from_pixel(200, 200, Luma([150])); let image = gray_bench_image(200, 200); b.iter(|| { let matched = match_histogram(&image, &target); black_box(matched); }); } #[bench] fn bench_match_histogram_mut(b: &mut Bencher) { let target = GrayImage::from_pixel(200, 200, Luma([150])); let mut image = gray_bench_image(200, 200); b.iter(|| { match_histogram_mut(&mut image, &target); }); } #[bench] fn bench_equalize_histogram(b: &mut Bencher) { let image = gray_bench_image(500, 500); b.iter(|| { let equalized = equalize_histogram(&image); black_box(equalized); }); } #[bench] fn bench_equalize_histogram_mut(b: &mut Bencher) { let mut image = gray_bench_image(500, 500); b.iter(|| { equalize_histogram_mut(&mut image); black_box(()); }); } #[bench] fn bench_threshold(b: &mut Bencher) { let image = gray_bench_image(500, 500); b.iter(|| { let thresholded = threshold(&image, 125, ThresholdType::Binary); black_box(thresholded); }); } #[bench] fn bench_threshold_mut(b: &mut Bencher) { let mut image = gray_bench_image(500, 500); b.iter(|| { threshold_mut(&mut image, 125, ThresholdType::Binary); black_box(()); }); } #[bench] fn bench_otsu_level(b: &mut Bencher) { let image = gray_bench_image(200, 200); b.iter(|| { let level = otsu_level(&image); black_box(level); }); } #[bench] fn bench_stretch_contrast(b: &mut Bencher) { let image = gray_bench_image(200, 200); b.iter(|| { let scaled = stretch_contrast(&image, 0, 255, 0, 255); black_box(scaled); }); } #[bench] fn bench_stretch_contrast_mut(b: &mut Bencher) { let mut image = gray_bench_image(200, 200); b.iter(|| { stretch_contrast_mut(&mut image, 0, 255, 0, 255); black_box(()); }); } } imageproc-0.25.0/src/corners.rs000064400000000000000000000601331046102023000144710ustar 00000000000000//! Functions for detecting corners, also known as interest points. use crate::{ definitions::{Position, Score}, point::Point, }; use image::{GenericImageView, GrayImage}; use rand::{rngs::StdRng, SeedableRng}; use rand_distr::Distribution; /// A location and score for a detected corner. /// The scores need not be comparable between different /// corner detectors. #[derive(Copy, Clone, Debug, PartialEq)] pub struct Corner { /// x-coordinate of the corner. pub x: u32, /// y-coordinate of the corner. pub y: u32, /// Score of the detected corner. pub score: f32, } impl Corner { /// A corner at location (x, y) with score `score`. pub fn new(x: u32, y: u32, score: f32) -> Corner { Corner { x, y, score } } } impl Position for Corner { /// x-coordinate of the corner. fn x(&self) -> u32 { self.x } /// y-coordinate of the corner. fn y(&self) -> u32 { self.y } } impl From for Point { fn from(value: Corner) -> Self { Point::new(value.x, value.y) } } impl Score for Corner { fn score(&self) -> f32 { self.score } } /// Variants of the [FAST](https://en.wikipedia.org/wiki/Features_from_accelerated_segment_test) /// corner detector. These classify a point based on its intensity relative to the 16 pixels /// in the Bresenham circle of radius 3 around it. A point P with intensity I is detected as a /// corner if all pixels in a sufficiently long contiguous section of this circle either /// all have intensity greater than I + t or all have intensity less than /// I - t, for some user-provided threshold t. The score of a corner is /// the greatest threshold for which the given pixel still qualifies as /// a corner. pub enum Fast { /// Corners require a section of length as least nine. Nine, /// Corners require a section of length as least twelve. Twelve, } /// Finds corners using FAST-12 features. See comment on `Fast`. pub fn corners_fast12(image: &GrayImage, threshold: u8) -> Vec { let (width, height) = image.dimensions(); let mut corners = vec![]; for y in 0..height { for x in 0..width { if is_corner_fast12(image, threshold, x, y) { let score = fast_corner_score(image, threshold, x, y, Fast::Twelve); corners.push(Corner::new(x, y, score as f32)); } } } corners } /// Finds corners using FAST-9 features. See comment on Fast enum. pub fn corners_fast9(image: &GrayImage, threshold: u8) -> Vec { let (width, height) = image.dimensions(); let mut corners = vec![]; for y in 0..height { for x in 0..width { if is_corner_fast9(image, threshold, x, y) { let score = fast_corner_score(image, threshold, x, y, Fast::Nine); corners.push(Corner::new(x, y, score as f32)); } } } corners } /// A FAST corner with associated orientation as described in [Rublee, et. al. /// (2012)][rublee]. /// /// [rublee]: http://www.gwylab.com/download/ORB_2012.pdf #[derive(Clone, Copy, PartialEq)] pub struct OrientedFastCorner { /// Location and FAST corner score of this corner in its associated image. pub corner: Corner, /// Orientation of this FAST corner as determined by computing the intensity /// centroid of the local patch around the corner. pub orientation: f32, } fn intensity_centroid(image: &GrayImage, x: u32, y: u32, radius: u32) -> f32 { let mut y_centroid: i32 = 0; let mut x_centroid: i32 = 0; let (width, height) = image.dimensions(); let x_min = if x < radius { 0 } else { x - radius }; let y_min = if y < radius { 0 } else { y - radius }; let y_max = u32::min(y + radius + 1, height); let x_max = u32::min(x + radius + 1, width); let (mut x_count, mut y_count) = (-(radius as i32), (radius as i32)); for y in y_min..y_max { for x in x_min..x_max { // UNSAFETY JUSTIFICATION // // Benefit // // Removing all unsafe pixel accesses in this function increases the // average runtime for bench_intensity_centroid by about 90%. // // Correctness // // x will always be greater than or equal to x_min and strictly less // than x_max due to the range in this for loop. x_min will never be // less than zero, and x_max will never be greater than the image // width, both due to the checks earlier in this function. The same // logic applies to y, y_min, and y_max. let pixel = unsafe { image.unsafe_get_pixel(x, y).0[0] }; x_centroid += x_count * (pixel as i32); x_count += 1; } x_count = -(radius as i32); } for x in x_min..x_max { for y in y_min..y_max { // See UNSAFETY JUSTIFICATION above. let pixel = unsafe { image.unsafe_get_pixel(x, y).0[0] }; y_centroid += y_count * (pixel as i32); y_count -= 1; } y_count = radius as i32; } // Important note: we flip the sign here because there are two coordinate // systems in play. One is pixel space with the origin in the top left, and // the other is ordinary Cartesian space with the origin in the bottom left. // To make the math in later rotation code match the usual convention, we // hide the coordinate conversion here. -(y_centroid as f32).atan2(x_centroid as f32) } /// Finds oriented FAST-9 corners as presented in [Rublee et. al. (2012)][rublee]. /// /// [rublee]: http://www.gwylab.com/download/ORB_2012.pdf pub fn oriented_fast( image: &GrayImage, threshold: Option, target_num_corners: usize, edge_radius: u32, seed: Option, ) -> Vec { let (width, height) = image.dimensions(); let (min_x, max_x) = (edge_radius, width - edge_radius); let (min_y, max_y) = (edge_radius, height - edge_radius); let mut corners = vec![]; let local_threshold = if let Some(t) = threshold { t } else { // Take a sample of random pixels, compute their FAST scores, and set the // threshold for the full image accordingly. const NUM_SAMPLE_POINTS: usize = 1000; let mut rng = if let Some(s) = seed { StdRng::seed_from_u64(s) } else { StdRng::from_entropy() }; let dist_x = rand::distributions::Uniform::new(min_x, max_x); let dist_y = rand::distributions::Uniform::new(min_y, max_y); let sample_size = NUM_SAMPLE_POINTS.min((width * height) as usize); let sample_coords: Vec> = (0..sample_size) .map(|_| Point::new(dist_x.sample(&mut rng), dist_y.sample(&mut rng))) .collect(); let mut fast_scores: Vec = sample_coords .iter() .map(|c| fast_corner_score(image, 0, c.x, c.y, Fast::Nine)) .collect(); fast_scores.sort(); let target_corner_fraction = (target_num_corners as f32) / ((width * height) as f32); let fraction_idx = (NUM_SAMPLE_POINTS as f32 * (1. - target_corner_fraction)) as usize; fast_scores[fraction_idx] }; // Iterate over every pixel in the image and find potential corners. for y in edge_radius..height - edge_radius { for x in edge_radius..width - edge_radius { if is_corner_fast9(image, local_threshold, x, y) { let score = fast_corner_score(image, local_threshold, x, y, Fast::Nine); corners.push(Corner::new(x, y, score as f32)); } } } // Sort descending by Harris corner measure. corners.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap()); // Keep the top corners and discard the rest. let top_corners = if corners.len() < target_num_corners { &corners } else { &corners[..target_num_corners] }; // Compute intensity centroids and return oriented FAST corners. top_corners .iter() .map(|c| OrientedFastCorner { corner: *c, orientation: intensity_centroid(image, c.x, c.y, 15), }) .collect() } /// The score of a corner detected using the FAST /// detector is the largest threshold for which this /// pixel is still a corner. We input the threshold at which /// the corner was detected as a lower bound on the search. /// Note that the corner check uses a strict inequality, so if /// the smallest intensity difference between the center pixel /// and a corner pixel is n then the corner will have a score of n - 1. pub fn fast_corner_score(image: &GrayImage, threshold: u8, x: u32, y: u32, variant: Fast) -> u8 { let mut max = 255u8; let mut min = threshold; loop { if max == min { return max; } let mean = ((max as u16 + min as u16) / 2u16) as u8; let probe = if max == min + 1 { max } else { mean }; let is_corner = match variant { Fast::Nine => is_corner_fast9(image, probe, x, y), Fast::Twelve => is_corner_fast12(image, probe, x, y), }; if is_corner { min = probe; } else { max = probe - 1; } } } // Note [FAST circle labels] // // 15 00 01 // 14 02 // 13 03 // 12 p 04 // 11 05 // 10 06 // 09 08 07 /// Checks if the given pixel is a corner according to the FAST9 detector. /// The current implementation is extremely inefficient. // TODO: Make this much faster! fn is_corner_fast9(image: &GrayImage, threshold: u8, x: u32, y: u32) -> bool { // UNSAFETY JUSTIFICATION // Benefit // Removing all unsafe pixel accesses in this file makes // bench_is_corner_fast9_9_contiguous_lighter_pixels 60% slower, and // bench_is_corner_fast12_12_noncontiguous 40% slower // Correctness // All pixel accesses in this function, and in the called get_circle, // access pixels with x-coordinate in the range [x - 3, x + 3] and // y-coordinate in the range [y - 3, y + 3]. The precondition below // guarantees that these are within image bounds. let (width, height) = image.dimensions(); if x >= u32::MAX - 3 || y >= u32::MAX - 3 || x < 3 || y < 3 || width <= x + 3 || height <= y + 3 { return false; } // JUSTIFICATION - see comment at the start of this function let c = unsafe { image.unsafe_get_pixel(x, y)[0] }; let low_thresh: i16 = c as i16 - threshold as i16; let high_thresh: i16 = c as i16 + threshold as i16; // See Note [FAST circle labels] // JUSTIFICATION - see comment at the start of this function let (p0, p4, p8, p12) = unsafe { ( image.unsafe_get_pixel(x, y - 3)[0] as i16, image.unsafe_get_pixel(x, y + 3)[0] as i16, image.unsafe_get_pixel(x + 3, y)[0] as i16, image.unsafe_get_pixel(x - 3, y)[0] as i16, ) }; let above = (p0 > high_thresh && p4 > high_thresh) || (p4 > high_thresh && p8 > high_thresh) || (p8 > high_thresh && p12 > high_thresh) || (p12 > high_thresh && p0 > high_thresh); let below = (p0 < low_thresh && p4 < low_thresh) || (p4 < low_thresh && p8 < low_thresh) || (p8 < low_thresh && p12 < low_thresh) || (p12 < low_thresh && p0 < low_thresh); if !above && !below { return false; } // JUSTIFICATION - see comment at the start of this function let pixels = unsafe { get_circle(image, x, y, p0, p4, p8, p12) }; // above and below could both be true (above && has_bright_span(&pixels, 9, high_thresh)) || (below && has_dark_span(&pixels, 9, low_thresh)) } /// Checks if the given pixel is a corner according to the FAST12 detector. fn is_corner_fast12(image: &GrayImage, threshold: u8, x: u32, y: u32) -> bool { // UNSAFETY JUSTIFICATION // Benefit // Removing all unsafe pixel accesses in this file makes // bench_is_corner_fast9_9_contiguous_lighter_pixels 60% slower, and // bench_is_corner_fast12_12_noncontiguous 40% slower // Correctness // All pixel accesses in this function, and in the called get_circle, // access pixels with x-coordinate in the range [x - 3, x + 3] and // y-coordinate in the range [y - 3, y + 3]. The precondition below // guarantees that these are within image bounds. let (width, height) = image.dimensions(); if x >= u32::MAX - 3 || y >= u32::MAX - 3 || x < 3 || y < 3 || width <= x + 3 || height <= y + 3 { return false; } // JUSTIFICATION - see comment at the start of this function let c = unsafe { image.unsafe_get_pixel(x, y)[0] }; let low_thresh: i16 = c as i16 - threshold as i16; let high_thresh: i16 = c as i16 + threshold as i16; // See Note [FAST circle labels] // JUSTIFICATION - see comment at the start of this function let (p0, p8) = unsafe { ( image.unsafe_get_pixel(x, y - 3)[0] as i16, image.unsafe_get_pixel(x, y + 3)[0] as i16, ) }; let mut above = p0 > high_thresh && p8 > high_thresh; let mut below = p0 < low_thresh && p8 < low_thresh; if !above && !below { return false; } // JUSTIFICATION - see comment at the start of this function let (p4, p12) = unsafe { ( image.unsafe_get_pixel(x + 3, y)[0] as i16, image.unsafe_get_pixel(x - 3, y)[0] as i16, ) }; above = above && ((p4 > high_thresh) || (p12 > high_thresh)); below = below && ((p4 < low_thresh) || (p12 < low_thresh)); if !above && !below { return false; } // TODO: Generate a list of pixel offsets once per image, // TODO: and use those offsets directly when reading pixels. // TODO: This is a little tricky as we can't always do it - we'd // TODO: need to distinguish between GenericImages and ImageBuffers. // TODO: We can also reduce the number of checks we do below. // JUSTIFICATION - see comment at the start of this function let pixels = unsafe { get_circle(image, x, y, p0, p4, p8, p12) }; // Exactly one of above or below is true if above { has_bright_span(&pixels, 12, high_thresh) } else { has_dark_span(&pixels, 12, low_thresh) } } /// # Safety /// /// The caller must ensure that: /// /// x + 3 < image.width() && /// x >= 3 && /// y + 3 < image.height() && /// y >= 3 /// #[inline] unsafe fn get_circle( image: &GrayImage, x: u32, y: u32, p0: i16, p4: i16, p8: i16, p12: i16, ) -> [i16; 16] { [ p0, image.unsafe_get_pixel(x + 1, y - 3)[0] as i16, image.unsafe_get_pixel(x + 2, y - 2)[0] as i16, image.unsafe_get_pixel(x + 3, y - 1)[0] as i16, p4, image.unsafe_get_pixel(x + 3, y + 1)[0] as i16, image.unsafe_get_pixel(x + 2, y + 2)[0] as i16, image.unsafe_get_pixel(x + 1, y + 3)[0] as i16, p8, image.unsafe_get_pixel(x - 1, y + 3)[0] as i16, image.unsafe_get_pixel(x - 2, y + 2)[0] as i16, image.unsafe_get_pixel(x - 3, y + 1)[0] as i16, p12, image.unsafe_get_pixel(x - 3, y - 1)[0] as i16, image.unsafe_get_pixel(x - 2, y - 2)[0] as i16, image.unsafe_get_pixel(x - 1, y - 3)[0] as i16, ] } /// True if the circle has a contiguous section of at least the given length, all /// of whose pixels have intensities strictly greater than the threshold. fn has_bright_span(circle: &[i16; 16], length: u8, threshold: i16) -> bool { search_span(circle, length, |c| *c > threshold) } /// True if the circle has a contiguous section of at least the given length, all /// of whose pixels have intensities strictly less than the threshold. fn has_dark_span(circle: &[i16; 16], length: u8, threshold: i16) -> bool { search_span(circle, length, |c| *c < threshold) } /// True if the circle has a contiguous section of at least the given length, all /// of whose pixels match f condition. fn search_span(circle: &[i16; 16], length: u8, f: F) -> bool where F: Fn(&i16) -> bool, { if length > 16 { return false; } let mut nb_ok = 0u8; let mut nb_ok_start = None; for c in circle.iter() { if f(c) { nb_ok += 1; if nb_ok == length { return true; } } else { if nb_ok_start.is_none() { nb_ok_start = Some(nb_ok); } nb_ok = 0; } } nb_ok + nb_ok_start.unwrap() >= length } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_corner_fast12_12_contiguous_darker_pixels() { let image = gray_image!( 10, 10, 00, 00, 00, 10, 10; 10, 00, 10, 10, 10, 00, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 10, 00, 10, 10, 10, 10, 10; 10, 10, 00, 00, 00, 10, 10); assert!(is_corner_fast12(&image, 8, 3, 3)); } #[test] fn test_is_corner_fast12_12_contiguous_darker_pixels_large_threshold() { let image = gray_image!( 10, 10, 00, 00, 00, 10, 10; 10, 00, 10, 10, 10, 00, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 10, 00, 10, 10, 10, 10, 10; 10, 10, 00, 00, 00, 10, 10); assert!(!is_corner_fast12(&image, 15, 3, 3)); } #[test] fn test_is_corner_fast12_12_contiguous_lighter_pixels() { let image = gray_image!( 00, 00, 10, 10, 10, 00, 00; 00, 10, 00, 00, 00, 10, 00; 10, 00, 00, 00, 00, 00, 00; 10, 00, 00, 00, 00, 00, 00; 10, 00, 00, 00, 00, 00, 00; 00, 10, 00, 00, 00, 00, 00; 00, 00, 10, 10, 10, 00, 00); assert!(is_corner_fast12(&image, 8, 3, 3)); } #[test] fn test_is_corner_fast12_12_noncontiguous() { let image = gray_image!( 10, 10, 00, 00, 00, 10, 10; 10, 00, 10, 10, 10, 00, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 10, 10, 10, 10, 10, 10, 00; 10, 00, 10, 10, 10, 10, 10; 10, 10, 00, 00, 00, 10, 10); assert!(!is_corner_fast12(&image, 8, 3, 3)); } #[test] fn test_is_corner_fast12_near_image_boundary() { let image = gray_image!( 10, 10, 00, 00, 00, 10, 10; 10, 00, 10, 10, 10, 00, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 10, 00, 10, 10, 10, 10, 10; 10, 10, 00, 00, 00, 10, 10); assert!(!is_corner_fast12(&image, 8, 1, 1)); } #[test] fn test_fast_corner_score_12() { let image = gray_image!( 10, 10, 00, 00, 00, 10, 10; 10, 00, 10, 10, 10, 00, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 10, 00, 10, 10, 10, 10, 10; 10, 10, 00, 00, 00, 10, 10); let score = fast_corner_score(&image, 5, 3, 3, Fast::Twelve); assert_eq!(score, 9); let score = fast_corner_score(&image, 9, 3, 3, Fast::Twelve); assert_eq!(score, 9); } #[test] fn test_is_corner_fast9_9_contiguous_darker_pixels() { let image = gray_image!( 10, 10, 00, 00, 00, 10, 10; 10, 00, 10, 10, 10, 00, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 10, 00, 10, 10, 10, 10, 10; 10, 10, 10, 10, 10, 10, 10); assert!(is_corner_fast9(&image, 8, 3, 3)); } #[test] fn test_is_corner_fast9_9_contiguous_lighter_pixels() { let image = gray_image!( 00, 00, 10, 10, 10, 00, 00; 00, 10, 00, 00, 00, 10, 00; 10, 00, 00, 00, 00, 00, 00; 10, 00, 00, 00, 00, 00, 00; 10, 00, 00, 00, 00, 00, 00; 00, 10, 00, 00, 00, 00, 00; 00, 00, 00, 00, 00, 00, 00); assert!(is_corner_fast9(&image, 8, 3, 3)); } #[test] fn test_intensity_centroid() { let image = gray_image!( 00, 00, 10, 10, 10, 00, 00; 00, 10, 00, 00, 00, 10, 00; 10, 00, 00, 00, 00, 00, 10; 10, 00, 00, 00, 00, 00, 10; 00, 00, 00, 00, 00, 00, 10; 00, 00, 00, 00, 00, 10, 00; 00, 00, 00, 10, 10, 00, 00); assert_eq!( intensity_centroid(&image, 3, 3, 3), -std::f32::consts::FRAC_PI_4 ); } #[test] fn test_is_corner_fast9_12_noncontiguous() { let image = gray_image!( 10, 10, 00, 00, 00, 10, 10; 10, 00, 10, 10, 10, 00, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 10, 10, 10, 10, 10, 10, 00; 10, 00, 10, 10, 10, 10, 10; 10, 10, 00, 00, 00, 10, 10); assert!(!is_corner_fast9(&image, 8, 3, 3)); } #[test] fn test_corner_score_fast9() { // 8 pixels with an intensity diff of 20, then 1 with a diff of 10 let image = gray_image!( 10, 10, 00, 00, 00, 10, 10; 10, 00, 10, 10, 10, 00, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 20, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 10, 10, 10, 10, 10, 10, 10; 10, 10, 10, 10, 10, 10, 10); let score = fast_corner_score(&image, 5, 3, 3, Fast::Nine); assert_eq!(score, 9); let score = fast_corner_score(&image, 9, 3, 3, Fast::Nine); assert_eq!(score, 9); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use test::{black_box, Bencher}; #[bench] fn bench_is_corner_fast12_12_noncontiguous(b: &mut Bencher) { let image = black_box(gray_image!( 10, 10, 00, 00, 00, 10, 10; 10, 00, 10, 10, 10, 00, 10; 00, 10, 10, 10, 10, 10, 10; 00, 10, 10, 10, 10, 10, 10; 10, 10, 10, 10, 10, 10, 00; 10, 00, 10, 10, 10, 10, 10; 10, 10, 00, 00, 00, 10, 10)); b.iter(|| black_box(is_corner_fast12(&image, 8, 3, 3))); } #[bench] fn bench_intensity_centroid(b: &mut Bencher) { let image = gray_image!( 00, 00, 10, 10, 10, 00, 00; 00, 10, 00, 00, 00, 10, 00; 10, 00, 00, 00, 00, 00, 10; 10, 00, 00, 00, 00, 00, 10; 00, 00, 00, 00, 00, 00, 10; 00, 00, 00, 00, 00, 10, 00; 00, 00, 00, 10, 10, 00, 00); b.iter(|| black_box(intensity_centroid(&image, 3, 3, 3))); } #[bench] fn bench_oriented_fast_corner(b: &mut Bencher) { let image = gray_image!( 00, 00, 10, 10, 10, 00, 00; 00, 10, 00, 00, 00, 10, 00; 10, 00, 00, 00, 00, 00, 10; 10, 00, 00, 00, 00, 00, 10; 00, 00, 00, 00, 00, 00, 10; 00, 00, 00, 00, 00, 10, 00; 00, 00, 00, 10, 10, 00, 00); b.iter(|| black_box(oriented_fast(&image, Some(0), 1, 0, Some(0xc0)))); } #[bench] fn bench_oriented_fast_non_corner(b: &mut Bencher) { let image = gray_image!( 00, 00, 00, 00, 00, 00, 00; 00, 00, 00, 00, 00, 00, 00; 00, 00, 00, 00, 00, 00, 00; 00, 00, 00, 00, 00, 00, 00; 00, 00, 00, 00, 00, 00, 00; 00, 00, 00, 00, 00, 00, 00; 00, 00, 00, 00, 00, 00, 00); b.iter(|| black_box(oriented_fast(&image, Some(255), 0, 0, Some(0xc0)))); } #[bench] fn bench_is_corner_fast9_9_contiguous_lighter_pixels(b: &mut Bencher) { let image = black_box(gray_image!( 00, 00, 10, 10, 10, 00, 00; 00, 10, 00, 00, 00, 10, 00; 10, 00, 00, 00, 00, 00, 00; 10, 00, 00, 00, 00, 00, 00; 10, 00, 00, 00, 00, 00, 00; 00, 10, 00, 00, 00, 00, 00; 00, 00, 00, 00, 00, 00, 00)); b.iter(|| black_box(is_corner_fast9(&image, 8, 3, 3))); } } imageproc-0.25.0/src/definitions.rs000064400000000000000000000115711046102023000153330ustar 00000000000000//! Trait definitions and type aliases. use image::{ImageBuffer, Luma, LumaA, Pixel, Rgb, Rgba}; /// An `ImageBuffer` containing Pixels of type P with storage `Vec`. /// Most operations in this library only support inputs of type `Image`, rather /// than arbitrary `image::GenericImage`s. This is obviously less flexible, but /// has the advantage of allowing many functions to be more performant. We may want /// to add more flexibility later, but this should not be at the expense of performance. /// When specialisation lands we should be able to do this by defining traits for images /// with contiguous storage. pub type Image

= ImageBuffer::Subpixel>>; /// Pixels which have a named Black value. pub trait HasBlack { /// Returns a black pixel of this type. fn black() -> Self; } /// Pixels which have a named White value. pub trait HasWhite { /// Returns a white pixel of this type. fn white() -> Self; } macro_rules! impl_black_white { ($for_:ty, $min:expr, $max:expr) => { impl HasBlack for $for_ { fn black() -> Self { $min } } impl HasWhite for $for_ { fn white() -> Self { $max } } }; } impl_black_white!(Luma, Luma([u8::MIN]), Luma([u8::MAX])); impl_black_white!(Luma, Luma([u16::MIN]), Luma([u16::MAX])); impl_black_white!( LumaA, LumaA([u8::MIN, u8::MAX]), LumaA([u8::MAX, u8::MAX]) ); impl_black_white!( LumaA, LumaA([u16::MIN, u16::MAX]), LumaA([u16::MAX, u16::MAX]) ); impl_black_white!(Rgb, Rgb([u8::MIN; 3]), Rgb([u8::MAX; 3])); impl_black_white!(Rgb, Rgb([u16::MIN; 3]), Rgb([u16::MAX; 3])); impl_black_white!( Rgba, Rgba([u8::MIN, u8::MIN, u8::MIN, u8::MAX]), Rgba([u8::MAX, u8::MAX, u8::MAX, u8::MAX]) ); impl_black_white!( Rgba, Rgba([u16::MIN, u16::MIN, u16::MIN, u16::MAX]), Rgba([u16::MAX, u16::MAX, u16::MAX, u16::MAX]) ); /// Something with a 2d position. pub trait Position { /// x-coordinate. fn x(&self) -> u32; /// y-coordinate. fn y(&self) -> u32; } /// Something with a score. pub trait Score { /// Score of this item. fn score(&self) -> f32; } /// A type to which we can clamp a value of type T. /// Implementations are not required to handle `NaN`s gracefully. pub trait Clamp { /// Clamp `x` to a valid value for this type. fn clamp(x: T) -> Self; } /// Creates an implementation of Clamp for type To. macro_rules! implement_clamp { ($from:ty, $to:ty, $min:expr, $max:expr, $min_from:expr, $max_from:expr) => { impl Clamp<$from> for $to { fn clamp(x: $from) -> $to { if x < $max_from as $from { if x > $min_from as $from { x as $to } else { $min } } else { $max } } } }; } /// Implements Clamp for T, for all input types T. macro_rules! implement_identity_clamp { ( $($t:ty),* ) => { $( impl Clamp<$t> for $t { fn clamp(x: $t) -> $t { x } } )* }; } implement_clamp!(i16, u8, u8::MIN, u8::MAX, u8::MIN as i16, u8::MAX as i16); implement_clamp!(u16, u8, u8::MIN, u8::MAX, u8::MIN as u16, u8::MAX as u16); implement_clamp!(i32, u8, u8::MIN, u8::MAX, u8::MIN as i32, u8::MAX as i32); implement_clamp!(u32, u8, u8::MIN, u8::MAX, u8::MIN as u32, u8::MAX as u32); implement_clamp!(f32, u8, u8::MIN, u8::MAX, u8::MIN as f32, u8::MAX as f32); implement_clamp!(f64, u8, u8::MIN, u8::MAX, u8::MIN as f64, u8::MAX as f64); implement_clamp!( i32, u16, u16::MIN, u16::MAX, u16::MIN as i32, u16::MAX as i32 ); implement_clamp!( f32, u16, u16::MIN, u16::MAX, u16::MIN as f32, u16::MAX as f32 ); implement_clamp!( f64, u16, u16::MIN, u16::MAX, u16::MIN as f64, u16::MAX as f64 ); implement_clamp!( i32, i16, i16::MIN, i16::MAX, i16::MIN as i32, i16::MAX as i32 ); implement_identity_clamp!(u8, i8, u16, i16, u32, i32, u64, i64, f32, f64); #[cfg(test)] mod tests { use super::Clamp; #[test] fn test_clamp_f32_u8() { let t: u8 = Clamp::clamp(255f32); assert_eq!(t, 255u8); let u: u8 = Clamp::clamp(300f32); assert_eq!(u, 255u8); let v: u8 = Clamp::clamp(0f32); assert_eq!(v, 0u8); let w: u8 = Clamp::clamp(-5f32); assert_eq!(w, 0u8); } #[test] fn test_clamp_f32_u16() { let t: u16 = Clamp::clamp(65535f32); assert_eq!(t, 65535u16); let u: u16 = Clamp::clamp(300000f32); assert_eq!(u, 65535u16); let v: u16 = Clamp::clamp(0f32); assert_eq!(v, 0u16); let w: u16 = Clamp::clamp(-5f32); assert_eq!(w, 0u16); } } imageproc-0.25.0/src/distance_transform.rs000064400000000000000000000526661046102023000167170ustar 00000000000000//! Functions for computing distance transforms - the distance of each pixel in an //! image from the nearest pixel of interest. use crate::definitions::Image; use image::{GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma}; use std::cmp::min; /// How to measure distance between coordinates. /// See [`distance_transform`] for examples. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Norm { /// `d((x1, y1), (x2, y2)) = abs(x1 - x2) + abs(y1 - y2)` /// /// Also known as the Manhattan or city block norm. L1, /// `d((x1, y1), (x2, y2)) = sqrt((x1 - x2)^2 + (y1 - y2)^2)` /// /// Also known as the Euclidean norm. /// /// Note that both [`distance_transform`] and the functions in the [`morphology`](crate::morphology) /// module represent distances as integer values, so cannot accurately represent `L2` norms. Instead, /// these functions approximate the `L2` norm by taking the ceiling of the true value. If you want accurate /// distances then use [`euclidean_squared_distance_transform`] instead, which returns floating point values. L2, /// `d((x1, y1), (x2, y2)) = max(abs(x1 - x2), abs(y1 - y2))` /// /// Also known as the chessboard norm. LInf, } /// Returns an image showing the distance of each pixel from a foreground pixel in the original image. /// /// A pixel belongs to the foreground if it has non-zero intensity. As the image /// has a bit-depth of 8, distances saturate at 255. /// /// When using `Norm::L2` this function returns the ceiling of the true distances. /// Use [`euclidean_squared_distance_transform`] if you need floating point distances. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::GrayImage; /// use imageproc::distance_transform::{distance_transform, Norm}; /// /// let image = gray_image!( /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 1, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0 /// ); /// /// // L1 norm /// let l1_distances = gray_image!( /// 4, 3, 2, 3, 4; /// 3, 2, 1, 2, 3; /// 2, 1, 0, 1, 2; /// 3, 2, 1, 2, 3; /// 4, 3, 2, 3, 4 /// ); /// /// assert_pixels_eq!(distance_transform(&image, Norm::L1), l1_distances); /// /// // L2 norm /// let l2_distances = gray_image!( /// 3, 3, 2, 3, 3; /// 3, 2, 1, 2, 3; /// 2, 1, 0, 1, 2; /// 3, 2, 1, 2, 3; /// 3, 3, 2, 3, 3 /// ); /// /// assert_pixels_eq!(distance_transform(&image, Norm::L2), l2_distances); /// /// // LInf norm /// let linf_distances = gray_image!( /// 2, 2, 2, 2, 2; /// 2, 1, 1, 1, 2; /// 2, 1, 0, 1, 2; /// 2, 1, 1, 1, 2; /// 2, 2, 2, 2, 2 /// ); /// /// assert_pixels_eq!(distance_transform(&image, Norm::LInf), linf_distances); /// /// # } /// ``` pub fn distance_transform(image: &GrayImage, norm: Norm) -> GrayImage { let mut out = image.clone(); distance_transform_mut(&mut out, norm); out } /// Updates an image in place so that each pixel contains its distance from a foreground pixel in the original image. /// /// A pixel belongs to the foreground if it has non-zero intensity. As the image has a bit-depth of 8, /// distances saturate at 255. /// /// When using `Norm::L2` this function returns the ceiling of the true distances. /// Use [`euclidean_squared_distance_transform`] if you need floating point distances. /// /// See [`distance_transform`] for examples. pub fn distance_transform_mut(image: &mut GrayImage, norm: Norm) { distance_transform_impl(image, norm, DistanceFrom::Foreground); } #[derive(PartialEq, Eq, Copy, Clone)] pub(crate) enum DistanceFrom { Foreground, Background, } pub(crate) fn distance_transform_impl(image: &mut GrayImage, norm: Norm, from: DistanceFrom) { match norm { Norm::LInf => distance_transform_impl_linf_or_l1::(image, from), Norm::L1 => distance_transform_impl_linf_or_l1::(image, from), Norm::L2 => { match from { DistanceFrom::Foreground => (), DistanceFrom::Background => image .iter_mut() .for_each(|p| *p = if *p == 0 { 1 } else { 0 }), } let float_dist: ImageBuffer, Vec> = euclidean_squared_distance_transform(image); image .iter_mut() .zip(float_dist.iter()) .for_each(|(u, v)| *u = v.sqrt().clamp(0.0, 255.0).ceil() as u8); } } } fn distance_transform_impl_linf_or_l1( image: &mut GrayImage, from: DistanceFrom, ) { let max_distance = Luma([min(image.width() + image.height(), 255u32) as u8]); // We use an unsafe code block for optimisation purposes here // We use the 'unsafe_get_pixel' and 'check' unsafe functions, // which are faster than safe functions, // and we guarantee that they are used safely // by making sure we are always within the bounds of the image unsafe { // Top-left to bottom-right for y in 0..image.height() { for x in 0..image.width() { if from == DistanceFrom::Foreground { if image.unsafe_get_pixel(x, y)[0] > 0u8 { image.unsafe_put_pixel(x, y, Luma([0u8])); continue; } } else if image.unsafe_get_pixel(x, y)[0] == 0u8 { image.unsafe_put_pixel(x, y, Luma([0u8])); continue; } image.unsafe_put_pixel(x, y, max_distance); if x > 0 { check(image, x, y, x - 1, y); } if y > 0 { check(image, x, y, x, y - 1); if IS_LINF { if x > 0 { check(image, x, y, x - 1, y - 1); } if x < image.width() - 1 { check(image, x, y, x + 1, y - 1); } } } } } // Bottom-right to top-left for y in (0..image.height()).rev() { for x in (0..image.width()).rev() { if x < image.width() - 1 { check(image, x, y, x + 1, y); } if y < image.height() - 1 { check(image, x, y, x, y + 1); if IS_LINF { if x < image.width() - 1 { check(image, x, y, x + 1, y + 1); } if x > 0 { check(image, x, y, x - 1, y + 1); } } } } } } } // Sets image[current_x, current_y] to min(image[current_x, current_y], image[candidate_x, candidate_y] + 1). // We avoid overflow by performing the arithmetic at type u16. We could use u8::saturating_add instead, but // (based on the benchmarks tests) this appears to be considerably slower. unsafe fn check( image: &mut GrayImage, current_x: u32, current_y: u32, candidate_x: u32, candidate_y: u32, ) { let current = image.unsafe_get_pixel(current_x, current_y)[0] as u16; let candidate_incr = image.unsafe_get_pixel(candidate_x, candidate_y)[0] as u16 + 1; if candidate_incr < current { image.unsafe_put_pixel(current_x, current_y, Luma([candidate_incr as u8])); } } /// Computes the square of the `L2` (Euclidean) distance transform of `image`. Distances are to the /// nearest foreground pixel, where a pixel is counted as foreground if it has non-zero value. /// /// Uses the algorithm from [Distance Transforms of Sampled Functions] to achieve time linear /// in the size of the image. /// /// [Distance Transforms of Sampled Functions]: https://www.cs.cornell.edu/~dph/papers/dt.pdf pub fn euclidean_squared_distance_transform(image: &Image>) -> Image> { let (width, height) = image.dimensions(); let mut result = ImageBuffer::new(width, height); let mut column_envelope = LowerEnvelope::new(height as usize); // Compute 1d transforms of each column for x in 0..width { let source = Column { image, column: x }; let mut sink = ColumnMut { image: &mut result, column: x, }; distance_transform_1d_mut(&source, &mut sink, &mut column_envelope); } let mut row_buffer = vec![0f64; width as usize]; let mut row_envelope = LowerEnvelope::new(width as usize); // Compute 1d transforms of each row for y in 0..height { for x in 0..width { row_buffer[x as usize] = result.get_pixel(x, y)[0]; } let mut sink = Row { image: &mut result, row: y, }; distance_transform_1d_mut(&row_buffer, &mut sink, &mut row_envelope); } result } struct LowerEnvelope { // Indices of the parabolas in the lower envelope. locations: Vec, // Points at which the parabola in the lower envelope // changes. The parabola centred at locations[i] has the least // values of all parabolas in the lower envelope for all // coordinates in [ boundaries[i], boundaries[i + 1] ). boundaries: Vec, } impl LowerEnvelope { fn new(image_side: usize) -> LowerEnvelope { LowerEnvelope { locations: vec![0; image_side], boundaries: vec![f64::NAN; image_side + 1], } } } trait Sink { fn put(&mut self, idx: usize, value: f64); fn len(&self) -> usize; } trait Source { fn get(&self, idx: usize) -> f64; fn len(&self) -> usize; } struct Row<'a> { image: &'a mut Image>, row: u32, } impl<'a> Sink for Row<'a> { fn put(&mut self, idx: usize, value: f64) { unsafe { self.image .unsafe_put_pixel(idx as u32, self.row, Luma([value])); } } fn len(&self) -> usize { self.image.width() as usize } } struct ColumnMut<'a> { image: &'a mut Image>, column: u32, } impl<'a> Sink for ColumnMut<'a> { fn put(&mut self, idx: usize, value: f64) { unsafe { self.image .unsafe_put_pixel(self.column, idx as u32, Luma([value])); } } fn len(&self) -> usize { self.image.height() as usize } } impl Source for Vec { fn get(&self, idx: usize) -> f64 { self[idx] } fn len(&self) -> usize { self.len() } } impl Source for [f64] { fn get(&self, idx: usize) -> f64 { self[idx] } fn len(&self) -> usize { self.len() } } struct Column<'a> { image: &'a Image>, column: u32, } impl<'a> Source for Column<'a> { fn get(&self, idx: usize) -> f64 { let pixel = unsafe { self.image.unsafe_get_pixel(self.column, idx as u32)[0] as f64 }; if pixel > 0f64 { 0f64 } else { f64::INFINITY } } fn len(&self) -> usize { self.image.height() as usize } } fn distance_transform_1d_mut(f: &S, result: &mut T, envelope: &mut LowerEnvelope) where S: Source, T: Sink, { assert!(result.len() == f.len()); assert!(envelope.boundaries.len() == f.len() + 1); assert!(envelope.locations.len() == f.len()); if f.len() == 0 { return; } // Index of rightmost parabola in the lower envelope let mut k = 0; // First parabola is the best current value as we've not looked // at any other yet envelope.locations[0] = 0; // First parabola has the lowest value for all x coordinates envelope.boundaries[0] = f64::NEG_INFINITY; envelope.boundaries[1] = f64::INFINITY; for q in 1..f.len() { if f.get(q) == f64::INFINITY { continue; } if k == 0 && f.get(envelope.locations[k]) == f64::INFINITY { envelope.locations[k] = q; envelope.boundaries[k] = f64::NEG_INFINITY; envelope.boundaries[k + 1] = f64::INFINITY; continue; } // Let p = locations[k], i.e. the centre of the rightmost // parabola in the current approximation to the lower envelope. // // We find the intersection of this parabola with // the parabola centred at q to determine if the latter // is part of the lower envelope (and if the former should // be removed from our current approximation to it). let mut s = intersection(f, envelope.locations[k], q); while s <= envelope.boundaries[k] { // The parabola centred at q is the best we've seen for an // intervals that extends past the lower bound of the region // where we believed that the parabola centred at p gave the // least value k -= 1; s = intersection(f, envelope.locations[k], q); } k += 1; envelope.locations[k] = q; envelope.boundaries[k] = s; envelope.boundaries[k + 1] = f64::INFINITY; } let mut k = 0; for q in 0..f.len() { while envelope.boundaries[k + 1] < q as f64 { k += 1; } let dist = q as f64 - envelope.locations[k] as f64; result.put(q, dist * dist + f.get(envelope.locations[k])); } } /// Returns the intersection of the parabolas f(p) + (x - p) ^ 2 and f(q) + (x - q) ^ 2. fn intersection(f: &S, p: usize, q: usize) -> f64 { // The intersection s of the two parabolas satisfies: // // f[q] + (q - s) ^ 2 = f[p] + (s - q) ^ 2 // // Rearranging gives: // // s = [( f[q] + q ^ 2 ) - ( f[p] + p ^ 2 )] / (2q - 2p) let fq = f.get(q); let fp = f.get(p); let p = p as f64; let q = q as f64; ((fq + q * q) - (fp + p * p)) / (2.0 * q - 2.0 * p) } #[cfg(test)] mod tests { use super::*; use crate::definitions::Image; use crate::property_testing::GrayTestImage; use crate::utils::pixel_diff_summary; use image::{GrayImage, Luma}; use quickcheck::{quickcheck, Arbitrary, Gen, TestResult}; use std::cmp::max; use std::f64; /// Avoid generating garbage floats during certain calculations below. #[derive(Debug, Clone)] struct BoundedFloat(f64); impl Arbitrary for BoundedFloat { fn arbitrary(g: &mut Gen) -> Self { let mut f; loop { f = f64::arbitrary(g); if f.is_normal() { f = f.clamp(-1_000_000.0, 1_000_000.0); break; } } BoundedFloat(f) } } #[test] fn test_distance_transform_saturation() { // A single foreground pixel in the top-left let image = GrayImage::from_fn(300, 3, |x, y| match (x, y) { (0, 0) => Luma([255u8]), _ => Luma([0u8]), }); // Distances should not overflow let expected = GrayImage::from_fn(300, 3, |x, y| Luma([min(255, max(x, y)) as u8])); let distances = distance_transform(&image, Norm::LInf); assert_pixels_eq!(distances, expected); } impl Sink for Vec { fn put(&mut self, idx: usize, value: f64) { self[idx] = value; } fn len(&self) -> usize { self.len() } } fn distance_transform_1d(f: &Vec) -> Vec { let mut r = vec![0.0; f.len()]; let mut e = LowerEnvelope::new(f.len()); distance_transform_1d_mut(f, &mut r, &mut e); r } #[test] fn test_distance_transform_1d_constant() { let f = vec![0.0, 0.0, 0.0]; let dists = distance_transform_1d(&f); assert_eq!(dists, &[0.0, 0.0, 0.0]); } #[test] fn test_distance_transform_1d_descending_gradient() { let f = vec![7.0, 5.0, 3.0, 1.0]; let dists = distance_transform_1d(&f); assert_eq!(dists, &[6.0, 4.0, 2.0, 1.0]); } #[test] fn test_distance_transform_1d_ascending_gradient() { let f = vec![1.0, 3.0, 5.0, 7.0]; let dists = distance_transform_1d(&f); assert_eq!(dists, &[1.0, 2.0, 4.0, 6.0]); } #[test] fn test_distance_transform_1d_with_infinities() { let f = vec![f64::INFINITY, f64::INFINITY, 5.0, f64::INFINITY]; let dists = distance_transform_1d(&f); assert_eq!(dists, &[9.0, 6.0, 5.0, 6.0]); } // Simple implementation of 1d distance transform which performs an // exhaustive search. Used to valid the more complicated lower-envelope // implementation against. fn distance_transform_1d_reference(f: &[f64]) -> Vec { let mut ret = vec![0.0; f.len()]; for q in 0..f.len() { ret[q] = (0..f.len()) .map(|p| { let dist = p as f64 - q as f64; dist * dist + f[p] }) .fold(f64::NAN, f64::min); } ret } #[cfg_attr(miri, ignore = "slow")] #[test] fn test_distance_transform_1d_matches_reference_implementation() { fn prop(f: Vec) -> bool { let v: Vec = f.into_iter().map(|n| n.0).collect(); let expected = distance_transform_1d_reference(&v); let actual = distance_transform_1d(&v); expected == actual } quickcheck(prop as fn(Vec) -> bool); } fn euclidean_squared_distance_transform_reference(image: &Image>) -> Image> { let (width, height) = image.dimensions(); let mut dists = Image::new(width, height); for y in 0..height { for x in 0..width { let mut min = f64::INFINITY; for yc in 0..height { for xc in 0..width { let pc = image.get_pixel(xc, yc)[0]; if pc > 0 { let dx = xc as f64 - x as f64; let dy = yc as f64 - y as f64; min = f64::min(min, dx * dx + dy * dy); } } } dists.put_pixel(x, y, Luma([min])); } } dists } #[cfg_attr(miri, ignore = "slow")] #[test] fn test_euclidean_squared_distance_transform_matches_reference_implementation() { fn prop(image: GrayTestImage) -> TestResult { let expected = euclidean_squared_distance_transform_reference(&image.0); let actual = euclidean_squared_distance_transform(&image.0); match pixel_diff_summary(&actual, &expected) { None => TestResult::passed(), Some(err) => TestResult::error(err), } } quickcheck(prop as fn(GrayTestImage) -> TestResult); } #[test] fn test_euclidean_squared_distance_transform_example() { let image = gray_image!( 1, 0, 0, 0, 0; 0, 1, 0, 0, 0; 1, 1, 1, 0, 0; 0, 0, 0, 0, 0; 0, 0, 1, 0, 0 ); let expected = gray_image!(type: f64, 0.0, 1.0, 2.0, 5.0, 8.0; 1.0, 0.0, 1.0, 2.0, 5.0; 0.0, 0.0, 0.0, 1.0, 4.0; 1.0, 1.0, 1.0, 2.0, 5.0; 4.0, 1.0, 0.0, 1.0, 4.0 ); let dist = euclidean_squared_distance_transform(&image); assert_pixels_eq_within!(dist, expected, 1e-6); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::utils::gray_bench_image; use test::{black_box, Bencher}; macro_rules! bench_euclidean_squared_distance_transform { ($name:ident, side: $s:expr) => { #[bench] fn $name(b: &mut Bencher) { let image = gray_bench_image($s, $s); b.iter(|| { let distance = euclidean_squared_distance_transform(&image); black_box(distance); }) } }; } bench_euclidean_squared_distance_transform!(bench_euclidean_squared_distance_transform_10, side: 10); bench_euclidean_squared_distance_transform!(bench_euclidean_squared_distance_transform_100, side: 100); bench_euclidean_squared_distance_transform!(bench_euclidean_squared_distance_transform_200, side: 200); macro_rules! bench_distance_transform { ($name:ident, $norm:expr, side: $s:expr) => { #[bench] fn $name(b: &mut Bencher) { let image = gray_bench_image($s, $s); b.iter(|| { let distance = distance_transform(&image, $norm); black_box(distance); }) } }; } bench_distance_transform!(bench_distance_transform_l1_10, Norm::L1, side: 10); bench_distance_transform!(bench_distance_transform_l1_100, Norm::L1, side: 100); bench_distance_transform!(bench_distance_transform_l1_200, Norm::L1, side: 200); bench_distance_transform!(bench_distance_transform_l2_10, Norm::L2, side: 10); bench_distance_transform!(bench_distance_transform_l2_100, Norm::L2, side: 100); bench_distance_transform!(bench_distance_transform_l2_200, Norm::L2, side: 200); bench_distance_transform!(bench_distance_transform_linf_10, Norm::LInf, side: 10); bench_distance_transform!(bench_distance_transform_linf_100, Norm::LInf, side: 100); bench_distance_transform!(bench_distance_transform_linf_200, Norm::LInf, side: 200); } imageproc-0.25.0/src/drawing/bezier.rs000064400000000000000000000072221046102023000157310ustar 00000000000000use crate::definitions::Image; use crate::drawing::line::draw_line_segment_mut; use crate::drawing::Canvas; use image::{GenericImage, ImageBuffer}; /// Draws a cubic Bézier curve on a new copy of an image. /// /// Draws as much of the curve as lies within image bounds. #[must_use = "the function does not modify the original image"] pub fn draw_cubic_bezier_curve( image: &I, start: (f32, f32), end: (f32, f32), control_a: (f32, f32), control_b: (f32, f32), color: I::Pixel, ) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_cubic_bezier_curve_mut(&mut out, start, end, control_a, control_b, color); out } /// Draws a cubic Bézier curve on an image in place. /// /// Draws as much of the curve as lies within image bounds. pub fn draw_cubic_bezier_curve_mut( canvas: &mut C, start: (f32, f32), end: (f32, f32), control_a: (f32, f32), control_b: (f32, f32), color: C::Pixel, ) where C: Canvas, { // Bezier Curve function from: https://pomax.github.io/bezierinfo/#control let cubic_bezier_curve = |t: f32| { let t2 = t * t; let t3 = t2 * t; let mt = 1.0 - t; let mt2 = mt * mt; let mt3 = mt2 * mt; let x = (start.0 * mt3) + (3.0 * control_a.0 * mt2 * t) + (3.0 * control_b.0 * mt * t2) + (end.0 * t3); let y = (start.1 * mt3) + (3.0 * control_a.1 * mt2 * t) + (3.0 * control_b.1 * mt * t2) + (end.1 * t3); (x.round(), y.round()) // round to nearest pixel, to avoid ugly line artifacts }; let distance = |point_a: (f32, f32), point_b: (f32, f32)| { ((point_a.0 - point_b.0).powi(2) + (point_a.1 - point_b.1).powi(2)).sqrt() }; // Approximate curve's length by adding distance between control points. let curve_length_bound: f32 = distance(start, control_a) + distance(control_a, control_b) + distance(control_b, end); // Use hyperbola function to give shorter curves a bias in number of line segments. let num_segments: i32 = ((curve_length_bound.powi(2) + 800.0).sqrt() / 8.0) as i32; // Sample points along the curve and connect them with line segments. let t_interval = 1f32 / (num_segments as f32); let mut t1 = 0f32; for i in 0..num_segments { let t2 = (i as f32 + 1.0) * t_interval; draw_line_segment_mut( canvas, cubic_bezier_curve(t1), cubic_bezier_curve(t2), color, ); t1 = t2; } } #[cfg(not(miri))] #[cfg(test)] mod benches { use image::{GrayImage, Luma}; macro_rules! bench_cubic_bezier_curve { ($name:ident, $start:expr, $end:expr, $control_a:expr, $control_b:expr) => { #[bench] fn $name(b: &mut test::Bencher) { use super::draw_cubic_bezier_curve_mut; let mut image = GrayImage::new(500, 500); let color = Luma([50u8]); b.iter(|| { draw_cubic_bezier_curve_mut( &mut image, $start, $end, $control_a, $control_b, color, ); test::black_box(&image); }); } }; } bench_cubic_bezier_curve!( bench_draw_cubic_bezier_curve_short, (100.0, 100.0), (130.0, 130.0), (110.0, 100.0), (120.0, 130.0) ); bench_cubic_bezier_curve!( bench_draw_cubic_bezier_curve_long, (100.0, 100.0), (400.0, 400.0), (500.0, 0.0), (0.0, 500.0) ); } imageproc-0.25.0/src/drawing/canvas.rs000064400000000000000000000066361046102023000157340ustar 00000000000000use image::{GenericImage, GenericImageView, Pixel}; /// A surface for drawing on - many drawing functions in this /// library are generic over a `Canvas` to allow the user to /// configure e.g. whether to use blending. /// /// All instances of `GenericImage` implement `Canvas`, with /// the behaviour of `draw_pixel` being equivalent to calling /// `set_pixel` with the same arguments. /// /// See [`Blend`](struct.Blend.html) for another example implementation /// of this trait - its implementation of `draw_pixel` alpha-blends /// the input value with the pixel's current value. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::{Pixel, Rgba, RgbaImage}; /// use imageproc::drawing::{Canvas, Blend}; /// /// // A trivial function which draws on a Canvas /// fn write_a_pixel(canvas: &mut C, c: C::Pixel) { /// canvas.draw_pixel(0, 0, c); /// } /// /// // Background color /// let solid_blue = Rgba([0u8, 0u8, 255u8, 255u8]); /// /// // Drawing color /// let translucent_red = Rgba([255u8, 0u8, 0u8, 127u8]); /// /// // Blended combination of background and drawing colors /// let mut alpha_blended = solid_blue; /// alpha_blended.blend(&translucent_red); /// /// // The implementation of Canvas for GenericImage overwrites existing pixels /// let mut image = RgbaImage::from_pixel(1, 1, solid_blue); /// write_a_pixel(&mut image, translucent_red); /// assert_eq!(*image.get_pixel(0, 0), translucent_red); /// /// // This behaviour can be customised by using a different Canvas type /// let mut image = Blend(RgbaImage::from_pixel(1, 1, solid_blue)); /// write_a_pixel(&mut image, translucent_red); /// assert_eq!(*image.0.get_pixel(0, 0), alpha_blended); /// # } /// ``` pub trait Canvas { /// The type of `Pixel` that can be drawn on this canvas. type Pixel: Pixel; /// The width and height of this canvas. fn dimensions(&self) -> (u32, u32); /// The width of this canvas. fn width(&self) -> u32 { self.dimensions().0 } /// The height of this canvas. fn height(&self) -> u32 { self.dimensions().1 } /// Returns the pixel located at (x, y). fn get_pixel(&self, x: u32, y: u32) -> Self::Pixel; /// Draw a pixel at the given coordinates. `x` and `y` /// should be within `dimensions` - if not then panicking /// is a valid implementation behaviour. fn draw_pixel(&mut self, x: u32, y: u32, color: Self::Pixel); } impl Canvas for I where I: GenericImage, { type Pixel = I::Pixel; fn dimensions(&self) -> (u32, u32) { ::dimensions(self) } fn get_pixel(&self, x: u32, y: u32) -> Self::Pixel { self.get_pixel(x, y) } fn draw_pixel(&mut self, x: u32, y: u32, color: Self::Pixel) { self.put_pixel(x, y, color) } } /// A canvas that blends pixels when drawing. /// /// See the documentation for [`Canvas`](trait.Canvas.html) /// for an example using this type. pub struct Blend(pub I); impl Canvas for Blend { type Pixel = I::Pixel; fn dimensions(&self) -> (u32, u32) { self.0.dimensions() } fn get_pixel(&self, x: u32, y: u32) -> Self::Pixel { self.0.get_pixel(x, y) } fn draw_pixel(&mut self, x: u32, y: u32, color: Self::Pixel) { let mut pix = self.0.get_pixel(x, y); pix.blend(&color); self.0.put_pixel(x, y, pix); } } imageproc-0.25.0/src/drawing/conics.rs000064400000000000000000000327111046102023000157300ustar 00000000000000use crate::definitions::Image; use crate::drawing::draw_if_in_bounds; use crate::drawing::line::draw_line_segment_mut; use crate::drawing::Canvas; use image::{GenericImage, ImageBuffer}; /// Draws the outline of an ellipse on a new copy of an image. /// /// Draws as much of an ellipse as lies inside the image bounds. /// /// Uses the [Midpoint Ellipse Drawing Algorithm](https://web.archive.org/web/20160128020853/http://tutsheap.com/c/mid-point-ellipse-drawing-algorithm/). /// (Modified from Bresenham's algorithm) /// /// The ellipse is axis-aligned and satisfies the following equation: /// /// (`x^2 / width_radius^2) + (y^2 / height_radius^2) = 1` #[must_use = "the function does not modify the original image"] pub fn draw_hollow_ellipse( image: &I, center: (i32, i32), width_radius: i32, height_radius: i32, color: I::Pixel, ) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_hollow_ellipse_mut(&mut out, center, width_radius, height_radius, color); out } /// Draws the outline of an ellipse on an image in place. /// /// Draws as much of an ellipse as lies inside the image bounds. /// /// Uses the [Midpoint Ellipse Drawing Algorithm](https://web.archive.org/web/20160128020853/http://tutsheap.com/c/mid-point-ellipse-drawing-algorithm/). /// (Modified from Bresenham's algorithm) /// /// The ellipse is axis-aligned and satisfies the following equation: /// /// `(x^2 / width_radius^2) + (y^2 / height_radius^2) = 1` pub fn draw_hollow_ellipse_mut( canvas: &mut C, center: (i32, i32), width_radius: i32, height_radius: i32, color: C::Pixel, ) where C: Canvas, { // Circle drawing algorithm is faster, so use it if the given ellipse is actually a circle. if width_radius == height_radius { draw_hollow_circle_mut(canvas, center, width_radius, color); return; } let draw_quad_pixels = |x0: i32, y0: i32, x: i32, y: i32| { draw_if_in_bounds(canvas, x0 + x, y0 + y, color); draw_if_in_bounds(canvas, x0 - x, y0 + y, color); draw_if_in_bounds(canvas, x0 + x, y0 - y, color); draw_if_in_bounds(canvas, x0 - x, y0 - y, color); }; draw_ellipse(draw_quad_pixels, center, width_radius, height_radius); } /// Draws an ellipse and its contents on a new copy of the image. /// /// Draw as much of the ellipse and its contents as lies inside the image bounds. /// /// Uses the [Midpoint Ellipse Drawing Algorithm](https://web.archive.org/web/20160128020853/http://tutsheap.com/c/mid-point-ellipse-drawing-algorithm/). /// (Modified from Bresenham's algorithm) /// /// The ellipse is axis-aligned and satisfies the following equation: /// /// `(x^2 / width_radius^2) + (y^2 / height_radius^2) <= 1` #[must_use = "the function does not modify the original image"] pub fn draw_filled_ellipse( image: &I, center: (i32, i32), width_radius: i32, height_radius: i32, color: I::Pixel, ) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_filled_ellipse_mut(&mut out, center, width_radius, height_radius, color); out } /// Draws an ellipse and its contents on an image in place. /// /// Draw as much of the ellipse and its contents as lies inside the image bounds. /// /// Uses the [Midpoint Ellipse Drawing Algorithm](https://web.archive.org/web/20160128020853/http://tutsheap.com/c/mid-point-ellipse-drawing-algorithm/). /// (Modified from Bresenham's algorithm) /// /// The ellipse is axis-aligned and satisfies the following equation: /// /// `(x^2 / width_radius^2) + (y^2 / height_radius^2) <= 1` pub fn draw_filled_ellipse_mut( canvas: &mut C, center: (i32, i32), width_radius: i32, height_radius: i32, color: C::Pixel, ) where C: Canvas, { // Circle drawing algorithm is faster, so use it if the given ellipse is actually a circle. if width_radius == height_radius { draw_filled_circle_mut(canvas, center, width_radius, color); return; } let draw_line_pairs = |x0: i32, y0: i32, x: i32, y: i32| { draw_line_segment_mut( canvas, ((x0 - x) as f32, (y0 + y) as f32), ((x0 + x) as f32, (y0 + y) as f32), color, ); draw_line_segment_mut( canvas, ((x0 - x) as f32, (y0 - y) as f32), ((x0 + x) as f32, (y0 - y) as f32), color, ); }; draw_ellipse(draw_line_pairs, center, width_radius, height_radius); } // Implements the Midpoint Ellipse Drawing Algorithm https://web.archive.org/web/20160128020853/http://tutsheap.com/c/mid-point-ellipse-drawing-algorithm/). (Modified from Bresenham's algorithm) // // Takes a function that determines how to render the points on the ellipse. fn draw_ellipse(mut render_func: F, center: (i32, i32), width_radius: i32, height_radius: i32) where F: FnMut(i32, i32, i32, i32), { let (x0, y0) = center; let w2 = (width_radius * width_radius) as f32; let h2 = (height_radius * height_radius) as f32; let mut x = 0; let mut y = height_radius; let mut px = 0.0; let mut py = 2.0 * w2 * y as f32; render_func(x0, y0, x, y); // Top and bottom regions. let mut p = h2 - (w2 * height_radius as f32) + (0.25 * w2); while px < py { x += 1; px += 2.0 * h2; if p < 0.0 { p += h2 + px; } else { y -= 1; py += -2.0 * w2; p += h2 + px - py; } render_func(x0, y0, x, y); } // Left and right regions. p = h2 * (x as f32 + 0.5).powi(2) + (w2 * (y - 1).pow(2) as f32) - w2 * h2; while y > 0 { y -= 1; py += -2.0 * w2; if p > 0.0 { p += w2 - py; } else { x += 1; px += 2.0 * h2; p += w2 - py + px; } render_func(x0, y0, x, y); } } /// Draws the outline of a circle on a new copy of an image. /// /// Draw as much of the circle as lies inside the image bounds. #[must_use = "the function does not modify the original image"] pub fn draw_hollow_circle( image: &I, center: (i32, i32), radius: i32, color: I::Pixel, ) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_hollow_circle_mut(&mut out, center, radius, color); out } /// Draws the outline of a circle on an image in place. /// /// Draw as much of the circle as lies inside the image bounds. pub fn draw_hollow_circle_mut(canvas: &mut C, center: (i32, i32), radius: i32, color: C::Pixel) where C: Canvas, { let mut x = 0i32; let mut y = radius; let mut p = 1 - radius; let x0 = center.0; let y0 = center.1; while x <= y { draw_if_in_bounds(canvas, x0 + x, y0 + y, color); draw_if_in_bounds(canvas, x0 + y, y0 + x, color); draw_if_in_bounds(canvas, x0 - y, y0 + x, color); draw_if_in_bounds(canvas, x0 - x, y0 + y, color); draw_if_in_bounds(canvas, x0 - x, y0 - y, color); draw_if_in_bounds(canvas, x0 - y, y0 - x, color); draw_if_in_bounds(canvas, x0 + y, y0 - x, color); draw_if_in_bounds(canvas, x0 + x, y0 - y, color); x += 1; if p < 0 { p += 2 * x + 1; } else { y -= 1; p += 2 * (x - y) + 1; } } } /// Draws a circle and its contents on an image in place. /// /// Draws as much of a circle and its contents as lies inside the image bounds. pub fn draw_filled_circle_mut(canvas: &mut C, center: (i32, i32), radius: i32, color: C::Pixel) where C: Canvas, { let mut x = 0i32; let mut y = radius; let mut p = 1 - radius; let x0 = center.0; let y0 = center.1; while x <= y { draw_line_segment_mut( canvas, ((x0 - x) as f32, (y0 + y) as f32), ((x0 + x) as f32, (y0 + y) as f32), color, ); draw_line_segment_mut( canvas, ((x0 - y) as f32, (y0 + x) as f32), ((x0 + y) as f32, (y0 + x) as f32), color, ); draw_line_segment_mut( canvas, ((x0 - x) as f32, (y0 - y) as f32), ((x0 + x) as f32, (y0 - y) as f32), color, ); draw_line_segment_mut( canvas, ((x0 - y) as f32, (y0 - x) as f32), ((x0 + y) as f32, (y0 - x) as f32), color, ); x += 1; if p < 0 { p += 2 * x + 1; } else { y -= 1; p += 2 * (x - y) + 1; } } } /// Draws a circle and its contents on a new copy of the image. /// /// Draws as much of a circle and its contents as lies inside the image bounds. #[must_use = "the function does not modify the original image"] pub fn draw_filled_circle( image: &I, center: (i32, i32), radius: i32, color: I::Pixel, ) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_filled_circle_mut(&mut out, center, radius, color); out } #[cfg(test)] mod tests { use super::draw_filled_ellipse_mut; use image::GenericImage; struct Ellipse { center: (i32, i32), width_radius: i32, height_radius: i32, } impl Ellipse { fn normalized_distance_from_center(&self, (x, y): (i32, i32)) -> f32 { let (cx, cy) = self.center; let (w, h) = (self.width_radius as f32, self.height_radius as f32); ((cx - x) as f32 / w).powi(2) + ((cy - y) as f32 / h).powi(2) } fn is_boundary_point(&self, (x, y): (i32, i32), boundary_eps: f32) -> bool { assert!(boundary_eps >= 0.0); (self.normalized_distance_from_center((x, y)) - 1.0).abs() < boundary_eps } fn is_inner_point(&self, (x, y): (i32, i32)) -> bool { self.normalized_distance_from_center((x, y)) < 1.0 } } fn check_filled_ellipse( img: &I, ellipse: Ellipse, inner_color: I::Pixel, outer_color: I::Pixel, boundary_eps: f32, ) where I::Pixel: core::fmt::Debug + PartialEq, { for x in 0..img.width() as i32 { for y in 0..img.height() as i32 { if ellipse.is_boundary_point((x, y), boundary_eps) { continue; } let pixel = img.get_pixel(x as u32, y as u32); if ellipse.is_inner_point((x, y)) { assert_eq!(pixel, inner_color); } else { assert_eq!(pixel, outer_color); } } } } #[cfg_attr(miri, ignore = "slow [>1480s]")] #[test] fn test_draw_filled_ellipse() { let ellipse = Ellipse { center: (960, 540), width_radius: 960, height_radius: 540, }; let inner_color = image::Rgb([255, 0, 0]); let outer_color = image::Rgb([0, 0, 0]); let mut img = image::RgbImage::new(1920, 1080); draw_filled_ellipse_mut( &mut img, ellipse.center, ellipse.width_radius, ellipse.height_radius, inner_color, ); const EPS: f32 = 0.0019; check_filled_ellipse(&img, ellipse, inner_color, outer_color, EPS); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use image::{GrayImage, Luma}; macro_rules! bench_hollow_ellipse { ($name:ident, $center:expr, $width_radius:expr, $height_radius:expr) => { #[bench] fn $name(b: &mut test::Bencher) { use super::draw_hollow_ellipse_mut; let mut image = GrayImage::new(500, 500); let color = Luma([50u8]); b.iter(|| { draw_hollow_ellipse_mut( &mut image, $center, $width_radius, $height_radius, color, ); test::black_box(&image); }); } }; } bench_hollow_ellipse!(bench_bench_hollow_ellipse_circle, (200, 200), 80, 80); bench_hollow_ellipse!(bench_bench_hollow_ellipse_vertical, (200, 200), 40, 100); bench_hollow_ellipse!(bench_bench_hollow_ellipse_horizontal, (200, 200), 100, 40); macro_rules! bench_filled_ellipse { ($name:ident, $center:expr, $width_radius:expr, $height_radius:expr) => { #[bench] fn $name(b: &mut test::Bencher) { use super::draw_filled_ellipse_mut; let mut image = GrayImage::new(500, 500); let color = Luma([50u8]); b.iter(|| { draw_filled_ellipse_mut( &mut image, $center, $width_radius, $height_radius, color, ); test::black_box(&image); }); } }; } bench_filled_ellipse!(bench_bench_filled_ellipse_circle, (200, 200), 80, 80); bench_filled_ellipse!(bench_bench_filled_ellipse_vertical, (200, 200), 40, 100); bench_filled_ellipse!(bench_bench_filled_ellipse_horizontal, (200, 200), 100, 40); } imageproc-0.25.0/src/drawing/cross.rs000064400000000000000000000077631046102023000156140ustar 00000000000000use crate::definitions::Image; use crate::drawing::Canvas; use image::{GenericImage, ImageBuffer}; /// Draws a colored cross on an image in place. /// /// Handles coordinates outside image bounds. #[rustfmt::skip] pub fn draw_cross_mut(canvas: &mut C, color: C::Pixel, x: i32, y: i32) where C: Canvas { let (width, height) = canvas.dimensions(); let idx = |x, y| (3 * (y + 1) + x + 1) as usize; let stencil = [0u8, 1u8, 0u8, 1u8, 1u8, 1u8, 0u8, 1u8, 0u8]; for sy in -1..2 { let iy = y + sy; if iy < 0 || iy >= height as i32 { continue; } for sx in -1..2 { let ix = x + sx; if ix < 0 || ix >= width as i32 { continue; } if stencil[idx(sx, sy)] == 1u8 { canvas.draw_pixel(ix as u32, iy as u32, color); } } } } /// Draws a colored cross on a new copy of an image. /// /// Handles coordinates outside image bounds. #[must_use = "the function does not modify the original image"] pub fn draw_cross(image: &I, color: I::Pixel, x: i32, y: i32) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_cross_mut(&mut out, color, x, y); out } #[cfg(test)] mod tests { use super::*; use image::{GrayImage, Luma}; #[test] fn test_draw_corner_inside_bounds() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 1, 2, 1, 1; 1, 2, 2, 2, 1; 1, 1, 2, 1, 1; 1, 1, 1, 1, 1); assert_pixels_eq!(draw_cross(&image, Luma([2u8]), 2, 2), expected); } #[test] fn test_draw_corner_partially_outside_left() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 2, 1, 1, 1, 1; 2, 2, 1, 1, 1; 2, 1, 1, 1, 1; 1, 1, 1, 1, 1); assert_pixels_eq!(draw_cross(&image, Luma([2u8]), 0, 2), expected); } #[test] fn test_draw_corner_partially_outside_right() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 1, 1, 1, 2; 1, 1, 1, 2, 2; 1, 1, 1, 1, 2; 1, 1, 1, 1, 1); assert_pixels_eq!(draw_cross(&image, Luma([2u8]), 4, 2), expected); } #[test] fn test_draw_corner_partially_outside_bottom() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 3, 1, 1; 1, 3, 3, 3, 1); assert_pixels_eq!(draw_cross(&image, Luma([3u8]), 2, 4), expected); } #[test] fn test_draw_corner_partially_outside_top() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 9, 9, 9, 1; 1, 1, 9, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1); assert_pixels_eq!(draw_cross(&image, Luma([9u8]), 2, 0), expected); } #[test] fn test_draw_corner_outside_bottom() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 9, 1, 1, 1, 1); assert_pixels_eq!(draw_cross(&image, Luma([9u8]), 0, 5), expected); } #[test] fn test_draw_corner_outside_right() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 9; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1); assert_pixels_eq!(draw_cross(&image, Luma([9u8]), 5, 0), expected); } } imageproc-0.25.0/src/drawing/line.rs000064400000000000000000000464441046102023000154110ustar 00000000000000use crate::definitions::Image; use crate::drawing::Canvas; use image::{GenericImage, ImageBuffer, Pixel}; use std::mem::{swap, transmute}; /// Iterates over the coordinates in a line segment using /// [Bresenham's line drawing algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm). pub struct BresenhamLineIter { dx: f32, dy: f32, x: i32, y: i32, error: f32, end_x: i32, is_steep: bool, y_step: i32, } impl BresenhamLineIter { /// Creates a [`BresenhamLineIter`](struct.BresenhamLineIter.html) which will iterate over the integer coordinates /// between `start` and `end`. pub fn new(start: (f32, f32), end: (f32, f32)) -> BresenhamLineIter { let (mut x0, mut y0) = (start.0, start.1); let (mut x1, mut y1) = (end.0, end.1); let is_steep = (y1 - y0).abs() > (x1 - x0).abs(); if is_steep { swap(&mut x0, &mut y0); swap(&mut x1, &mut y1); } if x0 > x1 { swap(&mut x0, &mut x1); swap(&mut y0, &mut y1); } let dx = x1 - x0; BresenhamLineIter { dx, dy: (y1 - y0).abs(), x: x0 as i32, y: y0 as i32, error: dx / 2f32, end_x: x1 as i32, is_steep, y_step: if y0 < y1 { 1 } else { -1 }, } } } impl Iterator for BresenhamLineIter { type Item = (i32, i32); fn next(&mut self) -> Option<(i32, i32)> { if self.x > self.end_x { None } else { let ret = if self.is_steep { (self.y, self.x) } else { (self.x, self.y) }; self.x += 1; self.error -= self.dy; if self.error < 0f32 { self.y += self.y_step; self.error += self.dx; } Some(ret) } } } fn in_bounds((x, y): (i32, i32), image: &I) -> bool { x >= 0 && x < image.width() as i32 && y >= 0 && y < image.height() as i32 } fn clamp_point(p: (f32, f32), image: &I) -> (f32, f32) { let x = p.0.clamp(0.0, (image.width() - 1) as f32); let y = p.1.clamp(0.0, (image.height() - 1) as f32); (x, y) } /// Iterates over the image pixels in a line segment using /// [Bresenham's line drawing algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm). pub struct BresenhamLinePixelIter<'a, P: Pixel> { iter: BresenhamLineIter, image: &'a Image

, } impl<'a, P: Pixel> BresenhamLinePixelIter<'a, P> { /// Creates a [`BresenhamLinePixelIter`](struct.BresenhamLinePixelIter.html) which will iterate over /// the image pixels with coordinates between `start` and `end`. pub fn new( image: &Image

, start: (f32, f32), end: (f32, f32), ) -> BresenhamLinePixelIter<'_, P> { assert!( image.width() >= 1 && image.height() >= 1, "BresenhamLinePixelIter does not support empty images" ); let iter = BresenhamLineIter::new(clamp_point(start, image), clamp_point(end, image)); BresenhamLinePixelIter { iter, image } } } impl<'a, P: Pixel> Iterator for BresenhamLinePixelIter<'a, P> { type Item = &'a P; fn next(&mut self) -> Option { self.iter .find(|&p| in_bounds(p, self.image)) .map(|(x, y)| self.image.get_pixel(x as u32, y as u32)) } } /// Iterates over the image pixels in a line segment using /// [Bresenham's line drawing algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm). pub struct BresenhamLinePixelIterMut<'a, P: Pixel> { iter: BresenhamLineIter, image: &'a mut Image

, } impl<'a, P: Pixel> BresenhamLinePixelIterMut<'a, P> { /// Creates a [`BresenhamLinePixelIterMut`](struct.BresenhamLinePixelIterMut.html) which will iterate over /// the image pixels with coordinates between `start` and `end`. pub fn new( image: &mut Image

, start: (f32, f32), end: (f32, f32), ) -> BresenhamLinePixelIterMut<'_, P> { assert!( image.width() >= 1 && image.height() >= 1, "BresenhamLinePixelIterMut does not support empty images" ); // The next two assertions are for https://github.com/image-rs/imageproc/issues/281 assert!(P::CHANNEL_COUNT > 0); assert!( image.width() < i32::MAX as u32 && image.height() < i32::MAX as u32, "Image dimensions are too large" ); let iter = BresenhamLineIter::new(clamp_point(start, image), clamp_point(end, image)); BresenhamLinePixelIterMut { iter, image } } } impl<'a, P: Pixel> Iterator for BresenhamLinePixelIterMut<'a, P> { type Item = &'a mut P; fn next(&mut self) -> Option { self.iter .find(|&p| in_bounds(p, self.image)) .map(|(x, y)| self.image.get_pixel_mut(x as u32, y as u32)) .map(|p| unsafe { transmute(p) }) } } /// Draws a line segment on a new copy of an image. /// /// Draws as much of the line segment between start and end as lies inside the image bounds. /// /// Uses [Bresenham's line drawing algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm). #[must_use = "the function does not modify the original image"] pub fn draw_line_segment( image: &I, start: (f32, f32), end: (f32, f32), color: I::Pixel, ) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_line_segment_mut(&mut out, start, end, color); out } /// Draws a line segment on an image in place. /// /// Draws as much of the line segment between start and end as lies inside the image bounds. /// /// Uses [Bresenham's line drawing algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm). pub fn draw_line_segment_mut(canvas: &mut C, start: (f32, f32), end: (f32, f32), color: C::Pixel) where C: Canvas, { let (width, height) = canvas.dimensions(); let in_bounds = |x, y| x >= 0 && x < width as i32 && y >= 0 && y < height as i32; let line_iterator = BresenhamLineIter::new(start, end); for point in line_iterator { let x = point.0; let y = point.1; if in_bounds(x, y) { canvas.draw_pixel(x as u32, y as u32, color); } } } /// Draws an antialised line segment on a new copy of an image. /// /// Draws as much of the line segment between `start` and `end` as lies inside the image bounds. /// /// The parameters of blend are (line color, original color, line weight). /// Consider using [`interpolate`](fn.interpolate.html) for blend. /// /// Uses [Xu's line drawing algorithm](https://en.wikipedia.org/wiki/Xiaolin_Wu%27s_line_algorithm). #[must_use = "the function does not modify the original image"] pub fn draw_antialiased_line_segment( image: &I, start: (i32, i32), end: (i32, i32), color: I::Pixel, blend: B, ) -> Image where I: GenericImage, B: Fn(I::Pixel, I::Pixel, f32) -> I::Pixel, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_antialiased_line_segment_mut(&mut out, start, end, color, blend); out } /// Draws an antialised line segment on an image in place. /// /// Draws as much of the line segment between `start` and `end` as lies inside the image bounds. /// /// The parameters of blend are (line color, original color, line weight). /// Consider using [`interpolate`](fn.interpolate.html) for blend. /// /// Uses [Xu's line drawing algorithm](https://en.wikipedia.org/wiki/Xiaolin_Wu%27s_line_algorithm). pub fn draw_antialiased_line_segment_mut( image: &mut I, start: (i32, i32), end: (i32, i32), color: I::Pixel, blend: B, ) where I: GenericImage, B: Fn(I::Pixel, I::Pixel, f32) -> I::Pixel, { let (mut x0, mut y0) = (start.0, start.1); let (mut x1, mut y1) = (end.0, end.1); let is_steep = (y1 - y0).abs() > (x1 - x0).abs(); if is_steep { if y0 > y1 { swap(&mut x0, &mut x1); swap(&mut y0, &mut y1); } let plotter = Plotter { image, transform: |x, y| (y, x), blend, }; plot_wu_line(plotter, (y0, x0), (y1, x1), color); } else { if x0 > x1 { swap(&mut x0, &mut x1); swap(&mut y0, &mut y1); } let plotter = Plotter { image, transform: |x, y| (x, y), blend, }; plot_wu_line(plotter, (x0, y0), (x1, y1), color); }; } fn plot_wu_line( mut plotter: Plotter<'_, I, T, B>, start: (i32, i32), end: (i32, i32), color: I::Pixel, ) where I: GenericImage, T: Fn(i32, i32) -> (i32, i32), B: Fn(I::Pixel, I::Pixel, f32) -> I::Pixel, { let dx = end.0 - start.0; let dy = end.1 - start.1; let gradient = dy as f32 / dx as f32; let mut fy = start.1 as f32; for x in start.0..(end.0 + 1) { plotter.plot(x, fy as i32, color, 1.0 - fy.fract()); plotter.plot(x, fy as i32 + 1, color, fy.fract()); fy += gradient; } } struct Plotter<'a, I, T, B> where I: GenericImage, T: Fn(i32, i32) -> (i32, i32), B: Fn(I::Pixel, I::Pixel, f32) -> I::Pixel, { image: &'a mut I, transform: T, blend: B, } impl<'a, I, T, B> Plotter<'a, I, T, B> where I: GenericImage, T: Fn(i32, i32) -> (i32, i32), B: Fn(I::Pixel, I::Pixel, f32) -> I::Pixel, { fn in_bounds(&self, x: i32, y: i32) -> bool { x >= 0 && x < self.image.width() as i32 && y >= 0 && y < self.image.height() as i32 } pub fn plot(&mut self, x: i32, y: i32, line_color: I::Pixel, line_weight: f32) { let (x_trans, y_trans) = (self.transform)(x, y); if self.in_bounds(x_trans, y_trans) { let original = self.image.get_pixel(x_trans as u32, y_trans as u32); let blended = (self.blend)(line_color, original, line_weight); self.image .put_pixel(x_trans as u32, y_trans as u32, blended); } } } #[cfg(test)] mod tests { use super::*; use image::{GrayImage, Luma}; // As draw_line_segment is implemented in terms of BresenhamLineIter we // haven't bothered wriing any tests specifically for BresenhamLineIter itself. // Octants for line directions: // // \ 5 | 6 / // 4 \ | / 7 // --- --- // 3 / | \ 0 // / 2 | 1 \ #[test] fn test_draw_line_segment_horizontal() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 4, 4, 4, 4, 4; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1); let right = draw_line_segment(&image, (-3f32, 1f32), (6f32, 1f32), Luma([4u8])); assert_pixels_eq!(right, expected); let left = draw_line_segment(&image, (6f32, 1f32), (-3f32, 1f32), Luma([4u8])); assert_pixels_eq!(left, expected); } #[test] fn test_draw_line_segment_oct0_and_oct4() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 9, 9, 1, 1; 1, 1, 1, 9, 9; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1); let oct0 = draw_line_segment(&image, (1f32, 1f32), (4f32, 2f32), Luma([9u8])); assert_pixels_eq!(oct0, expected); let oct4 = draw_line_segment(&image, (4f32, 2f32), (1f32, 1f32), Luma([9u8])); assert_pixels_eq!(oct4, expected); } #[test] fn test_draw_line_segment_diagonal() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 6, 1, 1, 1; 1, 1, 6, 1, 1; 1, 1, 1, 6, 1; 1, 1, 1, 1, 1); let down_right = draw_line_segment(&image, (1f32, 1f32), (3f32, 3f32), Luma([6u8])); assert_pixels_eq!(down_right, expected); let up_left = draw_line_segment(&image, (3f32, 3f32), (1f32, 1f32), Luma([6u8])); assert_pixels_eq!(up_left, expected); } #[test] fn test_draw_line_segment_oct1_and_oct5() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 5, 1, 1, 1, 1; 5, 1, 1, 1, 1; 5, 1, 1, 1, 1; 1, 5, 1, 1, 1; 1, 5, 1, 1, 1); let oct1 = draw_line_segment(&image, (0f32, 0f32), (1f32, 4f32), Luma([5u8])); assert_pixels_eq!(oct1, expected); let oct5 = draw_line_segment(&image, (1f32, 4f32), (0f32, 0f32), Luma([5u8])); assert_pixels_eq!(oct5, expected); } #[test] fn test_draw_line_segment_vertical() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 1, 1, 8, 1; 1, 1, 1, 8, 1; 1, 1, 1, 8, 1; 1, 1, 1, 1, 1); let down = draw_line_segment(&image, (3f32, 1f32), (3f32, 3f32), Luma([8u8])); assert_pixels_eq!(down, expected); let up = draw_line_segment(&image, (3f32, 3f32), (3f32, 1f32), Luma([8u8])); assert_pixels_eq!(up, expected); } #[test] fn test_draw_line_segment_oct2_and_oct6() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 4, 1, 1; 1, 1, 4, 1, 1; 1, 4, 1, 1, 1; 1, 4, 1, 1, 1; 1, 1, 1, 1, 1); let oct2 = draw_line_segment(&image, (2f32, 0f32), (1f32, 3f32), Luma([4u8])); assert_pixels_eq!(oct2, expected); let oct6 = draw_line_segment(&image, (1f32, 3f32), (2f32, 0f32), Luma([4u8])); assert_pixels_eq!(oct6, expected); } #[test] fn test_draw_line_segment_oct3_and_oct7() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 2, 2; 2, 2, 2, 1, 1); let oct3 = draw_line_segment(&image, (0f32, 4f32), (5f32, 3f32), Luma([2u8])); assert_pixels_eq!(oct3, expected); let oct7 = draw_line_segment(&image, (5f32, 3f32), (0f32, 4f32), Luma([2u8])); assert_pixels_eq!(oct7, expected); } #[test] fn test_draw_antialiased_line_segment_horizontal_and_vertical() { use crate::pixelops::interpolate; use image::imageops::rotate270; let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 2, 2, 2, 2; 1, 1, 1, 1, 1); let color = Luma([2u8]); // Deliberately ends one pixel out of bounds let right = draw_antialiased_line_segment(&image, (1, 3), (5, 3), color, interpolate); assert_pixels_eq!(right, expected); // Deliberately starts one pixel out of bounds let left = draw_antialiased_line_segment(&image, (5, 3), (1, 3), color, interpolate); assert_pixels_eq!(left, expected); // Deliberately starts one pixel out of bounds let down = draw_antialiased_line_segment(&image, (3, -1), (3, 3), color, interpolate); assert_pixels_eq!(down, rotate270(&expected)); // Deliberately end one pixel out of bounds let up = draw_antialiased_line_segment(&image, (3, 3), (3, -1), color, interpolate); assert_pixels_eq!(up, rotate270(&expected)); } #[test] fn test_draw_antialiased_line_segment_diagonal() { use crate::pixelops::interpolate; use image::imageops::rotate90; let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 1, 2, 1, 1; 1, 2, 1, 1, 1; 2, 1, 1, 1, 1; 1, 1, 1, 1, 1); let color = Luma([2u8]); let up_right = draw_antialiased_line_segment(&image, (0, 3), (2, 1), color, interpolate); assert_pixels_eq!(up_right, expected); let down_left = draw_antialiased_line_segment(&image, (2, 1), (0, 3), color, interpolate); assert_pixels_eq!(down_left, expected); let up_left = draw_antialiased_line_segment(&image, (1, 0), (3, 2), color, interpolate); assert_pixels_eq!(up_left, rotate90(&expected)); let down_right = draw_antialiased_line_segment(&image, (3, 2), (1, 0), color, interpolate); assert_pixels_eq!(down_right, rotate90(&expected)); } #[test] fn test_draw_antialiased_line_segment_oct7_and_oct3() { use crate::pixelops::interpolate; let image = GrayImage::from_pixel(5, 5, Luma([1u8])); // Gradient is 3/4 let expected = gray_image!( 1, 1, 1, 13, 50; 1, 1, 25, 37, 1; 1, 37, 25, 1, 1; 50, 13, 1, 1, 1; 1, 1, 1, 1, 1); let color = Luma([50u8]); let oct7 = draw_antialiased_line_segment(&image, (0, 3), (4, 0), color, interpolate); assert_pixels_eq!(oct7, expected); let oct3 = draw_antialiased_line_segment(&image, (4, 0), (0, 3), color, interpolate); assert_pixels_eq!(oct3, expected); } #[test] fn test_draw_line_segment_horizontal_using_bresenham_line_pixel_iter_mut() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 4, 4, 4, 4, 4; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 1, 1, 1); let mut right = image.clone(); { let right_iter = BresenhamLinePixelIterMut::new(&mut right, (-3f32, 1f32), (6f32, 1f32)); for p in right_iter { *p = Luma([4u8]); } } assert_pixels_eq!(right, expected); let mut left = image.clone(); { let left_iter = BresenhamLinePixelIterMut::new(&mut left, (6f32, 1f32), (-3f32, 1f32)); for p in left_iter { *p = Luma([4u8]); } } assert_pixels_eq!(left, expected); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use image::{GrayImage, Luma}; macro_rules! bench_antialiased_lines { ($name:ident, $start:expr, $end:expr) => { #[bench] fn $name(b: &mut test::Bencher) { use super::draw_antialiased_line_segment_mut; use crate::pixelops::interpolate; let mut image = GrayImage::new(500, 500); let color = Luma([50u8]); b.iter(|| { draw_antialiased_line_segment_mut(&mut image, $start, $end, color, interpolate); test::black_box(&image); }); } }; } bench_antialiased_lines!( bench_draw_antialiased_line_segment_horizontal, (10, 10), (450, 10) ); bench_antialiased_lines!( bench_draw_antialiased_line_segment_vertical, (10, 10), (10, 450) ); bench_antialiased_lines!( bench_draw_antialiased_line_segment_diagonal, (10, 10), (450, 450) ); bench_antialiased_lines!( bench_draw_antialiased_line_segment_shallow, (10, 10), (450, 80) ); } imageproc-0.25.0/src/drawing/mod.rs000064400000000000000000000031271046102023000152300ustar 00000000000000//! Helpers for drawing basic shapes on images. //! //! Every `draw_` function comes in two variants: one creates a new copy of the input image, one modifies the image in place. //! The latter is more memory efficient, but you lose the original image. mod bezier; pub use self::bezier::{draw_cubic_bezier_curve, draw_cubic_bezier_curve_mut}; mod canvas; pub use self::canvas::{Blend, Canvas}; mod conics; pub use self::conics::{ draw_filled_circle, draw_filled_circle_mut, draw_filled_ellipse, draw_filled_ellipse_mut, draw_hollow_circle, draw_hollow_circle_mut, draw_hollow_ellipse, draw_hollow_ellipse_mut, }; mod cross; pub use self::cross::{draw_cross, draw_cross_mut}; mod line; pub use self::line::{ draw_antialiased_line_segment, draw_antialiased_line_segment_mut, draw_line_segment, draw_line_segment_mut, BresenhamLineIter, BresenhamLinePixelIter, BresenhamLinePixelIterMut, }; mod polygon; pub use self::polygon::{ draw_antialiased_polygon, draw_antialiased_polygon_mut, draw_hollow_polygon, draw_hollow_polygon_mut, draw_polygon, draw_polygon_mut, }; mod rect; pub use self::rect::{ draw_filled_rect, draw_filled_rect_mut, draw_hollow_rect, draw_hollow_rect_mut, }; mod text; pub use self::text::{draw_text, draw_text_mut, text_size}; // Set pixel at (x, y) to color if this point lies within image bounds, // otherwise do nothing. fn draw_if_in_bounds(canvas: &mut C, x: i32, y: i32, color: C::Pixel) where C: Canvas, { if x >= 0 && x < canvas.width() as i32 && y >= 0 && y < canvas.height() as i32 { canvas.draw_pixel(x as u32, y as u32, color); } } imageproc-0.25.0/src/drawing/polygon.rs000064400000000000000000000202301046102023000161320ustar 00000000000000use crate::definitions::Image; use crate::drawing::line::{draw_antialiased_line_segment_mut, draw_line_segment_mut}; use crate::drawing::Canvas; use crate::point::Point; use image::{GenericImage, ImageBuffer}; use std::cmp::{max, min}; #[must_use = "the function does not modify the original image"] fn draw_polygon_with( image: &I, poly: &[Point], color: I::Pixel, plotter: L, ) -> Image where I: GenericImage, L: Fn(&mut Image, (f32, f32), (f32, f32), I::Pixel), { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_polygon_with_mut(&mut out, poly, color, plotter); out } fn draw_polygon_with_mut(canvas: &mut C, poly: &[Point], color: C::Pixel, plotter: L) where C: Canvas, L: Fn(&mut C, (f32, f32), (f32, f32), C::Pixel), { if poly.is_empty() { return; } if poly[0] == poly[poly.len() - 1] { panic!( "First point {:?} == last point {:?}", poly[0], poly[poly.len() - 1] ); } let mut y_min = i32::MAX; let mut y_max = i32::MIN; for p in poly { y_min = min(y_min, p.y); y_max = max(y_max, p.y); } let (width, height) = canvas.dimensions(); // Intersect polygon vertical range with image bounds y_min = max(0, min(y_min, height as i32 - 1)); y_max = max(0, min(y_max, height as i32 - 1)); let mut closed: Vec> = poly.to_vec(); closed.push(poly[0]); let edges: Vec<&[Point]> = closed.windows(2).collect(); let mut intersections = Vec::new(); for y in y_min..y_max + 1 { for edge in &edges { let p0 = edge[0]; let p1 = edge[1]; if p0.y <= y && p1.y >= y || p1.y <= y && p0.y >= y { if p0.y == p1.y { // Need to handle horizontal lines specially intersections.push(p0.x); intersections.push(p1.x); } else if p0.y == y || p1.y == y { if p1.y > y { intersections.push(p0.x); } if p0.y > y { intersections.push(p1.x); } } else { let fraction = (y - p0.y) as f32 / (p1.y - p0.y) as f32; let inter = p0.x as f32 + fraction * (p1.x - p0.x) as f32; intersections.push(inter.round() as i32); } } } intersections.sort_unstable(); intersections.chunks(2).for_each(|range| { let mut from = min(range[0], width as i32); let mut to = min(range[1], width as i32 - 1); if from < width as i32 && to >= 0 { // draw only if range appears on the canvas from = max(0, from); to = max(0, to); for x in from..to + 1 { canvas.draw_pixel(x as u32, y as u32, color); } } }); intersections.clear(); } for edge in &edges { let start = (edge[0].x as f32, edge[0].y as f32); let end = (edge[1].x as f32, edge[1].y as f32); plotter(canvas, start, end, color); } } /// Draws a polygon and its contents on a new copy of an image. /// /// Draws as much of a filled polygon as lies within image bounds. The provided /// list of points should be an open path, i.e. the first and last points must not be equal. /// An implicit edge is added from the last to the first point in the slice. pub fn draw_polygon(image: &I, poly: &[Point], color: I::Pixel) -> Image where I: GenericImage, { draw_polygon_with(image, poly, color, draw_line_segment_mut) } /// Draws a polygon and its contents on an image in place. /// /// Draws as much of a filled polygon as lies within image bounds. The provided /// list of points should be an open path, i.e. the first and last points must not be equal. /// An implicit edge is added from the last to the first point in the slice. pub fn draw_polygon_mut(canvas: &mut C, poly: &[Point], color: C::Pixel) where C: Canvas, { draw_polygon_with_mut(canvas, poly, color, draw_line_segment_mut); } /// Draws an anti-aliased polygon polygon and its contents on a new copy of an image. /// /// Draws as much of a filled polygon as lies within image bounds. The provided /// list of points should be an open path, i.e. the first and last points must not be equal. /// An implicit edge is added from the last to the first point in the slice. /// /// The parameters of blend are (line color, original color, line weight). /// Consider using [`interpolate`](fn.interpolate.html) for blend. pub fn draw_antialiased_polygon( image: &I, poly: &[Point], color: I::Pixel, blend: B, ) -> Image where I: GenericImage, B: Fn(I::Pixel, I::Pixel, f32) -> I::Pixel, { draw_polygon_with(image, poly, color, |image, start, end, color| { draw_antialiased_line_segment_mut( image, (start.0 as i32, start.1 as i32), (end.0 as i32, end.1 as i32), color, &blend, ) }) } /// Draws an anti-aliased polygon and its contents on an image in place. /// /// Draws as much of a filled polygon as lies within image bounds. The provided /// list of points should be an open path, i.e. the first and last points must not be equal. /// An implicit edge is added from the last to the first point in the slice. /// /// The parameters of blend are (line color, original color, line weight). /// Consider using [`interpolate`](fn.interpolate.html) for blend. pub fn draw_antialiased_polygon_mut( image: &mut I, poly: &[Point], color: I::Pixel, blend: B, ) where I: GenericImage, B: Fn(I::Pixel, I::Pixel, f32) -> I::Pixel, { draw_polygon_with_mut(image, poly, color, |image, start, end, color| { draw_antialiased_line_segment_mut( image, (start.0 as i32, start.1 as i32), (end.0 as i32, end.1 as i32), color, &blend, ) }); } /// Draws the outline of a polygon on an image in place. /// /// Draws as much of the outline of the polygon as lies within image bounds. The provided /// list of points should be in polygon order and be an open path, i.e. the first /// and last points must not be equal. The edges of the polygon will be drawn in the order /// that they are provided, and an implicit edge will be added from the last to the first /// point in the slice. pub fn draw_hollow_polygon( image: &mut I, poly: &[Point], color: I::Pixel, ) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_hollow_polygon_mut(&mut out, poly, color); out } /// Draws the outline of a polygon on an image in place. /// /// Draws as much of the outline of the polygon as lies within image bounds. The provided /// list of points should be in polygon order and be an open path, i.e. the first /// and last points must not be equal. The edges of the polygon will be drawn in the order /// that they are provided, and an implicit edge will be added from the last to the first /// point in the slice. pub fn draw_hollow_polygon_mut(canvas: &mut C, poly: &[Point], color: C::Pixel) where C: Canvas, { if poly.is_empty() { return; } if poly.len() < 2 { panic!( "Polygon only has {} points, but at least two are needed.", poly.len(), ); } if poly[0] == poly[poly.len() - 1] { panic!( "First point {:?} == last point {:?}", poly[0], poly[poly.len() - 1] ); } for window in poly.windows(2) { crate::drawing::draw_line_segment_mut( canvas, (window[0].x, window[0].y), (window[1].x, window[1].y), color, ); } let first = poly[0]; let last = poly.iter().last().unwrap(); crate::drawing::draw_line_segment_mut(canvas, (first.x, first.y), (last.x, last.y), color); } imageproc-0.25.0/src/drawing/rect.rs000064400000000000000000000125271046102023000154120ustar 00000000000000use crate::definitions::Image; use crate::drawing::line::draw_line_segment_mut; use crate::drawing::Canvas; use crate::rect::Rect; use image::{GenericImage, ImageBuffer}; use std::f32; /// Draws the outline of a rectangle on a new copy of an image. /// /// Draws as much of the boundary of the rectangle as lies inside the image bounds. #[must_use = "the function does not modify the original image"] pub fn draw_hollow_rect(image: &I, rect: Rect, color: I::Pixel) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_hollow_rect_mut(&mut out, rect, color); out } /// Draws the outline of a rectangle on an image in place. /// /// Draws as much of the boundary of the rectangle as lies inside the image bounds. pub fn draw_hollow_rect_mut(canvas: &mut C, rect: Rect, color: C::Pixel) where C: Canvas, { let left = rect.left() as f32; let right = rect.right() as f32; let top = rect.top() as f32; let bottom = rect.bottom() as f32; draw_line_segment_mut(canvas, (left, top), (right, top), color); draw_line_segment_mut(canvas, (left, bottom), (right, bottom), color); draw_line_segment_mut(canvas, (left, top), (left, bottom), color); draw_line_segment_mut(canvas, (right, top), (right, bottom), color); } /// Draws a rectangle and its contents on a new copy of an image. /// /// Draws as much of the rectangle and its contents as lies inside the image bounds. #[must_use = "the function does not modify the original image"] pub fn draw_filled_rect(image: &I, rect: Rect, color: I::Pixel) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_filled_rect_mut(&mut out, rect, color); out } /// Draws a rectangle and its contents on an image in place. /// /// Draws as much of the rectangle and its contents as lies inside the image bounds. pub fn draw_filled_rect_mut(canvas: &mut C, rect: Rect, color: C::Pixel) where C: Canvas, { let canvas_bounds = Rect::at(0, 0).of_size(canvas.width(), canvas.height()); if let Some(intersection) = canvas_bounds.intersect(rect) { for dy in 0..intersection.height() { for dx in 0..intersection.width() { let x = intersection.left() as u32 + dx; let y = intersection.top() as u32 + dy; canvas.draw_pixel(x, y, color); } } } } #[cfg(test)] mod tests { use super::*; use crate::drawing::Blend; use crate::rect::Rect; use image::{GrayImage, Luma, Pixel, Rgba, RgbaImage}; #[test] fn test_draw_hollow_rect() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 1, 1, 1, 1; 1, 1, 4, 4, 4; 1, 1, 4, 1, 4; 1, 1, 4, 4, 4); let actual = draw_hollow_rect(&image, Rect::at(2, 2).of_size(3, 3), Luma([4u8])); assert_pixels_eq!(actual, expected); } #[test] fn test_draw_filled_rect() { let image = GrayImage::from_pixel(5, 5, Luma([1u8])); let expected = gray_image!( 1, 1, 1, 1, 1; 1, 4, 4, 4, 1; 1, 4, 4, 4, 1; 1, 4, 4, 4, 1; 1, 1, 1, 1, 1); let actual = draw_filled_rect(&image, Rect::at(1, 1).of_size(3, 3), Luma([4u8])); assert_pixels_eq!(actual, expected); } #[test] fn test_draw_blended_filled_rect() { // https://github.com/image-rs/imageproc/issues/261 let white = Rgba([255u8, 255u8, 255u8, 255u8]); let blue = Rgba([0u8, 0u8, 255u8, 255u8]); let semi_transparent_red = Rgba([255u8, 0u8, 0u8, 127u8]); let mut image = Blend(RgbaImage::from_pixel(5, 5, white)); draw_filled_rect_mut(&mut image, Rect::at(1, 1).of_size(3, 3), blue); draw_filled_rect_mut( &mut image, Rect::at(2, 2).of_size(1, 1), semi_transparent_red, ); // The central pixel should be blended let mut blended = blue; blended.blend(&semi_transparent_red); #[rustfmt::skip] let expected = [white, white, white, white, white, white, blue, blue, blue, white, white, blue, blended, blue, white, white, blue, blue, blue, white, white, white, white, white, white]; let expected = RgbaImage::from_fn(5, 5, |x, y| expected[(y * 5 + x) as usize]); assert_pixels_eq!(image.0, expected); // Draw an opaque rectangle over the central pixel as a sanity check that // we're blending in the correct direction only. draw_filled_rect_mut(&mut image, Rect::at(2, 2).of_size(1, 1), blue); assert_eq!(*image.0.get_pixel(2, 2), blue); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::rect::Rect; use image::{Rgb, RgbImage}; use test::{black_box, Bencher}; #[bench] fn bench_draw_filled_rect_mut_rgb(b: &mut Bencher) { let mut image = RgbImage::new(200, 200); let color = Rgb([120u8, 60u8, 47u8]); let rect = Rect::at(50, 50).of_size(80, 90); b.iter(|| { draw_filled_rect_mut(&mut image, rect, color); black_box(&image); }); } } imageproc-0.25.0/src/drawing/text.rs000064400000000000000000000064121046102023000154350ustar 00000000000000use image::{GenericImage, ImageBuffer, Pixel}; use std::f32; use crate::definitions::{Clamp, Image}; use crate::drawing::Canvas; use crate::pixelops::weighted_sum; use ab_glyph::{point, Font, GlyphId, OutlinedGlyph, PxScale, Rect, ScaleFont}; fn layout_glyphs( scale: impl Into + Copy, font: &impl Font, text: &str, mut f: impl FnMut(OutlinedGlyph, Rect), ) -> (u32, u32) { let (mut w, mut h) = (0f32, 0f32); let font = font.as_scaled(scale); let mut last: Option = None; for c in text.chars() { let glyph_id = font.glyph_id(c); let glyph = glyph_id.with_scale_and_position(scale, point(w, font.ascent())); w += font.h_advance(glyph_id); if let Some(g) = font.outline_glyph(glyph) { if let Some(last) = last { w += font.kern(glyph_id, last); } last = Some(glyph_id); let bb = g.px_bounds(); h = h.max(bb.height()); f(g, bb); } } (w as u32, h as u32) } /// Get the width and height of the given text, rendered with the given font and scale. /// /// Note that this function *does not* support newlines, you must do this manually. pub fn text_size(scale: impl Into + Copy, font: &impl Font, text: &str) -> (u32, u32) { layout_glyphs(scale, font, text, |_, _| {}) } /// Draws colored text on an image in place. /// /// `scale` is augmented font scaling on both the x and y axis (in pixels). /// /// Note that this function *does not* support newlines, you must do this manually. pub fn draw_text_mut( canvas: &mut C, color: C::Pixel, x: i32, y: i32, scale: impl Into + Copy, font: &impl Font, text: &str, ) where C: Canvas, ::Subpixel: Into + Clamp, { let image_width = canvas.width() as i32; let image_height = canvas.height() as i32; layout_glyphs(scale, font, text, |g, bb| { g.draw(|gx, gy, gv| { let image_x = gx as i32 + x + bb.min.x.round() as i32; let image_y = gy as i32 + y + bb.min.y.round() as i32; let gv = gv.clamp(0.0, 1.0); if (0..image_width).contains(&image_x) && (0..image_height).contains(&image_y) { let image_x = image_x as u32; let image_y = image_y as u32; let pixel = canvas.get_pixel(image_x, image_y); let weighted_color = weighted_sum(pixel, color, 1.0 - gv, gv); canvas.draw_pixel(image_x, image_y, weighted_color); } }) }); } /// Draws colored text on a new copy of an image. /// /// `scale` is augmented font scaling on both the x and y axis (in pixels). /// /// Note that this function *does not* support newlines, you must do this manually. #[must_use = "the function does not modify the original image"] pub fn draw_text( image: &I, color: I::Pixel, x: i32, y: i32, scale: impl Into + Copy, font: &impl Font, text: &str, ) -> Image where I: GenericImage, ::Subpixel: Into + Clamp, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_text_mut(&mut out, color, x, y, scale, font, text); out } imageproc-0.25.0/src/edges.rs000064400000000000000000000160201046102023000141010ustar 00000000000000//! Functions for detecting edges in images. use crate::definitions::{HasBlack, HasWhite}; use crate::filter::gaussian_blur_f32; use crate::gradients::{horizontal_sobel, vertical_sobel}; use image::{GenericImageView, GrayImage, ImageBuffer, Luma}; use std::f32; /// Runs the canny edge detection algorithm. /// /// Returns a binary image where edge pixels have a value of 255 /// and non-edge pixels a value of 0. /// /// # Params /// /// - `low_threshold`: Low threshold for the hysteresis procedure. /// Edges with a strength higher than the low threshold will appear /// in the output image, if there are strong edges nearby. /// - `high_threshold`: High threshold for the hysteresis procedure. /// Edges with a strength higher than the high threshold will always /// appear as edges in the output image. /// /// The greatest possible edge strength (and so largest sensible threshold) /// is`sqrt(5) * 2 * 255`, or approximately 1140.39. /// /// This odd looking value is the result of using a standard /// definition of edge strength: the strength of an edge at a point `p` is /// defined to be `sqrt(dx^2 + dy^2)`, where `dx` and `dy` are the values /// of the horizontal and vertical Sobel gradients at `p`. pub fn canny(image: &GrayImage, low_threshold: f32, high_threshold: f32) -> GrayImage { assert!(high_threshold >= low_threshold); // Heavily based on the implementation proposed by wikipedia. // 1. Gaussian blur. const SIGMA: f32 = 1.4; let blurred = gaussian_blur_f32(image, SIGMA); // 2. Intensity of gradients. let gx = horizontal_sobel(&blurred); let gy = vertical_sobel(&blurred); let g: Vec = gx .iter() .zip(gy.iter()) .map(|(h, v)| (*h as f32).hypot(*v as f32)) .collect::>(); let g = ImageBuffer::from_raw(image.width(), image.height(), g).unwrap(); // 3. Non-maximum-suppression (Make edges thinner) let thinned = non_maximum_suppression(&g, &gx, &gy); // 4. Hysteresis to filter out edges based on thresholds. hysteresis(&thinned, low_threshold, high_threshold) } /// Finds local maxima to make the edges thinner. fn non_maximum_suppression( g: &ImageBuffer, Vec>, gx: &ImageBuffer, Vec>, gy: &ImageBuffer, Vec>, ) -> ImageBuffer, Vec> { const RADIANS_TO_DEGREES: f32 = 180f32 / f32::consts::PI; let mut out = ImageBuffer::from_pixel(g.width(), g.height(), Luma([0.0])); for y in 1..g.height() - 1 { for x in 1..g.width() - 1 { let x_gradient = gx[(x, y)][0] as f32; let y_gradient = gy[(x, y)][0] as f32; let mut angle = (y_gradient).atan2(x_gradient) * RADIANS_TO_DEGREES; if angle < 0.0 { angle += 180.0 } // Clamp angle. let clamped_angle = if !(22.5..157.5).contains(&angle) { 0 } else if (22.5..67.5).contains(&angle) { 45 } else if (67.5..112.5).contains(&angle) { 90 } else if (112.5..157.5).contains(&angle) { 135 } else { unreachable!() }; // Get the two perpendicular neighbors. let (cmp1, cmp2) = unsafe { match clamped_angle { 0 => (g.unsafe_get_pixel(x - 1, y), g.unsafe_get_pixel(x + 1, y)), 45 => ( g.unsafe_get_pixel(x + 1, y + 1), g.unsafe_get_pixel(x - 1, y - 1), ), 90 => (g.unsafe_get_pixel(x, y - 1), g.unsafe_get_pixel(x, y + 1)), 135 => ( g.unsafe_get_pixel(x - 1, y + 1), g.unsafe_get_pixel(x + 1, y - 1), ), _ => unreachable!(), } }; let pixel = *g.get_pixel(x, y); // If the pixel is not a local maximum, suppress it. if pixel[0] < cmp1[0] || pixel[0] < cmp2[0] { out.put_pixel(x, y, Luma([0.0])); } else { out.put_pixel(x, y, pixel); } } } out } /// Filter out edges with the thresholds. /// Non-recursive breadth-first search. fn hysteresis( input: &ImageBuffer, Vec>, low_thresh: f32, high_thresh: f32, ) -> ImageBuffer, Vec> { let max_brightness = Luma::white(); let min_brightness = Luma::black(); // Init output image as all black. let mut out = ImageBuffer::from_pixel(input.width(), input.height(), min_brightness); // Stack. Possible optimization: Use previously allocated memory, i.e. gx. let mut edges = Vec::with_capacity(((input.width() * input.height()) / 2) as usize); for y in 1..input.height() - 1 { for x in 1..input.width() - 1 { let inp_pix = *input.get_pixel(x, y); let out_pix = *out.get_pixel(x, y); // If the edge strength is higher than high_thresh, mark it as an edge. if inp_pix[0] >= high_thresh && out_pix[0] == 0 { out.put_pixel(x, y, max_brightness); edges.push((x, y)); // Track neighbors until no neighbor is >= low_thresh. while let Some((nx, ny)) = edges.pop() { let neighbor_indices = [ (nx + 1, ny), (nx + 1, ny + 1), (nx, ny + 1), (nx - 1, ny - 1), (nx - 1, ny), (nx - 1, ny + 1), ]; for neighbor_idx in &neighbor_indices { let in_neighbor = *input.get_pixel(neighbor_idx.0, neighbor_idx.1); let out_neighbor = *out.get_pixel(neighbor_idx.0, neighbor_idx.1); if in_neighbor[0] >= low_thresh && out_neighbor[0] == 0 { out.put_pixel(neighbor_idx.0, neighbor_idx.1, max_brightness); edges.push((neighbor_idx.0, neighbor_idx.1)); } } } } } } out } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::canny; use crate::drawing::draw_filled_rect_mut; use crate::rect::Rect; use ::test; use image::{GrayImage, Luma}; fn edge_detect_bench_image(width: u32, height: u32) -> GrayImage { let mut image = GrayImage::new(width, height); let (w, h) = (width as i32, height as i32); let large = Rect::at(w / 4, h / 4).of_size(width / 2, height / 2); let small = Rect::at(9, 9).of_size(3, 3); draw_filled_rect_mut(&mut image, large, Luma([255])); draw_filled_rect_mut(&mut image, small, Luma([255])); image } #[bench] fn bench_canny(b: &mut test::Bencher) { let image = edge_detect_bench_image(250, 250); b.iter(|| { let output = canny(&image, 250.0, 300.0); test::black_box(output); }); } } imageproc-0.25.0/src/filter/median.rs000064400000000000000000000362431046102023000155450ustar 00000000000000use crate::definitions::Image; use image::{GenericImageView, Pixel}; use std::cmp::{max, min}; /// Applies a median filter of given dimensions to an image. Each output pixel is the median /// of the pixels in a `(2 * x_radius + 1) * (2 * y_radius + 1)` kernel of pixels in the input image. /// /// Pads by continuity. Performs O(max(x_radius, y_radius)) operations per pixel. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::filter::median_filter; /// /// let image = gray_image!( /// 1, 2, 3; /// 200, 6, 7; /// 9, 100, 11 /// ); /// /// // Padding by continuity means that the values we use /// // for computing medians of boundary pixels are: /// // /// // 1 1 2 3 3 /// // ----------------- /// // 1 | 1 2 3 | 3 /// // /// // 200 | 200 6 7 | 7 /// // /// // 9 | 9 100 11 | 11 /// // ----------------- /// // 9 9 100 11 11 /// /// let filtered = gray_image!( /// 2, 3, 3; /// 9, 7, 7; /// 9, 11, 11 /// ); /// /// assert_pixels_eq!(median_filter(&image, 1, 1), filtered); /// # } /// ``` /// /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::filter::median_filter; /// /// // Image channels are handled independently. /// // This example sets the red channel to have the same /// // contents as the image from the grayscale example, /// // the green channel to a vertically inverted copy of that /// // image and the blue channel to be constant. /// // /// // See the grayscale image example for an explanation of how /// // boundary conditions are handled. /// /// let image = rgb_image!( /// [ 1, 9, 10], [ 2, 100, 10], [ 3, 11, 10]; /// [200, 200, 10], [ 6, 6, 10], [ 7, 7, 10]; /// [ 9, 1, 10], [100, 2, 10], [ 11, 3, 10] /// ); /// /// let filtered = rgb_image!( /// [ 2, 9, 10], [ 3, 11, 10], [ 3, 11, 10]; /// [ 9, 9, 10], [ 7, 7, 10], [ 7, 7, 10]; /// [ 9, 2, 10], [11, 3, 10], [11, 3, 10] /// ); /// /// assert_pixels_eq!(median_filter(&image, 1, 1), filtered); /// # } /// ``` /// /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::filter::median_filter; /// /// // This example uses a kernel with x_radius sets to 2 /// // and y_radius sets to 1, which leads to 5 * 3 kernel size. /// /// let image = gray_image!( /// 1, 2, 3, 4, 5; /// 255, 200, 4, 11, 7; /// 42, 17, 3, 2, 1; /// 9, 100, 11, 13, 14; /// 15, 87, 99, 21, 45 /// ); /// /// let filtered = gray_image!( /// 2, 3, 4, 5, 5; /// 17, 4, 4, 4, 4; /// 42, 13, 11, 11, 7; /// 15, 15, 15, 14, 14; /// 15, 15, 21, 45, 45 /// ); /// /// assert_pixels_eq!(median_filter(&image, 2, 1), filtered); /// # } /// ``` #[must_use = "the function does not modify the original image"] pub fn median_filter

(image: &Image

, x_radius: u32, y_radius: u32) -> Image

where P: Pixel, { let (width, height) = image.dimensions(); // Safety note: we rely on image dimensions being non-zero for uncheched indexing to be in bounds if width == 0 || height == 0 { return image.clone(); } // Safety note: we perform unchecked indexing in several places after checking at type i32 that a coordinate is in bounds if (width + x_radius) > i32::MAX as u32 || (height + y_radius) > i32::MAX as u32 { panic!("(width + x_radius) and (height + y_radius) must both be <= i32::MAX"); } let mut out = Image::

::new(width, height); let mut hist = initialise_histogram_for_top_left_pixel(image, x_radius, y_radius); slide_down_column(&mut hist, image, &mut out, 0, x_radius, y_radius); for x in 1..width { if x % 2 == 0 { slide_right(&mut hist, image, x, 0, x_radius, y_radius); slide_down_column(&mut hist, image, &mut out, x, x_radius, y_radius); } else { slide_right(&mut hist, image, x, height - 1, x_radius, y_radius); slide_up_column(&mut hist, image, &mut out, x, x_radius, y_radius); } } out } fn initialise_histogram_for_top_left_pixel

( image: &Image

, x_radius: u32, y_radius: u32, ) -> HistSet where P: Pixel, { let (width, height) = image.dimensions(); let kernel_size = (2 * x_radius + 1) * (2 * y_radius + 1); let num_channels = P::CHANNEL_COUNT; let mut hist = HistSet::new(num_channels, kernel_size); let rx = x_radius as i32; let ry = y_radius as i32; for dy in -ry..(ry + 1) { // Safety note: 0 <= py <= height - 1 let py = min(max(0, dy), height as i32 - 1) as u32; for dx in -rx..(rx + 1) { // Safety note: 0 <= px <= width - 1 let px = min(max(0, dx), width as i32 - 1) as u32; // Safety: px and py are in bounds as explained in the 'Safety note' comments above unsafe { hist.incr(image, px, py); } } } hist } fn slide_right

(hist: &mut HistSet, image: &Image

, x: u32, y: u32, rx: u32, ry: u32) where P: Pixel, { let (width, height) = image.dimensions(); // Safety note: unchecked indexing below relies on x and y being in bounds assert!(x < width); assert!(y < height); // Safety note: rx and ry are both >= 0 by construction let rx = rx as i32; let ry = ry as i32; // Safety note: 0 <= prev_x < width - rx - 1 < width let prev_x = max(0, x as i32 - rx - 1) as u32; // Safety note: 0 <= x + rx <= next_x <= width - 1 let next_x = min(x as i32 + rx, width as i32 - 1) as u32; for dy in -ry..(ry + 1) { // Safety note: 0 <= py <= height - 1 let py = min(max(0, y as i32 + dy), (height - 1) as i32) as u32; // Safety: prev_x and py are in bounds based on the 'Safety note' comments above unsafe { hist.decr(image, prev_x, py); } // Safety: next_x and py are in bounds based on the 'Safety note' comments above unsafe { hist.incr(image, next_x, py); } } } fn slide_down_column

( hist: &mut HistSet, image: &Image

, out: &mut Image

, x: u32, rx: u32, ry: u32, ) where P: Pixel, { let (width, height) = image.dimensions(); // Safety note: unchecked indexing below relies on x being in bounds assert!(x < width); // Safety note: rx and ry are both >= 0 by construction let rx = rx as i32; let ry = ry as i32; // Safety: hist.data.len() == P::CHANNEL_COUNT by construction unsafe { hist.set_to_median(out, x, 0); } for y in 1..height { // Safety note: 0 <= prev_y < height - ry - 1 < height let prev_y = max(0, y as i32 - ry - 1) as u32; // Safety note: 0 < 1 + ry <= next_y < height let next_y = min(y as i32 + ry, height as i32 - 1) as u32; for dx in -rx..(rx + 1) { // Safety note: 0 <= px < width let px = min(max(0, x as i32 + dx), (width - 1) as i32) as u32; // Safety: px and prev_y are in bounds based on the 'Safety note' comments above unsafe { hist.decr(image, px, prev_y); } // Safety: px and next_y are in bounds based on the 'Safety note' comments above unsafe { hist.incr(image, px, next_y); } } // Safety: hist.data.len() == P::CHANNEL_COUNT by construction unsafe { hist.set_to_median(out, x, y); } } } fn slide_up_column

( hist: &mut HistSet, image: &Image

, out: &mut Image

, x: u32, rx: u32, ry: u32, ) where P: Pixel, { let (width, height) = image.dimensions(); // Safety note: unchecked indexing below relies on x being in bounds assert!(x < width); // Safety note: rx and ry are both >= 0 by construction let rx = rx as i32; let ry = ry as i32; // Safety: hist.data.len() == P::CHANNEL_COUNT by construction unsafe { hist.set_to_median(out, x, height - 1); } for y in (0..(height - 1)).rev() { // Safety note: 0 < ry + 1 <= prev_y <= height - 1 let prev_y = min(y as i32 + ry + 1, height as i32 - 1) as u32; // Safety note: 0 <= next_y < height - 1 - ry < height let next_y = max(0, y as i32 - ry) as u32; for dx in -rx..(rx + 1) { // Safety note: 0 <= px <= width - 1 let px = min(max(0, x as i32 + dx), (width - 1) as i32) as u32; // Safety: px and prev_y are in bounds based on the 'Safety note' comments above unsafe { hist.decr(image, px, prev_y); } // Safety: px and next_y are in bounds based on the 'Safety note' comments above unsafe { hist.incr(image, px, next_y); } } // Safety: hist.data.len() == P::CHANNEL_COUNT by construction unsafe { hist.set_to_median(out, x, y); } } } // A collection of 256-slot histograms, one per image channel. // Used to implement median_filter. struct HistSet { // One histogram per image channel. data: Vec<[u32; 256]>, // Calls to `median` will only return the correct answer // if there are `expected_count` entries in the relevant // histogram in `data`. expected_count: u32, } impl HistSet { fn new(num_channels: u8, expected_count: u32) -> HistSet { // Can't use vec![[0u32; 256], num_channels as usize] // because arrays of length > 32 aren't cloneable. let mut data = Vec::with_capacity(num_channels as usize); for _ in 0..num_channels { data.push([0u32; 256]); } HistSet { data, expected_count, } } /// Safety: requires x and y to be within image bounds and P::CHANNEL_COUNT <= self.data.len() unsafe fn incr

(&mut self, image: &Image

, x: u32, y: u32) where P: Pixel, { let pixel = image.unsafe_get_pixel(x, y); let channels = pixel.channels(); for c in 0..channels.len() { let p = *channels.get_unchecked(c) as usize; let hist = self.data.get_unchecked_mut(c); *hist.get_unchecked_mut(p) += 1; } } /// Safety: requires x and y to be within image bounds and P::CHANNEL_COUNT <= self.data.len() unsafe fn decr

(&mut self, image: &Image

, x: u32, y: u32) where P: Pixel, { let pixel = image.unsafe_get_pixel(x, y); let channels = pixel.channels(); for c in 0..channels.len() { let p = *channels.get_unchecked(c) as usize; let hist = self.data.get_unchecked_mut(c); *hist.get_unchecked_mut(p) -= 1; } } /// Safety: requires P::CHANNEL_COUNT <= self.data.len() unsafe fn set_to_median

(&self, image: &mut Image

, x: u32, y: u32) where P: Pixel, { let target = image.get_pixel_mut(x, y); let channels = target.channels_mut(); for c in 0..channels.len() { *channels.get_unchecked_mut(c) = self.channel_median(c as u8); } } /// Safety: requires c < self.data.len() unsafe fn channel_median(&self, c: u8) -> u8 { let hist = self.data.get_unchecked(c as usize); let mut count = 0; for i in 0..256 { count += *hist.get_unchecked(i); if 2 * count >= self.expected_count { return i as u8; } } 255 } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::utils::gray_bench_image; use test::{black_box, Bencher}; macro_rules! bench_median_filter { ($name:ident, side: $s:expr, x_radius: $rx:expr, y_radius: $ry:expr) => { #[bench] fn $name(b: &mut Bencher) { let image = gray_bench_image($s, $s); b.iter(|| { let filtered = median_filter(&image, $rx, $ry); black_box(filtered); }) } }; } bench_median_filter!(bench_median_filter_s100_r1, side: 100, x_radius: 1,y_radius: 1); bench_median_filter!(bench_median_filter_s100_r4, side: 100, x_radius: 4,y_radius: 4); bench_median_filter!(bench_median_filter_s100_r8, side: 100, x_radius: 8,y_radius: 8); // benchmark on non-square kernels bench_median_filter!(bench_median_filter_s100_rx1_ry4, side: 100, x_radius: 1,y_radius: 4); bench_median_filter!(bench_median_filter_s100_rx1_ry8, side: 100, x_radius: 1,y_radius: 8); bench_median_filter!(bench_median_filter_s100_rx4_ry8, side: 100, x_radius: 4,y_radius: 1); bench_median_filter!(bench_median_filter_s100_rx8_ry1, side: 100, x_radius: 8,y_radius: 1); } #[cfg(test)] mod tests { use super::*; use crate::property_testing::GrayTestImage; use crate::utils::pixel_diff_summary; use image::{GrayImage, Luma}; use quickcheck::{quickcheck, TestResult}; use std::cmp::{max, min}; // Reference implementation of median filter - written to be as simple as possible, // to validate faster versions against. fn reference_median_filter(image: &GrayImage, x_radius: u32, y_radius: u32) -> GrayImage { let (width, height) = image.dimensions(); if width == 0 || height == 0 { return image.clone(); } let mut out = GrayImage::new(width, height); let x_filter_side = (2 * x_radius + 1) as usize; let y_filter_side = (2 * y_radius + 1) as usize; let mut neighbors = vec![0u8; x_filter_side * y_filter_side]; let rx = x_radius as i32; let ry = y_radius as i32; for y in 0..height { for x in 0..width { let mut idx = 0; for dy in -ry..(ry + 1) { for dx in -rx..(rx + 1) { let px = min(max(0, x as i32 + dx), (width - 1) as i32) as u32; let py = min(max(0, y as i32 + dy), (height - 1) as i32) as u32; neighbors[idx] = image.get_pixel(px, py)[0]; idx += 1; } } neighbors.sort(); let m = median(&neighbors); out.put_pixel(x, y, Luma([m])); } } out } fn median(sorted: &[u8]) -> u8 { let mid = sorted.len() / 2; sorted[mid] } #[cfg_attr(miri, ignore = "slow")] #[test] fn test_median_filter_matches_reference_implementation() { fn prop(image: GrayTestImage, x_radius: u32, y_radius: u32) -> TestResult { let x_radius = x_radius % 5; let y_radius = y_radius % 5; let expected = reference_median_filter(&image.0, x_radius, y_radius); let actual = median_filter(&image.0, x_radius, y_radius); match pixel_diff_summary(&actual, &expected) { None => TestResult::passed(), Some(err) => TestResult::error(err), } } quickcheck(prop as fn(GrayTestImage, u32, u32) -> TestResult); } } imageproc-0.25.0/src/filter/mod.rs000064400000000000000000001155131046102023000150650ustar 00000000000000//! Functions for filtering images. mod median; pub use self::median::median_filter; mod sharpen; pub use self::sharpen::*; use image::{GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Pixel, Primitive}; use crate::definitions::{Clamp, Image}; use crate::integral_image::{column_running_sum, row_running_sum}; use crate::map::{ChannelMap, WithChannel}; use num::Num; use std::cmp::{max, min}; use std::f32; /// Denoise 8-bit grayscale image using bilateral filtering. /// /// # Arguments /// /// * `image` - Grayscale image to be filtered. /// * `window_size` - Window size for filtering. /// * `sigma_color` - Standard deviation for grayscale distance. A larger value results /// in averaging of pixels with larger grayscale differences. /// * `sigma_spatial` - Standard deviation for range distance. A larger value results in /// averaging of pixels separated by larger distances. /// /// This is a denoising filter designed to preserve edges. It averages pixels based on their spatial /// closeness and radiometric similarity \[1\]. Spatial closeness is measured by the Gaussian function /// of the Euclidean distance between two pixels with user-specified standard deviation /// (`sigma_spatial`). Radiometric similarity is measured by the Gaussian function of the difference /// between two grayscale values with user-specified standard deviation (`sigma_color`). /// /// # References /// /// \[1\] C. Tomasi and R. Manduchi. "Bilateral Filtering for Gray and Color /// Images." IEEE International Conference on Computer Vision (1998) /// 839-846. DOI: 10.1109/ICCV.1998.710815 /// /// # Panics /// /// 1. If `image.width() > i32::MAX as u32` or `image.height() > i32::MAX as u32`. /// 2. If `image.is_empty()`. /// /// # Examples /// /// ``` /// use imageproc::filter::bilateral_filter; /// use imageproc::utils::gray_bench_image; /// let image = gray_bench_image(500, 500); /// let filtered = bilateral_filter(&image, 10, 10., 3.); /// ``` #[must_use = "the function does not modify the original image"] pub fn bilateral_filter( image: &GrayImage, window_size: u32, sigma_color: f32, sigma_spatial: f32, ) -> Image> { /// Un-normalized Gaussian weights for look-up tables. fn gaussian_weight(x: f32, sigma_squared: f32) -> f32 { (-0.5 * x.powi(2) / sigma_squared).exp() } /// Create look-up table of Gaussian weights for color dimension. fn compute_color_lut(bins: u32, sigma: f32, max_value: f32) -> Vec { let step_size = max_value / bins as f32; let sigma_squared = sigma.powi(2); (0..bins) .map(|x| x as f32 * step_size) .map(|x| gaussian_weight(x, sigma_squared)) .collect() } /// Create look-up table of weights corresponding to flattened 2-D Gaussian kernel. fn compute_spatial_lut(window_size: u32, sigma: f32) -> Vec { let window_start = (-(window_size as f32) / 2.0).floor() as i32; let window_end = (window_size as f32 / 2.0).floor() as i32 + 1; let window_range = window_start..window_end; let cc = window_range.clone().cycle().take(window_range.len().pow(2)); let n = window_size as usize + 1; let rr = window_range.flat_map(|i| std::iter::repeat(i).take(n)); let sigma_squared = sigma.powi(2); rr.zip(cc) .map(|(r, c)| { let dist = ((r as f32).powi(2) + (c as f32).powi(2)).sqrt(); gaussian_weight(dist, sigma_squared) }) .collect() } let max_value = *image.iter().max().unwrap() as f32; let n_bins = 255u32; // for color or > 8-bit, make n_bins a user input for tuning accuracy. let color_lut = compute_color_lut(n_bins, sigma_color, max_value); let color_dist_scale = n_bins as f32 / max_value; let max_color_bin = (n_bins - 1) as usize; let range_lut = compute_spatial_lut(window_size, sigma_spatial); let window_size = window_size as i32; let window_extent = (window_size - 1) / 2; let (width, height) = image.dimensions(); assert!(width <= i32::MAX as u32); assert!(height <= i32::MAX as u32); Image::from_fn(width, height, |col, row| { let mut total_val = 0f32; let mut total_weight = 0f32; debug_assert!(image.in_bounds(col, row)); // Safety: `Image::from_fn` yields `col` in [0, width) and `row` in [0, height). let window_center_val = unsafe { image.unsafe_get_pixel(col, row)[0] } as i32; for window_row in -window_extent..window_extent + 1 { let window_row_abs = (row as i32 + window_row).clamp(0, height.saturating_sub(1) as i32) as u32; let kr = window_row + window_extent; for window_col in -window_extent..window_extent + 1 { let window_col_abs = (col as i32 + window_col).clamp(0, width.saturating_sub(1) as i32) as u32; debug_assert!(image.in_bounds(window_col_abs, window_row_abs)); // Safety: we clamped `window_row_abs` and `window_col_abs` to be in bounds. let val = unsafe { image.unsafe_get_pixel(window_col_abs, window_row_abs)[0] }; let kc = window_col + window_extent; let range_bin = (kr * window_size + kc) as usize; let color_dist = (window_center_val - val as i32).abs() as f32; let color_bin = ((color_dist * color_dist_scale) as usize).min(max_color_bin); let weight = range_lut[range_bin] * color_lut[color_bin]; total_val += val as f32 * weight; total_weight += weight; } } let new_val = (total_val / total_weight).round() as u8; Luma([new_val]) }) } /// Convolves an 8bpp grayscale image with a kernel of width (2 * `x_radius` + 1) /// and height (2 * `y_radius` + 1) whose entries are equal and /// sum to one. i.e. each output pixel is the unweighted mean of /// a rectangular region surrounding its corresponding input pixel. /// We handle locations where the kernel would extend past the image's /// boundary by treating the image as if its boundary pixels were /// repeated indefinitely. // TODO: for small kernels we probably want to do the convolution // TODO: directly instead of using an integral image. // TODO: more formats! #[must_use = "the function does not modify the original image"] pub fn box_filter(image: &GrayImage, x_radius: u32, y_radius: u32) -> Image> { let (width, height) = image.dimensions(); let mut out = ImageBuffer::new(width, height); if width == 0 || height == 0 { return out; } let kernel_width = 2 * x_radius + 1; let kernel_height = 2 * y_radius + 1; let mut row_buffer = vec![0; (width + 2 * x_radius) as usize]; for y in 0..height { row_running_sum(image, y, &mut row_buffer, x_radius); let val = row_buffer[(2 * x_radius) as usize] / kernel_width; unsafe { debug_assert!(out.in_bounds(0, y)); out.unsafe_put_pixel(0, y, Luma([val as u8])); } for x in 1..width { // TODO: This way we pay rounding errors for each of the // TODO: x and y convolutions. Is there a better way? let u = (x + 2 * x_radius) as usize; let l = (x - 1) as usize; let val = (row_buffer[u] - row_buffer[l]) / kernel_width; unsafe { debug_assert!(out.in_bounds(x, y)); out.unsafe_put_pixel(x, y, Luma([val as u8])); } } } let mut col_buffer = vec![0; (height + 2 * y_radius) as usize]; for x in 0..width { column_running_sum(&out, x, &mut col_buffer, y_radius); let val = col_buffer[(2 * y_radius) as usize] / kernel_height; unsafe { debug_assert!(out.in_bounds(x, 0)); out.unsafe_put_pixel(x, 0, Luma([val as u8])); } for y in 1..height { let u = (y + 2 * y_radius) as usize; let l = (y - 1) as usize; let val = (col_buffer[u] - col_buffer[l]) / kernel_height; unsafe { debug_assert!(out.in_bounds(x, y)); out.unsafe_put_pixel(x, y, Luma([val as u8])); } } } out } /// A 2D kernel, used to filter images via convolution. pub struct Kernel<'a, K> { data: &'a [K], width: u32, height: u32, } impl<'a, K: Num + Copy + 'a> Kernel<'a, K> { /// Construct a kernel from a slice and its dimensions. The input slice is /// in row-major form. /// /// # Panics /// /// If `width == 0 || height == 0`. pub fn new(data: &'a [K], width: u32, height: u32) -> Kernel<'a, K> { assert!(width > 0 && height > 0, "width and height must be non-zero"); assert!( width * height == data.len() as u32, "Invalid kernel len: expecting {}, found {}", width * height, data.len() ); Kernel { data, width, height, } } /// Returns 2d correlation of an image. Intermediate calculations are performed /// at type K, and the results converted to pixel Q via f. Pads by continuity. pub fn filter(&self, image: &Image

, mut f: F) -> Image where P: Pixel,

::Subpixel: Into, Q: Pixel, F: FnMut(&mut Q::Subpixel, K), { let (width, height) = image.dimensions(); let mut out = Image::::new(width, height); let num_channels = P::CHANNEL_COUNT as usize; let zero = K::zero(); let mut acc = vec![zero; num_channels]; let (k_width, k_height) = (self.width as i64, self.height as i64); let (width, height) = (width as i64, height as i64); for y in 0..height { for x in 0..width { for k_y in 0..k_height { let y_p = min(height - 1, max(0, y + k_y - k_height / 2)) as u32; for k_x in 0..k_width { let x_p = min(width - 1, max(0, x + k_x - k_width / 2)) as u32; debug_assert!(image.in_bounds(x_p, y_p)); debug_assert!(((k_y * k_width + k_x) as usize) < self.data.len()); accumulate( &mut acc, unsafe { &image.unsafe_get_pixel(x_p, y_p) }, unsafe { *self.data.get_unchecked((k_y * k_width + k_x) as usize) }, ); } } let out_channels = out.get_pixel_mut(x as u32, y as u32).channels_mut(); for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) { f(c, *a); *a = zero; } } } out } } #[inline] fn gaussian(x: f32, r: f32) -> f32 { ((2.0 * f32::consts::PI).sqrt() * r).recip() * (-x.powi(2) / (2.0 * r.powi(2))).exp() } /// Construct a one dimensional float-valued kernel for performing a Gaussian blur /// with standard deviation sigma. fn gaussian_kernel_f32(sigma: f32) -> Vec { let kernel_radius = (2.0 * sigma).ceil() as usize; let mut kernel_data = vec![0.0; 2 * kernel_radius + 1]; for i in 0..kernel_radius + 1 { let value = gaussian(i as f32, sigma); kernel_data[kernel_radius + i] = value; kernel_data[kernel_radius - i] = value; } let sum: f32 = kernel_data.iter().sum(); kernel_data.iter_mut().for_each(|x| *x /= sum); kernel_data } /// Blurs an image using a Gaussian of standard deviation sigma. /// The kernel used has type f32 and all intermediate calculations are performed /// at this type. /// /// # Panics /// /// Panics if `sigma <= 0.0`. // TODO: Integer type kernel, approximations via repeated box filter. #[must_use = "the function does not modify the original image"] pub fn gaussian_blur_f32

(image: &Image

, sigma: f32) -> Image

where P: Pixel,

::Subpixel: Into + Clamp, { assert!(sigma > 0.0, "sigma must be > 0.0"); let kernel = gaussian_kernel_f32(sigma); separable_filter_equal(image, &kernel) } /// Returns 2d correlation of view with the outer product of the 1d /// kernels `h_kernel` and `v_kernel`. #[must_use = "the function does not modify the original image"] pub fn separable_filter(image: &Image

, h_kernel: &[K], v_kernel: &[K]) -> Image

where P: Pixel,

::Subpixel: Into + Clamp, K: Num + Copy, { let h = horizontal_filter(image, h_kernel); vertical_filter(&h, v_kernel) } /// Returns 2d correlation of an image with the outer product of the 1d /// kernel filter with itself. #[must_use = "the function does not modify the original image"] pub fn separable_filter_equal(image: &Image

, kernel: &[K]) -> Image

where P: Pixel,

::Subpixel: Into + Clamp, K: Num + Copy, { separable_filter(image, kernel, kernel) } /// Returns 2d correlation of an image with a 3x3 row-major kernel. Intermediate calculations are /// performed at type K, and the results clamped to subpixel type S. Pads by continuity. #[must_use = "the function does not modify the original image"] pub fn filter3x3(image: &Image

, kernel: &[K]) -> Image> where P::Subpixel: Into, S: Clamp + Primitive, P: WithChannel, K: Num + Copy, { let kernel = Kernel::new(kernel, 3, 3); kernel.filter(image, |channel, acc| *channel = S::clamp(acc)) } /// Returns horizontal correlations between an image and a 1d kernel. /// Pads by continuity. Intermediate calculations are performed at /// type K. #[must_use = "the function does not modify the original image"] pub fn horizontal_filter(image: &Image

, kernel: &[K]) -> Image

where P: Pixel,

::Subpixel: Into + Clamp, K: Num + Copy, { // Don't replace this with a call to Kernel::filter without // checking the benchmark results. At the time of writing this // specialised implementation is faster. let (width, height) = image.dimensions(); let mut out = Image::

::new(width, height); let zero = K::zero(); let mut acc = vec![zero; P::CHANNEL_COUNT as usize]; let k_width = kernel.len() as i32; // Typically the image side will be much larger than the kernel length. // In that case we can remove a lot of bounds checks for most pixels. if k_width >= width as i32 { for y in 0..height { for x in 0..width { for (i, k) in kernel.iter().enumerate() { let x_unchecked = (x as i32) + i as i32 - k_width / 2; let x_p = max(0, min(x_unchecked, width as i32 - 1)) as u32; debug_assert!(image.in_bounds(x_p, y)); let p = unsafe { image.unsafe_get_pixel(x_p, y) }; accumulate(&mut acc, &p, *k); } let out_channels = out.get_pixel_mut(x, y).channels_mut(); for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) { *c =

::Subpixel::clamp(*a); *a = zero; } } } return out; } let half_k = k_width / 2; for y in 0..height { // Left margin - need to check lower bound only for x in 0..half_k { for (i, k) in kernel.iter().enumerate() { let x_unchecked = x + i as i32 - k_width / 2; let x_p = max(0, x_unchecked) as u32; debug_assert!(image.in_bounds(x_p, y)); let p = unsafe { image.unsafe_get_pixel(x_p, y) }; accumulate(&mut acc, &p, *k); } let out_channels = out.get_pixel_mut(x as u32, y).channels_mut(); for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) { *c =

::Subpixel::clamp(*a); *a = zero; } } // Neither margin - don't need bounds check on either side for x in half_k..(width as i32 - half_k) { for (i, k) in kernel.iter().enumerate() { let x_unchecked = x + i as i32 - k_width / 2; let x_p = x_unchecked as u32; debug_assert!(image.in_bounds(x_p, y)); let p = unsafe { image.unsafe_get_pixel(x_p, y) }; accumulate(&mut acc, &p, *k); } let out_channels = out.get_pixel_mut(x as u32, y).channels_mut(); for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) { *c =

::Subpixel::clamp(*a); *a = zero; } } // Right margin - need to check upper bound only for x in (width as i32 - half_k)..(width as i32) { for (i, k) in kernel.iter().enumerate() { let x_unchecked = x + i as i32 - k_width / 2; let x_p = min(x_unchecked, width as i32 - 1) as u32; debug_assert!(image.in_bounds(x_p, y)); let p = unsafe { image.unsafe_get_pixel(x_p, y) }; accumulate(&mut acc, &p, *k); } let out_channels = out.get_pixel_mut(x as u32, y).channels_mut(); for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) { *c =

::Subpixel::clamp(*a); *a = zero; } } } out } /// Returns horizontal correlations between an image and a 1d kernel. /// Pads by continuity. #[must_use = "the function does not modify the original image"] pub fn vertical_filter(image: &Image

, kernel: &[K]) -> Image

where P: Pixel,

::Subpixel: Into + Clamp, K: Num + Copy, { // Don't replace this with a call to Kernel::filter without // checking the benchmark results. At the time of writing this // specialised implementation is faster. let (width, height) = image.dimensions(); let mut out = Image::

::new(width, height); let zero = K::zero(); let mut acc = vec![zero; P::CHANNEL_COUNT as usize]; let k_height = kernel.len() as i32; // Typically the image side will be much larger than the kernel length. // In that case we can remove a lot of bounds checks for most pixels. if k_height >= height as i32 { for y in 0..height { for x in 0..width { for (i, k) in kernel.iter().enumerate() { let y_unchecked = (y as i32) + i as i32 - k_height / 2; let y_p = max(0, min(y_unchecked, height as i32 - 1)) as u32; debug_assert!(image.in_bounds(x, y_p)); let p = unsafe { image.unsafe_get_pixel(x, y_p) }; accumulate(&mut acc, &p, *k); } let out_channels = out.get_pixel_mut(x, y).channels_mut(); for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) { *c =

::Subpixel::clamp(*a); *a = zero; } } } return out; } let half_k = k_height / 2; // Top margin - need to check lower bound only for y in 0..half_k { for x in 0..width { for (i, k) in kernel.iter().enumerate() { let y_unchecked = y + i as i32 - k_height / 2; let y_p = max(0, y_unchecked) as u32; debug_assert!(image.in_bounds(x, y_p)); let p = unsafe { image.unsafe_get_pixel(x, y_p) }; accumulate(&mut acc, &p, *k); } let out_channels = out.get_pixel_mut(x, y as u32).channels_mut(); for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) { *c =

::Subpixel::clamp(*a); *a = zero; } } } // Neither margin - don't need bounds check on either side for y in half_k..(height as i32 - half_k) { for x in 0..width { for (i, k) in kernel.iter().enumerate() { let y_unchecked = y + i as i32 - k_height / 2; let y_p = y_unchecked as u32; debug_assert!(image.in_bounds(x, y_p)); let p = unsafe { image.unsafe_get_pixel(x, y_p) }; accumulate(&mut acc, &p, *k); } let out_channels = out.get_pixel_mut(x, y as u32).channels_mut(); for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) { *c =

::Subpixel::clamp(*a); *a = zero; } } } // Right margin - need to check upper bound only for y in (height as i32 - half_k)..(height as i32) { for x in 0..width { for (i, k) in kernel.iter().enumerate() { let y_unchecked = y + i as i32 - k_height / 2; let y_p = min(y_unchecked, height as i32 - 1) as u32; debug_assert!(image.in_bounds(x, y_p)); let p = unsafe { image.unsafe_get_pixel(x, y_p) }; accumulate(&mut acc, &p, *k); } let out_channels = out.get_pixel_mut(x, y as u32).channels_mut(); for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) { *c =

::Subpixel::clamp(*a); *a = zero; } } } out } fn accumulate(acc: &mut [K], pixel: &P, weight: K) where P: Pixel,

::Subpixel: Into, K: Num + Copy, { for i in 0..(P::CHANNEL_COUNT as usize) { acc[i] = acc[i] + pixel.channels()[i].into() * weight; } } /// Calculates the Laplacian of an image. /// /// The Laplacian is computed by filtering the image using the following 3x3 kernel: /// ```notrust /// 0, 1, 0, /// 1, -4, 1, /// 0, 1, 0 /// ``` #[must_use = "the function does not modify the original image"] pub fn laplacian_filter(image: &GrayImage) -> Image> { let kernel: [i16; 9] = [0, 1, 0, 1, -4, 1, 0, 1, 0]; filter3x3(image, &kernel) } #[cfg(test)] mod tests { use super::*; use crate::definitions::{Clamp, Image}; use crate::utils::gray_bench_image; use image::{GrayImage, ImageBuffer, Luma}; use std::cmp::{max, min}; use test::black_box; #[test] fn test_bilateral_filter() { let image = gray_image!( 1, 2, 3; 4, 5, 6; 7, 8, 9); let expect = gray_image!( 2, 3, 4; 5, 5, 6; 6, 7, 8); let actual = bilateral_filter(&image, 3, 10., 3.); assert_pixels_eq!(expect, actual); } #[test] fn test_box_filter_handles_empty_images() { let _ = box_filter(&GrayImage::new(0, 0), 3, 3); let _ = box_filter(&GrayImage::new(1, 0), 3, 3); let _ = box_filter(&GrayImage::new(0, 1), 3, 3); } #[test] fn test_box_filter() { let image = gray_image!( 1, 2, 3; 4, 5, 6; 7, 8, 9); // For this image we get the same answer from the two 1d // convolutions as from doing the 2d convolution in one step // (but we needn't in general, as in the former case we're // clipping to an integer value twice). let expected = gray_image!( 2, 3, 3; 4, 5, 5; 6, 7, 7); assert_pixels_eq!(box_filter(&image, 1, 1), expected); } #[test] fn test_separable_filter() { let image = gray_image!( 1, 2, 3; 4, 5, 6; 7, 8, 9); // Lazily copying the box_filter test case let expected = gray_image!( 2, 3, 3; 4, 5, 5; 6, 7, 7); let kernel = vec![1f32 / 3f32; 3]; let filtered = separable_filter_equal(&image, &kernel); assert_pixels_eq!(filtered, expected); } #[test] fn test_separable_filter_integer_kernel() { let image = gray_image!( 1, 2, 3; 4, 5, 6; 7, 8, 9); let expected = gray_image!( 21, 27, 33; 39, 45, 51; 57, 63, 69); let kernel = vec![1i32; 3]; let filtered = separable_filter_equal(&image, &kernel); assert_pixels_eq!(filtered, expected); } /// Reference implementation of horizontal_filter. Used to validate /// the (presumably faster) actual implementation. fn horizontal_filter_reference(image: &GrayImage, kernel: &[f32]) -> GrayImage { let (width, height) = image.dimensions(); let mut out = GrayImage::new(width, height); for y in 0..height { for x in 0..width { let mut acc = 0f32; for k in 0..kernel.len() { let mut x_unchecked = x as i32 + k as i32 - (kernel.len() / 2) as i32; x_unchecked = max(0, x_unchecked); x_unchecked = min(x_unchecked, width as i32 - 1); let x_checked = x_unchecked as u32; let color = image.get_pixel(x_checked, y)[0]; let weight = kernel[k]; acc += color as f32 * weight; } let clamped = >::clamp(acc); out.put_pixel(x, y, Luma([clamped])); } } out } /// Reference implementation of vertical_filter. Used to validate /// the (presumably faster) actual implementation. fn vertical_filter_reference(image: &GrayImage, kernel: &[f32]) -> GrayImage { let (width, height) = image.dimensions(); let mut out = GrayImage::new(width, height); for y in 0..height { for x in 0..width { let mut acc = 0f32; for k in 0..kernel.len() { let mut y_unchecked = y as i32 + k as i32 - (kernel.len() / 2) as i32; y_unchecked = max(0, y_unchecked); y_unchecked = min(y_unchecked, height as i32 - 1); let y_checked = y_unchecked as u32; let color = image.get_pixel(x, y_checked)[0]; let weight = kernel[k]; acc += color as f32 * weight; } let clamped = >::clamp(acc); out.put_pixel(x, y, Luma([clamped])); } } out } macro_rules! test_against_reference_implementation { ($test_name:ident, $under_test:ident, $reference_impl:ident) => { #[test] fn $test_name() { // I think the interesting edge cases here are determined entirely // by the relative sizes of the kernel and the image side length, so // I'm just enumerating over small values instead of generating random // examples via quickcheck. for height in 0..5 { for width in 0..5 { for kernel_length in 0..15 { let image = gray_bench_image(width, height); let kernel: Vec = (0..kernel_length).map(|i| i as f32 % 1.35).collect(); let expected = $reference_impl(&image, &kernel); let actual = $under_test(&image, &kernel); assert_pixels_eq!(actual, expected); } } } } }; } test_against_reference_implementation!( test_horizontal_filter_matches_reference_implementation, horizontal_filter, horizontal_filter_reference ); test_against_reference_implementation!( test_vertical_filter_matches_reference_implementation, vertical_filter, vertical_filter_reference ); #[test] fn test_horizontal_filter() { let image = gray_image!( 1, 4, 1; 4, 7, 4; 1, 4, 1); let expected = gray_image!( 2, 2, 2; 5, 5, 5; 2, 2, 2); let kernel = vec![1f32 / 3f32; 3]; let filtered = horizontal_filter(&image, &kernel); assert_pixels_eq!(filtered, expected); } #[test] fn test_horizontal_filter_with_kernel_wider_than_image_does_not_panic() { let image = gray_image!( 1, 4, 1; 4, 7, 4; 1, 4, 1); let kernel = vec![1f32 / 10f32; 10]; black_box(horizontal_filter(&image, &kernel)); } #[test] fn test_vertical_filter() { let image = gray_image!( 1, 4, 1; 4, 7, 4; 1, 4, 1); let expected = gray_image!( 2, 5, 2; 2, 5, 2; 2, 5, 2); let kernel = vec![1f32 / 3f32; 3]; let filtered = vertical_filter(&image, &kernel); assert_pixels_eq!(filtered, expected); } #[test] fn test_vertical_filter_with_kernel_taller_than_image_does_not_panic() { let image = gray_image!( 1, 4, 1; 4, 7, 4; 1, 4, 1); let kernel = vec![1f32 / 10f32; 10]; black_box(vertical_filter(&image, &kernel)); } #[test] fn test_filter3x3_with_results_outside_input_channel_range() { #[rustfmt::skip] let kernel: Vec = vec![ -1, 0, 1, -2, 0, 2, -1, 0, 1 ]; let image = gray_image!( 3, 2, 1; 6, 5, 4; 9, 8, 7); let expected = gray_image!(type: i16, -4, -8, -4; -4, -8, -4; -4, -8, -4 ); let filtered = filter3x3(&image, &kernel); assert_pixels_eq!(filtered, expected); } #[test] #[should_panic] fn test_kernel_must_be_nonempty() { let k: Vec = Vec::new(); let _ = Kernel::new(&k, 0, 0); } #[test] fn test_kernel_filter_with_even_kernel_side() { let image = gray_image!( 3, 2; 4, 1); let k = vec![1u8, 2u8]; let kernel = Kernel::new(&k, 2, 1); let filtered = kernel.filter(&image, |c, a| *c = a); let expected = gray_image!( 9, 7; 12, 6); assert_pixels_eq!(filtered, expected); } #[test] fn test_kernel_filter_with_empty_image() { let image = gray_image!(); let k = vec![2u8]; let kernel = Kernel::new(&k, 1, 1); let filtered = kernel.filter(&image, |c, a| *c = a); let expected = gray_image!(); assert_pixels_eq!(filtered, expected); } #[test] fn test_kernel_filter_with_kernel_dimensions_larger_than_image() { let image = gray_image!( 9, 4; 8, 1); #[rustfmt::skip] let k: Vec = vec![ 0.1, 0.2, 0.1, 0.2, 0.4, 0.2, 0.1, 0.2, 0.1 ]; let kernel = Kernel::new(&k, 3, 3); let filtered: Image> = kernel.filter(&image, |c, a| *c = >::clamp(a)); let expected = gray_image!( 11, 7; 10, 5); assert_pixels_eq!(filtered, expected); } #[test] #[should_panic] fn test_gaussian_blur_f32_rejects_zero_sigma() { let image = gray_image!( 1, 2, 3; 4, 5, 6; 7, 8, 9 ); let _ = gaussian_blur_f32(&image, 0.0); } #[test] #[should_panic] fn test_gaussian_blur_f32_rejects_negative_sigma() { let image = gray_image!( 1, 2, 3; 4, 5, 6; 7, 8, 9 ); let _ = gaussian_blur_f32(&image, -0.5); } #[test] fn test_gaussian_on_u8_white_idempotent() { let image = ImageBuffer::, Vec>::from_pixel(12, 12, Luma([255])); let image2 = gaussian_blur_f32(&image, 6f32); assert_pixels_eq_within!(image2, image, 0); } #[test] fn test_gaussian_on_f32_white_idempotent() { let image = ImageBuffer::, Vec>::from_pixel(12, 12, Luma([1.0])); let image2 = gaussian_blur_f32(&image, 6f32); assert_pixels_eq_within!(image2, image, 1e-6); } } #[cfg(not(miri))] #[cfg(test)] mod proptests { use super::*; use crate::proptest_utils::arbitrary_image; use proptest::prelude::*; proptest! { #[test] fn proptest_bilateral_filter( img in arbitrary_image::>(1..40, 1..40), window_size in 0..25u32, sigma_color in any::(), sigma_spatial in any::(), ) { let out = bilateral_filter(&img, window_size, sigma_color, sigma_spatial); assert_eq!(out.dimensions(), img.dimensions()); } #[test] fn proptest_box_filter( img in arbitrary_image::>(0..200, 0..200), x_radius in 0..100u32, y_radius in 0..100u32, ) { let out = box_filter(&img, x_radius, y_radius); assert_eq!(out.dimensions(), img.dimensions()); } #[test] fn proptest_gaussian_blur_f32( img in arbitrary_image::>(0..20, 0..20), sigma in (0.0..150f32).prop_filter("contract", |&x| x > 0.0), ) { let out = gaussian_blur_f32(&img, sigma); assert_eq!(out.dimensions(), img.dimensions()); } #[test] fn proptest_kernel_luma_f32( img in arbitrary_image::>(0..30, 0..30), ker in arbitrary_image::>(1..20, 1..20), ) { let kernel = Kernel::new(&ker, ker.width(), ker.height()); let out: Image> = kernel.filter(&img, |dst, src| { *dst = src; }); assert_eq!(out.dimensions(), img.dimensions()); } #[test] fn proptest_filter3x3( img in arbitrary_image::>(0..50, 0..50), ker in proptest::collection::vec(any::(), 9), ) { let out: Image> = filter3x3(&img, &ker); assert_eq!(out.dimensions(), img.dimensions()); } #[test] fn proptest_horizontal_filter_luma_f32( img in arbitrary_image::>(0..50, 0..50), ker in proptest::collection::vec(any::(), 0..50), ) { let out = horizontal_filter(&img, &ker); assert_eq!(out.dimensions(), img.dimensions()); } #[test] fn proptest_vertical_filter_luma_f32( img in arbitrary_image::>(0..50, 0..50), ker in proptest::collection::vec(any::(), 0..50), ) { let out = vertical_filter(&img, &ker); assert_eq!(out.dimensions(), img.dimensions()); } #[test] fn proptest_laplacian_filter( img in arbitrary_image::>(0..120, 0..120), ) { let out = laplacian_filter(&img); assert_eq!(out.dimensions(), img.dimensions()); } } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::definitions::Image; use crate::utils::{gray_bench_image, rgb_bench_image}; use image::imageops::blur; use image::{GenericImage, ImageBuffer, Luma, Rgb}; use test::{black_box, Bencher}; #[bench] fn bench_bilateral_filter(b: &mut Bencher) { let image = gray_bench_image(500, 500); b.iter(|| { let filtered = bilateral_filter(&image, 10, 10., 3.); black_box(filtered); }); } #[bench] fn bench_box_filter(b: &mut Bencher) { let image = gray_bench_image(500, 500); b.iter(|| { let filtered = box_filter(&image, 7, 7); black_box(filtered); }); } #[bench] fn bench_separable_filter(b: &mut Bencher) { let image = gray_bench_image(300, 300); let h_kernel = vec![1f32 / 5f32; 5]; let v_kernel = vec![0.1f32, 0.4f32, 0.3f32, 0.1f32, 0.1f32]; b.iter(|| { let filtered = separable_filter(&image, &h_kernel, &v_kernel); black_box(filtered); }); } #[bench] fn bench_horizontal_filter(b: &mut Bencher) { let image = gray_bench_image(500, 500); let kernel = vec![1f32 / 5f32; 5]; b.iter(|| { let filtered = horizontal_filter(&image, &kernel); black_box(filtered); }); } #[bench] fn bench_vertical_filter(b: &mut Bencher) { let image = gray_bench_image(500, 500); let kernel = vec![1f32 / 5f32; 5]; b.iter(|| { let filtered = vertical_filter(&image, &kernel); black_box(filtered); }); } #[bench] fn bench_filter3x3_i32_filter(b: &mut Bencher) { let image = gray_bench_image(500, 500); #[rustfmt::skip] let kernel: Vec = vec![ -1, 0, 1, -2, 0, 2, -1, 0, 1 ]; b.iter(|| { let filtered: ImageBuffer, Vec> = filter3x3::<_, _, i16>(&image, &kernel); black_box(filtered); }); } /// Baseline implementation of Gaussian blur is that provided by image::imageops. /// We can also use this to validate correctness of any implementations we add here. fn gaussian_baseline_rgb(image: &I, stdev: f32) -> Image> where I: GenericImage>, { blur(image, stdev) } #[bench] #[ignore] // Gives a baseline performance using code from another library fn bench_baseline_gaussian_stdev_1(b: &mut Bencher) { let image = rgb_bench_image(100, 100); b.iter(|| { let blurred = gaussian_baseline_rgb(&image, 1f32); black_box(blurred); }); } #[bench] #[ignore] // Gives a baseline performance using code from another library fn bench_baseline_gaussian_stdev_3(b: &mut Bencher) { let image = rgb_bench_image(100, 100); b.iter(|| { let blurred = gaussian_baseline_rgb(&image, 3f32); black_box(blurred); }); } #[bench] #[ignore] // Gives a baseline performance using code from another library fn bench_baseline_gaussian_stdev_10(b: &mut Bencher) { let image = rgb_bench_image(100, 100); b.iter(|| { let blurred = gaussian_baseline_rgb(&image, 10f32); black_box(blurred); }); } #[bench] fn bench_gaussian_f32_stdev_1(b: &mut Bencher) { let image = rgb_bench_image(100, 100); b.iter(|| { let blurred = gaussian_blur_f32(&image, 1f32); black_box(blurred); }); } #[bench] fn bench_gaussian_f32_stdev_3(b: &mut Bencher) { let image = rgb_bench_image(100, 100); b.iter(|| { let blurred = gaussian_blur_f32(&image, 3f32); black_box(blurred); }); } #[bench] fn bench_gaussian_f32_stdev_10(b: &mut Bencher) { let image = rgb_bench_image(100, 100); b.iter(|| { let blurred = gaussian_blur_f32(&image, 10f32); black_box(blurred); }); } } imageproc-0.25.0/src/filter/sharpen.rs000064400000000000000000000022431046102023000157410ustar 00000000000000use super::{filter3x3, gaussian_blur_f32}; use crate::{ definitions::{Clamp, Image}, map::{map_colors2, map_subpixels}, }; use image::{GrayImage, Luma}; /// Sharpens a grayscale image by applying a 3x3 approximation to the Laplacian. #[must_use = "the function does not modify the original image"] pub fn sharpen3x3(image: &GrayImage) -> GrayImage { let identity_minus_laplacian = [0, -1, 0, -1, 5, -1, 0, -1, 0]; filter3x3(image, &identity_minus_laplacian) } /// Sharpens a grayscale image using a Gaussian as a low-pass filter. /// /// * `sigma` is the standard deviation of the Gaussian filter used. /// * `amount` controls the level of sharpening. `output = input + amount * edges`. // TODO: remove unnecessary allocations, support colour images #[must_use = "the function does not modify the original image"] pub fn sharpen_gaussian(image: &GrayImage, sigma: f32, amount: f32) -> GrayImage { let image = map_subpixels(image, |x| x as f32); let smooth: Image> = gaussian_blur_f32(&image, sigma); map_colors2(&image, &smooth, |p, q| { let v = (1.0 + amount) * p[0] - amount * q[0]; Luma([>::clamp(v)]) }) } imageproc-0.25.0/src/geometric_transformations.rs000064400000000000000000001117521046102023000203110ustar 00000000000000//! Geometric transformations of images. This includes rotations, translation, and general //! projective transformations. use crate::definitions::{Clamp, Image}; use image::{GenericImageView, ImageBuffer, Pixel}; #[cfg(feature = "rayon")] use rayon::prelude::*; use std::{cmp, ops::Mul}; #[derive(Copy, Clone, Debug)] enum TransformationClass { Translation, Affine, Projection, } /// A 2d projective transformation, stored as a row major 3x3 matrix. /// /// Transformations combine by pre-multiplication, i.e. applying `P * Q` is equivalent to /// applying `Q` and then applying `P`. For example, the following defines a rotation /// about the point (320.0, 240.0). /// /// ``` /// use imageproc::geometric_transformations::*; /// use std::f32::consts::PI; /// /// let (cx, cy) = (320.0, 240.0); /// /// let c_rotation = Projection::translate(cx, cy) /// * Projection::rotate(PI / 6.0) /// * Projection::translate(-cx, -cy); /// ``` /// /// See ./examples/projection.rs for more examples. #[derive(Copy, Clone, Debug)] pub struct Projection { transform: [f32; 9], inverse: [f32; 9], class: TransformationClass, } impl Projection { /// Creates a 2d projective transform from a row-major 3x3 matrix in homogeneous coordinates. /// /// Returns `None` if the matrix is not invertible. pub fn from_matrix(transform: [f32; 9]) -> Option { let transform = normalize(transform); let class = class_from_matrix(transform); try_inverse(&transform).map(|inverse| Projection { transform, inverse, class, }) } /// Combine the transformation with another one. The resulting transformation is equivalent to /// applying this transformation followed by the `other` transformation. pub fn and_then(self, other: Projection) -> Projection { other * self } /// A translation by (tx, ty). #[rustfmt::skip] pub fn translate(tx: f32, ty: f32) -> Projection { Projection { transform: [ 1.0, 0.0, tx, 0.0, 1.0, ty, 0.0, 0.0, 1.0 ], inverse: [ 1.0, 0.0, -tx, 0.0, 1.0, -ty, 0.0, 0.0, 1.0 ], class: TransformationClass::Translation, } } /// A clockwise rotation around the top-left corner of the image by theta radians. #[rustfmt::skip] pub fn rotate(theta: f32) -> Projection { let (s, c) = theta.sin_cos(); Projection { transform: [ c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0 ], inverse: [ c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0 ], class: TransformationClass::Affine, } } /// An anisotropic scaling (sx, sy). /// /// Note that the `warp` function does not change the size of the input image. /// If you want to resize an image then use the `imageops` module in the `image` crate. #[rustfmt::skip] pub fn scale(sx: f32, sy: f32) -> Projection { Projection { transform: [ sx, 0.0, 0.0, 0.0, sy, 0.0, 0.0, 0.0, 1.0 ], inverse: [ 1.0 / sx, 0.0, 0.0, 0.0, 1.0 / sy, 0.0, 0.0, 0.0, 1.0 ], class: TransformationClass::Affine, } } /// Inverts the transformation. pub fn invert(self) -> Projection { Projection { transform: self.inverse, inverse: self.transform, class: self.class, } } /// Calculates a projection from a set of four control point pairs. pub fn from_control_points(from: [(f32, f32); 4], to: [(f32, f32); 4]) -> Option { use approx::AbsDiffEq; use nalgebra::{linalg::SVD, OMatrix, OVector, U8}; let (xf1, yf1, xf2, yf2, xf3, yf3, xf4, yf4) = ( from[0].0 as f64, from[0].1 as f64, from[1].0 as f64, from[1].1 as f64, from[2].0 as f64, from[2].1 as f64, from[3].0 as f64, from[3].1 as f64, ); let (x1, y1, x2, y2, x3, y3, x4, y4) = ( to[0].0 as f64, to[0].1 as f64, to[1].0 as f64, to[1].1 as f64, to[2].0 as f64, to[2].1 as f64, to[3].0 as f64, to[3].1 as f64, ); #[rustfmt::skip] let a = OMatrix::<_, U8, U8>::from_row_slice(&[ 0.0, 0.0, 0.0, -xf1, -yf1, -1.0, y1 * xf1, y1 * yf1, xf1, yf1, 1.0, 0.0, 0.0, 0.0, -x1 * xf1, -x1 * yf1, 0.0, 0.0, 0.0, -xf2, -yf2, -1.0, y2 * xf2, y2 * yf2, xf2, yf2, 1.0, 0.0, 0.0, 0.0, -x2 * xf2, -x2 * yf2, 0.0, 0.0, 0.0, -xf3, -yf3, -1.0, y3 * xf3, y3 * yf3, xf3, yf3, 1.0, 0.0, 0.0, 0.0, -x3 * xf3, -x3 * yf3, 0.0, 0.0, 0.0, -xf4, -yf4, -1.0, y4 * xf4, y4 * yf4, xf4, yf4, 1.0, 0.0, 0.0, 0.0, -x4 * xf4, -x4 * yf4, ]); let b = OVector::<_, U8>::from_row_slice(&[-y1, x1, -y2, x2, -y3, x3, -y4, x4]); SVD::try_new(a, true, true, f64::default_epsilon(), 0) .and_then(|svd| svd.solve(&b, f64::default_epsilon()).ok()) .and_then(|h| { let mut transform = [ h[0] as f32, h[1] as f32, h[2] as f32, h[3] as f32, h[4] as f32, h[5] as f32, h[6] as f32, h[7] as f32, 1.0, ]; transform = normalize(transform); let class = class_from_matrix(transform); try_inverse(&transform).map(|inverse| Projection { transform, inverse, class, }) }) } // Helper functions used as optimization in warp. #[inline(always)] fn map_projective(&self, x: f32, y: f32) -> (f32, f32) { let t = &self.transform; let d = t[6] * x + t[7] * y + t[8]; ( (t[0] * x + t[1] * y + t[2]) / d, (t[3] * x + t[4] * y + t[5]) / d, ) } #[inline(always)] fn map_affine(&self, x: f32, y: f32) -> (f32, f32) { let t = &self.transform; ((t[0] * x + t[1] * y + t[2]), (t[3] * x + t[4] * y + t[5])) } #[inline(always)] fn map_translation(&self, x: f32, y: f32) -> (f32, f32) { let t = &self.transform; let tx = t[2]; let ty = t[5]; (x + tx, y + ty) } } impl Mul for Projection { type Output = Projection; fn mul(self, rhs: Projection) -> Projection { use TransformationClass as TC; let t = mul3x3(self.transform, rhs.transform); let i = mul3x3(rhs.inverse, self.inverse); let class = match (self.class, rhs.class) { (TC::Translation, TC::Translation) => TC::Translation, (TC::Translation, TC::Affine) => TC::Affine, (TC::Affine, TC::Translation) => TC::Affine, (TC::Affine, TC::Affine) => TC::Affine, (_, _) => TC::Projection, }; Projection { transform: t, inverse: i, class, } } } impl<'a, 'b> Mul<&'b Projection> for &'a Projection { type Output = Projection; fn mul(self, rhs: &Projection) -> Projection { *self * *rhs } } impl Mul<(f32, f32)> for Projection { type Output = (f32, f32); fn mul(self, rhs: (f32, f32)) -> (f32, f32) { let (x, y) = rhs; match self.class { TransformationClass::Translation => self.map_translation(x, y), TransformationClass::Affine => self.map_affine(x, y), TransformationClass::Projection => self.map_projective(x, y), } } } impl<'a, 'b> Mul<&'b (f32, f32)> for &'a Projection { type Output = (f32, f32); fn mul(self, rhs: &(f32, f32)) -> (f32, f32) { *self * *rhs } } /// Rotates an image clockwise about its center. /// The output image has the same dimensions as the input. Output pixels /// whose pre-image lies outside the input image are set to `default`. pub fn rotate_about_center

( image: &Image

, theta: f32, interpolation: Interpolation, default: P, ) -> Image

where P: Pixel + Send + Sync,

::Subpixel: Send + Sync,

::Subpixel: Into + Clamp, { let (w, h) = image.dimensions(); rotate( image, (w as f32 / 2.0, h as f32 / 2.0), theta, interpolation, default, ) } /// Rotates an image clockwise about the provided center by theta radians. /// The output image has the same dimensions as the input. Output pixels /// whose pre-image lies outside the input image are set to `default`. pub fn rotate

( image: &Image

, center: (f32, f32), theta: f32, interpolation: Interpolation, default: P, ) -> Image

where P: Pixel + Send + Sync,

::Subpixel: Send + Sync,

::Subpixel: Into + Clamp, { let (cx, cy) = center; let projection = Projection::translate(cx, cy) * Projection::rotate(theta) * Projection::translate(-cx, -cy); warp(image, &projection, interpolation, default) } /// Translates the input image by t. Note that image coordinates increase from /// top left to bottom right. Output pixels whose pre-image are not in the input /// image are set to the boundary pixel in the input image nearest to their pre-image. // TODO: it's confusing that this has different behaviour to // TODO: attempting the equivalent transformation using Projection. pub fn translate

(image: &Image

, t: (i32, i32)) -> Image

where P: Pixel, { let (width, height) = image.dimensions(); let (tx, ty) = t; let (w, h) = (width as i32, height as i32); let num_channels = P::CHANNEL_COUNT as usize; let mut out = ImageBuffer::new(width, height); for y in 0..height { let y_in = cmp::max(0, cmp::min(y as i32 - ty, h - 1)); if tx > 0 { let p_min = *image.get_pixel(0, y_in as u32); for x in 0..tx.min(w) { out.put_pixel(x as u32, y, p_min); } if tx < w { let in_base = (y_in as usize * width as usize) * num_channels; let out_base = (y as usize * width as usize + (tx as usize)) * num_channels; let len = (w - tx) as usize * num_channels; (*out)[out_base..][..len].copy_from_slice(&(**image)[in_base..][..len]); } } else { let p_max = *image.get_pixel(width - 1, y_in as u32); for x in (w + tx).max(0)..w { out.put_pixel(x as u32, y, p_max); } if w + tx > 0 { let in_base = (y_in as usize * width as usize + (tx.unsigned_abs() as usize)) * num_channels; let out_base = (y as usize * width as usize) * num_channels; let len = (w + tx) as usize * num_channels; (*out)[out_base..][..len].copy_from_slice(&(**image)[in_base..][..len]); } } } out } /// Applies a projective transformation to an image. /// /// The returned image has the same dimensions as `image`. Output pixels /// whose pre-image lies outside the input image are set to `default`. /// /// The provided projection defines a mapping from locations in the input image to their /// corresponding location in the output image. pub fn warp

( image: &Image

, projection: &Projection, interpolation: Interpolation, default: P, ) -> Image

where P: Pixel + Send + Sync,

::Subpixel: Send + Sync,

::Subpixel: Into + Clamp, { let (width, height) = image.dimensions(); let mut out = ImageBuffer::new(width, height); warp_into(image, projection, interpolation, default, &mut out); out } /// Applies a projective transformation to an image, writing to a provided output. /// /// See the [`warp`](fn.warp.html) documentation for more information. pub fn warp_into

( image: &Image

, projection: &Projection, interpolation: Interpolation, default: P, out: &mut Image

, ) where P: Pixel + Send + Sync,

::Subpixel: Send + Sync,

::Subpixel: Into + Clamp + Sync, { let projection = projection.invert(); let nn = |x, y| interpolate_nearest(image, x, y, default); let bl = |x, y| interpolate_bilinear(image, x, y, default); let bc = |x, y| interpolate_bicubic(image, x, y, default); let wp = |x, y| projection.map_projective(x, y); let wa = |x, y| projection.map_affine(x, y); let wt = |x, y| projection.map_translation(x, y); use Interpolation as I; use TransformationClass as TC; match (interpolation, projection.class) { (I::Nearest, TC::Translation) => warp_inner(out, wt, nn), (I::Nearest, TC::Affine) => warp_inner(out, wa, nn), (I::Nearest, TC::Projection) => warp_inner(out, wp, nn), (I::Bilinear, TC::Translation) => warp_inner(out, wt, bl), (I::Bilinear, TC::Affine) => warp_inner(out, wa, bl), (I::Bilinear, TC::Projection) => warp_inner(out, wp, bl), (I::Bicubic, TC::Translation) => warp_inner(out, wt, bc), (I::Bicubic, TC::Affine) => warp_inner(out, wa, bc), (I::Bicubic, TC::Projection) => warp_inner(out, wp, bc), } } /// Warps an image using the provided function to define the pre-image of each output pixel. /// /// # Examples /// Applying a wave pattern. /// ``` /// use image::{ImageBuffer, Luma}; /// use imageproc::utils::gray_bench_image; /// use imageproc::geometric_transformations::*; /// /// let image = gray_bench_image(300, 300); /// let warped = warp_with( /// &image, /// |x, y| (x, y + (x / 30.0).sin()), /// Interpolation::Nearest, /// Luma([0u8]) /// ); /// ``` pub fn warp_with( image: &Image

, mapping: F, interpolation: Interpolation, default: P, ) -> Image

where F: Fn(f32, f32) -> (f32, f32) + Sync + Send, P: Pixel + Send + Sync,

::Subpixel: Send + Sync,

::Subpixel: Into + Clamp, { let (width, height) = image.dimensions(); let mut out = ImageBuffer::new(width, height); warp_into_with(image, mapping, interpolation, default, &mut out); out } /// Warps an image using the provided function to define the pre-image of each output pixel, /// writing into a preallocated output. /// /// See the [`warp_with`](fn.warp_with.html) documentation for more information. pub fn warp_into_with( image: &Image

, mapping: F, interpolation: Interpolation, default: P, out: &mut Image

, ) where F: Fn(f32, f32) -> (f32, f32) + Send + Sync, P: Pixel + Send + Sync,

::Subpixel: Send + Sync,

::Subpixel: Into + Clamp, { let nn = |x, y| interpolate_nearest(image, x, y, default); let bl = |x, y| interpolate_bilinear(image, x, y, default); let bc = |x, y| interpolate_bicubic(image, x, y, default); use Interpolation as I; match interpolation { I::Nearest => warp_inner(out, mapping, nn), I::Bilinear => warp_inner(out, mapping, bl), I::Bicubic => warp_inner(out, mapping, bc), } } // Work horse of all warp functions // TODO: make faster by avoiding boundary checks in inner section of src image fn warp_inner(out: &mut Image

, mapping: Fc, get_pixel: Fi) where P: Pixel,

::Subpixel: Send + Sync,

::Subpixel: Into + Clamp, Fc: Fn(f32, f32) -> (f32, f32) + Send + Sync, Fi: Fn(f32, f32) -> P + Send + Sync, { let width = out.width(); let raw_out = out.as_mut(); let pitch = P::CHANNEL_COUNT as usize * width as usize; #[cfg(feature = "rayon")] let chunks = raw_out.par_chunks_mut(pitch); #[cfg(not(feature = "rayon"))] let chunks = raw_out.chunks_mut(pitch); chunks.enumerate().for_each(|(y, row)| { for (x, slice) in row.chunks_mut(P::CHANNEL_COUNT as usize).enumerate() { let (px, py) = mapping(x as f32, y as f32); *P::from_slice_mut(slice) = get_pixel(px, py); } }); } // Classifies transformation by looking up transformation matrix coefficients fn class_from_matrix(mx: [f32; 9]) -> TransformationClass { if (mx[6] - 0.0).abs() < 1e-10 && (mx[7] - 0.0).abs() < 1e-10 && (mx[8] - 1.0).abs() < 1e-10 { if (mx[0] - 1.0).abs() < 1e-10 && (mx[1] - 0.0).abs() < 1e-10 && (mx[3] - 0.0).abs() < 1e-10 && (mx[4] - 1.0).abs() < 1e-10 { TransformationClass::Translation } else { TransformationClass::Affine } } else { TransformationClass::Projection } } fn normalize(mx: [f32; 9]) -> [f32; 9] { [ mx[0] / mx[8], mx[1] / mx[8], mx[2] / mx[8], mx[3] / mx[8], mx[4] / mx[8], mx[5] / mx[8], mx[6] / mx[8], mx[7] / mx[8], 1.0, ] } // TODO: write me in f64 fn try_inverse(t: &[f32; 9]) -> Option<[f32; 9]> { let [t00, t01, t02, t10, t11, t12, t20, t21, t22] = t; let m00 = t11 * t22 - t12 * t21; let m01 = t10 * t22 - t12 * t20; let m02 = t10 * t21 - t11 * t20; let det = t00 * m00 - t01 * m01 + t02 * m02; if det.abs() < 1e-10 { return None; } let m10 = t01 * t22 - t02 * t21; let m11 = t00 * t22 - t02 * t20; let m12 = t00 * t21 - t01 * t20; let m20 = t01 * t12 - t02 * t11; let m21 = t00 * t12 - t02 * t10; let m22 = t00 * t11 - t01 * t10; #[rustfmt::skip] let inv = [ m00 / det, -m10 / det, m20 / det, -m01 / det, m11 / det, -m21 / det, m02 / det, -m12 / det, m22 / det, ]; Some(normalize(inv)) } fn mul3x3(a: [f32; 9], b: [f32; 9]) -> [f32; 9] { let [a00, a01, a02, a10, a11, a12, a20, a21, a22] = a; let [b00, b01, b02, b10, b11, b12, b20, b21, b22] = b; [ a00 * b00 + a01 * b10 + a02 * b20, a00 * b01 + a01 * b11 + a02 * b21, a00 * b02 + a01 * b12 + a02 * b22, a10 * b00 + a11 * b10 + a12 * b20, a10 * b01 + a11 * b11 + a12 * b21, a10 * b02 + a11 * b12 + a12 * b22, a20 * b00 + a21 * b10 + a22 * b20, a20 * b01 + a21 * b11 + a22 * b21, a20 * b02 + a21 * b12 + a22 * b22, ] } fn blend_cubic

(px0: &P, px1: &P, px2: &P, px3: &P, x: f32) -> P where P: Pixel, P::Subpixel: Into + Clamp, { let mut outp = *px0; for i in 0..(P::CHANNEL_COUNT as usize) { let p0 = px0.channels()[i].into(); let p1 = px1.channels()[i].into(); let p2 = px2.channels()[i].into(); let p3 = px3.channels()[i].into(); #[rustfmt::skip] let pval = p1 + 0.5 * x * (p2 - p0 + x * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3 + x * (3.0 * (p1 - p2) + p3 - p0))); outp.channels_mut()[i] =

::Subpixel::clamp(pval); } outp } fn interpolate_bicubic

(image: &Image

, x: f32, y: f32, default: P) -> P where P: Pixel,

::Subpixel: Into + Clamp, { let left = x.floor() - 1f32; let right = left + 4f32; let top = y.floor() - 1f32; let bottom = top + 4f32; let x_weight = x - (left + 1f32); let y_weight = y - (top + 1f32); let mut col: [P; 4] = [default, default, default, default]; let (width, height) = image.dimensions(); if left < 0f32 || right >= width as f32 || top < 0f32 || bottom >= height as f32 { default } else { for row in top as u32..bottom as u32 { let (p0, p1, p2, p3): (P, P, P, P) = unsafe { ( image.unsafe_get_pixel(left as u32, row), image.unsafe_get_pixel(left as u32 + 1, row), image.unsafe_get_pixel(left as u32 + 2, row), image.unsafe_get_pixel(left as u32 + 3, row), ) }; let c = blend_cubic(&p0, &p1, &p2, &p3, x_weight); col[row as usize - top as usize] = c; } blend_cubic(&col[0], &col[1], &col[2], &col[3], y_weight) } } fn blend_bilinear

( top_left: P, top_right: P, bottom_left: P, bottom_right: P, right_weight: f32, bottom_weight: f32, ) -> P where P: Pixel, P::Subpixel: Into + Clamp, { let top = top_left.map2(&top_right, |u, v| { P::Subpixel::clamp((1f32 - right_weight) * u.into() + right_weight * v.into()) }); let bottom = bottom_left.map2(&bottom_right, |u, v| { P::Subpixel::clamp((1f32 - right_weight) * u.into() + right_weight * v.into()) }); top.map2(&bottom, |u, v| { P::Subpixel::clamp((1f32 - bottom_weight) * u.into() + bottom_weight * v.into()) }) } fn interpolate_bilinear

(image: &Image

, x: f32, y: f32, default: P) -> P where P: Pixel,

::Subpixel: Into + Clamp, { let left = x.floor(); let right = left + 1f32; let top = y.floor(); let bottom = top + 1f32; let right_weight = x - left; let bottom_weight = y - top; // default if out of bound let (width, height) = image.dimensions(); if left < 0f32 || right >= width as f32 || top < 0f32 || bottom >= height as f32 { default } else { let (tl, tr, bl, br) = unsafe { ( image.unsafe_get_pixel(left as u32, top as u32), image.unsafe_get_pixel(right as u32, top as u32), image.unsafe_get_pixel(left as u32, bottom as u32), image.unsafe_get_pixel(right as u32, bottom as u32), ) }; blend_bilinear(tl, tr, bl, br, right_weight, bottom_weight) } } #[inline(always)] fn interpolate_nearest(image: &Image

, x: f32, y: f32, default: P) -> P { if x < -0.5 || y < -0.5 { return default; } let (width, height) = image.dimensions(); let rx = (x + 0.5) as u32; let ry = (y + 0.5) as u32; if rx >= width || ry >= height { default } else { unsafe { image.unsafe_get_pixel(rx, ry) } } } /// How to handle pixels whose pre-image lies between input pixels. #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum Interpolation { /// Choose the nearest pixel to the pre-image of the /// output pixel. Nearest, /// Bilinearly interpolate between the four pixels /// closest to the pre-image of the output pixel. Bilinear, /// Bicubicly interpolate between the four pixels /// closest to the pre-image of the output pixel. Bicubic, } #[cfg(test)] mod tests { use super::*; use image::Luma; #[test] fn test_rotate_nearest_zero_radians() { let image = gray_image!( 00, 01, 02; 10, 11, 12); let rotated = rotate( &image, (0f32, 0f32), 0f32, Interpolation::Nearest, Luma([99u8]), ); assert_pixels_eq!(rotated, image); } #[test] fn text_rotate_nearest_quarter_turn_clockwise() { let image = gray_image!( 00, 01, 02; 10, 11, 12); let expected = gray_image!( 11, 01, 99; 12, 02, 99); let c = Projection::translate(1.0, 0.0); let rot = c * Projection::rotate(90f32.to_radians()) * c.invert(); let rotated = warp(&image, &rot, Interpolation::Nearest, Luma([99u8])); assert_pixels_eq!(rotated, expected); } #[test] fn text_rotate_nearest_half_turn_anticlockwise() { let image = gray_image!( 00, 01, 02; 10, 11, 12); let expected = gray_image!( 12, 11, 10; 02, 01, 00); let c = Projection::translate(1.0, 0.5); let rot = c * Projection::rotate((-180f32).to_radians()) * c.invert(); let rotated = warp(&image, &rot, Interpolation::Nearest, Luma([99u8])); assert_pixels_eq!(rotated, expected); } #[test] fn test_translate_positive_x_positive_y() { let image = gray_image!( 00, 01, 02; 10, 11, 12; 20, 21, 22); let expected = gray_image!( 00, 00, 01; 00, 00, 01; 10, 10, 11); let translated = translate(&image, (1, 1)); assert_pixels_eq!(translated, expected); } #[test] fn test_translate_positive_x_negative_y() { let image = gray_image!( 00, 01, 02; 10, 11, 12; 20, 21, 22); let expected = gray_image!( 10, 10, 11; 20, 20, 21; 20, 20, 21); let translated = translate(&image, (1, -1)); assert_pixels_eq!(translated, expected); } #[test] fn test_translate_negative_x() { let image = gray_image!( 00, 01, 02; 10, 11, 12; 20, 21, 22); let expected = gray_image!( 01, 02, 02; 11, 12, 12; 21, 22, 22); let translated = translate(&image, (-1, 0)); assert_pixels_eq!(translated, expected); } #[test] fn test_translate_large_x_large_y() { let image = gray_image!( 00, 01, 02; 10, 11, 12; 20, 21, 22); let expected = gray_image!( 00, 00, 00; 00, 00, 00; 00, 00, 00); // Translating by more than the image width and height let translated = translate(&image, (5, 5)); assert_pixels_eq!(translated, expected); } #[test] fn test_translate_positive_x_positive_y_projection() { let image = gray_image!( 00, 01, 02; 10, 11, 12; 20, 21, 22); let expected = gray_image!( 00, 00, 00; 00, 00, 01; 00, 10, 11); let translated = warp( &image, &Projection::translate(1.0, 1.0), Interpolation::Nearest, Luma([0u8]), ); assert_pixels_eq!(translated, expected); } #[test] fn test_translate_positive_x_negative_y_projection() { let image = gray_image!( 00, 01, 02; 10, 11, 12; 20, 21, 22); let expected = gray_image!( 00, 10, 11; 00, 20, 21; 00, 00, 00); let translated = warp( &image, &Projection::translate(1.0, -1.0), Interpolation::Nearest, Luma([0u8]), ); assert_pixels_eq!(translated, expected); } #[test] fn test_translate_large_x_large_y_projection() { let image = gray_image!( 00, 01, 02; 10, 11, 12; 20, 21, 22); let expected = gray_image!( 00, 00, 00; 00, 00, 00; 00, 00, 00); // Translating by more than the image width and height let translated = warp( &image, &Projection::translate(5.0, 5.0), Interpolation::Nearest, Luma([0u8]), ); assert_pixels_eq!(translated, expected); } #[test] fn test_affine() { let image = gray_image!( 00, 01, 02; 10, 11, 12; 20, 21, 22); let expected = gray_image!( 00, 00, 00; 00, 00, 01; 00, 10, 11); #[rustfmt::skip] let aff = Projection::from_matrix([ 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0 ]).unwrap(); let translated_nearest = warp(&image, &aff, Interpolation::Nearest, Luma([0u8])); assert_pixels_eq!(translated_nearest, expected); let translated_bilinear = warp(&image, &aff, Interpolation::Bilinear, Luma([0u8])); assert_pixels_eq!(translated_bilinear, expected); } #[test] fn test_affine_bicubic() { let image = gray_image!( 99, 01, 02, 03, 04; 10, 11, 12, 13, 14; 20, 21, 22, 23, 24; 30, 31, 32, 33, 34; 40, 41, 42, 43, 44); // Expect 2 pixels each side lost due to kernel size let expected = gray_image!( 00, 00, 00, 00, 00; 00, 00, 00, 00, 00; 00, 00, 11, 00, 00; 00, 00, 00, 00, 00; 00, 00, 00, 00, 00); #[rustfmt::skip] let aff = Projection::from_matrix([ 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0 ]).unwrap(); let translated_bicubic = warp(&image, &aff, Interpolation::Bicubic, Luma([0u8])); assert_pixels_eq!(translated_bicubic, expected); } #[cfg_attr(miri, ignore = "Miri detected UB in nalgebra")] #[test] fn test_from_control_points_translate() { let from = [(0f32, 0.0), (50.0, 50.0), (50.0, 0.0), (0.0, 50.0)]; let to = [(10f32, 5.0), (60.0, 55.0), (60.0, 5.0), (10.0, 55.0)]; let p = Projection::from_control_points(from, to); assert!(p.is_some()); let out = p.unwrap() * (0f32, 0f32); assert_approx_eq!(out.0, 10.0, 1e-10); assert_approx_eq!(out.1, 5.0, 1e-10); } #[cfg_attr(miri, ignore = "Miri detected UB in nalgebra")] #[test] fn test_from_control_points() { let from = [(0f32, 0.0), (50.0, 50.0), (50.0, 0.0), (0.0, 50.0)]; let to = [(16f32, 20.0), (50.0, 50.0), (50.0, 0.0), (0.0, 50.0)]; let p = Projection::from_control_points(from, to); assert!(p.is_some()); let out = p.unwrap() * (0f32, 0f32); assert_approx_eq!(out.0, 16.0, 1e-10); assert_approx_eq!(out.1, 20.0, 1e-10); } #[cfg_attr(miri, ignore = "Miri detected UB in nalgebra")] #[test] fn test_from_control_points_2() { let from = [ (67.24537, 427.96024), (65.51512, 67.96736), (569.6426, 62.33165), (584.4605, 425.33667), ]; let to = [(0.0, 0.0), (640.0, 0.0), (640.0, 480.0), (0.0, 480.0)]; let p = Projection::from_control_points(from, to); assert!(p.is_some()); } #[cfg_attr(miri, ignore = "Miri detected UB in nalgebra")] #[test] /// Test case from https://github.com/image-rs/imageproc/issues/412 fn test_from_control_points_nofreeze() { let from = [ (0.0, 0.0), (250.0, 17.481735), (7.257017, 82.94814), (250.0, 104.18543), ]; let to = [(0.0, 0.0), (249.0, 0.0), (0.0, 105.0), (249.0, 105.0)]; Projection::from_control_points(from, to); } #[cfg_attr(miri, ignore = "Miri detected UB in nalgebra")] #[test] fn test_from_control_points_known_transform() { let t = Projection::translate(10f32, 10f32); let p = t * Projection::rotate(90f32.to_radians()) * t.invert(); let from = [(0f32, 0.0), (50.0, 50.0), (50.0, 0.0), (0.0, 50.0)]; let to = [p * from[0], p * from[1], p * from[2], p * from[3]]; let p_est = Projection::from_control_points(from, to); assert!(p_est.is_some()); let p_est = p_est.unwrap(); for i in 0..50 { for j in 0..50 { let pt = (i as f32, j as f32); assert_approx_eq!((p * pt).0, (p_est * pt).0, 1e-3); assert_approx_eq!((p * pt).1, (p_est * pt).1, 1e-3); } } } #[cfg_attr(miri, ignore = "Miri detected UB in nalgebra")] #[test] fn test_from_control_points_colinear() { let from = [(0f32, 0.0), (50.0, 50.0), (50.0, 0.0), (0.0, 50.0)]; let to = [(0f32, 5.0), (0.0, 55.0), (0.0, 5.0), (10.0, 55.0)]; let p = Projection::from_control_points(from, to); // Should fail if 3 points are colinear assert!(p.is_none()); } #[cfg_attr(miri, ignore = "Miri detected UB in nalgebra")] #[test] fn test_from_control_points_translation() { let p = Projection::translate(10f32, 15f32); let from = [(0f32, 0.0), (50.0, 50.0), (50.0, 0.0), (0.0, 50.0)]; let to = [(10f32, 15.0), (60.0, 65.0), (60.0, 15.0), (10.0, 65.0)]; let p_est = Projection::from_control_points(from, to).unwrap(); for i in 0..50 { for j in 0..50 { let pt = (i as f32, j as f32); assert_approx_eq!((p * pt).0, (p_est * pt).0, 1e-3); assert_approx_eq!((p * pt).1, (p_est * pt).1, 1e-3); } } } #[cfg_attr(miri, ignore = "Miri detected UB in nalgebra")] #[test] fn test_from_control_points_underdetermined() { let from = [ (307.12073f32, 3.2), (330.89783, 3.2), (21.333334, 248.17337), (21.333334, 230.34056), ]; let to = [(0.0f32, 0.0), (3.0, 0.0), (3.0, 3.0), (0.0, 3.0)]; let p = Projection::from_control_points(from, to); p.unwrap(); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::utils::gray_bench_image; use image::{GrayImage, Luma}; use test::{black_box, Bencher}; #[bench] fn bench_rotate_nearest(b: &mut Bencher) { let image = GrayImage::from_pixel(200, 200, Luma([15u8])); let c = Projection::translate(3.0, 3.0); let rot = c * Projection::rotate(1f32.to_degrees()) * c.invert(); b.iter(|| { let rotated = warp(&image, &rot, Interpolation::Nearest, Luma([98u8])); black_box(rotated); }); } #[bench] fn bench_rotate_bilinear(b: &mut Bencher) { let image = GrayImage::from_pixel(200, 200, Luma([15u8])); let c = Projection::translate(3.0, 3.0); let rot = c * Projection::rotate(1f32.to_degrees()) * c.invert(); b.iter(|| { let rotated = warp(&image, &rot, Interpolation::Bilinear, Luma([98u8])); black_box(rotated); }); } #[bench] fn bench_rotate_bicubic(b: &mut Bencher) { let image = GrayImage::from_pixel(200, 200, Luma([15u8])); let c = Projection::translate(3.0, 3.0); let rot = c * Projection::rotate(1f32.to_degrees()) * c.invert(); b.iter(|| { let rotated = warp(&image, &rot, Interpolation::Bicubic, Luma([98u8])); black_box(rotated); }); } #[bench] fn bench_translate(b: &mut Bencher) { let image = gray_bench_image(500, 500); b.iter(|| { let translated = translate(&image, (30, 30)); black_box(translated); }); } #[bench] fn bench_translate_projection(b: &mut Bencher) { let image = gray_bench_image(500, 500); let t = Projection::translate(-30.0, -30.0); b.iter(|| { let translated = warp(&image, &t, Interpolation::Nearest, Luma([0u8])); black_box(translated); }); } #[bench] fn bench_translate_with(b: &mut Bencher) { let image = gray_bench_image(500, 500); b.iter(|| { let (width, height) = image.dimensions(); let mut out = ImageBuffer::new(width, height); warp_into_with( &image, |x, y| (x - 30.0, y - 30.0), Interpolation::Nearest, Luma([0u8]), &mut out, ); black_box(out); }); } #[bench] fn bench_affine_nearest(b: &mut Bencher) { let image = GrayImage::from_pixel(200, 200, Luma([15u8])); #[rustfmt::skip] let aff = Projection::from_matrix([ 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, 0.0, 0.0, 1.0 ]).unwrap(); b.iter(|| { let transformed = warp(&image, &aff, Interpolation::Nearest, Luma([0u8])); black_box(transformed); }); } #[bench] fn bench_affine_bilinear(b: &mut Bencher) { let image = GrayImage::from_pixel(200, 200, Luma([15u8])); #[rustfmt::skip] let aff = Projection::from_matrix([ 1.8, -0.2, 5.0, 0.2, 1.9, 6.0, 0.0002, 0.0003, 1.0 ]).unwrap(); b.iter(|| { let transformed = warp(&image, &aff, Interpolation::Bilinear, Luma([0u8])); black_box(transformed); }); } #[bench] fn bench_affine_bicubic(b: &mut test::Bencher) { let image = GrayImage::from_pixel(200, 200, Luma([15u8])); #[rustfmt::skip] let aff = Projection::from_matrix([ 1.8, -0.2, 5.0, 0.2, 1.9, 6.0, 0.0002, 0.0003, 1.0 ]).unwrap(); b.iter(|| { let transformed = warp(&image, &aff, Interpolation::Bicubic, Luma([0u8])); black_box(transformed); }); } #[bench] fn bench_from_control_points(b: &mut Bencher) { let from = [(0f32, 0.0), (50.0, 50.0), (50.0, 0.0), (0.0, 50.0)]; let to = [(10f32, 5.0), (60.0, 55.0), (60.0, 5.0), (10.0, 55.0)]; b.iter(|| { let proj = Projection::from_control_points(from, to); black_box(proj); }); } } imageproc-0.25.0/src/geometry.rs000064400000000000000000000301321046102023000146450ustar 00000000000000//! Computational geometry functions, for example finding convex hulls. use crate::point::{distance, Line, Point, Rotation}; use num::{cast, NumCast}; use std::cmp::{Ord, Ordering}; use std::f64::{self, consts::PI}; /// Computes the length of an arc. If `closed` is set to `true` then the distance /// between the last and the first point is included in the total length. pub fn arc_length(arc: &[Point], closed: bool) -> f64 where T: NumCast + Copy, { let mut length = arc.windows(2).map(|pts| distance(pts[0], pts[1])).sum(); if arc.len() > 2 && closed { length += distance(arc[0], arc[arc.len() - 1]); } length } /// Approximates a polygon using the [Douglas–Peucker algorithm]. /// /// [Douglas–Peucker algorithm]: https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm pub fn approximate_polygon_dp(curve: &[Point], epsilon: f64, closed: bool) -> Vec> where T: NumCast + Copy, { if epsilon <= 0.0 { panic!("epsilon must be greater than 0.0"); } // Find the point with the maximum distance let mut dmax = 0.0; let mut index = 0; let end = curve.len() - 1; let line = Line::from_points(curve[0].to_f64(), curve[end].to_f64()); for (i, point) in curve.iter().enumerate().skip(1) { let d = line.distance_from_point(point.to_f64()); if d > dmax { index = i; dmax = d; } } // If max distance is greater than epsilon, recursively simplify let mut res = if dmax > epsilon { // Recursive call let mut partial1 = approximate_polygon_dp(&curve[0..=index], epsilon, false); let partial2 = approximate_polygon_dp(&curve[index..=end], epsilon, false); // Build the result list partial1.pop(); partial1.extend(partial2); partial1 } else { vec![curve[0], curve[end]] }; if closed { res.pop(); } res } /// Calculates the area of the contour using the [shoelace formula]. /// The returned value is always non-negative. /// /// [shoelace formula]: https://en.wikipedia.org/wiki/Shoelace_formula pub fn contour_area(points: &[Point]) -> f64 where T: NumCast + Copy, { oriented_contour_area(points).abs() } /// Calculates the oriented area of the contour using the [shoelace formula]. /// The returned value may be negative depending on the contour orientation (clockwise or counter-clockwise). /// /// [shoelace formula]: https://en.wikipedia.org/wiki/Shoelace_formula pub fn oriented_contour_area(points: &[Point]) -> f64 where T: NumCast + Copy, { if points.len() < 3 { return 0.0; } let mut prev = points.last().unwrap().to_f64(); 0.5 * points.iter().map(|p| p.to_f64()).fold(0.0, |mut accum, p| { accum += prev.x * p.y - prev.y * p.x; prev = p; accum }) } /// Finds the rectangle of least area that includes all input points. This rectangle need not be axis-aligned. /// /// The returned points are the [top left, top right, bottom right, bottom left] points of this rectangle. pub fn min_area_rect(points: &[Point]) -> [Point; 4] where T: NumCast + Copy + Ord, { let hull = convex_hull(points); match hull.len() { 0 => panic!("no points are defined"), 1 => [hull[0]; 4], 2 => [hull[0], hull[1], hull[1], hull[0]], _ => rotating_calipers(&hull), } } /// An implementation of [rotating calipers] used for determining the /// bounding rectangle with the smallest area. /// /// [rotating calipers]: https://en.wikipedia.org/wiki/Rotating_calipers fn rotating_calipers(points: &[Point]) -> [Point; 4] where T: NumCast + Copy, { let mut edge_angles: Vec = points .windows(2) .map(|e| { let edge = e[1].to_f64() - e[0].to_f64(); ((edge.y.atan2(edge.x) + PI) % (PI / 2.)).abs() }) .collect(); edge_angles.dedup(); let mut min_area = f64::MAX; let mut res = [Point::new(0.0, 0.0); 4]; for angle in edge_angles { let rotation = Rotation::new(angle); let rotated_points = points.iter().map(|p| p.to_f64().rotate(rotation)); let (min_x, max_x, min_y, max_y) = rotated_points.fold((f64::MAX, f64::MIN, f64::MAX, f64::MIN), |acc, p| { ( acc.0.min(p.x), acc.1.max(p.x), acc.2.min(p.y), acc.3.max(p.y), ) }); let area = (max_x - min_x) * (max_y - min_y); if area < min_area { min_area = area; res[0] = Point::new(max_x, min_y).invert_rotation(rotation); res[1] = Point::new(min_x, min_y).invert_rotation(rotation); res[2] = Point::new(min_x, max_y).invert_rotation(rotation); res[3] = Point::new(max_x, max_y).invert_rotation(rotation); } } res.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap()); let i1 = if res[1].y > res[0].y { 0 } else { 1 }; let i2 = if res[3].y > res[2].y { 2 } else { 3 }; let i3 = if res[3].y > res[2].y { 3 } else { 2 }; let i4 = if res[1].y > res[0].y { 1 } else { 0 }; [ Point::new( cast(res[i1].x.floor()).unwrap(), cast(res[i1].y.floor()).unwrap(), ), Point::new( cast(res[i2].x.ceil()).unwrap(), cast(res[i2].y.floor()).unwrap(), ), Point::new( cast(res[i3].x.ceil()).unwrap(), cast(res[i3].y.ceil()).unwrap(), ), Point::new( cast(res[i4].x.floor()).unwrap(), cast(res[i4].y.ceil()).unwrap(), ), ] } /// Finds the convex hull of a set of points, using the [Graham scan algorithm]. /// /// [Graham scan algorithm]: https://en.wikipedia.org/wiki/Graham_scan pub fn convex_hull(points: impl Into>>) -> Vec> where T: NumCast + Copy + Ord, { let mut points = points.into(); if points.is_empty() { return vec![]; } let mut start_point_pos = 0; let mut start_point = points[0]; for (i, &point) in points.iter().enumerate().skip(1) { if point.y < start_point.y || point.y == start_point.y && point.x < start_point.x { start_point_pos = i; start_point = point; } } points.swap(0, start_point_pos); points.remove(0); points.sort_by( |a, b| match orientation(start_point.to_i32(), a.to_i32(), b.to_i32()) { Orientation::Collinear => { if distance(start_point, *a) < distance(start_point, *b) { Ordering::Less } else { Ordering::Greater } } Orientation::Clockwise => Ordering::Greater, Orientation::CounterClockwise => Ordering::Less, }, ); let mut iter = points.iter().peekable(); let mut remaining_points = Vec::with_capacity(points.len()); while let Some(mut p) = iter.next() { while iter.peek().is_some() && orientation( start_point.to_i32(), p.to_i32(), iter.peek().unwrap().to_i32(), ) == Orientation::Collinear { p = iter.next().unwrap(); } remaining_points.push(p); } let mut stack: Vec> = vec![Point::new( cast(start_point.x).unwrap(), cast(start_point.y).unwrap(), )]; for p in points { while stack.len() > 1 && orientation( stack[stack.len() - 2].to_i32(), stack[stack.len() - 1].to_i32(), p.to_i32(), ) != Orientation::CounterClockwise { stack.pop(); } stack.push(p); } stack } #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum Orientation { Collinear, Clockwise, CounterClockwise, } /// Determines whether p -> q -> r is a left turn, a right turn, or the points are collinear. fn orientation(p: Point, q: Point, r: Point) -> Orientation { let val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); match val.cmp(&0) { Ordering::Equal => Orientation::Collinear, Ordering::Greater => Orientation::Clockwise, Ordering::Less => Orientation::CounterClockwise, } } #[cfg(test)] mod tests { use super::*; use crate::point::Point; #[test] fn test_arc_length() { assert_eq!(arc_length::(&[], false), 0.0); assert_eq!(arc_length(&[Point::new(1.0, 1.0)], false), 0.0); assert_eq!( arc_length(&[Point::new(1.0, 1.0), Point::new(4.0, 5.0)], false), 5.0 ); assert_eq!( arc_length( &[ Point::new(1.0, 1.0), Point::new(4.0, 5.0), Point::new(9.0, 17.0) ], false ), 18.0 ); assert_eq!( arc_length( &[ Point::new(1.0, 1.0), Point::new(4.0, 5.0), Point::new(9.0, 17.0) ], true ), 18.0 + (8f64.powf(2.0) + 16f64.powf(2.0)).sqrt() ); } #[test] fn convex_hull_points() { let star = vec![ Point::new(100, 20), Point::new(90, 35), Point::new(60, 25), Point::new(90, 40), Point::new(80, 55), Point::new(101, 50), Point::new(130, 60), Point::new(115, 45), Point::new(140, 30), Point::new(120, 35), ]; let points = convex_hull(star); assert_eq!( points, [ Point::new(100, 20), Point::new(140, 30), Point::new(130, 60), Point::new(80, 55), Point::new(60, 25) ] ); } #[test] fn convex_hull_points_empty_vec() { let points = convex_hull::(&[]); assert_eq!(points, []); } #[test] fn convex_hull_points_with_negative_values() { let star = vec![ Point::new(100, -20), Point::new(90, 5), Point::new(60, -15), Point::new(90, 0), Point::new(80, 15), Point::new(101, 10), Point::new(130, 20), Point::new(115, 5), Point::new(140, -10), Point::new(120, -5), ]; let points = convex_hull(star); assert_eq!( points, [ Point::new(100, -20), Point::new(140, -10), Point::new(130, 20), Point::new(80, 15), Point::new(60, -15) ] ); } #[test] fn test_min_area() { assert_eq!( min_area_rect(&[ Point::new(100, 20), Point::new(140, 30), Point::new(130, 60), Point::new(80, 55), Point::new(60, 25) ]), [ Point::new(60, 16), Point::new(141, 24), Point::new(137, 61), Point::new(57, 53) ] ) } #[test] fn test_contour_area() { let points = [ Point::new(3, 4), Point::new(5, 11), Point::new(12, 8), Point::new(9, 5), Point::new(5, 6), ]; let oriented_area = oriented_contour_area(&points); assert_eq!(oriented_area, -30.0); let area = contour_area(&points); assert_eq!(area, 30.0); let rect = vec![ Point::new(1, 1), Point::new(1, 2), Point::new(1, 3), Point::new(1, 4), Point::new(2, 4), Point::new(3, 4), Point::new(3, 3), Point::new(3, 2), Point::new(3, 1), Point::new(2, 1), ]; let oriented_area = oriented_contour_area(&rect); assert_eq!(oriented_area, -6.0); let area = contour_area(&rect); assert_eq!(area, 6.0); } } imageproc-0.25.0/src/gradients.rs000064400000000000000000000263771046102023000150120ustar 00000000000000//! Functions for computing gradients of image intensities. use crate::definitions::{HasBlack, Image}; use crate::filter::filter3x3; use crate::map::{ChannelMap, WithChannel}; use image::{GenericImage, GenericImageView, GrayImage, Luma, Pixel}; use itertools::multizip; /// Sobel filter for detecting vertical gradients. /// /// Used by the [`vertical_sobel`](fn.vertical_sobel.html) function. #[rustfmt::skip] pub static VERTICAL_SOBEL: [i32; 9] = [ -1, -2, -1, 0, 0, 0, 1, 2, 1]; /// Sobel filter for detecting horizontal gradients. /// /// Used by the [`horizontal_sobel`](fn.horizontal_sobel.html) function. #[rustfmt::skip] pub static HORIZONTAL_SOBEL: [i32; 9] = [ -1, 0, 1, -2, 0, 2, -1, 0, 1]; /// Scharr filter for detecting vertical gradients. /// /// Used by the [`vertical_scharr`](fn.vertical_scharr.html) function. #[rustfmt::skip] pub static VERTICAL_SCHARR: [i32; 9] = [ -3, -10, -3, 0, 0, 0, 3, 10, 3]; /// Scharr filter for detecting horizontal gradients. /// /// Used by the [`horizontal_scharr`](fn.horizontal_scharr.html) function. #[rustfmt::skip] pub static HORIZONTAL_SCHARR: [i32; 9] = [ -3, 0, 3, -10, 0, 10, -3, 0, 3]; /// Prewitt filter for detecting vertical gradients. /// /// Used by the [`vertical_prewitt`](fn.vertical_prewitt.html) function. #[rustfmt::skip] pub static VERTICAL_PREWITT: [i32; 9] = [ -1, -1, -1, 0, 0, 0, 1, 1, 1]; /// Prewitt filter for detecting horizontal gradients. /// /// Used by the [`horizontal_prewitt`](fn.horizontal_prewitt.html) function. #[rustfmt::skip] pub static HORIZONTAL_PREWITT: [i32; 9] = [ -1, 0, 1, -1, 0, 1, -1, 0, 1]; /// Convolves an image with the [`HORIZONTAL_SOBEL`](static.HORIZONTAL_SOBEL.html) /// kernel to detect horizontal gradients. pub fn horizontal_sobel(image: &GrayImage) -> Image> { filter3x3(image, &HORIZONTAL_SOBEL) } /// Convolves an image with the [`VERTICAL_SOBEL`](static.VERTICAL_SOBEL.html) /// kernel to detect vertical gradients. pub fn vertical_sobel(image: &GrayImage) -> Image> { filter3x3(image, &VERTICAL_SOBEL) } /// Convolves an image with the [`HORIZONTAL_SCHARR`](static.HORIZONTAL_SCHARR.html) /// kernel to detect horizontal gradients. pub fn horizontal_scharr(image: &GrayImage) -> Image> { filter3x3(image, &HORIZONTAL_SCHARR) } /// Convolves an image with the [`VERTICAL_SCHARR`](static.VERTICAL_SCHARR.html) /// kernel to detect vertical gradients. pub fn vertical_scharr(image: &GrayImage) -> Image> { filter3x3(image, &VERTICAL_SCHARR) } /// Convolves an image with the [`HORIZONTAL_PREWITT`](static.HORIZONTAL_PREWITT.html) /// kernel to detect horizontal gradients. pub fn horizontal_prewitt(image: &GrayImage) -> Image> { filter3x3(image, &HORIZONTAL_PREWITT) } /// Convolves an image with the [`VERTICAL_PREWITT`](static.VERTICAL_PREWITT.html) /// kernel to detect vertical gradients. pub fn vertical_prewitt(image: &GrayImage) -> Image> { filter3x3(image, &VERTICAL_PREWITT) } /// Returns the magnitudes of gradients in an image using Sobel filters. pub fn sobel_gradients(image: &GrayImage) -> Image> { gradients(image, &HORIZONTAL_SOBEL, &VERTICAL_SOBEL, |p| p) } /// Computes per-channel gradients using Sobel filters and calls `f` /// to compute each output pixel. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::gradients::{sobel_gradient_map}; /// use image::Luma; /// use std::cmp; /// /// // A shallow horizontal gradient in the red /// // channel, a steeper vertical gradient in the /// // blue channel, constant in the green channel. /// let input = rgb_image!( /// [0, 5, 0], [1, 5, 0], [2, 5, 0]; /// [0, 5, 2], [1, 5, 2], [2, 5, 2]; /// [0, 5, 4], [1, 5, 4], [2, 5, 4] /// ); /// /// // Computing independent per-channel gradients. /// let channel_gradient = rgb_image!(type: u16, /// [ 4, 0, 8], [ 8, 0, 8], [ 4, 0, 8]; /// [ 4, 0, 16], [ 8, 0, 16], [ 4, 0, 16]; /// [ 4, 0, 8], [ 8, 0, 8], [ 4, 0, 8] /// ); /// /// assert_pixels_eq!( /// sobel_gradient_map(&input, |p| p), /// channel_gradient /// ); /// /// // Defining the gradient of an RGB image to be the /// // mean of its per-channel gradients. /// let mean_gradient = gray_image!(type: u16, /// 4, 5, 4; /// 6, 8, 6; /// 4, 5, 4 /// ); /// /// assert_pixels_eq!( /// sobel_gradient_map(&input, |p| { /// let mean = (p[0] + p[1] + p[2]) / 3; /// Luma([mean]) /// }), /// mean_gradient /// ); /// /// // Defining the gradient of an RGB image to be the pixelwise /// // maximum of its per-channel gradients. /// let max_gradient = gray_image!(type: u16, /// 8, 8, 8; /// 16, 16, 16; /// 8, 8, 8 /// ); /// /// assert_pixels_eq!( /// sobel_gradient_map(&input, |p| { /// let max = cmp::max(cmp::max(p[0], p[1]), p[2]); /// Luma([max]) /// }), /// max_gradient /// ); /// # } pub fn sobel_gradient_map(image: &Image

, f: F) -> Image where P: Pixel + WithChannel + WithChannel, Q: Pixel, ChannelMap: HasBlack, F: Fn(ChannelMap) -> Q, { gradients(image, &HORIZONTAL_SOBEL, &VERTICAL_SOBEL, f) } /// Returns the magnitudes of gradients in an image using Prewitt filters. pub fn prewitt_gradients(image: &GrayImage) -> Image> { gradients(image, &HORIZONTAL_PREWITT, &VERTICAL_PREWITT, |p| p) } // TODO: Returns directions as well as magnitudes. // TODO: Support filtering without allocating a fresh image - filtering functions could // TODO: take some kind of pixel-sink. This would allow us to compute gradient magnitudes // TODO: and directions without allocating intermediates for vertical and horizontal gradients. fn gradients( image: &Image

, horizontal_kernel: &[i32; 9], vertical_kernel: &[i32; 9], f: F, ) -> Image where P: Pixel + WithChannel + WithChannel, Q: Pixel, ChannelMap: HasBlack, F: Fn(ChannelMap) -> Q, { let horizontal: Image> = filter3x3::<_, _, i16>(image, horizontal_kernel); let vertical: Image> = filter3x3::<_, _, i16>(image, vertical_kernel); let (width, height) = image.dimensions(); let mut out = Image::::new(width, height); // This would be more concise using itertools::multizip over image pixels, but that increased runtime by around 20% for y in 0..height { for x in 0..width { // JUSTIFICATION // Benefit // Using checked indexing here makes this sobel_gradients 1.1x slower, // as measured by bench_sobel_gradients // Correctness // x and y are in bounds for image by construction, // vertical and horizontal are the result of calling filter3x3 on image, // and filter3x3 returns an image of the same size as its input let (h, v) = unsafe { ( horizontal.unsafe_get_pixel(x, y), vertical.unsafe_get_pixel(x, y), ) }; let mut p = ChannelMap::::black(); for (h, v, p) in multizip((h.channels(), v.channels(), p.channels_mut())) { *p = gradient_magnitude(*h as f32, *v as f32); } // JUSTIFICATION // Benefit // Using checked indexing here makes this sobel_gradients 1.1x slower, // as measured by bench_sobel_gradients // Correctness // x and y are in bounds for image by construction, // and out has the same dimensions unsafe { out.unsafe_put_pixel(x, y, f(p)); } } } out } #[inline] fn gradient_magnitude(dx: f32, dy: f32) -> u16 { (dx.powi(2) + dy.powi(2)).sqrt() as u16 } #[cfg(test)] mod tests { use super::*; use image::{ImageBuffer, Luma}; #[rustfmt::skip::macros(gray_image)] #[test] fn test_gradients_constant_image() { let image = ImageBuffer::from_pixel(5, 5, Luma([15u8])); let expected = ImageBuffer::from_pixel(5, 5, Luma([0i16])); assert_pixels_eq!(horizontal_sobel(&image), expected); assert_pixels_eq!(vertical_sobel(&image), expected); assert_pixels_eq!(horizontal_scharr(&image), expected); assert_pixels_eq!(vertical_scharr(&image), expected); assert_pixels_eq!(horizontal_prewitt(&image), expected); assert_pixels_eq!(vertical_prewitt(&image), expected); } #[test] fn test_horizontal_sobel_gradient_image() { let image = gray_image!( 3, 2, 1; 6, 5, 4; 9, 8, 7); let expected = gray_image!(type: i16, -4, -8, -4; -4, -8, -4; -4, -8, -4); let filtered = horizontal_sobel(&image); assert_pixels_eq!(filtered, expected); } #[test] fn test_vertical_sobel_gradient_image() { let image = gray_image!( 3, 6, 9; 2, 5, 8; 1, 4, 7); let expected = gray_image!(type: i16, -4, -4, -4; -8, -8, -8; -4, -4, -4); let filtered = vertical_sobel(&image); assert_pixels_eq!(filtered, expected); } #[test] fn test_horizontal_scharr_gradient_image() { let image = gray_image!( 3, 2, 1; 6, 5, 4; 9, 8, 7); let expected = gray_image!(type: i16, -16, -32, -16; -16, -32, -16; -16, -32, -16); let filtered = horizontal_scharr(&image); assert_pixels_eq!(filtered, expected); } #[test] fn test_vertical_scharr_gradient_image() { let image = gray_image!( 3, 6, 9; 2, 5, 8; 1, 4, 7); let expected = gray_image!(type: i16, -16, -16, -16; -32, -32, -32; -16, -16, -16); let filtered = vertical_scharr(&image); assert_pixels_eq!(filtered, expected); } #[test] fn test_horizontal_prewitt_gradient_image() { let image = gray_image!( 3, 2, 1; 6, 5, 4; 9, 8, 7); let expected = gray_image!(type: i16, -3, -6, -3; -3, -6, -3; -3, -6, -3); let filtered = horizontal_prewitt(&image); assert_pixels_eq!(filtered, expected); } #[test] fn test_vertical_prewitt_gradient_image() { let image = gray_image!( 3, 6, 9; 2, 5, 8; 1, 4, 7); let expected = gray_image!(type: i16, -3, -3, -3; -6, -6, -6; -3, -3, -3); let filtered = vertical_prewitt(&image); assert_pixels_eq!(filtered, expected); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::utils::gray_bench_image; use test::{black_box, Bencher}; #[bench] fn bench_sobel_gradients(b: &mut Bencher) { let image = gray_bench_image(500, 500); b.iter(|| { let gradients = sobel_gradients(&image); black_box(gradients); }); } } imageproc-0.25.0/src/haar.rs000064400000000000000000000613621046102023000137360ustar 00000000000000//! Functions for creating and evaluating [Haar-like features]. //! //! [Haar-like features]: https://en.wikipedia.org/wiki/Haar-like_features use crate::definitions::{HasBlack, HasWhite, Image}; use image::{GenericImage, GenericImageView, ImageBuffer, Luma}; use itertools::Itertools; use std::marker::PhantomData; use std::ops::Range; /// A [Haar-like feature]. /// /// [Haar-like feature]: https://en.wikipedia.org/wiki/Haar-like_features #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct HaarFeature { sign: Sign, feature_type: HaarFeatureType, block_size: Size, left: u8, top: u8, } /// Whether the top left region in a Haar-like feature is counted /// with positive or negative sign. #[derive(Copy, Clone, PartialEq, Eq, Debug)] enum Sign { /// Top left region is counted with a positive sign. Positive, /// Top left region is counted with a negative sign. Negative, } /// The type of a Haar-like feature determines the number of regions it contains and their orientation. /// The diagrams in the comments for each variant use the symbols (*, &) to represent either /// (+, -) or (-, +), depending on which `Sign` the feature type is used with. #[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)] pub enum HaarFeatureType { /// Two horizontally-adjacent regions of equal width. ///

    ///      -----------
    ///     |  *  |  &  |
    ///      -----------
    /// 
TwoRegionHorizontal, /// Three horizontally-adjacent regions of equal width. ///
    ///      -----------------
    ///     |  *  |  &  |  *  |
    ///      -----------------
    /// 
ThreeRegionHorizontal, /// Two vertically-adjacent regions of equal height. ///
    ///      -----
    ///     |  *  |
    ///      -----
    ///     |  &  |
    ///      -----
    /// 
TwoRegionVertical, /// Three vertically-adjacent regions of equal height. ///
    ///      -----
    ///     |  *  |
    ///      -----
    ///     |  &  |
    ///      -----
    ///     |  *  |
    ///      -----
    /// 
ThreeRegionVertical, /// Four regions arranged in a two-by-two grid. The two columns /// have equal width and the two rows have equal height. ///
    ///      -----------
    ///     |  *  |  &  |
    ///      -----------
    ///     |  &  |  *  |
    ///      -----------
    /// 
FourRegion, } impl HaarFeatureType { // The width and height of Haar-like feature, in blocks. fn shape(&self) -> Size { match *self { HaarFeatureType::TwoRegionHorizontal => Size::new(2, 1), HaarFeatureType::ThreeRegionHorizontal => Size::new(3, 1), HaarFeatureType::TwoRegionVertical => Size::new(1, 2), HaarFeatureType::ThreeRegionVertical => Size::new(1, 3), HaarFeatureType::FourRegion => Size::new(2, 2), } } } impl HaarFeature { /// Evaluates the Haar-like feature on an integral image. pub fn evaluate(&self, integral: &Image>) -> i32 { // This check increases the run time of bench_evaluate_all_features_10x10 // by approximately 16%. Without it this function is unsafe as insufficiently // large input images result in out of bounds accesses. // // We could alternatively create a new API where an image and a set of filters // are validated to be compatible up front, or just mark the function // as unsafe and document the requirement on image size. let size = feature_size(self.feature_type, self.block_size); assert!(integral.width() > size.width as u32 + self.left as u32); assert!(integral.height() > size.height as u32 + self.top as u32); // The corners of each block are lettered. Not all letters are evaluated for each feature type. // A B C D // // E F G H // // I J K L // // M N O let a = self.block_boundary(0, 0); let b = self.block_boundary(1, 0); let e = self.block_boundary(0, 1); let f = self.block_boundary(1, 1); #[rustfmt::skip] let sum = match self.feature_type { HaarFeatureType::TwoRegionHorizontal => { let c = self.block_boundary(2, 0); let g = self.block_boundary(2, 1); unsafe { read(integral, a) - 2 * read(integral, b) + read(integral, c) - read(integral, e) + 2 * read(integral, f) - read(integral, g) } } HaarFeatureType::ThreeRegionHorizontal => { let c = self.block_boundary(2, 0); let g = self.block_boundary(2, 1); let d = self.block_boundary(3, 0); let h = self.block_boundary(3, 1); unsafe { read(integral, a) - 2 * read(integral, b) + 2 * read(integral, c) - read(integral, d) - read(integral, e) + 2 * read(integral, f) - 2 * read(integral, g) + read(integral, h) } } HaarFeatureType::TwoRegionVertical => { let i = self.block_boundary(0, 2); let j = self.block_boundary(1, 2); unsafe { read(integral, a) - read(integral, b) - 2 * read(integral, e) + 2 * read(integral, f) + read(integral, i) - read(integral, j) } } HaarFeatureType::ThreeRegionVertical => { let i = self.block_boundary(0, 2); let j = self.block_boundary(1, 2); let m = self.block_boundary(0, 3); let n = self.block_boundary(1, 3); unsafe { read(integral, a) - read(integral, b) - 2 * read(integral, e) + 2 * read(integral, f) + 2 * read(integral, i) - 2 * read(integral, j) - read(integral, m) + read(integral, n) } } HaarFeatureType::FourRegion => { let c = self.block_boundary(2, 0); let g = self.block_boundary(2, 1); let i = self.block_boundary(0, 2); let j = self.block_boundary(1, 2); let k = self.block_boundary(2, 2); unsafe { read(integral, a) - 2 * read(integral, b) + read(integral, c) - 2 * read(integral, e) + 4 * read(integral, f) - 2 * read(integral, g) + read(integral, i) - 2 * read(integral, j) + read(integral, k) } } }; let mul = if self.sign == Sign::Positive { 1i32 } else { -1i32 }; sum * mul } fn block_boundary(&self, x: u8, y: u8) -> (u8, u8) { ( self.left + x * self.block_width(), self.top + y * self.block_height(), ) } /// Width of this feature in blocks. fn blocks_wide(&self) -> u8 { self.feature_type.shape().width } /// Height of this feature in blocks. fn blocks_high(&self) -> u8 { self.feature_type.shape().height } /// Width of each block in pixels. fn block_width(&self) -> u8 { self.block_size.width } /// Height of each block in pixels. fn block_height(&self) -> u8 { self.block_size.height } } unsafe fn read(integral: &Image>, location: (u8, u8)) -> i32 { integral.unsafe_get_pixel(location.0 as u32, location.1 as u32)[0] as i32 } // The total width and height of a feature with the given type and block size. fn feature_size(feature_type: HaarFeatureType, block_size: Size) -> Size { let shape = feature_type.shape(); Size::new( shape.width * block_size.width, shape.height * block_size.height, ) } /// Returns a vector of all valid Haar-like features for an image with given width and height. pub fn enumerate_haar_features(frame_width: u8, frame_height: u8) -> Vec { let frame_size = Size::new(frame_width, frame_height); let feature_types = [ HaarFeatureType::TwoRegionHorizontal, HaarFeatureType::ThreeRegionHorizontal, HaarFeatureType::TwoRegionVertical, HaarFeatureType::ThreeRegionVertical, HaarFeatureType::FourRegion, ]; feature_types .into_iter() .flat_map(|feature_type| haar_features_of_type(feature_type, frame_size)) .collect() } fn haar_features_of_type( feature_type: HaarFeatureType, frame_size: Size, ) -> Vec { let mut features = Vec::new(); for block_size in block_sizes(feature_type.shape(), frame_size) { for (left, top) in feature_positions(feature_size(feature_type, block_size), frame_size) { for &sign in [Sign::Positive, Sign::Negative].iter() { features.push(HaarFeature { sign, feature_type, block_size, left, top, }); } } } features } // Indicates that a size size is measured in pixels, e.g. the width of an individual block within a Haar-like feature. #[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, PartialOrd, Ord)] struct Pixels(u8); // Indicates that a size is measured in blocks, e.g. the width of a Haar-like feature in blocks. #[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, PartialOrd, Ord)] struct Blocks(u8); // A Size, measured either in pixels (T = Pixels) or in blocks (T = Blocks) #[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)] struct Size { width: u8, height: u8, units: PhantomData, } impl Size { fn new(width: u8, height: u8) -> Size { Size { width, height, units: PhantomData, } } } // Returns the valid block sizes for a feature of shape `feature_shape` in a frame of size `frame_size`. fn block_sizes(feature_shape: Size, frame_size: Size) -> Vec> { (1..frame_size.width / feature_shape.width + 1) .cartesian_product(1..frame_size.height / feature_shape.height + 1) .map(|(w, h)| Size::new(w, h)) .collect() } // Returns the start positions for an interval of length `inner` for which the // interval is wholly contained within an interval of length `outer`. fn start_positions(inner: u8, outer: u8) -> Range { let upper = if inner > outer { 0 } else { outer - inner + 1 }; 0..upper } // Returns all valid (left, top) coordinates for a feature of the given total size fn feature_positions(feature_size: Size, frame_size: Size) -> Vec<(u8, u8)> { start_positions(feature_size.width, frame_size.width) .cartesian_product(start_positions(feature_size.height, frame_size.height)) .collect() } /// Returns the number of distinct Haar-like features for an image of the given dimensions. /// /// Includes positive and negative, two and three region, vertical and horizontal features, /// as well as positive and negative four region features. /// /// Consider a `k`-region horizontal feature in an image of height `1` and width `w`. The largest valid block size /// for such a feature is `M = floor(w / k)`, and for a block size `s` there are `(w + 1) - 2 * s` /// valid locations for the leftmost column of this feature. /// Summing over `s` gives `M * (w + 1) - k * [(M * (M + 1)) / 2]`. /// /// An equivalent argument applies vertically. pub fn number_of_haar_features(width: u32, height: u32) -> u32 { let num_positive_features = // Two-region horizontal num_features(width, 2) * num_features(height, 1) // Three-region horizontal + num_features(width, 3) * num_features(height, 1) // Two-region vertical + num_features(width, 1) * num_features(height, 2) // Three-region vertical + num_features(width, 1) * num_features(height, 3) // Four-region + num_features(width, 2) * num_features(height, 2); num_positive_features * 2 } fn num_features(image_side: u32, num_blocks: u32) -> u32 { let m = image_side / num_blocks; m * (image_side + 1) - num_blocks * ((m * (m + 1)) / 2) } /// Draws the given Haar-like feature on an image, drawing pixels /// with a positive sign white and those with a negative sign black. pub fn draw_haar_feature(image: &I, feature: HaarFeature) -> Image where I: GenericImage, I::Pixel: HasBlack + HasWhite, { let mut out = ImageBuffer::new(image.width(), image.height()); out.copy_from(image, 0, 0).unwrap(); draw_haar_feature_mut(&mut out, feature); out } /// Draws the given Haar-like feature on an image in place, drawing pixels /// with a positive sign white and those with a negative sign black. pub fn draw_haar_feature_mut(image: &mut I, feature: HaarFeature) where I: GenericImage, I::Pixel: HasBlack + HasWhite, { let parity_shift = if feature.sign == Sign::Positive { 0 } else { 1 }; for w in 0..feature.blocks_wide() { for h in 0..feature.blocks_high() { let parity = (w + h + parity_shift) % 2; let color = if parity == 0 { I::Pixel::white() } else { I::Pixel::black() }; for x in 0..feature.block_width() { for y in 0..feature.block_height() { let px = feature.left + w * feature.block_width() + x; let py = feature.top + h * feature.block_height() + y; image.put_pixel(px as u32, py as u32, color); } } } } } #[cfg(test)] mod tests { use super::*; use crate::integral_image::{integral_image, sum_image_pixels}; use crate::utils::gray_bench_image; #[test] fn test_block_sizes() { assert_eq!( block_sizes( HaarFeatureType::TwoRegionHorizontal.shape(), Size::new(1, 1) ), vec![] ); assert_eq!( block_sizes( HaarFeatureType::TwoRegionHorizontal.shape(), Size::new(2, 1) ), vec![Size::new(1, 1)] ); assert_eq!( block_sizes( HaarFeatureType::TwoRegionHorizontal.shape(), Size::new(5, 1) ), vec![Size::new(1, 1), Size::new(2, 1)] ); assert_eq!( block_sizes(HaarFeatureType::TwoRegionVertical.shape(), Size::new(1, 2)), vec![Size::new(1, 1)] ); } #[test] fn test_feature_positions() { assert_eq!(feature_positions(Size::new(2, 3), Size::new(2, 2)), vec![]); assert_eq!( feature_positions(Size::new(2, 2), Size::new(2, 2)), vec![(0, 0)] ); assert_eq!( feature_positions(Size::new(2, 2), Size::new(3, 2)), vec![(0, 0), (1, 0)] ); assert_eq!( feature_positions(Size::new(2, 2), Size::new(3, 3)), vec![(0, 0), (0, 1), (1, 0), (1, 1)] ); } #[test] fn test_number_of_haar_features() { for h in 0..6 { for w in 0..6 { let features = enumerate_haar_features(w, h); let actual = features.len() as u32; let expected = number_of_haar_features(w as u32, h as u32); assert_eq!(actual, expected, "w = {}, h = {}", w, h); } } } #[test] #[should_panic] fn test_haar_invalid_image_size_top_left() { let image = gray_image!(type: u32, 0, 0; 0, 1); let feature = HaarFeature { sign: Sign::Positive, feature_type: HaarFeatureType::TwoRegionHorizontal, block_size: Size::new(1, 1), left: 0, top: 0, }; // For a haar feature of width 2 the input image needs to have width // at least 2, and so its integral image needs to have width at least 3. let _ = feature.evaluate(&image); } #[test] fn test_haar_valid_image_size_top_left() { let image = gray_image!(type: u32, 0, 0, 0; 0, 1, 1); let feature = HaarFeature { sign: Sign::Positive, feature_type: HaarFeatureType::TwoRegionHorizontal, block_size: Size::new(1, 1), left: 0, top: 0, }; let x = feature.evaluate(&image); assert_eq!(x, 1); } #[test] #[should_panic] fn test_haar_invalid_image_size_with_offset_feature() { let image = gray_image!(type: u32, 0, 0, 0; 0, 1, 1); let feature = HaarFeature { sign: Sign::Positive, feature_type: HaarFeatureType::TwoRegionHorizontal, block_size: Size::new(1, 1), left: 1, top: 0, }; // The feature's left offset would result in attempting to // read outside the image boundaries let _ = feature.evaluate(&image); } #[test] fn test_haar_valid_image_size_with_offset_feature() { let image = gray_image!(type: u32, 0, 0, 0, 0; 0, 1, 1, 1); let feature = HaarFeature { sign: Sign::Positive, feature_type: HaarFeatureType::TwoRegionHorizontal, block_size: Size::new(1, 1), left: 1, top: 0, }; let x = feature.evaluate(&image); assert_eq!(x, 0); } #[test] fn test_two_region_horizontal() { let image = gray_image!( 1u8, 2u8, 3u8, 4u8, 5u8; /***+++++++++*****---------***/ 6u8, /**/7u8, 8u8,/**/ 9u8, 0u8;/**/ 9u8, /**/8u8, 7u8,/**/ 6u8, 5u8;/**/ 4u8, /**/3u8, 2u8,/**/ 1u8, 0u8;/**/ /***+++++++++*****---------***/ 6u8, 5u8, 4u8, 2u8, 1u8 ); let integral = integral_image(&image); let feature = HaarFeature { sign: Sign::Positive, feature_type: HaarFeatureType::TwoRegionHorizontal, block_size: Size::new(2, 3), left: 1, top: 1, }; assert_eq!(feature.evaluate(&integral), 14i32); } #[test] fn test_three_region_vertical() { let image = gray_image!( /*****************/ /*-*/1u8, 2u8,/*-*/ 3u8, 4u8, 5u8; /*****************/ /*+*/6u8, 7u8,/*+*/ 8u8, 9u8, 0u8; /*****************/ /*-*/9u8, 8u8,/*-*/ 7u8, 6u8, 5u8; /*****************/ 4u8, 3u8, 2u8, 1u8, 0u8; 6u8, 5u8, 4u8, 2u8, 1u8); let integral = integral_image(&image); let feature = HaarFeature { sign: Sign::Negative, feature_type: HaarFeatureType::ThreeRegionVertical, block_size: Size::new(2, 1), left: 0, top: 0, }; assert_eq!(feature.evaluate(&integral), -7i32); } #[test] fn test_four_region() { let image = gray_image!( /*****************************/ 1u8,/**/2u8, 3u8,/**/ 4u8, 5u8;/**/ 6u8,/**/7u8, 8u8,/**/ 9u8, 0u8;/**/ /*****************************/ 9u8,/**/8u8, 7u8,/**/ 6u8, 5u8;/**/ 4u8,/**/3u8, 2u8,/**/ 1u8, 0u8;/**/ /*****************************/ 6u8, 5u8, 4u8, 2u8, 1u8); let integral = integral_image(&image); let feature = HaarFeature { sign: Sign::Positive, feature_type: HaarFeatureType::FourRegion, block_size: Size::new(2, 2), left: 1, top: 0, }; assert_eq!(feature.evaluate(&integral), -6i32); } // Reference implementation of Haar-like feature evaluation, to validate faster implementations against. fn reference_evaluate(feature: HaarFeature, integral: &Image>) -> i32 { let parity_shift = if feature.sign == Sign::Positive { 0 } else { 1 }; let mut sum = 0i32; for w in 0..feature.blocks_wide() { let left = feature.left + feature.block_width() * w; let right = left + feature.block_width() - 1; for h in 0..feature.blocks_high() { let top = feature.top + feature.block_height() * h; let bottom = top + feature.block_height() - 1; let parity = (w + h + parity_shift) & 1; let multiplier = 1 - 2 * (parity as i32); let block_sum = sum_image_pixels( integral, left as u32, top as u32, right as u32, bottom as u32, )[0] as i32; sum += multiplier * block_sum; } } sum } #[test] fn test_haar_evaluate_against_reference_implementation() { for w in 0..6 { for h in 0..6 { let features = enumerate_haar_features(w, h); let image = gray_bench_image(w as u32, h as u32); let integral = integral_image(&image); for feature in features { let actual = feature.evaluate(&integral); let expected = reference_evaluate(feature, &integral); assert_eq!(actual, expected, "w = {}, h = {}", w, h); } } } } #[test] fn test_draw_haar_feature_two_region_horizontal() { let image = gray_image!( 1u8, 2u8, 3u8, 4u8, 5u8; /***+++++++++*****---------***/ 6u8, /**/7u8, 8u8,/**/ 9u8, 0u8;/**/ 9u8, /**/8u8, 7u8,/**/ 6u8, 5u8;/**/ 4u8, /**/3u8, 2u8,/**/ 1u8, 0u8;/**/ /***+++++++++*****---------***/ 6u8, 5u8, 4u8, 2u8, 1u8); let feature = HaarFeature { sign: Sign::Positive, feature_type: HaarFeatureType::TwoRegionHorizontal, block_size: Size::new(2, 3), left: 1, top: 1, }; let actual = draw_haar_feature(&image, feature); let expected = gray_image!( 1u8, 2u8, 3u8, 4u8, 5u8; /***+++++++++++++*****---------***/ 6u8, /**/255u8, 255u8,/**/ 0u8, 0u8;/**/ 9u8, /**/255u8, 255u8,/**/ 0u8, 0u8;/**/ 4u8, /**/255u8, 255u8,/**/ 0u8, 0u8;/**/ /***+++++++++++++*****---------***/ 6u8, 5u8, 4u8, 2u8, 1u8); assert_pixels_eq!(actual, expected); } #[test] fn test_draw_haar_feature_four_region() { let image = gray_image!( /*****************************/ 1u8,/**/2u8, 3u8,/**/ 4u8, 5u8;/**/ 6u8,/**/7u8, 8u8,/**/ 9u8, 0u8;/**/ /*****************************/ 9u8,/**/8u8, 7u8,/**/ 6u8, 5u8;/**/ 4u8,/**/3u8, 2u8,/**/ 1u8, 0u8;/**/ /*****************************/ 6u8, 5u8, 4u8, 2u8, 1u8); let feature = HaarFeature { sign: Sign::Positive, feature_type: HaarFeatureType::FourRegion, block_size: Size::new(2, 2), left: 1, top: 0, }; let actual = draw_haar_feature(&image, feature); let expected = gray_image!( /*************************************/ 1u8,/**/255u8, 255u8,/**/ 0u8, 0u8; /**/ 6u8,/**/255u8, 255u8,/**/ 0u8, 0u8; /**/ /*************************************/ 9u8,/**/0u8, 0u8, /**/ 255u8, 255u8;/**/ 4u8,/**/0u8, 0u8, /**/ 255u8, 255u8;/**/ /*************************************/ 6u8, 5u8, 4u8, 2u8, 1u8); assert_pixels_eq!(actual, expected); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::{integral_image::integral_image, utils::gray_bench_image}; #[bench] fn bench_evaluate_all_features_10x10(b: &mut test::Bencher) { // 10050 features in total let features = enumerate_haar_features(10, 10); let image = gray_bench_image(10, 10); let integral = integral_image(&image); b.iter(|| { for feature in &features { let x = feature.evaluate(&integral); test::black_box(x); } }); } } imageproc-0.25.0/src/hog.rs000064400000000000000000000567121046102023000136030ustar 00000000000000//! [HoG features](https://en.wikipedia.org/wiki/Histogram_of_oriented_gradients) //! and helpers for visualizing them. use crate::definitions::{Clamp, Image}; use crate::gradients::{horizontal_sobel, vertical_sobel}; use crate::math::l2_norm; use image::{GenericImage, GrayImage, ImageBuffer, Luma}; use num::Zero; use std::f32; /// Parameters for HoG descriptors. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct HogOptions { /// Number of gradient orientation bins. pub orientations: usize, /// Whether gradients in opposite directions are treated as equal. pub signed: bool, /// Width and height of cell in pixels. pub cell_side: usize, /// Width and height of block in cells. pub block_side: usize, /// Offset of the start of one block from the next in cells. pub block_stride: usize, // TODO: choice of normalisation - for now we just scale to unit L2 norm } impl HogOptions { /// User-provided options, prior to validation. pub fn new( orientations: usize, signed: bool, cell_side: usize, block_side: usize, block_stride: usize, ) -> HogOptions { HogOptions { orientations, signed, cell_side, block_side, block_stride, } } } /// HoG options plus values calculated from these options and the desired /// image dimensions. Validation must occur when instances of this struct /// are created - functions receiving a spec will assume that it is valid. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct HogSpec { /// Original options. options: HogOptions, /// Number of non-overlapping cells required to cover the image's width. cells_wide: usize, /// Number of non-overlapping cells required to cover the image height. cells_high: usize, /// Number of (possibly overlapping) blocks required to cover the image's width. blocks_wide: usize, /// Number of (possibly overlapping) blocks required to cover the image's height. blocks_high: usize, } impl HogSpec { /// Returns an error message if image dimensions aren't compatible with the provided options. pub fn from_options(width: u32, height: u32, options: HogOptions) -> Result { let (cells_wide, cells_high) = Self::checked_cell_dimensions(width as usize, height as usize, options)?; let (blocks_wide, blocks_high) = Self::checked_block_dimensions(cells_wide, cells_high, options)?; Ok(HogSpec { options, cells_wide, cells_high, blocks_wide, blocks_high, }) } fn invalid_options_message(errors: &[String]) -> String { format!("Invalid HoG options: {0}", errors.join(", ")) } /// Returns (cells wide, cells high), or an error message if cell side doesn't evenly divide width and height. fn checked_cell_dimensions( width: usize, height: usize, options: HogOptions, ) -> Result<(usize, usize), String> { let mut errors: Vec = vec![]; if width % options.cell_side != 0 { errors.push(format!( "cell side {} does not evenly divide width {}", options.cell_side, width )); } if height % options.cell_side != 0 { errors.push(format!( "cell side {} does not evenly divide height {}", options.cell_side, height )); } if !errors.is_empty() { return Err(Self::invalid_options_message(&errors)); } Ok((width / options.cell_side, height / options.cell_side)) } /// Returns (blocks wide, blocks high), or an error message if the block size and stride don't evenly cover /// the grid of cells. fn checked_block_dimensions( cells_wide: usize, cells_high: usize, options: HogOptions, ) -> Result<(usize, usize), String> { let mut errors: Vec = vec![]; if (cells_wide - options.block_side) % options.block_stride != 0 { errors.push(format!( "block stride {} does not evenly divide (cells wide {} - block side {})", options.block_stride, cells_wide, options.block_side )); } if (cells_high - options.block_side) % options.block_stride != 0 { errors.push(format!( "block stride {} does not evenly divide (cells high {} - block side {})", options.block_stride, cells_high, options.block_side )); } if !errors.is_empty() { return Err(Self::invalid_options_message(&errors)); } Ok(( num_blocks(cells_wide, options.block_side, options.block_stride), num_blocks(cells_high, options.block_side, options.block_stride), )) } /// The total size in floats of the HoG descriptor with these dimensions. pub fn descriptor_length(&self) -> usize { self.blocks_wide * self.blocks_high * self.block_descriptor_length() } /// The size in floats of the descriptor for a single block. fn block_descriptor_length(&self) -> usize { self.options.orientations * self.options.block_side.pow(2) } /// Dimensions of a grid of cell histograms, viewed as a 3d array. /// Innermost dimension is orientation bin, then horizontal cell location, /// then vertical cell location. fn cell_grid_lengths(&self) -> [usize; 3] { [self.options.orientations, self.cells_wide, self.cells_high] } /// Dimensions of a grid of block descriptors, viewed as a 3d array. /// Innermost dimension is block descriptor position, then horizontal block location, /// then vertical block location. fn block_grid_lengths(&self) -> [usize; 3] { [ self.block_descriptor_length(), self.blocks_wide, self.blocks_high, ] } /// Dimensions of a single block descriptor, viewed as a 3d array. /// Innermost dimension is histogram bin, then horizontal cell location, then /// vertical cell location. fn block_internal_lengths(&self) -> [usize; 3] { [ self.options.orientations, self.options.block_side, self.options.block_side, ] } /// Area of an image cell in pixels. fn cell_area(&self) -> usize { self.options.cell_side * self.options.cell_side } } /// Number of blocks required to cover `num_cells` cells when each block is /// `block_side` long and blocks are staggered by `block_stride`. Assumes that /// options are compatible. fn num_blocks(num_cells: usize, block_side: usize, block_stride: usize) -> usize { (num_cells + block_stride - block_side) / block_stride } /// Computes the HoG descriptor of an image, or None if the provided /// options are incompatible with the image size. // TODO: support color images by taking the channel with maximum gradient at each point pub fn hog(image: &GrayImage, options: HogOptions) -> Result, String> { match HogSpec::from_options(image.width(), image.height(), options) { Err(e) => Err(e), Ok(spec) => { let mut grid: Array3d = cell_histograms(image, spec); let grid_view = grid.view_mut(); let descriptor = hog_descriptor_from_hist_grid(grid_view, spec); Ok(descriptor) } } } /// Computes the HoG descriptor of an image. Assumes that the spec and grid /// dimensions are consistent. fn hog_descriptor_from_hist_grid(grid: View3d<'_, f32>, spec: HogSpec) -> Vec { let mut descriptor = Array3d::new(spec.block_grid_lengths()); { let mut block_view = descriptor.view_mut(); for by in 0..spec.blocks_high { for bx in 0..spec.blocks_wide { let block_data = block_view.inner_slice_mut(bx, by); let mut block = View3d::from_raw(block_data, spec.block_internal_lengths()); for iy in 0..spec.options.block_side { let cy = by * spec.options.block_stride + iy; for ix in 0..spec.options.block_side { let cx = bx * spec.options.block_stride + ix; let slice = block.inner_slice_mut(ix, iy); let hist = grid.inner_slice(cx, cy); copy(hist, slice); } } } } for by in 0..spec.blocks_high { for bx in 0..spec.blocks_wide { let norm = block_norm(&block_view, bx, by); if norm > 0f32 { let block_mut = block_view.inner_slice_mut(bx, by); for i in 0..block_mut.len() { block_mut[i] /= norm; } } } } } descriptor.data } /// L2 norm of the block descriptor at given location within an image descriptor. fn block_norm(view: &View3d<'_, f32>, bx: usize, by: usize) -> f32 { let block_data = view.inner_slice(bx, by); l2_norm(block_data) } fn copy(from: &[T], to: &mut [T]) { to.clone_from_slice(&from[..to.len()]); } /// Computes orientation histograms for each cell of an image. Assumes that /// the provided dimensions are valid. pub fn cell_histograms(image: &GrayImage, spec: HogSpec) -> Array3d { let (width, height) = image.dimensions(); let mut grid = Array3d::new(spec.cell_grid_lengths()); let cell_area = spec.cell_area() as f32; let cell_side = spec.options.cell_side as f32; let horizontal = horizontal_sobel(image); let vertical = vertical_sobel(image); let interval = orientation_bin_width(spec.options); let range = direction_range(spec.options); for y in 0..height { let mut grid_view = grid.view_mut(); let y_inter = Interpolation::from_position(y as f32 / cell_side); for x in 0..width { let x_inter = Interpolation::from_position(x as f32 / cell_side); let h = horizontal.get_pixel(x, y)[0] as f32; let v = vertical.get_pixel(x, y)[0] as f32; let m = (h.powi(2) + v.powi(2)).sqrt(); let mut d = v.atan2(h); if d < 0f32 { d += range; } if !spec.options.signed && d >= f32::consts::PI { d -= f32::consts::PI; } let o_inter = Interpolation::from_position_wrapping(d / interval, spec.options.orientations); for iy in 0..2usize { let py = y_inter.indices[iy]; for ix in 0..2usize { let px = x_inter.indices[ix]; for io in 0..2usize { let po = o_inter.indices[io]; if contains_outer(&grid_view, px, py) { let wy = y_inter.weights[iy]; let wx = x_inter.weights[ix]; let wo = o_inter.weights[io]; let up = wy * wx * wo * m / cell_area; let current = *grid_view.at_mut([po, px, py]); *grid_view.at_mut([po, px, py]) = current + up; } } } } } } grid } /// True if the given outer two indices into a view are within bounds. fn contains_outer(view: &View3d<'_, T>, u: usize, v: usize) -> bool { u < view.lengths[1] && v < view.lengths[2] } /// Width of an orientation histogram bin in radians. fn orientation_bin_width(options: HogOptions) -> f32 { direction_range(options) / (options.orientations as f32) } /// Length of the range of possible directions in radians. fn direction_range(options: HogOptions) -> f32 { if options.signed { 2f32 * f32::consts::PI } else { f32::consts::PI } } /// Indices and weights for an interpolated value. #[derive(Debug, Copy, Clone, PartialEq)] struct Interpolation { indices: [usize; 2], weights: [f32; 2], } impl Interpolation { /// Creates new interpolation with provided indices and weights. fn new(indices: [usize; 2], weights: [f32; 2]) -> Interpolation { Interpolation { indices, weights } } /// Interpolates between two indices, without wrapping. fn from_position(pos: f32) -> Interpolation { let fraction = pos - pos.floor(); Self::new( [pos as usize, pos as usize + 1], [1f32 - fraction, fraction], ) } /// Interpolates between two indices, wrapping the right index. /// Assumes that the left index is within bounds. fn from_position_wrapping(pos: f32, length: usize) -> Interpolation { let mut right = (pos as usize) + 1; if right >= length { right = 0; } let fraction = pos - pos.floor(); Self::new([pos as usize, right], [1f32 - fraction, fraction]) } } /// Visualises an array of orientation histograms. /// The dimensions of the provided Array3d are orientation bucket, /// horizontal location of the cell, then vertical location of the cell. /// Note that we ignore block-level aggregation or normalisation here. /// Each rendered star has side length `star_side`, so the image will have /// width `grid.lengths[1] * star_side` and height `grid.lengths[2] * star_side`. pub fn render_hist_grid(star_side: u32, grid: &View3d<'_, f32>, signed: bool) -> Image> { let width = grid.lengths[1] as u32 * star_side; let height = grid.lengths[2] as u32 * star_side; let mut out = ImageBuffer::new(width, height); for y in 0..grid.lengths[2] { let y_window = y as u32 * star_side; for x in 0..grid.lengths[1] { let x_window = x as u32 * star_side; let mut window = out.sub_image(x_window, y_window, star_side, star_side); let hist = grid.inner_slice(x, y); draw_star_mut(window.inner_mut(), hist, signed); } } out } /// Draws a ray from the center of an image in place, in a direction theta radians /// clockwise from the y axis (recall that image coordinates increase from /// top left to bottom right). fn draw_ray_mut(image: &mut I, theta: f32, color: I::Pixel) where I: GenericImage, { use crate::drawing::draw_line_segment_mut; use std::cmp; let (width, height) = image.dimensions(); let scale = cmp::max(width, height) as f32 / 2f32; let start_x = (width / 2) as f32; let start_y = (height / 2) as f32; let start = (start_x, start_y); let x_step = -scale * theta.sin(); let y_step = scale * theta.cos(); let end = (start_x + x_step, start_y + y_step); draw_line_segment_mut(image, start, end, color); } /// Draws a visualisation of a histogram of edge orientation strengths as a collection of rays /// emanating from the centre of a square image. The intensity of each ray is /// proportional to the value of the bucket centred on its direction. fn draw_star_mut(image: &mut I, hist: &[f32], signed: bool) where I: GenericImage>, { let orientations = hist.len() as f32; for bucket in 0..hist.len() { if signed { let dir = (2f32 * f32::consts::PI * bucket as f32) / orientations; let intensity = Clamp::clamp(hist[bucket]); draw_ray_mut(image, dir, Luma([intensity])); } else { let dir = (f32::consts::PI * bucket as f32) / orientations; let intensity = Clamp::clamp(hist[bucket]); draw_ray_mut(image, dir, Luma([intensity])); draw_ray_mut(image, dir + f32::consts::PI, Luma([intensity])); } } } /// A 3d array that owns its data. pub struct Array3d { /// The owned data. data: Vec, /// Lengths of the dimensions, from innermost (i.e. fastest-varying) to outermost. lengths: [usize; 3], } /// A view into a 3d array. pub struct View3d<'a, T> { /// The underlying data. data: &'a mut [T], /// Lengths of the dimensions, from innermost (i.e. fastest-varying) to outermost. lengths: [usize; 3], } impl Array3d { /// Allocates a new Array3d with the given dimensions. fn new(lengths: [usize; 3]) -> Array3d { let data = vec![Zero::zero(); data_length(lengths)]; Array3d { data, lengths } } /// Provides a 3d view of the data. pub fn view_mut(&mut self) -> View3d<'_, T> { View3d::from_raw(&mut self.data, self.lengths) } } impl<'a, T> View3d<'a, T> { /// Constructs index from existing data and the lengths of the desired dimensions. fn from_raw(data: &'a mut [T], lengths: [usize; 3]) -> View3d<'a, T> { View3d { data, lengths } } /// A mutable reference from a 3d index. fn at_mut(&mut self, indices: [usize; 3]) -> &mut T { &mut self.data[self.offset(indices)] } /// All entries with the given outer dimensions. As the first dimension /// is fastest varying, this is a contiguous slice. fn inner_slice(&self, x1: usize, x2: usize) -> &[T] { let offset = self.offset([0, x1, x2]); &self.data[offset..offset + self.lengths[0]] } /// All entries with the given outer dimensions. As the first dimension /// is fastest varying, this is a contiguous slice. fn inner_slice_mut(&mut self, x1: usize, x2: usize) -> &mut [T] { let offset = self.offset([0, x1, x2]); &mut self.data[offset..offset + self.lengths[0]] } fn offset(&self, indices: [usize; 3]) -> usize { indices[2] * self.lengths[1] * self.lengths[0] + indices[1] * self.lengths[0] + indices[0] } } /// Length of array needed for the given dimensions. fn data_length(lengths: [usize; 3]) -> usize { lengths[0] * lengths[1] * lengths[2] } #[cfg(test)] mod tests { use super::*; use ::test; #[test] fn test_num_blocks() { // ----- // *** // *** assert_eq!(num_blocks(5, 3, 2), 2); // ----- // ***** assert_eq!(num_blocks(5, 5, 2), 1); // ---- // ** // ** assert_eq!(num_blocks(4, 2, 2), 2); // --- // * // * // * assert_eq!(num_blocks(3, 1, 1), 3); } #[test] fn test_hog_spec_valid_options() { assert_eq!( HogSpec::from_options(40, 40, HogOptions::new(8, true, 5, 2, 1)) .unwrap() .descriptor_length(), 1568 ); assert_eq!( HogSpec::from_options(40, 40, HogOptions::new(9, true, 4, 2, 1)) .unwrap() .descriptor_length(), 2916 ); assert_eq!( HogSpec::from_options(40, 40, HogOptions::new(8, true, 4, 2, 1)) .unwrap() .descriptor_length(), 2592 ); } #[test] fn test_hog_spec_invalid_options() { let opts = HogOptions { orientations: 8, signed: true, cell_side: 3, block_side: 4, block_stride: 2, }; let expected = "Invalid HoG options: block stride 2 does not evenly divide (cells wide 7 - block side 4), \ block stride 2 does not evenly divide (cells high 7 - block side 4)"; assert_eq!( HogSpec::from_options(21, 21, opts), Err(expected.to_owned()) ); } #[test] fn test_interpolation_from_position() { assert_eq!( Interpolation::from_position(10f32), Interpolation::new([10, 11], [1f32, 0f32]) ); assert_eq!( Interpolation::from_position(10.25f32), Interpolation::new([10, 11], [0.75f32, 0.25f32]) ); } #[test] fn test_interpolation_from_position_wrapping() { assert_eq!( Interpolation::from_position_wrapping(10f32, 11), Interpolation::new([10, 0], [1f32, 0f32]) ); assert_eq!( Interpolation::from_position_wrapping(10.25f32, 11), Interpolation::new([10, 0], [0.75f32, 0.25f32]) ); assert_eq!( Interpolation::from_position_wrapping(10f32, 12), Interpolation::new([10, 11], [1f32, 0f32]) ); assert_eq!( Interpolation::from_position_wrapping(10.25f32, 12), Interpolation::new([10, 11], [0.75f32, 0.25f32]) ); } #[test] fn test_hog_descriptor_from_hist_grid() { // A grid of cells 3 wide and 2 high. Each cell contains a histogram of 2 items. // There are two blocks, the left covering the leftmost 2x2 region, and the // right covering the rightmost 2x2 region. These regions overlap by one cell column. // There's no significance to the contents of the histograms used here, we're // just checking that the values are binned and normalised correctly. let opts = HogOptions { orientations: 2, signed: true, cell_side: 5, block_side: 2, block_stride: 1, }; let spec = HogSpec::from_options(15, 10, opts).unwrap(); let mut grid = Array3d::::new([2, 3, 2]); let mut view = grid.view_mut(); { let tl = view.inner_slice_mut(0, 0); copy(&[1f32, 3f32, 2f32], tl); } { let tm = view.inner_slice_mut(1, 0); copy(&[2f32, 3f32, 5f32], tm); } { let tr = view.inner_slice_mut(2, 0); copy(&[0f32, 1f32, 0f32], tr); } { let bl = view.inner_slice_mut(0, 1); copy(&[5f32, 0f32, 7f32], bl); } { let bm = view.inner_slice_mut(1, 1); copy(&[3f32, 7f32, 9f32], bm); } { let br = view.inner_slice_mut(2, 1); copy(&[6f32, 1f32, 4f32], br); } let descriptor = hog_descriptor_from_hist_grid(view, spec); assert_eq!(descriptor.len(), 16); let counts = [1, 3, 2, 3, 5, 0, 3, 7, 2, 3, 0, 1, 3, 7, 6, 1]; let mut expected = [0f32; 16]; let left_norm = 106f32.sqrt(); let right_norm = 109f32.sqrt(); for i in 0..8 { expected[i] = counts[i] as f32 / left_norm; } for i in 8..16 { expected[i] = counts[i] as f32 / right_norm; } assert_eq!(descriptor, expected); } #[test] fn test_direction_interpolation_within_bounds() { let image = gray_image!( 2, 1, 0; 2, 1, 0; 2, 1, 0); let opts_signed = HogOptions { orientations: 8, signed: true, cell_side: 3, block_side: 1, block_stride: 1, }; let desc_signed = hog(&image, opts_signed); test::black_box(desc_signed.unwrap()); let opts_unsigned = HogOptions { orientations: 8, signed: false, cell_side: 3, block_side: 1, block_stride: 1, }; let desc_unsigned = hog(&image, opts_unsigned); test::black_box(desc_unsigned.unwrap()); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::utils::gray_bench_image; use ::test; #[bench] fn bench_hog(b: &mut test::Bencher) { let image = gray_bench_image(88, 88); let opts = HogOptions { orientations: 8, signed: true, cell_side: 8, block_side: 3, block_stride: 2, }; b.iter(|| { let desc = hog(&image, opts); test::black_box(desc.unwrap()); }); } } imageproc-0.25.0/src/hough.rs000064400000000000000000000420761046102023000141360ustar 00000000000000//! Line detection via the [Hough transform]. //! //! [Hough transform]: https://en.wikipedia.org/wiki/Hough_transform use crate::definitions::Image; use crate::drawing::draw_line_segment_mut; use crate::suppress::suppress_non_maximum; use image::{GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Pixel}; use std::f32; /// A detected line, in polar coordinates. #[derive(Copy, Clone, Debug, PartialEq)] pub struct PolarLine { /// Signed distance of the line from the origin (top-left of the image), in pixels. pub r: f32, /// Clockwise angle in degrees between the x-axis and the line. /// Always between 0 and 180. pub angle_in_degrees: u32, } /// Options for Hough line detection. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct LineDetectionOptions { /// Number of votes required to be detected as a line. pub vote_threshold: u32, /// Non-maxima suppression is applied to accumulator buckets before /// returning lines. Only lines which have the greatest vote in the /// block centred on them of side length `2 * suppression_radius + 1` /// are returned. Set to `0` if you don't want to apply non-maxima suppression. pub suppression_radius: u32, } /// Detects lines in a binary input image using the Hough transform. /// /// Points are considered to be in the foreground (and thus vote for lines) /// if their intensity is non-zero. /// /// See ./examples/hough.rs for example usage. pub fn detect_lines(image: &GrayImage, options: LineDetectionOptions) -> Vec { let (width, height) = image.dimensions(); // The maximum possible radius is the diagonal of the image. let rmax = ((width * width + height * height) as f64).sqrt() as i32; // Measure angles in degrees, and use bins of width 1 pixel and height 1 degree. // We use the convention that distances are positive for angles in (0, 180] and // negative for angles in [180, 360). let mut acc: ImageBuffer, Vec> = ImageBuffer::new(2 * rmax as u32 + 1, 180u32); // Precalculate values of (cos(m), sin(m)) let lut: Vec<(f32, f32)> = (0..180u32) .map(|deg| (deg as f32).to_radians()) .map(f32::sin_cos) .collect(); for y in 0..height { for x in 0..width { let p = unsafe { image.unsafe_get_pixel(x, y)[0] }; if p > 0 { for (m, (s, c)) in lut.iter().enumerate() { let r = (x as f32) * c + (y as f32) * s; let d = r as i32 + rmax; if d <= 2 * rmax && d >= 0 { unsafe { let vote_incr = acc.unsafe_get_pixel(d as u32, m as u32)[0] + 1; acc.unsafe_put_pixel(d as u32, m as u32, Luma([vote_incr])); } } } } } } let acc_sup = suppress_non_maximum(&acc, options.suppression_radius); let mut lines = Vec::new(); for m in 0..acc_sup.height() { for r in 0..acc_sup.width() { let votes = unsafe { acc_sup.unsafe_get_pixel(r, m)[0] }; if votes >= options.vote_threshold { let line = PolarLine { r: (r as i32 - rmax) as f32, angle_in_degrees: m, }; lines.push(line); } } } lines } /// Draws each element of `lines` on `image` in the provided `color`. /// /// See ./examples/hough.rs for example usage. pub fn draw_polar_lines

(image: &Image

, lines: &[PolarLine], color: P) -> Image

where P: Pixel, { let mut out = image.clone(); draw_polar_lines_mut(&mut out, lines, color); out } /// Draws each element of `lines` on `image` in the provided `color`. /// /// See ./examples/hough.rs for example usage. pub fn draw_polar_lines_mut

(image: &mut Image

, lines: &[PolarLine], color: P) where P: Pixel, { for line in lines { draw_polar_line(image, *line, color); } } fn draw_polar_line

(image: &mut Image

, line: PolarLine, color: P) where P: Pixel, { if let Some((s, e)) = intersection_points(line, image.width(), image.height()) { draw_line_segment_mut(image, s, e, color); } } /// Returns the intersection points of a `PolarLine` with an image of given width and height, /// or `None` if the line and image bounding box are disjoint. The x value of an intersection /// point lies within the closed interval [0, image_width] and the y value within the closed /// interval [0, image_height]. fn intersection_points( line: PolarLine, image_width: u32, image_height: u32, ) -> Option<((f32, f32), (f32, f32))> { let r = line.r; let m = line.angle_in_degrees; let w = image_width as f32; let h = image_height as f32; // Vertical line if m == 0 { return if r >= 0.0 && r <= w { Some(((r, 0.0), (r, h))) } else { None }; } // Horizontal line if m == 90 { return if r >= 0.0 && r <= h { Some(((0.0, r), (w, r))) } else { None }; } let theta = (m as f32).to_radians(); let (sin, cos) = theta.sin_cos(); let right_y = cos.mul_add(-w, r) / sin; let left_y = r / sin; let bottom_x = sin.mul_add(-h, r) / cos; let top_x = r / cos; let mut start = None; if right_y >= 0.0 && right_y <= h { let right_intersect = (w, right_y); if let Some(s) = start { return Some((s, right_intersect)); } start = Some(right_intersect); } if left_y >= 0.0 && left_y <= h { let left_intersect = (0.0, left_y); if let Some(s) = start { return Some((s, left_intersect)); } start = Some(left_intersect); } if bottom_x >= 0.0 && bottom_x <= w { let bottom_intersect = (bottom_x, h); if let Some(s) = start { return Some((s, bottom_intersect)); } start = Some(bottom_intersect); } if top_x >= 0.0 && top_x <= w { let top_intersect = (top_x, 0.0); if let Some(s) = start { return Some((s, top_intersect)); } } None } #[cfg(test)] mod tests { use super::*; use image::{GrayImage, Luma}; fn assert_points_eq( actual: Option<((f32, f32), (f32, f32))>, expected: Option<((f32, f32), (f32, f32))>, ) { match (actual, expected) { (None, None) => {} (Some(ps), Some(qs)) => { let points_eq = |p: (f32, f32), q: (f32, f32)| { (p.0 - q.0).abs() < 1.0e-6 && (p.1 - q.1).abs() < 1.0e-6 }; match (points_eq(ps.0, qs.0), points_eq(ps.1, qs.1)) { (true, true) => {} _ => { panic!("Expected {:?}, got {:?}", expected, actual); } }; } (Some(_), None) => { panic!("Expected None, got {:?}", actual); } (None, Some(_)) => { panic!("Expected {:?}, got None", expected); } } } #[test] fn intersection_points_zero_signed_distance() { // Vertical assert_points_eq( intersection_points( PolarLine { r: 0.0, angle_in_degrees: 0, }, 10, 5, ), Some(((0.0, 0.0), (0.0, 5.0))), ); // Horizontal assert_points_eq( intersection_points( PolarLine { r: 0.0, angle_in_degrees: 90, }, 10, 5, ), Some(((0.0, 0.0), (10.0, 0.0))), ); // Bottom left to top right assert_points_eq( intersection_points( PolarLine { r: 0.0, angle_in_degrees: 45, }, 10, 5, ), Some(((0.0, 0.0), (0.0, 0.0))), ); // Top left to bottom right assert_points_eq( intersection_points( PolarLine { r: 0.0, angle_in_degrees: 135, }, 10, 5, ), Some(((0.0, 0.0), (5.0, 5.0))), ); // Top left to bottom right, square image (because a previous version of the code // got this case wrong) assert_points_eq( intersection_points( PolarLine { r: 0.0, angle_in_degrees: 135, }, 10, 10, ), Some(((10.0, 10.0), (0.0, 0.0))), ); } #[test] fn intersection_points_positive_signed_distance() { // Vertical intersecting image assert_points_eq( intersection_points( PolarLine { r: 9.0, angle_in_degrees: 0, }, 10, 5, ), Some(((9.0, 0.0), (9.0, 5.0))), ); // Vertical outside image assert_points_eq( intersection_points( PolarLine { r: 8.0, angle_in_degrees: 0, }, 5, 10, ), None, ); // Horizontal intersecting image assert_points_eq( intersection_points( PolarLine { r: 9.0, angle_in_degrees: 90, }, 5, 10, ), Some(((0.0, 9.0), (5.0, 9.0))), ); // Horizontal outside image assert_points_eq( intersection_points( PolarLine { r: 8.0, angle_in_degrees: 90, }, 10, 5, ), None, ); // Positive gradient assert_points_eq( intersection_points( PolarLine { r: 5.0, angle_in_degrees: 45, }, 10, 5, ), Some(((50f32.sqrt() - 5.0, 5.0), (50f32.sqrt(), 0.0))), ); } #[test] fn intersection_points_negative_signed_distance() { // Vertical assert_points_eq( intersection_points( PolarLine { r: -1.0, angle_in_degrees: 0, }, 10, 5, ), None, ); // Horizontal assert_points_eq( intersection_points( PolarLine { r: -1.0, angle_in_degrees: 90, }, 5, 10, ), None, ); // Negative gradient assert_points_eq( intersection_points( PolarLine { r: -5.0, angle_in_degrees: 135, }, 10, 5, ), Some(((10.0, 10.0 - 50f32.sqrt()), (50f32.sqrt(), 0.0))), ); } // -------------------- // | | // | | // | ***** ***** | // | | // | | // -------------------- fn separated_horizontal_line_segment() -> GrayImage { let white = Luma([255u8]); let mut image = GrayImage::new(20, 5); for i in 5..10 { image.put_pixel(i, 2, white); } for i in 12..17 { image.put_pixel(i, 2, white); } image } #[test] fn detect_lines_horizontal_below_threshold() { let image = separated_horizontal_line_segment(); let options = LineDetectionOptions { vote_threshold: 11, suppression_radius: 0, }; let detected = detect_lines(&image, options); assert_eq!(detected.len(), 0); } #[test] fn detect_lines_horizontal_above_threshold() { let image = separated_horizontal_line_segment(); let options = LineDetectionOptions { vote_threshold: 10, suppression_radius: 8, }; let detected = detect_lines(&image, options); assert_eq!(detected.len(), 1); let line = detected[0]; assert_eq!(line.r, 1f32); assert_eq!(line.angle_in_degrees, 90); } fn image_with_polar_line( width: u32, height: u32, r: f32, angle_in_degrees: u32, color: Luma, ) -> GrayImage { let mut image = GrayImage::new(width, height); draw_polar_line( &mut image, PolarLine { r, angle_in_degrees, }, color, ); image } #[test] fn draw_polar_line_horizontal() { let actual = image_with_polar_line(5, 5, 2.0, 90, Luma([1])); let expected = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 1, 1, 1, 1, 1; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0); assert_pixels_eq!(actual, expected); } #[test] fn draw_polar_line_vertical() { let actual = image_with_polar_line(5, 5, 2.0, 0, Luma([1])); let expected = gray_image!( 0, 0, 1, 0, 0; 0, 0, 1, 0, 0; 0, 0, 1, 0, 0; 0, 0, 1, 0, 0; 0, 0, 1, 0, 0); assert_pixels_eq!(actual, expected); } #[test] fn draw_polar_line_bottom_left_to_top_right() { let actual = image_with_polar_line(5, 5, 3.0, 45, Luma([1])); let expected = gray_image!( 0, 0, 0, 0, 1; 0, 0, 0, 1, 0; 0, 0, 1, 0, 0; 0, 1, 0, 0, 0; 1, 0, 0, 0, 0); assert_pixels_eq!(actual, expected); } #[test] fn draw_polar_line_top_left_to_bottom_right() { let actual = image_with_polar_line(5, 5, 0.0, 135, Luma([1])); let expected = gray_image!( 1, 0, 0, 0, 0; 0, 1, 0, 0, 0; 0, 0, 1, 0, 0; 0, 0, 0, 1, 0; 0, 0, 0, 0, 1); assert_pixels_eq!(actual, expected); } macro_rules! test_detect_line { ($name:ident, $r:expr, $angle:expr) => { #[cfg_attr(miri, ignore = "slow")] #[test] fn $name() { let options = LineDetectionOptions { vote_threshold: 10, suppression_radius: 8, }; let image = image_with_polar_line(100, 100, $r, $angle, Luma([255])); let detected = detect_lines(&image, options); assert_eq!(detected.len(), 1); let line = detected[0]; assert_approx_eq!(line.r, $r, 1.1); assert_approx_eq!(line.angle_in_degrees as f32, $angle as f32, 5.0); } }; } test_detect_line!(detect_line_50_45, 50.0, 45); test_detect_line!(detect_line_eps_135, 0.001, 135); // https://github.com/image-rs/imageproc/issues/280 test_detect_line!(detect_line_neg10_120, -10.0, 120); } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use image::{GrayImage, ImageBuffer, Luma}; use test::{black_box, Bencher}; macro_rules! bench_detect_lines { ($name:ident, $r:expr, $angle:expr) => { #[bench] fn $name(b: &mut Bencher) { let options = LineDetectionOptions { vote_threshold: 10, suppression_radius: 8, }; let mut image = GrayImage::new(100, 100); draw_polar_line( &mut image, PolarLine { r: $r, angle_in_degrees: $angle, }, Luma([255u8]), ); b.iter(|| { let lines = detect_lines(&image, options); black_box(lines); }); } }; } bench_detect_lines!(bench_detect_line_50_45, 50.0, 45); bench_detect_lines!(bench_detect_line_eps_135, 0.001, 135); bench_detect_lines!(bench_detect_line_neg10_120, -10.0, 120); fn chessboard(width: u32, height: u32) -> GrayImage { ImageBuffer::from_fn(width, height, |x, y| { if (x + y) % 2 == 0 { Luma([255u8]) } else { Luma([0u8]) } }) } #[bench] fn bench_detect_lines(b: &mut Bencher) { let image = chessboard(100, 100); let options = LineDetectionOptions { vote_threshold: 10, suppression_radius: 3, }; b.iter(|| { let lines = detect_lines(&image, options); black_box(lines); }); } } imageproc-0.25.0/src/integral_image.rs000064400000000000000000000503711046102023000157700ustar 00000000000000//! Functions for computing [integral images](https://en.wikipedia.org/wiki/Summed_area_table) //! and running sums of rows and columns. use crate::definitions::Image; use crate::map::{ChannelMap, WithChannel}; use image::{GenericImageView, GrayImage, Luma, Pixel, Primitive, Rgb, Rgba}; use std::ops::AddAssign; /// Computes the 2d running sum of an image. Channels are summed independently. /// /// An integral image I has width and height one greater than its source image F, /// and is defined by I(x, y) = sum of F(x', y') for x' < x, y' < y, i.e. each pixel /// in the integral image contains the sum of the pixel intensities of all input pixels /// that are strictly above it and strictly to its left. In particular, the left column /// and top row of an integral image are all 0, and the value of the bottom right pixel of /// an integral image is equal to the sum of all pixels in the source image. /// /// Integral images have the helpful property of allowing us to /// compute the sum of pixel intensities in a rectangular region of an image /// in constant time. Specifically, given a rectangle [l, r] * [t, b] in F, /// the sum of the pixels in this rectangle is /// I(r + 1, b + 1) - I(r + 1, t) - I(l, b + 1) + I(l, t). /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::integral_image::{integral_image, sum_image_pixels}; /// /// let image = gray_image!( /// 1, 2, 3; /// 4, 5, 6); /// /// let integral = gray_image!(type: u32, /// 0, 0, 0, 0; /// 0, 1, 3, 6; /// 0, 5, 12, 21); /// /// assert_pixels_eq!(integral_image::<_, u32>(&image), integral); /// /// // Compute the sum of all pixels in the right two columns /// assert_eq!(sum_image_pixels(&integral, 1, 0, 2, 1)[0], 2 + 3 + 5 + 6); /// /// // Compute the sum of all pixels in the top row /// assert_eq!(sum_image_pixels(&integral, 0, 0, 2, 0)[0], 1 + 2 + 3); /// # } /// ``` pub fn integral_image(image: &Image

) -> Image> where P: Pixel + WithChannel, T: From + Primitive + AddAssign, { integral_image_impl(image, false) } /// Computes the 2d running sum of the squares of the intensities in an image. Channels are summed /// independently. /// /// See the [`integral_image`](fn.integral_image.html) documentation for more information on integral images. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::integral_image::{integral_squared_image, sum_image_pixels}; /// /// let image = gray_image!( /// 1, 2, 3; /// 4, 5, 6); /// /// let integral = gray_image!(type: u32, /// 0, 0, 0, 0; /// 0, 1, 5, 14; /// 0, 17, 46, 91); /// /// assert_pixels_eq!(integral_squared_image::<_, u32>(&image), integral); /// /// // Compute the sum of the squares of all pixels in the right two columns /// assert_eq!(sum_image_pixels(&integral, 1, 0, 2, 1)[0], 4 + 9 + 25 + 36); /// /// // Compute the sum of the squares of all pixels in the top row /// assert_eq!(sum_image_pixels(&integral, 0, 0, 2, 0)[0], 1 + 4 + 9); /// # } /// ``` pub fn integral_squared_image(image: &Image

) -> Image> where P: Pixel + WithChannel, T: From + Primitive + AddAssign, { integral_image_impl(image, true) } /// Implementation of `integral_image` and `integral_squared_image`. fn integral_image_impl(image: &Image

, square: bool) -> Image> where P: Pixel + WithChannel, T: From + Primitive + AddAssign, { // TODO: Make faster, add a new IntegralImage type // TODO: to make it harder to make off-by-one errors when computing sums of regions. let (in_width, in_height) = image.dimensions(); let out_width = in_width + 1; let out_height = in_height + 1; let mut out = Image::new(out_width, out_height); if in_width == 0 || in_height == 0 { return out; } let zero = T::zero(); let mut sum = vec![zero; P::CHANNEL_COUNT as usize]; for y in 0..in_height { sum.iter_mut().for_each(|x| { *x = zero; }); for x in 0..in_width { // JUSTIFICATION // Benefit // Using checked indexing here makes bench_integral_image_rgb take 1.05x as long // (The results are noisy, but this seems to be reproducible. I've not checked the generated assembly.) // Correctness // x and y are within bounds by definition of in_width and in_height let input = unsafe { image.unsafe_get_pixel(x, y) }; for (s, &c) in sum.iter_mut().zip(input.channels()) { let pix: T = c.into(); *s += if square { pix * pix } else { pix }; } // JUSTIFICATION // Benefit // Using checked indexing here makes bench_integral_image_rgb take 1.05x as long // (The results are noisy, but this seems to be reproducible. I've not checked the generated assembly.) // Correctness // 0 <= x < in_width, 0 <= y < in_height and out has width in_width + 1 and height in_height + 1 let above = unsafe { out.unsafe_get_pixel(x + 1, y) }; // For some reason there's no unsafe_get_pixel_mut, so to update the existing // pixel here we need to use the method with bounds checking let current = out.get_pixel_mut(x + 1, y + 1); // Using zip here makes this slower. for c in 0..P::CHANNEL_COUNT as usize { current.channels_mut()[c] = above.channels()[c] + sum[c]; } } } out } /// Hack to get around lack of const generics. See comment on `sum_image_pixels`. pub trait ArrayData { /// The type of the data for this array. /// e.g. `[T; 1]` for `Luma`, `[T; 3]` for `Rgb`. type DataType; /// Get the data from this pixel as a constant length array. fn data(&self) -> Self::DataType; /// Add the elements of two data arrays elementwise. fn add(lhs: Self::DataType, other: Self::DataType) -> Self::DataType; /// Subtract the elements of two data arrays elementwise. fn sub(lhs: Self::DataType, other: Self::DataType) -> Self::DataType; } impl ArrayData for Luma { type DataType = [T; 1]; fn data(&self) -> Self::DataType { [self.channels()[0]] } fn add(lhs: Self::DataType, rhs: Self::DataType) -> Self::DataType { [lhs[0] + rhs[0]] } fn sub(lhs: Self::DataType, rhs: Self::DataType) -> Self::DataType { [lhs[0] - rhs[0]] } } impl ArrayData for Rgb where Rgb: Pixel, T: Primitive, { type DataType = [T; 3]; fn data(&self) -> Self::DataType { [self.channels()[0], self.channels()[1], self.channels()[2]] } fn add(lhs: Self::DataType, rhs: Self::DataType) -> Self::DataType { [lhs[0] + rhs[0], lhs[1] + rhs[1], lhs[2] + rhs[2]] } fn sub(lhs: Self::DataType, rhs: Self::DataType) -> Self::DataType { [lhs[0] - rhs[0], lhs[1] - rhs[1], lhs[2] - rhs[2]] } } impl ArrayData for Rgba where Rgba: Pixel, T: Primitive, { type DataType = [T; 4]; fn data(&self) -> Self::DataType { [ self.channels()[0], self.channels()[1], self.channels()[2], self.channels()[3], ] } fn add(lhs: Self::DataType, rhs: Self::DataType) -> Self::DataType { [ lhs[0] + rhs[0], lhs[1] + rhs[1], lhs[2] + rhs[2], lhs[3] + rhs[3], ] } fn sub(lhs: Self::DataType, rhs: Self::DataType) -> Self::DataType { [ lhs[0] - rhs[0], lhs[1] - rhs[1], lhs[2] - rhs[2], lhs[3] - rhs[3], ] } } /// Sums the pixels in positions [left, right] * [top, bottom] in F, where `integral_image` is the /// integral image of F. /// /// The of `ArrayData` here is due to lack of const generics. This library contains /// implementations of `ArrayData` for `Luma`, `Rgb` and `Rgba` for any element type `T` that /// implements `Primitive`. In that case, this function returns `[T; 1]` for an image /// whose pixels are of type `Luma`, `[T; 3]` for `Rgb` pixels and `[T; 4]` for `Rgba` pixels. /// /// See the [`integral_image`](fn.integral_image.html) documentation for examples. pub fn sum_image_pixels

( integral_image: &Image

, left: u32, top: u32, right: u32, bottom: u32, ) -> P::DataType where P: Pixel + ArrayData + Copy, { // TODO: better type-safety. It's too easy to pass the original image in here by mistake. // TODO: it's also hard to see what the four u32s mean at the call site - use a Rect instead. let (a, b, c, d) = ( integral_image.get_pixel(right + 1, bottom + 1).data(), integral_image.get_pixel(left, top).data(), integral_image.get_pixel(right + 1, top).data(), integral_image.get_pixel(left, bottom + 1).data(), ); P::sub(P::sub(P::add(a, b), c), d) } /// Computes the variance of [left, right] * [top, bottom] in F, where `integral_image` is the /// integral image of F and `integral_squared_image` is the integral image of the squares of the /// pixels in F. /// /// See the [`integral_image`](fn.integral_image.html) documentation for more information on integral images. /// ///# Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use std::f64; /// use imageproc::integral_image::{integral_image, integral_squared_image, variance}; /// /// let image = gray_image!( /// 1, 2, 3; /// 4, 5, 6); /// /// let integral = integral_image(&image); /// let integral_squared = integral_squared_image(&image); /// /// // Compute the variance of the pixels in the right two columns /// let mean: f64 = (2.0 + 3.0 + 5.0 + 6.0) / 4.0; /// let var = ((2.0 - mean).powi(2) /// + (3.0 - mean).powi(2) /// + (5.0 - mean).powi(2) /// + (6.0 - mean).powi(2)) / 4.0; /// /// assert_eq!(variance(&integral, &integral_squared, 1, 0, 2, 1), var); /// # } /// ``` pub fn variance( integral_image: &Image>, integral_squared_image: &Image>, left: u32, top: u32, right: u32, bottom: u32, ) -> f64 { // TODO: same improvements as for sum_image_pixels, plus check that the given rect is valid. let n = (right - left + 1) as f64 * (bottom - top + 1) as f64; let sum_sq = sum_image_pixels(integral_squared_image, left, top, right, bottom)[0]; let sum = sum_image_pixels(integral_image, left, top, right, bottom)[0]; (sum_sq as f64 - (sum as f64).powi(2) / n) / n } /// Computes the running sum of one row of image, padded /// at the beginning and end. The padding is by continuity. /// Takes a reference to buffer so that this can be reused /// for all rows in an image. /// /// # Panics /// - If `buffer.len() < 2 * padding + image.width()`. /// - If `row >= image.height()`. /// - If `image.width() == 0`. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::integral_image::row_running_sum; /// /// let image = gray_image!( /// 1, 2, 3; /// 4, 5, 6); /// /// // Buffer has length two greater than image width, hence padding of 1 /// let mut buffer = [0; 5]; /// row_running_sum(&image, 0, &mut buffer, 1); /// /// // The image is padded by continuity on either side /// assert_eq!(buffer, [1, 2, 4, 7, 10]); /// # } /// ``` pub fn row_running_sum(image: &GrayImage, row: u32, buffer: &mut [u32], padding: u32) { // TODO: faster, more formats let (width, height) = image.dimensions(); let (width, padding) = (width as usize, padding as usize); assert!( buffer.len() >= width + 2 * padding, "Buffer length {} is less than {} + 2 * {}", buffer.len(), width, padding ); assert!(row < height, "row out of bounds: {} >= {}", row, height); assert!(width > 0, "image is empty"); let row_data = &(**image)[width * row as usize..][..width]; let first = row_data[0] as u32; let last = row_data[width - 1] as u32; let mut sum = 0; for b in &mut buffer[..padding] { sum += first; *b = sum; } for (b, p) in buffer[padding..].iter_mut().zip(row_data) { sum += *p as u32; *b = sum; } for b in &mut buffer[padding + width..] { sum += last; *b = sum; } } /// Computes the running sum of one column of image, padded /// at the top and bottom. The padding is by continuity. /// Takes a reference to buffer so that this can be reused /// for all columns in an image. /// /// # Panics /// - If `buffer.len() < 2 * padding + image.height()`. /// - If `column >= image.width()`. /// - If `image.height() == 0`. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::integral_image::column_running_sum; /// /// let image = gray_image!( /// 1, 4; /// 2, 5; /// 3, 6); /// /// // Buffer has length two greater than image height, hence padding of 1 /// let mut buffer = [0; 5]; /// column_running_sum(&image, 0, &mut buffer, 1); /// /// // The image is padded by continuity on top and bottom /// assert_eq!(buffer, [1, 2, 4, 7, 10]); /// # } /// ``` pub fn column_running_sum(image: &GrayImage, column: u32, buffer: &mut [u32], padding: u32) { // TODO: faster, more formats let (width, height) = image.dimensions(); assert!( // assertion 1 buffer.len() >= height as usize + 2 * padding as usize, "Buffer length {} is less than {} + 2 * {}", buffer.len(), height, padding ); assert!( // assertion 2 column < width, "column out of bounds: {} >= {}", column, width ); assert!( // assertion 3 height > 0, "image is empty" ); let first = image.get_pixel(column, 0)[0] as u32; let last = image.get_pixel(column, height - 1)[0] as u32; let mut sum = 0; for b in &mut buffer[..padding as usize] { sum += first; *b = sum; } // JUSTIFICATION: // Benefit // Using checked indexing here makes this function take 1.8x longer, as measured by bench_column_running_sum // Correctness // column is in bounds due to assertion 2. // height + padding - 1 < buffer.len() due to assertions 1 and 3. unsafe { for y in 0..height { sum += image.unsafe_get_pixel(column, y)[0] as u32; *buffer.get_unchecked_mut(y as usize + padding as usize) = sum; } } for b in &mut buffer[padding as usize + height as usize..] { sum += last; *b = sum; } } #[cfg(test)] mod tests { use super::*; use crate::definitions::Image; use crate::property_testing::GrayTestImage; use crate::utils::pixel_diff_summary; use image::{GenericImage, ImageBuffer, Luma}; use quickcheck::{quickcheck, TestResult}; #[test] fn test_integral_image_gray() { let image = gray_image!( 1, 2, 3; 4, 5, 6); let expected = gray_image!(type: u32, 0, 0, 0, 0; 0, 1, 3, 6; 0, 5, 12, 21); assert_pixels_eq!(integral_image::<_, u32>(&image), expected); } #[test] fn test_integral_image_rgb() { let image = rgb_image!( [1, 11, 21], [2, 12, 22], [3, 13, 23]; [4, 14, 24], [5, 15, 25], [6, 16, 26]); let expected = rgb_image!(type: u32, [0, 0, 0], [0, 0, 0], [ 0, 0, 0], [ 0, 0, 0]; [0, 0, 0], [1, 11, 21], [ 3, 23, 43], [ 6, 36, 66]; [0, 0, 0], [5, 25, 45], [12, 52, 92], [21, 81, 141]); assert_pixels_eq!(integral_image::<_, u32>(&image), expected); } #[test] fn test_sum_image_pixels() { let image = gray_image!( 1, 2; 3, 4); let integral = integral_image::<_, u32>(&image); // Top left assert_eq!(sum_image_pixels(&integral, 0, 0, 0, 0)[0], 1); // Top row assert_eq!(sum_image_pixels(&integral, 0, 0, 1, 0)[0], 3); // Left column assert_eq!(sum_image_pixels(&integral, 0, 0, 0, 1)[0], 4); // Whole image assert_eq!(sum_image_pixels(&integral, 0, 0, 1, 1)[0], 10); // Top right assert_eq!(sum_image_pixels(&integral, 1, 0, 1, 0)[0], 2); // Right column assert_eq!(sum_image_pixels(&integral, 1, 0, 1, 1)[0], 6); // Bottom left assert_eq!(sum_image_pixels(&integral, 0, 1, 0, 1)[0], 3); // Bottom row assert_eq!(sum_image_pixels(&integral, 0, 1, 1, 1)[0], 7); // Bottom right assert_eq!(sum_image_pixels(&integral, 1, 1, 1, 1)[0], 4); } #[test] fn test_sum_image_pixels_rgb() { let image = rgb_image!( [1, 2, 3], [ 4, 5, 6]; [7, 8, 9], [10, 11, 12]); let integral = integral_image::<_, u32>(&image); // Top left assert_eq!(sum_image_pixels(&integral, 0, 0, 0, 0), [1, 2, 3]); // Top row assert_eq!(sum_image_pixels(&integral, 0, 0, 1, 0), [5, 7, 9]); // Left column assert_eq!(sum_image_pixels(&integral, 0, 0, 0, 1), [8, 10, 12]); // Whole image assert_eq!(sum_image_pixels(&integral, 0, 0, 1, 1), [22, 26, 30]); // Top right assert_eq!(sum_image_pixels(&integral, 1, 0, 1, 0), [4, 5, 6]); // Right column assert_eq!(sum_image_pixels(&integral, 1, 0, 1, 1), [14, 16, 18]); // Bottom left assert_eq!(sum_image_pixels(&integral, 0, 1, 0, 1), [7, 8, 9]); // Bottom row assert_eq!(sum_image_pixels(&integral, 0, 1, 1, 1), [17, 19, 21]); // Bottom right assert_eq!(sum_image_pixels(&integral, 1, 1, 1, 1), [10, 11, 12]); } /// Simple implementation of integral_image to validate faster versions against. fn integral_image_ref(image: &I) -> Image> where I: GenericImage>, { let (in_width, in_height) = image.dimensions(); let (out_width, out_height) = (in_width + 1, in_height + 1); let mut out = ImageBuffer::from_pixel(out_width, out_height, Luma([0u32])); for y in 1..out_height { for x in 0..out_width { let mut sum = 0u32; for iy in 0..y { for ix in 0..x { sum += image.get_pixel(ix, iy)[0] as u32; } } out.put_pixel(x, y, Luma([sum])); } } out } #[cfg_attr(miri, ignore = "slow")] #[test] fn test_integral_image_matches_reference_implementation() { fn prop(image: GrayTestImage) -> TestResult { let expected = integral_image_ref(&image.0); let actual = integral_image(&image.0); match pixel_diff_summary(&actual, &expected) { None => TestResult::passed(), Some(err) => TestResult::error(err), } } quickcheck(prop as fn(GrayTestImage) -> TestResult); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::utils::{gray_bench_image, rgb_bench_image}; use ::test; #[bench] fn bench_integral_image_gray(b: &mut test::Bencher) { let image = gray_bench_image(500, 500); b.iter(|| { let integral = integral_image::<_, u32>(&image); test::black_box(integral); }); } #[bench] fn bench_integral_image_rgb(b: &mut test::Bencher) { let image = rgb_bench_image(500, 500); b.iter(|| { let integral = integral_image::<_, u32>(&image); test::black_box(integral); }); } #[bench] fn bench_row_running_sum(b: &mut test::Bencher) { let image = gray_bench_image(1000, 1); let mut buffer = [0; 1010]; b.iter(|| { row_running_sum(&image, 0, &mut buffer, 5); }); } #[bench] fn bench_column_running_sum(b: &mut test::Bencher) { let image = gray_bench_image(100, 1000); let mut buffer = [0; 1010]; b.iter(|| { column_running_sum(&image, 0, &mut buffer, 5); }); } } imageproc-0.25.0/src/lib.rs000064400000000000000000000032141046102023000135610ustar 00000000000000//! An image processing library based on the //! [image] crate. //! //! Note that the image crate contains some image //! processing functions (including image resizing) in its //! `imageops` module, so check there if you cannot find //! a standard image processing function in this crate. //! //! [image]: https://github.com/image-rs/image #![deny(missing_docs)] #![cfg_attr(test, feature(test))] #![allow( clippy::cast_lossless, clippy::too_many_arguments, clippy::needless_range_loop, clippy::useless_let_if_seq, clippy::match_wild_err_arm, clippy::needless_doctest_main, clippy::range_plus_one, clippy::trivially_copy_pass_by_ref, clippy::nonminimal_bool, clippy::expect_fun_call, clippy::many_single_char_names, clippy::zero_prefixed_literal )] #[cfg(test)] extern crate test; #[cfg(test)] #[macro_use] extern crate assert_approx_eq; #[cfg(test)] mod proptest_utils; #[macro_use] pub mod utils; pub mod binary_descriptors; pub mod contours; pub mod contrast; pub mod corners; pub mod definitions; pub mod distance_transform; pub mod drawing; pub mod edges; pub mod filter; pub mod geometric_transformations; pub mod geometry; pub mod gradients; pub mod haar; pub mod hog; pub mod hough; pub mod integral_image; pub mod local_binary_patterns; pub mod map; pub mod math; pub mod morphology; pub mod noise; pub mod pixelops; pub mod point; #[cfg(any(feature = "property-testing", test))] pub mod property_testing; pub mod rect; pub mod region_labelling; pub mod seam_carving; pub mod stats; pub mod suppress; pub mod template_matching; pub mod union_find; #[cfg(feature = "display-window")] pub mod window; pub use image; imageproc-0.25.0/src/local_binary_patterns.rs000064400000000000000000000312021046102023000173670ustar 00000000000000//! Functions for computing [local binary patterns](https://en.wikipedia.org/wiki/Local_binary_patterns). use image::{GenericImage, Luma}; use std::cmp; /// Computes the basic local binary pattern of a pixel, or None /// if it's too close to the image boundary. /// /// The neighbors of a pixel p are enumerated in the following order: /// ///

/// 7  0  1
/// 6  p  2
/// 5  4  3
/// 
/// /// The nth most significant bit of the local binary pattern at p is 1 /// if p is strictly brighter than the neighbor in position n. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::local_binary_patterns::local_binary_pattern; /// /// let image = gray_image!( /// 06, 11, 14; /// 09, 10, 10; /// 19, 00, 22); /// /// let expected = 0b11010000; /// let pattern = local_binary_pattern(&image, 1, 1).unwrap(); /// assert_eq!(pattern, expected); /// # } /// ``` pub fn local_binary_pattern(image: &I, x: u32, y: u32) -> Option where I: GenericImage>, { let (width, height) = image.dimensions(); if width == 0 || height == 0 { return None; } // TODO: It might be better to make this function private, and // TODO: require the caller to only provide valid x and y coordinates // TODO: the function may probably need to be unsafe then to leverage // TODO: on `unsafe_get_pixel` if x == 0 || x >= width - 1 || y == 0 || y >= height - 1 { return None; } // TODO: As with the fast corner detectors, this would be more efficient if // TODO: generated a list of pixel offsets once per image, and used those // TODO: offsets directly when reading pixels. To do this we'd need some traits // TODO: for images whose pixels are stored in contiguous rows/columns. let mut pattern = 0u8; // The sampled pixels have the following labels. // // 7 0 1 // 6 p 2 // 5 4 3 // // The nth bit of a pattern is 1 if the pixel p // is strictly brighter than the neighbor in position n. let (center, neighbors) = unsafe { ( image.unsafe_get_pixel(x, y)[0], [ image.unsafe_get_pixel(x, y - 1)[0], image.unsafe_get_pixel(x + 1, y - 1)[0], image.unsafe_get_pixel(x + 1, y)[0], image.unsafe_get_pixel(x + 1, y + 1)[0], image.unsafe_get_pixel(x, y + 1)[0], image.unsafe_get_pixel(x - 1, y + 1)[0], image.unsafe_get_pixel(x - 1, y)[0], image.unsafe_get_pixel(x - 1, y - 1)[0], ], ) }; for i in 0..8 { pattern |= (1 & (neighbors[i] < center) as u8) << i; } Some(pattern) } /// Returns the least value of all rotations of a byte. /// /// # Examples /// ``` /// use imageproc::local_binary_patterns::min_shift; /// /// let byte = 0b10110100; /// assert_eq!(min_shift(byte), 0b00101101); /// ``` pub fn min_shift(byte: u8) -> u8 { let mut min = byte; for i in 1..8 { min = cmp::min(min, byte.rotate_right(i)); } min } /// Number of bit transitions in a byte, counting the last and final bits as adjacent. /// /// # Examples /// ``` /// use imageproc::local_binary_patterns::count_transitions; /// /// let a = 0b11110000; /// assert_eq!(count_transitions(a), 2); /// let b = 0b00000000; /// assert_eq!(count_transitions(b), 0); /// let c = 0b10011001; /// assert_eq!(count_transitions(c), 4); /// let d = 0b10110010; /// assert_eq!(count_transitions(d), 6); /// ``` pub fn count_transitions(byte: u8) -> u32 { (byte ^ byte.rotate_right(1)).count_ones() } /// Maps uniform bytes (i.e. those with at most two bit transitions) to their /// least circular shifts, and non-uniform bytes to 10101010 (an arbitrarily chosen /// non-uniform representative). pub static UNIFORM_REPRESENTATIVE_2: [u8; 256] = [ 0, // 0 1, // 1 1, // 2 3, // 3 1, // 4 170, // 5 3, // 6 7, // 7 1, // 8 170, // 9 170, // 10 170, // 11 3, // 12 170, // 13 7, // 14 15, // 15 1, // 16 170, // 17 170, // 18 170, // 19 170, // 20 170, // 21 170, // 22 170, // 23 3, // 24 170, // 25 170, // 26 170, // 27 7, // 28 170, // 29 15, // 30 31, // 31 1, // 32 170, // 33 170, // 34 170, // 35 170, // 36 170, // 37 170, // 38 170, // 39 170, // 40 170, // 41 170, // 42 170, // 43 170, // 44 170, // 45 170, // 46 170, // 47 3, // 48 170, // 49 170, // 50 170, // 51 170, // 52 170, // 53 170, // 54 170, // 55 7, // 56 170, // 57 170, // 58 170, // 59 15, // 60 170, // 61 31, // 62 63, // 63 1, // 64 170, // 65 170, // 66 170, // 67 170, // 68 170, // 69 170, // 70 170, // 71 170, // 72 170, // 73 170, // 74 170, // 75 170, // 76 170, // 77 170, // 78 170, // 79 170, // 80 170, // 81 170, // 82 170, // 83 170, // 84 170, // 85 170, // 86 170, // 87 170, // 88 170, // 89 170, // 90 170, // 91 170, // 92 170, // 93 170, // 94 170, // 95 3, // 96 170, // 97 170, // 98 170, // 99 170, // 100 170, // 101 170, // 102 170, // 103 170, // 104 170, // 105 170, // 106 170, // 107 170, // 108 170, // 109 170, // 110 170, // 111 7, // 112 170, // 113 170, // 114 170, // 115 170, // 116 170, // 117 170, // 118 170, // 119 15, // 120 170, // 121 170, // 122 170, // 123 31, // 124 170, // 125 63, // 126 127, // 127 1, // 128 3, // 129 170, // 130 7, // 131 170, // 132 170, // 133 170, // 134 15, // 135 170, // 136 170, // 137 170, // 138 170, // 139 170, // 140 170, // 141 170, // 142 31, // 143 170, // 144 170, // 145 170, // 146 170, // 147 170, // 148 170, // 149 170, // 150 170, // 151 170, // 152 170, // 153 170, // 154 170, // 155 170, // 156 170, // 157 170, // 158 63, // 159 170, // 160 170, // 161 170, // 162 170, // 163 170, // 164 170, // 165 170, // 166 170, // 167 170, // 168 170, // 169 170, // 170 170, // 171 170, // 172 170, // 173 170, // 174 170, // 175 170, // 176 170, // 177 170, // 178 170, // 179 170, // 180 170, // 181 170, // 182 170, // 183 170, // 184 170, // 185 170, // 186 170, // 187 170, // 188 170, // 189 170, // 190 127, // 191 3, // 192 7, // 193 170, // 194 15, // 195 170, // 196 170, // 197 170, // 198 31, // 199 170, // 200 170, // 201 170, // 202 170, // 203 170, // 204 170, // 205 170, // 206 63, // 207 170, // 208 170, // 209 170, // 210 170, // 211 170, // 212 170, // 213 170, // 214 170, // 215 170, // 216 170, // 217 170, // 218 170, // 219 170, // 220 170, // 221 170, // 222 127, // 223 7, // 224 15, // 225 170, // 226 31, // 227 170, // 228 170, // 229 170, // 230 63, // 231 170, // 232 170, // 233 170, // 234 170, // 235 170, // 236 170, // 237 170, // 238 127, // 239 15, // 240 31, // 241 170, // 242 63, // 243 170, // 244 170, // 245 170, // 246 127, // 247 31, // 248 63, // 249 170, // 250 127, // 251 63, // 252 127, // 253 127, // 254 255, // 255 ]; /// Lookup table for the least circular shift of a byte. pub static MIN_SHIFT: [u8; 256] = [ 0, // 0 1, // 1 1, // 2 3, // 3 1, // 4 5, // 5 3, // 6 7, // 7 1, // 8 9, // 9 5, // 10 11, // 11 3, // 12 13, // 13 7, // 14 15, // 15 1, // 16 17, // 17 9, // 18 19, // 19 5, // 20 21, // 21 11, // 22 23, // 23 3, // 24 25, // 25 13, // 26 27, // 27 7, // 28 29, // 29 15, // 30 31, // 31 1, // 32 9, // 33 17, // 34 25, // 35 9, // 36 37, // 37 19, // 38 39, // 39 5, // 40 37, // 41 21, // 42 43, // 43 11, // 44 45, // 45 23, // 46 47, // 47 3, // 48 19, // 49 25, // 50 51, // 51 13, // 52 53, // 53 27, // 54 55, // 55 7, // 56 39, // 57 29, // 58 59, // 59 15, // 60 61, // 61 31, // 62 63, // 63 1, // 64 5, // 65 9, // 66 13, // 67 17, // 68 21, // 69 25, // 70 29, // 71 9, // 72 37, // 73 37, // 74 45, // 75 19, // 76 53, // 77 39, // 78 61, // 79 5, // 80 21, // 81 37, // 82 53, // 83 21, // 84 85, // 85 43, // 86 87, // 87 11, // 88 43, // 89 45, // 90 91, // 91 23, // 92 87, // 93 47, // 94 95, // 95 3, // 96 11, // 97 19, // 98 27, // 99 25, // 100 43, // 101 51, // 102 59, // 103 13, // 104 45, // 105 53, // 106 91, // 107 27, // 108 91, // 109 55, // 110 111, // 111 7, // 112 23, // 113 39, // 114 55, // 115 29, // 116 87, // 117 59, // 118 119, // 119 15, // 120 47, // 121 61, // 122 111, // 123 31, // 124 95, // 125 63, // 126 127, // 127 1, // 128 3, // 129 5, // 130 7, // 131 9, // 132 11, // 133 13, // 134 15, // 135 17, // 136 19, // 137 21, // 138 23, // 139 25, // 140 27, // 141 29, // 142 31, // 143 9, // 144 25, // 145 37, // 146 39, // 147 37, // 148 43, // 149 45, // 150 47, // 151 19, // 152 51, // 153 53, // 154 55, // 155 39, // 156 59, // 157 61, // 158 63, // 159 5, // 160 13, // 161 21, // 162 29, // 163 37, // 164 45, // 165 53, // 166 61, // 167 21, // 168 53, // 169 85, // 170 87, // 171 43, // 172 91, // 173 87, // 174 95, // 175 11, // 176 27, // 177 43, // 178 59, // 179 45, // 180 91, // 181 91, // 182 111, // 183 23, // 184 55, // 185 87, // 186 119, // 187 47, // 188 111, // 189 95, // 190 127, // 191 3, // 192 7, // 193 11, // 194 15, // 195 19, // 196 23, // 197 27, // 198 31, // 199 25, // 200 39, // 201 43, // 202 47, // 203 51, // 204 55, // 205 59, // 206 63, // 207 13, // 208 29, // 209 45, // 210 61, // 211 53, // 212 87, // 213 91, // 214 95, // 215 27, // 216 59, // 217 91, // 218 111, // 219 55, // 220 119, // 221 111, // 222 127, // 223 7, // 224 15, // 225 23, // 226 31, // 227 39, // 228 47, // 229 55, // 230 63, // 231 29, // 232 61, // 233 87, // 234 95, // 235 59, // 236 111, // 237 119, // 238 127, // 239 15, // 240 31, // 241 47, // 242 63, // 243 61, // 244 95, // 245 111, // 246 127, // 247 31, // 248 63, // 249 95, // 250 127, // 251 63, // 252 127, // 253 127, // 254 255, // 255 ]; #[cfg(test)] mod tests { use super::*; #[test] fn test_uniform_representative_2() { let a = 0b11110000; assert_eq!(UNIFORM_REPRESENTATIVE_2[a], 0b00001111); let b = 0b00000000; assert_eq!(UNIFORM_REPRESENTATIVE_2[b], 0b00000000); let c = 0b10011001; assert_eq!(UNIFORM_REPRESENTATIVE_2[c], 0b10101010); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use image::{GrayImage, Luma}; use test::{black_box, Bencher}; #[bench] fn bench_local_binary_pattern(b: &mut Bencher) { let image = GrayImage::from_fn(100, 100, |x, y| Luma([x as u8 % 2 + y as u8 % 2])); b.iter(|| { for y in 0..20 { for x in 0..20 { let pattern = local_binary_pattern(&image, x, y); black_box(pattern); } } }); } } imageproc-0.25.0/src/map.rs000064400000000000000000000317251046102023000136000ustar 00000000000000//! Functions for mapping over pixels, colors or subpixels of images. use image::{GenericImage, ImageBuffer, Luma, LumaA, Pixel, Primitive, Rgb, Rgba}; use crate::definitions::Image; /// The type obtained by replacing the channel type of a given `Pixel` type. /// The output type must have the same name of channels as the input type, or /// several algorithms will produce incorrect results or panic. pub trait WithChannel: Pixel { /// The new pixel type. type Pixel: Pixel; } /// Alias to make uses of `WithChannel` less syntactically noisy. pub type ChannelMap = >::Pixel; impl WithChannel for Rgb where Rgb: Pixel, Rgb: Pixel, T: Primitive, U: Primitive, { type Pixel = Rgb; } impl WithChannel for Rgba where Rgba: Pixel, Rgba: Pixel, T: Primitive, U: Primitive, { type Pixel = Rgba; } impl WithChannel for Luma where T: Primitive, U: Primitive, { type Pixel = Luma; } impl WithChannel for LumaA where T: Primitive, U: Primitive, { type Pixel = LumaA; } /// Applies `f` to each subpixel of the input image. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::map::map_subpixels; /// /// let image = gray_image!( /// 1, 2; /// 3, 4); /// /// let scaled = gray_image!(type: i16, /// -2, -4; /// -6, -8); /// /// assert_pixels_eq!( /// map_subpixels(&image, |x| -2 * (x as i16)), /// scaled); /// # } /// ``` pub fn map_subpixels(image: &I, f: F) -> Image> where I: GenericImage, P: WithChannel, S: Primitive, F: Fn(P::Subpixel) -> S, { let (width, height) = image.dimensions(); let mut out: ImageBuffer, Vec> = ImageBuffer::new(width, height); for y in 0..height { for x in 0..width { let out_channels = out.get_pixel_mut(x, y).channels_mut(); for c in 0..P::CHANNEL_COUNT { out_channels[c as usize] = f(unsafe { *image .unsafe_get_pixel(x, y) .channels() .get_unchecked(c as usize) }); } } } out } /// Applies `f` to each subpixel of the input image in place. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::map::map_subpixels_mut; /// /// let mut image = gray_image!( /// 1, 2; /// 3, 4); /// /// let want = gray_image!( /// 2, 4; /// 6, 8); /// /// map_subpixels_mut(&mut image, |x| 2 * x); /// /// assert_pixels_eq!( /// image, /// want); /// # } /// ``` pub fn map_subpixels_mut(image: &mut I, f: F) where I: GenericImage, P: Pixel, F: Fn(P::Subpixel) -> P::Subpixel, { let (width, height) = image.dimensions(); for y in 0..height { for x in 0..width { let mut pixel = image.get_pixel(x, y); let channels = pixel.channels_mut(); for c in 0..P::CHANNEL_COUNT as usize { channels[c] = f(unsafe { *image.unsafe_get_pixel(x, y).channels().get_unchecked(c) }); } image.put_pixel(x, y, pixel); } } } /// Applies `f` to the color of each pixel in the input image. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Rgb; /// use imageproc::map::map_colors; /// /// let image = gray_image!( /// 1, 2; /// 3, 4); /// /// let rgb = rgb_image!( /// [1, 2, 3], [2, 4, 6]; /// [3, 6, 9], [4, 8, 12]); /// /// assert_pixels_eq!( /// map_colors(&image, |p| { Rgb([p[0], (2 * p[0]), (3 * p[0])]) }), /// rgb); /// # } /// ``` pub fn map_colors(image: &I, f: F) -> Image where I: GenericImage, P: Pixel, Q: Pixel, F: Fn(P) -> Q, { let (width, height) = image.dimensions(); let mut out: ImageBuffer> = ImageBuffer::new(width, height); for y in 0..height { for x in 0..width { unsafe { let pix = image.unsafe_get_pixel(x, y); out.unsafe_put_pixel(x, y, f(pix)); } } } out } /// Applies `f` to the color of each pixel in the input image in place. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Luma; /// use imageproc::map::map_colors_mut; /// /// let mut image = gray_image!( /// 1, 2; /// 3, 4); /// /// let want = gray_image!( /// 2, 4; /// 6, 8); /// /// map_colors_mut(&mut image, |p| Luma([2 * p[0]])); /// /// assert_pixels_eq!( /// image, /// want); /// # } /// ``` pub fn map_colors_mut(image: &mut I, f: F) where I: GenericImage, P: Pixel, F: Fn(P) -> P, { let (width, height) = image.dimensions(); for y in 0..height { for x in 0..width { unsafe { let pix = image.unsafe_get_pixel(x, y); image.unsafe_put_pixel(x, y, f(pix)); } } } } /// Applies `f` to the colors of the pixels in the input images. /// /// Requires `image1` and `image2` to have the same dimensions. /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Luma; /// use imageproc::map::map_colors2; /// /// let image1 = gray_image!( /// 1, 2, /// 3, 4 /// ); /// /// let image2 = gray_image!( /// 10, 20, /// 30, 40 /// ); /// /// let sum = gray_image!( /// 11, 22, /// 33, 44 /// ); /// /// assert_pixels_eq!( /// map_colors2(&image1, &image2, |p, q| Luma([p[0] + q[0]])), /// sum /// ); /// # } /// ``` pub fn map_colors2(image1: &I, image2: &J, f: F) -> Image where I: GenericImage, J: GenericImage, P: Pixel, Q: Pixel, R: Pixel, F: Fn(P, Q) -> R, { assert_eq!(image1.dimensions(), image2.dimensions()); let (width, height) = image1.dimensions(); let mut out: ImageBuffer> = ImageBuffer::new(width, height); for y in 0..height { for x in 0..width { unsafe { let p = image1.unsafe_get_pixel(x, y); let q = image2.unsafe_get_pixel(x, y); out.unsafe_put_pixel(x, y, f(p, q)); } } } out } /// Applies `f` to each pixel in the input image. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Rgb; /// use imageproc::map::map_pixels; /// /// let image = gray_image!( /// 1, 2; /// 3, 4); /// /// let rgb = rgb_image!( /// [1, 0, 0], [2, 1, 0]; /// [3, 0, 1], [4, 1, 1]); /// /// assert_pixels_eq!( /// map_pixels(&image, |x, y, p| { /// Rgb([p[0], x as u8, y as u8]) /// }), /// rgb); /// # } /// ``` pub fn map_pixels(image: &I, f: F) -> Image where I: GenericImage, P: Pixel, Q: Pixel, F: Fn(u32, u32, P) -> Q, { let (width, height) = image.dimensions(); let mut out: ImageBuffer> = ImageBuffer::new(width, height); for y in 0..height { for x in 0..width { unsafe { let pix = image.unsafe_get_pixel(x, y); out.unsafe_put_pixel(x, y, f(x, y, pix)); } } } out } /// Applies `f` to each pixel in the input image in place. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Luma; /// use imageproc::map::map_pixels_mut; /// /// let mut image = gray_image!( /// 1, 2; /// 3, 4); /// /// let want = gray_image!( /// 1, 3; /// 4, 6); /// /// map_pixels_mut(&mut image, |x, y, p| { /// Luma([p[0] + x as u8 + y as u8]) /// }); /// /// assert_pixels_eq!( /// image, /// want); /// # } /// ``` pub fn map_pixels_mut(image: &mut I, f: F) where I: GenericImage, P: Pixel, F: Fn(u32, u32, P) -> P, { let (width, height) = image.dimensions(); for y in 0..height { for x in 0..width { unsafe { let pix = image.unsafe_get_pixel(x, y); image.unsafe_put_pixel(x, y, f(x, y, pix)); } } } } /// Creates a grayscale image by extracting the red channel of an RGB image. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Luma; /// use imageproc::map::red_channel; /// /// let image = rgb_image!( /// [1, 2, 3], [2, 4, 6]; /// [3, 6, 9], [4, 8, 12]); /// /// let expected = gray_image!( /// 1, 2; /// 3, 4); /// /// let actual = red_channel(&image); /// assert_pixels_eq!(actual, expected); /// # } /// ``` pub fn red_channel(image: &I) -> Image> where I: GenericImage>, Rgb: Pixel, C: Primitive, { map_colors(image, |p| Luma([p[0]])) } /// Creates an RGB image by embedding a grayscale image in its red channel. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Luma; /// use imageproc::map::as_red_channel; /// /// let image = gray_image!( /// 1, 2; /// 3, 4); /// /// let expected = rgb_image!( /// [1, 0, 0], [2, 0, 0]; /// [3, 0, 0], [4, 0, 0]); /// /// let actual = as_red_channel(&image); /// assert_pixels_eq!(actual, expected); /// # } /// ``` pub fn as_red_channel(image: &I) -> Image> where I: GenericImage>, Rgb: Pixel, C: Primitive, { map_colors(image, |p| { let mut cs = [C::zero(); 3]; cs[0] = p[0]; Rgb(cs) }) } /// Creates a grayscale image by extracting the green channel of an RGB image. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Luma; /// use imageproc::map::green_channel; /// /// let image = rgb_image!( /// [1, 2, 3], [2, 4, 6]; /// [3, 6, 9], [4, 8, 12]); /// /// let expected = gray_image!( /// 2, 4; /// 6, 8); /// /// let actual = green_channel(&image); /// assert_pixels_eq!(actual, expected); /// # } /// ``` pub fn green_channel(image: &I) -> Image> where I: GenericImage>, Rgb: Pixel, C: Primitive, { map_colors(image, |p| Luma([p[1]])) } /// Creates an RGB image by embedding a grayscale image in its green channel. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Luma; /// use imageproc::map::as_green_channel; /// /// let image = gray_image!( /// 1, 2; /// 3, 4); /// /// let expected = rgb_image!( /// [0, 1, 0], [0, 2, 0]; /// [0, 3, 0], [0, 4, 0]); /// /// let actual = as_green_channel(&image); /// assert_pixels_eq!(actual, expected); /// # } /// ``` pub fn as_green_channel(image: &I) -> Image> where I: GenericImage>, Rgb: Pixel, C: Primitive, { map_colors(image, |p| { let mut cs = [C::zero(); 3]; cs[1] = p[0]; Rgb(cs) }) } /// Creates a grayscale image by extracting the blue channel of an RGB image. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Luma; /// use imageproc::map::blue_channel; /// /// let image = rgb_image!( /// [1, 2, 3], [2, 4, 6]; /// [3, 6, 9], [4, 8, 12]); /// /// let expected = gray_image!( /// 3, 6; /// 9, 12); /// /// let actual = blue_channel(&image); /// assert_pixels_eq!(actual, expected); /// # } /// ``` pub fn blue_channel(image: &I) -> Image> where I: GenericImage>, Rgb: Pixel, C: Primitive, { map_colors(image, |p| Luma([p[2]])) } /// Creates an RGB image by embedding a grayscale image in its blue channel. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Luma; /// use imageproc::map::as_blue_channel; /// /// let image = gray_image!( /// 1, 2; /// 3, 4); /// /// let expected = rgb_image!( /// [0, 0, 1], [0, 0, 2]; /// [0, 0, 3], [0, 0, 4]); /// /// let actual = as_blue_channel(&image); /// assert_pixels_eq!(actual, expected); /// # } /// ``` pub fn as_blue_channel(image: &I) -> Image> where I: GenericImage>, Rgb: Pixel, C: Primitive, { map_colors(image, |p| { let mut cs = [C::zero(); 3]; cs[2] = p[0]; Rgb(cs) }) } imageproc-0.25.0/src/math.rs000064400000000000000000000004231046102023000137430ustar 00000000000000//! Assorted mathematical helper functions. /// L1 norm of a vector. pub fn l1_norm(xs: &[f32]) -> f32 { xs.iter().fold(0f32, |acc, x| acc + x.abs()) } /// L2 norm of a vector. pub fn l2_norm(xs: &[f32]) -> f32 { xs.iter().fold(0f32, |acc, x| acc + x * x).sqrt() } imageproc-0.25.0/src/morphology.rs000064400000000000000000002076141046102023000152240ustar 00000000000000//! Functions for computing [morphological operators]. //! //! [morphological operators]: https://homepages.inf.ed.ac.uk/rbf/HIPR2/morops.htm use crate::{ distance_transform::{distance_transform_impl, distance_transform_mut, DistanceFrom, Norm}, point::Point, }; use image::{GrayImage, Luma}; use itertools::Itertools; /// Sets all pixels within distance `k` of a foreground pixel to white. /// /// A pixel is treated as belonging to the foreground if it has non-zero intensity. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::GrayImage; /// use imageproc::morphology::dilate; /// use imageproc::distance_transform::Norm; /// /// let image = gray_image!( /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 255, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0 /// ); /// /// // L1 norm /// let l1_dilated = gray_image!( /// 0, 0, 0, 0, 0; /// 0, 0, 255, 0, 0; /// 0, 255, 255, 255, 0; /// 0, 0, 255, 0, 0; /// 0, 0, 0, 0, 0 /// ); /// /// assert_pixels_eq!(dilate(&image, Norm::L1, 1), l1_dilated); /// /// // L2 norm /// // /// // When computing distances using the L2 norm we take the ceiling of the true values. /// // This means that using the L2 norm gives the same results as the L1 norm for `k <= 2`. /// let l2_dilated = gray_image!( /// 0, 0, 0, 0, 0; /// 0, 0, 255, 0, 0; /// 0, 255, 255, 255, 0; /// 0, 0, 255, 0, 0; /// 0, 0, 0, 0, 0 /// ); /// /// assert_pixels_eq!(dilate(&image, Norm::L2, 1), l2_dilated); /// /// // LInf norm /// let linf_dilated = gray_image!( /// 0, 0, 0, 0, 0; /// 0, 255, 255, 255, 0; /// 0, 255, 255, 255, 0; /// 0, 255, 255, 255, 0; /// 0, 0, 0, 0, 0 /// ); /// /// assert_pixels_eq!(dilate(&image, Norm::LInf, 1), linf_dilated); /// # } /// ``` pub fn dilate(image: &GrayImage, norm: Norm, k: u8) -> GrayImage { let mut out = image.clone(); dilate_mut(&mut out, norm, k); out } /// Sets all pixels within distance `k` of a foreground pixel to white. /// /// A pixel is treated as belonging to the foreground if it has non-zero intensity. /// /// See the [`dilate`](fn.dilate.html) documentation for examples. pub fn dilate_mut(image: &mut GrayImage, norm: Norm, k: u8) { distance_transform_mut(image, norm); for p in image.iter_mut() { *p = if *p <= k { 255 } else { 0 }; } } /// Sets all pixels within distance `k` of a background pixel to black. /// /// A pixel is treated as belonging to the foreground if it has non-zero intensity. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::GrayImage; /// use imageproc::morphology::erode; /// use imageproc::distance_transform::Norm; /// /// let image = gray_image!( /// 0, 0, 0, 0, 0, 0, 0, 0, 0; /// 0, 255, 255, 255, 255, 255, 255, 255, 0; /// 0, 255, 255, 255, 255, 255, 255, 255, 0; /// 0, 255, 255, 255, 255, 255, 255, 255, 0; /// 0, 255, 255, 255, 0, 255, 255, 255, 0; /// 0, 255, 255, 255, 255, 255, 255, 255, 0; /// 0, 255, 255, 255, 255, 255, 255, 255, 0; /// 0, 255, 255, 255, 255, 255, 255, 255, 0; /// 0, 0, 0, 0, 0, 0, 0, 0, 0 /// ); /// /// // L1 norm - the outermost foreground pixels are eroded, /// // as well as those horizontally and vertically adjacent /// // to the centre background pixel. /// let l1_eroded = gray_image!( /// 0, 0, 0, 0, 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0, 0, 0, 0, 0; /// 0, 0, 255, 255, 255, 255, 255, 0, 0; /// 0, 0, 255, 255, 0, 255, 255, 0, 0; /// 0, 0, 255, 0, 0, 0, 255, 0, 0; /// 0, 0, 255, 255, 0, 255, 255, 0, 0; /// 0, 0, 255, 255, 255, 255, 255, 0, 0; /// 0, 0, 0, 0, 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0, 0, 0, 0, 0 /// ); /// /// assert_pixels_eq!(erode(&image, Norm::L1, 1), l1_eroded); /// /// // L2 norm /// // /// // When computing distances using the L2 norm we take the ceiling of the true values. /// // This means that using the L2 norm gives the same results as the L1 norm for `k <= 2`. /// let l2_eroded = gray_image!( /// 0, 0, 0, 0, 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0, 0, 0, 0, 0; /// 0, 0, 255, 255, 255, 255, 255, 0, 0; /// 0, 0, 255, 255, 0, 255, 255, 0, 0; /// 0, 0, 255, 0, 0, 0, 255, 0, 0; /// 0, 0, 255, 255, 0, 255, 255, 0, 0; /// 0, 0, 255, 255, 255, 255, 255, 0, 0; /// 0, 0, 0, 0, 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0, 0, 0, 0, 0 /// ); /// /// assert_pixels_eq!(erode(&image, Norm::L2, 1), l2_eroded); /// /// // LInf norm - all pixels eroded using the L1 norm are eroded, /// // as well as the pixels diagonally adjacent to the centre pixel. /// let linf_eroded = gray_image!( /// 0, 0, 0, 0, 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0, 0, 0, 0, 0; /// 0, 0, 255, 255, 255, 255, 255, 0, 0; /// 0, 0, 255, 0, 0, 0, 255, 0, 0; /// 0, 0, 255, 0, 0, 0, 255, 0, 0; /// 0, 0, 255, 0, 0, 0, 255, 0, 0; /// 0, 0, 255, 255, 255, 255, 255, 0, 0; /// 0, 0, 0, 0, 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0, 0, 0, 0, 0 /// ); /// /// assert_pixels_eq!(erode(&image, Norm::LInf, 1), linf_eroded); /// # } /// ``` pub fn erode(image: &GrayImage, norm: Norm, k: u8) -> GrayImage { let mut out = image.clone(); erode_mut(&mut out, norm, k); out } /// Sets all pixels within distance `k` of a background pixel to black. /// /// A pixel is treated as belonging to the foreground if it has non-zero intensity. /// /// See the [`erode`](fn.erode.html) documentation for examples. pub fn erode_mut(image: &mut GrayImage, norm: Norm, k: u8) { distance_transform_impl(image, norm, DistanceFrom::Background); for p in image.iter_mut() { *p = if *p <= k { 0 } else { 255 }; } } /// Erosion followed by dilation. /// /// See the [`erode`](fn.erode.html) and [`dilate`](fn.dilate.html) /// documentation for definitions of dilation and erosion. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::morphology::open; /// use imageproc::distance_transform::Norm; /// /// // Isolated regions of foreground pixels are removed. /// let cross = gray_image!( /// 0, 0, 0, 0, 0; /// 0, 0, 255, 0, 0; /// 0, 255, 255, 255, 0; /// 0, 0, 255, 0, 0; /// 0, 0, 0, 0, 0 /// ); /// /// let opened_cross = gray_image!( /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0 /// ); /// /// assert_pixels_eq!( /// open(&cross, Norm::LInf, 1), /// opened_cross /// ); /// /// // Large blocks survive unchanged. /// let blob = gray_image!( /// 0, 0, 0, 0, 0; /// 0, 255, 255, 255, 0; /// 0, 255, 255, 255, 0; /// 0, 255, 255, 255, 0; /// 0, 0, 0, 0, 0 /// ); /// /// assert_pixels_eq!( /// open(&blob, Norm::LInf, 1), /// blob /// ); /// # } /// ``` pub fn open(image: &GrayImage, norm: Norm, k: u8) -> GrayImage { let mut out = image.clone(); open_mut(&mut out, norm, k); out } /// Erosion followed by dilation. /// /// See the [`open`](fn.open.html) documentation for examples, /// and the [`erode`](fn.erode.html) and [`dilate`](fn.dilate.html) /// documentation for definitions of dilation and erosion. pub fn open_mut(image: &mut GrayImage, norm: Norm, k: u8) { erode_mut(image, norm, k); dilate_mut(image, norm, k); } /// Dilation followed by erosion. /// /// See the [`erode`](fn.erode.html) and [`dilate`](fn.dilate.html) /// documentation for definitions of dilation and erosion. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::morphology::close; /// use imageproc::distance_transform::Norm; /// /// // Small holes are closed - hence the name. /// let small_hole = gray_image!( /// 255, 255, 255, 255; /// 255, 0, 0, 255; /// 255, 0, 0, 255; /// 255, 255, 255, 255 /// ); /// /// let closed_small_hole = gray_image!( /// 255, 255, 255, 255; /// 255, 255, 255, 255; /// 255, 255, 255, 255; /// 255, 255, 255, 255 /// ); /// /// assert_pixels_eq!( /// close(&small_hole, Norm::LInf, 1), /// closed_small_hole /// ); /// /// // Large holes survive unchanged. /// let large_hole = gray_image!( /// 255, 255, 255, 255, 255; /// 255, 0, 0, 0, 255; /// 255, 0, 0, 0, 255; /// 255, 0, 0, 0, 255; /// 255, 255, 255, 255, 255 /// ); /// /// assert_pixels_eq!( /// close(&large_hole, Norm::LInf, 1), /// large_hole /// ); /// /// // A dot gains a layer of foreground pixels /// // when dilated and loses them again when eroded, /// // resulting in no change. /// let dot = gray_image!( /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 255, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0 /// ); /// /// assert_pixels_eq!( /// close(&dot, Norm::LInf, 1), /// dot /// ); /// /// // A dot near the boundary gains pixels in the top-left /// // of the image which are not within distance 1 of any /// // background pixels, so are not removed by erosion. /// let dot_near_boundary = gray_image!( /// 0, 0, 0, 0, 0; /// 0, 255, 0, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0 /// ); /// /// let closed_dot_near_boundary = gray_image!( /// 255, 255, 0, 0, 0; /// 255, 255, 0, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0 /// ); /// /// assert_pixels_eq!( /// close(&dot_near_boundary, Norm::LInf, 1), /// closed_dot_near_boundary /// ); /// # } /// ``` pub fn close(image: &GrayImage, norm: Norm, k: u8) -> GrayImage { let mut out = image.clone(); close_mut(&mut out, norm, k); out } /// Dilation followed by erosion. /// /// See the [`close`](fn.close.html) documentation for examples, /// and the [`erode`](fn.erode.html) and [`dilate`](fn.dilate.html) /// documentation for definitions of dilation and erosion. pub fn close_mut(image: &mut GrayImage, norm: Norm, k: u8) { dilate_mut(image, norm, k); erode_mut(image, norm, k); } /// A mask used in grayscale morphological operations. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Mask { /// For any optimisation/arithmetic purposes, it is guaranteed that: /// - all the integer values will be strictly between -512 and 512 /// - all tuples will be sorted in reverse lexicographic order, line by line ((-1,-1),(0,-1),(1,-1),(-1,0),(0,0),...) /// - no tuple shall appear twice elements: Vec>, } macro_rules! lines { ($mask:expr) => { $mask .elements .iter() .group_by(|p| p.y) .into_iter() .map(|(y, line)| (y, line.map(|p| p.x))) }; } impl Mask { /// Creates a mask containing the points `(x, y) - (center_x, center_y)` for each `(x, y)` with non-zero intensity in `image`. /// /// Mask contents are represented using signed integers, so `(center_x, center_y)` is not required to be within bounds for `image`. However, `image` /// is restricted to have a side length of at most 511 pixels. /// /// # Panics /// If `image.width() >= 512` or `image.height() >= 512`. pub fn from_image(image: &GrayImage, center_x: u8, center_y: u8) -> Self { assert!( image.width() < 512, "the input image must be at most 511 pixels wide" ); assert!( image.height() < 512, "the input image must be at most 511 pixels high" ); let center = Point::new(center_x, center_y).to_i16(); let elements = image .enumerate_pixels() .filter(|(_, _, &p)| p[0] != 0) .map(|(x, y, _)| Point::new(x, y).to_i16()) .map(|p| p - center) .collect(); Self::new(elements) } fn new(elements: Vec>) -> Self { assert!(elements.len() <= (511 * 511) as usize); debug_assert!(elements.iter().tuple_windows().all(|(a, b)| { if a.y == b.y { a.x < b.x } else { a.y < b.y } })); Self { elements } } /// Creates a square mask of side length `2 * radius + 1`. /// /// # Example /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::morphology::Mask; /// /// // Mask::square(1) is a 3x3 square mask centered at the origin. /// let square = gray_image!( /// 255, 255, 255; /// 255, 255, 255; /// 255, 255, 255 /// ); /// assert_eq!(Mask::square(1), Mask::from_image(&square, 1, 1)); /// # } /// ``` pub fn square(radius: u8) -> Self { let radius = i16::from(radius); let range = -radius..=radius; let elements = range .clone() .cartesian_product(range) .map(|(y, x)| Point::new(x, y)) .collect(); Self::new(elements) } /// Creates a diamond-shaped mask containing all points with `L1` norm at most `radius`. /// /// # Example /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::morphology::Mask; /// /// // Mask::diamond(1) is a 3x3 cross centered at the origin. /// let diamond_1 = gray_image!( /// 0, 255, 0; /// 255, 255, 255; /// 0, 255, 0 /// ); /// assert_eq!(Mask::diamond(1), Mask::from_image(&diamond_1, 1, 1)); /// /// // Mask::diamond(2) is a 5x5 diamond centered at the origin. /// let diamond_2 = gray_image!( /// 0, 0, 255, 0, 0; /// 0, 255, 255, 255, 0; /// 255, 255, 255, 255, 255; /// 0, 255, 255, 255, 0; /// 0, 0, 255, 0, 0 /// ); /// assert_eq!(Mask::diamond(2), Mask::from_image(&diamond_2, 2, 2)); /// # } /// ``` pub fn diamond(radius: u8) -> Self { let cap = 1 + 2 * usize::from(radius) * (usize::from(radius) + 1); let mut elements = Vec::with_capacity(cap); let radius = i16::from(radius); let points = (-radius..=radius) .flat_map(|y| ((y.abs() - radius)..=(radius - y.abs())).map(move |x| Point::new(x, y))); elements.extend(points); Self::new(elements) } /// Creates a disk-shaped mask containing all points with `L2` norm at most `radius`. /// /// When computing distances using the L2 norm we take the ceiling of the true values. /// This means that using the L2 norm gives the same results as the `L1` norm for `radius <= 2`. /// /// # Example /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::morphology::Mask; /// /// // For radius <= 2, Mask::disk(radius) is the same as Mask::diamond(radius). /// let disk_2 = gray_image!( /// 0, 0, 255, 0, 0; /// 0, 231, 204, 101, 0; /// 149, 193, 188, 137, 199; /// 0, 222, 182, 114, 0; /// 0, 0, 217, 0, 0 /// ); /// assert_eq!(Mask::disk(2), Mask::from_image(&disk_2, 2, 2)); /// /// // Mask::disk(3) is a filled circle of radius 3. /// let disk_3 = gray_image!( /// 0, 0, 0, 255, 0, 0, 0; /// 0, 255, 255, 255, 255, 255, 0; /// 0, 255, 255, 255, 255, 255, 0; /// 255, 255, 255, 255, 255, 255, 255; /// 0, 255, 255, 255, 255, 255, 0; /// 0, 255, 255, 255, 255, 255, 0; /// 0, 0, 0, 255, 0, 0, 0 /// ); /// assert_eq!(Mask::disk(3), Mask::from_image(&disk_3, 3, 3)); /// # } /// ``` pub fn disk(radius: u8) -> Self { let radius_squared = u32::from(radius).pow(2); let half_widths_per_height = std::iter::successors( Some((-i16::from(radius), 0u8)), |&(last_height, last_half_width)| { if last_height == i16::from(radius) { return None; }; let next_height = last_height + 1; let height_squared = (u32::from(next_height.unsigned_abs())).pow(2); let next_half_width = if next_height <= 0 { // upper part of the circle => increasing width (u32::from(last_half_width)..) .find(|x| (x + 1).pow(2) + height_squared > radius_squared)? } else { // lower part of the circle => decreasing width (0u32..=last_half_width.into()) .rev() .find(|&x| x.pow(2) + height_squared <= radius_squared)? }; Some((next_height, next_half_width.try_into().unwrap())) }, ); let cap = half_widths_per_height .clone() .map(|(_, half_width)| 2 * usize::from(half_width) + 1) .sum(); let mut elements = Vec::with_capacity(cap); let points = half_widths_per_height.flat_map(|(y, half_width)| { (-i16::from(half_width)..=i16::from(half_width)).map(move |x| Point::new(x, y)) }); elements.extend(points); Self::new(elements) } } fn mask_reduce u8>( image: &GrayImage, mask: &Mask, neutral: u8, operator: F, ) -> GrayImage { let mut result = GrayImage::from_pixel(image.width(), image.height(), Luma([neutral])); for (y, line_group) in lines!(mask) { let y = i64::from(y); let line = line_group.collect::>(); let input_rows = image .chunks(image.width() as usize) .skip(y.try_into().unwrap_or(0)); let output_rows = result .chunks_mut(image.width() as usize) .skip((-y).try_into().unwrap_or(0)); for (input_row, output_row) in input_rows.zip(output_rows) { for x in line.iter().copied() { let inputs = input_row.iter().skip(x.try_into().unwrap_or(0)); let outputs = output_row.iter_mut().skip((-x).try_into().unwrap_or(0)); for (&input, output) in inputs.zip(outputs) { *output = operator(input, *output); } } } } result } /// Computes the morphologic dilation of `image` with the given mask. /// /// For each input pixel, the output pixel will be the maximum of all pixels included /// in the mask at that position. If the mask doesn't intersect any input pixel at some point, /// it will default to a value of [`u8::MIN`]. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::GrayImage; /// use imageproc::morphology::{Mask, grayscale_dilate}; /// /// let image = gray_image!( /// 7, 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0, 0; /// 0, 0, 99, 0, 0, 0; /// 0, 0, 0, 0, 0, 0; /// 0, 0, 0, 0, 0, 222 /// ); /// /// // Using a diamond mask /// let diamond_dilated = gray_image!( /// 7, 7, 0, 0, 0, 0; /// 7, 0, 99, 0, 0, 0; /// 0, 99, 99, 99, 0, 0; /// 0, 0, 99, 0, 0, 222; /// 0, 0, 0, 0, 222, 222 /// ); /// assert_pixels_eq!(grayscale_dilate(&image, &Mask::diamond(1)), diamond_dilated); /// /// // Using a disk mask /// let disk_dilated = gray_image!( /// 99, 99, 99, 99, 99, 0; /// 99, 99, 99, 99, 99, 222; /// 99, 99, 99, 222, 222, 222; /// 99, 99, 99, 222, 222, 222; /// 99, 99, 222, 222, 222, 222 /// ); /// assert_pixels_eq!(grayscale_dilate(&image, &Mask::disk(3)), disk_dilated); /// /// // Using a square mask /// let square_dilated = gray_image!( /// 7, 7, 0, 0, 0, 0; /// 7, 99, 99, 99, 0, 0; /// 0, 99, 99, 99, 0, 0; /// 0, 99, 99, 99, 222, 222; /// 0, 0, 0, 0, 222, 222 /// ); /// assert_pixels_eq!(grayscale_dilate(&image, &Mask::square(1)), square_dilated); /// /// // Using an arbitrary mask /// let column_mask = Mask::from_image( /// &gray_image!( /// 255; /// 255; /// 255; /// 255 /// ), /// 0, 1 /// ); /// let column_dilated = gray_image!( /// 7, 0, 99, 0, 0, 0; /// 7, 0, 99, 0, 0, 0; /// 0, 0, 99, 0, 0, 222; /// 0, 0, 99, 0, 0, 222; /// 0, 0, 0, 0, 0, 222 /// ); /// assert_pixels_eq!(grayscale_dilate(&image, &column_mask), column_dilated); /// # } /// ``` pub fn grayscale_dilate(image: &GrayImage, mask: &Mask) -> GrayImage { mask_reduce(image, mask, u8::MIN, u8::max) } /// Computes the morphologic erosion of `image` with the given mask. /// /// For each input pixel, the output pixel will be the minimum of all pixels included /// in the mask at that position. If the mask doesn't intersect any input pixel at some point, /// it will default to a value of [`u8::MAX`]. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::GrayImage; /// use imageproc::morphology::{Mask, grayscale_erode}; /// /// let image = gray_image!( /// 7, 99, 99, 99, 99, 222; /// 99, 99, 99, 99, 99, 222; /// 99, 99, 99, 99, 222, 222; /// 7, 99, 99, 99, 222, 222; /// 99, 99, 99, 222, 222, 222 /// ); /// /// // Using a diamond mask /// let diamond_eroded = gray_image!( /// 7, 7, 99, 99, 99, 99; /// 7, 99, 99, 99, 99, 99; /// 7, 99, 99, 99, 99, 222; /// 7, 7, 99, 99, 99, 222; /// 7, 99, 99, 99, 222, 222 /// ); /// assert_pixels_eq!(grayscale_erode(&image, &Mask::diamond(1)), diamond_eroded); /// /// // Using a disk mask /// let disk_eroded = gray_image!( /// 7, 7, 7, 7, 99, 99; /// 7, 7, 7, 99, 99, 99; /// 7, 7, 7, 99, 99, 99; /// 7, 7, 7, 7, 99, 99; /// 7, 7, 7, 99, 99, 99 /// ); /// assert_pixels_eq!(grayscale_erode(&image, &Mask::disk(3)), disk_eroded); /// /// // Using a square mask /// let square_eroded = gray_image!( /// 7, 7, 99, 99, 99, 99; /// 7, 7, 99, 99, 99, 99; /// 7, 7, 99, 99, 99, 99; /// 7, 7, 99, 99, 99, 222; /// 7, 7, 99, 99, 99, 222 /// ); /// assert_pixels_eq!(grayscale_erode(&image, &Mask::square(1)), square_eroded); /// /// // Using an arbitrary mask /// let column_mask = Mask::from_image( /// &gray_image!( /// 255; /// 255; /// 255; /// 255 /// ), /// 0, 1 /// ); /// let column_eroded = gray_image!( /// 7, 99, 99, 99, 99, 222; /// 7, 99, 99, 99, 99, 222; /// 7, 99, 99, 99, 99, 222; /// 7, 99, 99, 99, 222, 222; /// 7, 99, 99, 99, 222, 222 /// ); /// assert_pixels_eq!(grayscale_erode(&image, &column_mask), column_eroded); /// # } /// ``` pub fn grayscale_erode(image: &GrayImage, mask: &Mask) -> GrayImage { mask_reduce(image, mask, u8::MAX, u8::min) } /// Grayscale erosion followed by grayscale dilation. /// /// See the [`grayscale_dilate`](fn.grayscale_dilate.html) /// and [`grayscale_erode`](fn.grayscale_erode.html) /// documentation for definitions of dilation and erosion. /// ////// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::GrayImage; /// use imageproc::morphology::{Mask, grayscale_open}; /// /// let image = gray_image!( /// 100, 99, 99, 99, 222, 99; /// 99, 99, 99, 222, 222, 222; /// 99, 7, 99, 99, 222, 99; /// 7, 7, 7, 99, 99, 99; /// 99, 7, 99, 99, 99, 99 /// ); /// /// // Isolated regions of foreground pixels are removed, /// // while isolated zones of background are maintained /// let image_opened = gray_image!( /// 99, 99, 99, 99, 99, 99; /// 99, 99, 99, 99, 99, 99; /// 7, 7, 99, 99, 99, 99; /// 7, 7, 7, 99, 99, 99; /// 7, 7, 7, 99, 99, 99 /// ); /// assert_pixels_eq!(grayscale_open(&image, &Mask::square(1)), image_opened); /// /// // grayscale_open is idempotent - applying it a second time has no effect. /// assert_pixels_eq!(grayscale_open(&image_opened, &Mask::square(1)), image_opened); /// # } /// ``` pub fn grayscale_open(image: &GrayImage, mask: &Mask) -> GrayImage { grayscale_dilate(&grayscale_erode(image, mask), mask) } /// Grayscale dilation followed by grayscale erosion. /// /// See the [`grayscale_dilate`](fn.grayscale_dilate.html) /// and [`grayscale_erode`](fn.grayscale_erode.html) /// documentation for definitions of dilation and erosion. /// ////// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::GrayImage; /// use imageproc::morphology::{Mask, grayscale_close}; /// /// let image = gray_image!( /// 50, 99, 99, 99, 222, 99; /// 99, 99, 99, 222, 222, 222; /// 99, 7, 99, 99, 222, 99; /// 7, 7, 7, 99, 99, 99; /// 99, 7, 99, 99, 99, 99 /// ); /// /// // Isolated regions of background pixels are removed, /// // while isolated zones of foreground pixels are maintained /// let image_closed = gray_image!( /// 99, 99, 99, 222, 222, 222; /// 99, 99, 99, 222, 222, 222; /// 99, 99, 99, 99, 222, 222; /// 99, 99, 99, 99, 99, 99; /// 99, 99, 99, 99, 99, 99 /// ); /// assert_pixels_eq!(grayscale_close(&image, &Mask::square(1)), image_closed); /// /// // grayscale_close is idempotent - applying it a second time has no effect. /// assert_pixels_eq!(grayscale_close(&image_closed, &Mask::square(1)), image_closed); /// # } /// ``` pub fn grayscale_close(image: &GrayImage, mask: &Mask) -> GrayImage { grayscale_erode(&grayscale_dilate(image, mask), mask) } #[cfg(test)] mod tests { use super::*; #[test] fn test_dilate_point_l1_0() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::L1, 0); assert_pixels_eq!(dilated, image); } #[test] fn test_dilate_point_l1_1() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::L1, 1); let expected = gray_image!( 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 255, 255, 255, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_dilate_point_l1_2() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::L1, 2); let expected = gray_image!( 0, 0, 255, 0, 0; 0, 255, 255, 255, 0; 255, 255, 255, 255, 255; 0, 255, 255, 255, 0; 0, 0, 255, 0, 0 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_dilate_point_l1_4() { let image = gray_image!( 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 255, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::L1, 4); let expected = gray_image!( 0, 0, 0, 0, 255, 0, 0, 0, 0; 0, 0, 0, 255, 255, 255, 0, 0, 0; 0, 0, 255, 255, 255, 255, 255, 0, 0; 0, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 255; 0, 255, 255, 255, 255, 255, 255, 255, 0; 0, 0, 255, 255, 255, 255, 255, 0, 0; 0, 0, 0, 255, 255, 255, 0, 0, 0; 0, 0, 0, 0, 255, 0, 0, 0, 0 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_dilate_point_l2_0() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::L2, 0); assert_pixels_eq!(dilated, image); } #[test] fn test_dilate_point_l2_1() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::L2, 1); let expected = gray_image!( 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 255, 255, 255, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_dilate_point_l2_2() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::L2, 2); let expected = gray_image!( 0, 0, 255, 0, 0; 0, 255, 255, 255, 0; 255, 255, 255, 255, 255; 0, 255, 255, 255, 0; 0, 0, 255, 0, 0 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_dilate_point_l2_4() { let image = gray_image!( 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 255, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::L2, 4); let expected = gray_image!( 0, 0, 0, 0, 255, 0, 0, 0, 0; 0, 0, 255, 255, 255, 255, 255, 0, 0; 0, 255, 255, 255, 255, 255, 255, 255, 0; 0, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 255; 0, 255, 255, 255, 255, 255, 255, 255, 0; 0, 255, 255, 255, 255, 255, 255, 255, 0; 0, 0, 255, 255, 255, 255, 255, 0, 0; 0, 0, 0, 0, 255, 0, 0, 0, 0 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_dilate_point_linf_0() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::LInf, 0); assert_pixels_eq!(dilated, image); } #[test] fn test_dilate_point_linf_1() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::LInf, 1); let expected = gray_image!( 0, 0, 0, 0, 0; 0, 255, 255, 255, 0; 0, 255, 255, 255, 0; 0, 255, 255, 255, 0; 0, 0, 0, 0, 0 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_dilate_point_linf_2() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::LInf, 2); let expected = gray_image!( 255, 255, 255, 255, 255; 255, 255, 255, 255, 255; 255, 255, 255, 255, 255; 255, 255, 255, 255, 255; 255, 255, 255, 255, 255 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_dilate_point_linf_4() { let image = gray_image!( 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 255, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0; 0, 0, 0, 0, 0, 0, 0, 0, 0 ); let dilated = dilate(&image, Norm::LInf, 4); let expected = gray_image!( 255, 255, 255, 255, 255, 255, 255, 255, 255; 255, 255, 255, 255, 255, 255, 255, 255, 255; 255, 255, 255, 255, 255, 255, 255, 255, 255; 255, 255, 255, 255, 255, 255, 255, 255, 255; 255, 255, 255, 255, 255, 255, 255, 255, 255; 255, 255, 255, 255, 255, 255, 255, 255, 255; 255, 255, 255, 255, 255, 255, 255, 255, 255; 255, 255, 255, 255, 255, 255, 255, 255, 255; 255, 255, 255, 255, 255, 255, 255, 255, 255 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_erode_point_l1_0() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let eroded = erode(&image, Norm::L1, 0); assert_pixels_eq!(eroded, image); } #[test] fn test_erode_point_l1_1() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let eroded = erode(&image, Norm::L1, 1); let expected = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); assert_pixels_eq!(eroded, expected); } #[test] fn test_erode_dented_wall_l1_4() { let image = gray_image!( 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 0, 0, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0 ); let dilated = erode(&image, Norm::L1, 4); let expected = gray_image!( 255, 255, 255, 255, 0, 0, 0, 0, 0; 255, 255, 255, 255, 0, 0, 0, 0, 0; 255, 255, 255, 255, 0, 0, 0, 0, 0; 255, 255, 255, 0, 0, 0, 0, 0, 0; 255, 255, 0, 0, 0, 0, 0, 0, 0; 255, 255, 255, 0, 0, 0, 0, 0, 0; 255, 255, 255, 255, 0, 0, 0, 0, 0; 255, 255, 255, 255, 0, 0, 0, 0, 0; 255, 255, 255, 255, 0, 0, 0, 0, 0 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_erode_point_l2_0() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let eroded = erode(&image, Norm::L2, 0); assert_pixels_eq!(eroded, image); } #[test] fn test_erode_point_l2_1() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let eroded = erode(&image, Norm::L2, 1); let expected = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); assert_pixels_eq!(eroded, expected); } #[test] fn test_erode_dented_wall_l2_4() { let image = gray_image!( 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 0, 0, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0 ); let dilated = erode(&image, Norm::L2, 4); let expected = gray_image!( 255, 255, 255, 255, 0, 0, 0, 0, 0; 255, 255, 255, 255, 0, 0, 0, 0, 0; 255, 255, 255, 0, 0, 0, 0, 0, 0; 255, 255, 255, 0, 0, 0, 0, 0, 0; 255, 255, 0, 0, 0, 0, 0, 0, 0; 255, 255, 255, 0, 0, 0, 0, 0, 0; 255, 255, 255, 0, 0, 0, 0, 0, 0; 255, 255, 255, 255, 0, 0, 0, 0, 0; 255, 255, 255, 255, 0, 0, 0, 0, 0 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_erode_point_linf_0() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let eroded = erode(&image, Norm::LInf, 0); assert_pixels_eq!(eroded, image); } #[test] fn test_erode_point_linf_1() { let image = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 255, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); let eroded = erode(&image, Norm::LInf, 1); let expected = gray_image!( 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0; 0, 0, 0, 0, 0 ); assert_pixels_eq!(eroded, expected); } #[test] fn test_erode_dented_wall_linf_4() { let image = gray_image!( 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 0, 0, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0; 255, 255, 255, 255, 255, 255, 255, 255, 0 ); let dilated = erode(&image, Norm::LInf, 4); let expected = gray_image!( 255, 255, 0, 0, 0, 0, 0, 0, 0; 255, 255, 0, 0, 0, 0, 0, 0, 0; 255, 255, 0, 0, 0, 0, 0, 0, 0; 255, 255, 0, 0, 0, 0, 0, 0, 0; 255, 255, 0, 0, 0, 0, 0, 0, 0; 255, 255, 0, 0, 0, 0, 0, 0, 0; 255, 255, 0, 0, 0, 0, 0, 0, 0; 255, 255, 0, 0, 0, 0, 0, 0, 0; 255, 255, 0, 0, 0, 0, 0, 0, 0 ); assert_pixels_eq!(dilated, expected); } #[test] fn test_mask_from_image_equality() { let ring_mask_base = gray_image!( 100, 75, 255, 222; 84, 0, 0, 1; 99, 0, 0, 22; 255, 7, 255, 20 ); let other_ring_mask_base = gray_image!( 18, 172, 13, 5; 45, 0, 0, 101; 222, 0, 0, 93; 1, 9, 212, 35 ); assert_eq!( Mask::from_image(&ring_mask_base, 1, 1), Mask::from_image(&other_ring_mask_base, 1, 1) ); } #[test] fn test_mask_from_image_displacement_inequality() { let mask_base = gray_image!( 100, 75, 255, 222; 84, 0, 0, 1; 99, 0, 0, 22; 255, 7, 255, 20 ); assert_ne!( Mask::from_image(&mask_base, 1, 1), Mask::from_image(&mask_base, 2, 2) ); } #[test] fn test_mask_from_image_empty() { let mask_base = gray_image!(0); assert!(Mask::from_image(&mask_base, 1, 1).elements.is_empty()) } /// this tests that it doesn't panic #[test] fn test_mask_from_image_outside() { let mask_base = gray_image!( 100, 75, 255, 222; 84, 0, 0, 1; 99, 0, 0, 22; 255, 7, 255, 20 ); let _ = Mask::from_image(&mask_base, 20, 20); } #[test] #[should_panic] fn test_mask_from_image_out_of_bounds() { let mask_base = GrayImage::new(600, 5); Mask::from_image(&mask_base, 5, 5); } #[test] fn test_masks_0() { let mask_base = gray_image!(72); assert_eq!(Mask::from_image(&mask_base, 0, 0), Mask::square(0)); assert_eq!(Mask::from_image(&mask_base, 0, 0), Mask::diamond(0)); assert_eq!(Mask::from_image(&mask_base, 0, 0), Mask::disk(0)) } #[test] fn test_mask_square_1() { let mask_base = gray_image!( 72, 31, 148; 2, 219, 173; 48, 7, 200 ); assert_eq!(Mask::from_image(&mask_base, 1, 1), Mask::square(1)); } #[test] fn test_mask_square_2() { let mask_base = gray_image!( 217, 188, 101, 222, 137; 231, 204, 255, 182, 193; 193, 101, 188, 217, 149; 217, 188, 231, 222, 137; 101, 204, 222, 255, 193 ); assert_eq!(Mask::from_image(&mask_base, 2, 2), Mask::square(2)); } #[test] fn test_mask_square_3() { let mask_base = gray_image!( 217, 188, 101, 222, 137, 101, 222; 231, 204, 255, 222, 137, 222, 255; 193, 101, 188, 217, 217, 222, 188; 217, 188, 231, 222, 137, 217, 149; 193, 255, 193, 188, 231, 222, 188; 217, 188, 231, 149, 101, 188, 149; 101, 204, 222, 255, 193, 255, 182 ); assert_eq!(Mask::from_image(&mask_base, 3, 3), Mask::square(3)); } #[test] fn test_mask_diamond_1() { let mask_base = gray_image!( 0, 31, 0; 2, 219, 173; 0, 7, 0 ); assert_eq!(Mask::from_image(&mask_base, 1, 1), Mask::diamond(1)); } #[test] fn test_mask_diamond_2() { let mask_base = gray_image!( 0, 0, 255, 0, 0; 0, 231, 204, 101, 0; 149, 193, 188, 137, 199; 0, 222, 182, 114, 0; 0, 0, 217, 0, 0 ); assert_eq!(Mask::from_image(&mask_base, 2, 2), Mask::diamond(2)); } #[test] fn test_mask_diamond_3() { let mask_base = gray_image!( 0, 0, 0, 222, 0, 0, 0; 0, 0, 255, 222, 137, 0, 0; 0, 101, 188, 217, 217, 222, 0; 217, 188, 231, 222, 137, 217, 149; 0, 255, 193, 188, 231, 222, 0; 0, 0, 231, 149, 101, 0, 0; 0, 0, 0, 255, 0, 0, 0 ); assert_eq!(Mask::from_image(&mask_base, 3, 3), Mask::diamond(3)); } #[test] fn test_mask_disk_1() { let mask_base = gray_image!( 0, 31, 0; 2, 219, 173; 0, 7, 0 ); assert_eq!(Mask::from_image(&mask_base, 1, 1), Mask::disk(1)); } #[test] fn test_mask_disk_2() { let mask_base = gray_image!( 0, 0, 255, 0, 0; 0, 231, 204, 101, 0; 149, 193, 188, 137, 199; 0, 222, 182, 114, 0; 0, 0, 217, 0, 0 ); assert_eq!(Mask::from_image(&mask_base, 2, 2), Mask::disk(2)); } #[test] fn test_mask_disk_3() { let mask_base = gray_image!( 0, 0, 0, 222, 0, 0, 0; 0, 149, 255, 222, 137, 101, 0; 0, 101, 188, 217, 217, 222, 0; 217, 188, 231, 222, 137, 217, 149; 0, 255, 193, 188, 231, 222, 0; 0, 137, 231, 149, 101, 188, 0; 0, 0, 0, 255, 0, 0, 0 ); assert_eq!(Mask::from_image(&mask_base, 3, 3), Mask::disk(3)); } #[test] fn test_grayscale_dilate_0() { let image = gray_image!( 217, 188, 101, 222, 137, 101, 222; 231, 204, 255, 222, 137, 222, 255; 193, 101, 188, 217, 217, 222, 188; 217, 188, 231, 222, 137, 217, 149; 193, 255, 193, 188, 231, 222, 188; 217, 188, 231, 149, 101, 188, 149; 101, 204, 222, 255, 193, 255, 182 ); assert_eq!(grayscale_dilate(&image, &Mask::square(0)), image.clone()); } #[test] fn test_grayscale_dilate_diamond_1() { let image = gray_image!( 80, 80, 80, 80, 80, 80, 80; 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let dilated = gray_image!( 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80 ); assert_eq!(grayscale_dilate(&image, &Mask::diamond(1)), dilated); } #[test] fn test_grayscale_dilate_diamond_3() { let image = gray_image!( 80, 80, 80, 80, 80, 80, 80; 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let dilated = gray_image!( 80, 80, 80, 212, 212, 212, 212; 80, 80, 212, 212, 212, 212, 212; 80, 212, 212, 212, 212, 212, 212; 80, 80, 212, 212, 212, 212, 212; 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 80; 80, 80, 80, 80, 80, 80, 80 ); assert_eq!(grayscale_dilate(&image, &Mask::diamond(3)), dilated); } #[test] fn test_grayscale_dilate_square_1() { let image = gray_image!( 80, 80, 80, 80, 80, 80, 80; 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let dilated = gray_image!( 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 80, 80, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80 ); assert_eq!(grayscale_dilate(&image, &Mask::square(1)), dilated); } #[test] fn test_grayscale_dilate_square_3() { let image = gray_image!( 80, 80, 80, 80, 80, 80, 80; 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let dilated = gray_image!( 80, 212, 212, 212, 212, 212, 212; 80, 212, 212, 212, 212, 212, 212; 80, 212, 212, 212, 212, 212, 212; 80, 212, 212, 212, 212, 212, 212; 80, 212, 212, 212, 212, 212, 212; 80, 212, 212, 212, 212, 212, 212; 80, 80, 80, 80, 80, 80, 80 ); assert_eq!(grayscale_dilate(&image, &Mask::square(3)), dilated); } #[test] fn test_grayscale_dilate_disk_1() { let image = gray_image!( 80, 80, 80, 80, 80, 80, 80; 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let dilated = gray_image!( 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80 ); assert_eq!(grayscale_dilate(&image, &Mask::disk(1)), dilated); } #[test] fn test_grayscale_dilate_disk_3() { let image = gray_image!( 80, 80, 80, 80, 80, 80, 80; 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let dilated = gray_image!( 80, 80, 212, 212, 212, 212, 212; 80, 80, 212, 212, 212, 212, 212; 80, 212, 212, 212, 212, 212, 212; 80, 80, 212, 212, 212, 212, 212; 80, 80, 212, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 80; 80, 80, 80, 80, 80, 80, 80 ); assert_eq!(grayscale_dilate(&image, &Mask::disk(3)), dilated); } #[test] fn test_grayscale_dilate_arbitrary() { let mask = Mask::from_image( &gray_image!( 15, 7; 0, 17; 0, 253; 0, 22 ), 1, 2, ); let image = gray_image!( 80, 80, 80, 80, 80, 80, 80; 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let dilated = gray_image!( 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 80; 80, 80, 80, 80, 212, 212, 80; 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80 ); assert_eq!(grayscale_dilate(&image, &mask), dilated); } #[test] fn test_grayscale_dilate_default_value() { let mask = Mask::from_image(&gray_image!(), 0, 0); let image = gray_image!( 80, 80, 80, 80, 80, 80, 80; 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let dilated = gray_image!( u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN; u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN; u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN; u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN; u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN; u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN; u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN, u8::MIN ); assert_eq!(grayscale_dilate(&image, &mask), dilated); } #[test] fn test_grayscale_erode_0() { let image = gray_image!( 217, 188, 101, 222, 137, 101, 222; 231, 204, 255, 222, 137, 222, 255; 193, 101, 188, 217, 217, 222, 188; 217, 188, 231, 222, 137, 217, 149; 193, 255, 193, 188, 231, 222, 188; 217, 188, 231, 149, 101, 188, 149; 101, 204, 222, 255, 193, 255, 182 ); assert_eq!(grayscale_erode(&image, &Mask::square(0)), image.clone()); } #[test] fn test_grayscale_erode_diamond_1() { let image = gray_image!( 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 0, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let eroded = gray_image!( 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 80, 212, 212; 0, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 0, 80, 80, 80; 0, 0, 0, 0, 80, 80, 80 ); assert_eq!(grayscale_erode(&image, &Mask::diamond(1)), eroded); } #[test] fn test_grayscale_erode_diamond_3() { let image = gray_image!( 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 0, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let eroded = gray_image!( 0, 80, 80, 80, 80, 80, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 0, 80, 80, 80; 0, 0, 0, 0, 0, 80, 80; 0, 0, 0, 0, 0, 0, 80; 0, 0, 0, 0, 0, 0, 80 ); assert_eq!(grayscale_erode(&image, &Mask::diamond(3)), eroded); } #[test] fn test_grayscale_erode_square_1() { let image = gray_image!( 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 0, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let eroded = gray_image!( 80, 80, 80, 80, 80, 212, 212; 80, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 212; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 0, 80, 80, 80; 0, 0, 0, 0, 80, 80, 80; 0, 0, 0, 0, 80, 80, 80 ); assert_eq!(grayscale_erode(&image, &Mask::square(1)), eroded); } #[test] fn test_grayscale_erode_square_3() { let image = gray_image!( 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 0, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let eroded = gray_image!( 0, 0, 0, 0, 80, 80, 80; 0, 0, 0, 0, 0, 80, 80; 0, 0, 0, 0, 0, 0, 80; 0, 0, 0, 0, 0, 0, 80; 0, 0, 0, 0, 0, 0, 80; 0, 0, 0, 0, 0, 0, 80; 0, 0, 0, 0, 0, 0, 80 ); assert_eq!(grayscale_erode(&image, &Mask::square(3)), eroded); } #[test] fn test_grayscale_erode_disk_1() { let image = gray_image!( 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 0, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let eroded = gray_image!( 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 80, 212, 212; 0, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 0, 80, 80, 80; 0, 0, 0, 0, 80, 80, 80 ); assert_eq!(grayscale_erode(&image, &Mask::disk(1)), eroded); } #[test] fn test_grayscale_erode_disk_3() { let image = gray_image!( 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 0, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let eroded = gray_image!( 0, 80, 80, 80, 80, 80, 212; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 0, 80, 80, 80; 0, 0, 0, 0, 0, 80, 80; 0, 0, 0, 0, 0, 80, 80; 0, 0, 0, 0, 0, 0, 80; 0, 0, 0, 0, 0, 0, 80 ); assert_eq!(grayscale_erode(&image, &Mask::disk(3)), eroded); } #[test] fn test_grayscale_erode_arbitrary() { let mask = Mask::from_image( &gray_image!( 15, 7; 1, 17; 0, 253; 0, 22 ), 1, 2, ); let image = gray_image!( 80, 80, 80, 212, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 0, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let eroded = gray_image!( 80, 80, 80, 80, 212, 212, 212; 80, 80, 80, 80, 212, 212, 212; 0, 80, 80, 80, 80, 212, 212; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 0, 80, 80, 80 ); assert_eq!(grayscale_erode(&image, &mask), eroded); } #[test] fn test_grayscale_erode_default_value() { let mask = Mask::from_image(&gray_image!(), 0, 0); let image = gray_image!( 80, 80, 80, 80, 80, 80, 80; 80, 80, 80, 80, 80, 212, 80; 80, 80, 80, 80, 212, 212, 80; 0, 80, 80, 80, 80, 80, 80; 0, 0, 80, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80; 0, 0, 0, 80, 80, 80, 80 ); let dilated = gray_image!( u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX; u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX; u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX; u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX; u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX; u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX; u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX ); assert_eq!(grayscale_erode(&image, &mask), dilated); } } #[cfg(not(miri))] #[cfg(test)] mod proptests { use super::*; use crate::proptest_utils::{arbitrary_image, arbitrary_image_with}; use proptest::prelude::*; fn reference_mask_disk(radius: u8) -> Mask { let range = -(radius as i16)..=(radius as i16); let elements = range .clone() .cartesian_product(range) .filter(|(y, x)| { (x.unsigned_abs() as u32).pow(2) + (y.unsigned_abs() as u32).pow(2) <= (radius as u32).pow(2) }) .map(|(y, x)| Point::new(x, y)) .collect(); Mask::new(elements) } proptest! { #[test] fn proptest_mask_from_white_image( img in arbitrary_image_with(Just(255), 0..=511, 0..=511), x in any::(), y in any::(), ) { Mask::from_image(&img, x, y); } #[test] fn proptest_mask_from_image( img in arbitrary_image(0..=511, 0..=511), x in any::(), y in any::(), ) { Mask::from_image(&img, x, y); } #[test] fn proptest_mask_square(radius in any::()) { Mask::square(radius); } #[test] fn proptest_mask_diamond(radius in any::()) { Mask::diamond(radius); } #[test] fn proptest_mask_disk(radius in any::()) { let actual = Mask::disk(radius); let expected = reference_mask_disk(radius); assert_eq!(actual, expected); } } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use ::test::*; use image::{GrayImage, Luma}; use std::cmp::{max, min}; fn square() -> GrayImage { GrayImage::from_fn(500, 500, |x, y| { if min(x, y) > 100 && max(x, y) < 300 { Luma([255u8]) } else { Luma([0u8]) } }) } #[bench] fn bench_erode_l1_5(b: &mut Bencher) { let image = square(); b.iter(|| { let dilated = dilate(&image, Norm::L1, 5); black_box(dilated); }) } #[bench] fn bench_dilate_l2_5(b: &mut Bencher) { let image = square(); b.iter(|| { let dilated = dilate(&image, Norm::L2, 5); black_box(dilated); }) } #[bench] fn bench_dilate_linf_5(b: &mut Bencher) { let image = square(); b.iter(|| { let dilated = dilate(&image, Norm::LInf, 5); black_box(dilated); }) } #[bench] fn bench_grayscale_mask_from_image(b: &mut Bencher) { let image = GrayImage::from_fn(200, 200, |x, y| Luma([(x + y % 3) as u8])); b.iter(|| { let mask = Mask::from_image(&image, 100, 100); black_box(mask); }) } macro_rules! bench_grayscale_mask { ($name:ident, $f:expr) => { #[bench] fn $name(b: &mut Bencher) { b.iter(|| { let mask = $f(100); black_box(mask); }) } }; } bench_grayscale_mask!(bench_grayscale_square_mask, Mask::square); bench_grayscale_mask!(bench_grayscale_diamond_mask, Mask::diamond); bench_grayscale_mask!(bench_grayscale_disk_mask, Mask::disk); macro_rules! bench_grayscale_operator { ($name:ident, $f:expr, $mask:expr, $img_size:expr) => { #[bench] fn $name(b: &mut Bencher) { let image = GrayImage::from_fn($img_size, $img_size, |x, y| Luma([(x + y % 3) as u8])); let mask = $mask; b.iter(|| { let processed = $f(&image, &mask); black_box(processed); }) } }; } bench_grayscale_operator!( bench_grayscale_op_erode_small_image_point, grayscale_erode, Mask::diamond(0), 50 ); bench_grayscale_operator!( bench_grayscale_op_erode_medium_image_point, grayscale_erode, Mask::diamond(0), 200 ); bench_grayscale_operator!( bench_grayscale_op_erode_big_image_point, grayscale_erode, Mask::diamond(0), 1000 ); bench_grayscale_operator!( bench_grayscale_op_erode_small_image_diamond, grayscale_erode, Mask::diamond(5), 50 ); bench_grayscale_operator!( bench_grayscale_op_erode_medium_image_diamond, grayscale_erode, Mask::diamond(5), 200 ); bench_grayscale_operator!( bench_grayscale_op_erode_big_image_diamond, grayscale_erode, Mask::diamond(5), 1000 ); bench_grayscale_operator!( bench_grayscale_op_erode_small_image_large_square, grayscale_erode, Mask::square(25), 50 ); bench_grayscale_operator!( bench_grayscale_op_erode_medium_image_large_square, grayscale_erode, Mask::square(25), 200 ); bench_grayscale_operator!( bench_grayscale_op_dilate_small_image_point, grayscale_dilate, Mask::diamond(0), 50 ); bench_grayscale_operator!( bench_grayscale_op_dilate_medium_image_point, grayscale_dilate, Mask::diamond(0), 200 ); bench_grayscale_operator!( bench_grayscale_op_dilate_big_image_point, grayscale_dilate, Mask::diamond(0), 1000 ); bench_grayscale_operator!( bench_grayscale_op_dilate_small_image_diamond, grayscale_dilate, Mask::diamond(5), 50 ); bench_grayscale_operator!( bench_grayscale_op_dilate_medium_image_diamond, grayscale_dilate, Mask::diamond(5), 200 ); bench_grayscale_operator!( bench_grayscale_op_dilate_big_image_diamond, grayscale_dilate, Mask::diamond(5), 1000 ); bench_grayscale_operator!( bench_grayscale_op_dilate_small_image_large_square, grayscale_dilate, Mask::square(25), 50 ); bench_grayscale_operator!( bench_grayscale_op_dilate_medium_image_large_square, grayscale_dilate, Mask::square(25), 200 ); } imageproc-0.25.0/src/noise.rs000064400000000000000000000052631046102023000141360ustar 00000000000000//! Functions for adding synthetic noise to images. use crate::definitions::{Clamp, HasBlack, HasWhite, Image}; use image::Pixel; use rand::{rngs::StdRng, SeedableRng}; use rand_distr::{Distribution, Normal, Uniform}; /// Adds independent additive Gaussian noise to all channels /// of an image, with the given mean and standard deviation. pub fn gaussian_noise

(image: &Image

, mean: f64, stddev: f64, seed: u64) -> Image

where P: Pixel, P::Subpixel: Into + Clamp, { let mut out = image.clone(); gaussian_noise_mut(&mut out, mean, stddev, seed); out } /// Adds independent additive Gaussian noise to all channels /// of an image in place, with the given mean and standard deviation. pub fn gaussian_noise_mut

(image: &mut Image

, mean: f64, stddev: f64, seed: u64) where P: Pixel, P::Subpixel: Into + Clamp, { let mut rng: StdRng = SeedableRng::seed_from_u64(seed); let normal = Normal::new(mean, stddev).unwrap(); for p in image.pixels_mut() { for c in p.channels_mut() { let noise = normal.sample(&mut rng); *c = P::Subpixel::clamp((*c).into() + noise); } } } /// Converts pixels to black or white at the given `rate` (between 0.0 and 1.0). /// Black and white occur with equal probability. pub fn salt_and_pepper_noise

(image: &Image

, rate: f64, seed: u64) -> Image

where P: Pixel + HasBlack + HasWhite, { let mut out = image.clone(); salt_and_pepper_noise_mut(&mut out, rate, seed); out } /// Converts pixels to black or white in place at the given `rate` (between 0.0 and 1.0). /// Black and white occur with equal probability. pub fn salt_and_pepper_noise_mut

(image: &mut Image

, rate: f64, seed: u64) where P: Pixel + HasBlack + HasWhite, { let mut rng: StdRng = SeedableRng::seed_from_u64(seed); let uniform = Uniform::new(0.0, 1.0); for p in image.pixels_mut() { if uniform.sample(&mut rng) > rate { continue; } let r = uniform.sample(&mut rng); *p = if r >= 0.5 { P::white() } else { P::black() }; } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use image::GrayImage; use test::{black_box, Bencher}; #[bench] fn bench_gaussian_noise_mut(b: &mut Bencher) { let mut image = GrayImage::new(100, 100); b.iter(|| { gaussian_noise_mut(&mut image, 30.0, 40.0, 1); }); black_box(image); } #[bench] fn bench_salt_and_pepper_noise_mut(b: &mut Bencher) { let mut image = GrayImage::new(100, 100); b.iter(|| { salt_and_pepper_noise_mut(&mut image, 0.3, 1); }); black_box(image); } } imageproc-0.25.0/src/pixelops.rs000064400000000000000000000066631046102023000146710ustar 00000000000000//! Pixel manipulations. use crate::definitions::Clamp; use image::Pixel; /// Adds pixels with the given weights. Results are clamped to prevent arithmetical overflows. /// /// # Examples /// ``` /// # extern crate image; /// # extern crate imageproc; /// # fn main() { /// use image::Rgb; /// use imageproc::pixelops::weighted_sum; /// /// let left = Rgb([10u8, 20u8, 30u8]); /// let right = Rgb([100u8, 80u8, 60u8]); /// /// let sum = weighted_sum(left, right, 0.7, 0.3); /// assert_eq!(sum, Rgb([37, 38, 39])); /// # } /// ``` pub fn weighted_sum(left: P, right: P, left_weight: f32, right_weight: f32) -> P where P::Subpixel: Into + Clamp, { left.map2(&right, |p, q| { weighted_channel_sum(p, q, left_weight, right_weight) }) } /// Equivalent to `weighted_sum(left, right, left_weight, 1 - left_weight)`. /// /// # Examples /// ``` /// # extern crate image; /// # extern crate imageproc; /// # fn main() { /// use image::Rgb; /// use imageproc::pixelops::interpolate; /// /// let left = Rgb([10u8, 20u8, 30u8]); /// let right = Rgb([100u8, 80u8, 60u8]); /// /// let sum = interpolate(left, right, 0.7); /// assert_eq!(sum, Rgb([37, 38, 39])); /// # } /// ``` pub fn interpolate(left: P, right: P, left_weight: f32) -> P where P::Subpixel: Into + Clamp, { weighted_sum(left, right, left_weight, 1.0 - left_weight) } #[inline(always)] fn weighted_channel_sum(left: C, right: C, left_weight: f32, right_weight: f32) -> C where C: Into + Clamp, { Clamp::clamp(left.into() * left_weight + right.into() * right_weight) } #[cfg(test)] mod tests { use super::*; #[test] fn test_weighted_channel_sum() { // Midpoint assert_eq!(weighted_channel_sum(10u8, 20u8, 0.5, 0.5), 15u8); // Mainly left assert_eq!(weighted_channel_sum(10u8, 20u8, 0.9, 0.1), 11u8); // Clamped assert_eq!(weighted_channel_sum(150u8, 150u8, 1.8, 0.8), 255u8); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use image::{Luma, Rgb}; use test::{black_box, Bencher}; #[bench] fn bench_weighted_sum_rgb(b: &mut Bencher) { b.iter(|| { let left = black_box(Rgb([10u8, 20u8, 33u8])); let right = black_box(Rgb([80u8, 70u8, 60u8])); let left_weight = black_box(0.3); let right_weight = black_box(0.7); black_box(weighted_sum(left, right, left_weight, right_weight)); }) } #[bench] fn bench_weighted_sum_gray(b: &mut Bencher) { b.iter(|| { let left = black_box(Luma([10u8])); let right = black_box(Luma([80u8])); let left_weight = black_box(0.3); let right_weight = black_box(0.7); black_box(weighted_sum(left, right, left_weight, right_weight)); }) } #[bench] fn bench_interpolate_rgb(b: &mut Bencher) { b.iter(|| { let left = black_box(Rgb([10u8, 20u8, 33u8])); let right = black_box(Rgb([80u8, 70u8, 60u8])); let left_weight = black_box(0.3); black_box(interpolate(left, right, left_weight)); }) } #[bench] fn bench_interpolate_gray(b: &mut Bencher) { b.iter(|| { let left = black_box(Luma([10u8])); let right = black_box(Luma([80u8])); let left_weight = black_box(0.3); black_box(interpolate(left, right, left_weight)); }) } } imageproc-0.25.0/src/point.rs000064400000000000000000000105361046102023000141510ustar 00000000000000//! A 2d point type. use num::{Num, NumCast}; use std::ops::{Add, AddAssign, Sub, SubAssign}; /// A 2d point. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Point { /// x-coordinate. pub x: T, /// y-coordinate. pub y: T, } impl Point { /// Construct a point at (x, y). pub fn new(x: T, y: T) -> Point { Point:: { x, y } } } impl Add for Point { type Output = Self; fn add(self, other: Point) -> Point { Point::new(self.x + other.x, self.y + other.y) } } impl AddAssign for Point { fn add_assign(&mut self, rhs: Self) { self.x = self.x + rhs.x; self.y = self.y + rhs.y; } } impl Sub for Point { type Output = Self; fn sub(self, other: Point) -> Point { Point::new(self.x - other.x, self.y - other.y) } } impl SubAssign for Point { fn sub_assign(&mut self, rhs: Self) { self.x = self.x - rhs.x; self.y = self.y - rhs.y; } } impl Point { /// Converts to a Point. Panics if the cast fails. pub(crate) fn to_f64(&self) -> Point { Point::new(self.x.to_f64().unwrap(), self.y.to_f64().unwrap()) } /// Converts to a Point. Panics if the cast fails. pub(crate) fn to_i32(&self) -> Point { Point::new(self.x.to_i32().unwrap(), self.y.to_i32().unwrap()) } /// Converts to a Point. Panics if the cast fails. pub(crate) fn to_i16(&self) -> Point { Point::new(self.x.to_i16().unwrap(), self.y.to_i16().unwrap()) } } /// Returns the Euclidean distance between two points. pub(crate) fn distance(p: Point, q: Point) -> f64 { distance_sq(p, q).sqrt() } /// Returns the square of the Euclidean distance between two points. pub(crate) fn distance_sq(p: Point, q: Point) -> f64 { let p = p.to_f64(); let q = q.to_f64(); (p.x - q.x).powf(2.0) + (p.y - q.y).powf(2.0) } /// A fixed rotation. This struct exists solely to cache the values of `sin(theta)` and `cos(theta)` when /// applying a fixed rotation to multiple points. #[derive(Debug, Copy, Clone, PartialEq)] pub(crate) struct Rotation { sin_theta: f64, cos_theta: f64, } impl Rotation { /// A rotation of `theta` radians. pub(crate) fn new(theta: f64) -> Rotation { let (sin_theta, cos_theta) = theta.sin_cos(); Rotation { sin_theta, cos_theta, } } } impl Point { /// Rotates a point. pub(crate) fn rotate(&self, rotation: Rotation) -> Point { let x = self.x * rotation.cos_theta + self.y * rotation.sin_theta; let y = self.y * rotation.cos_theta - self.x * rotation.sin_theta; Point::new(x, y) } /// Inverts a rotation. pub(crate) fn invert_rotation(&self, rotation: Rotation) -> Point { let x = self.x * rotation.cos_theta - self.y * rotation.sin_theta; let y = self.y * rotation.cos_theta + self.x * rotation.sin_theta; Point::new(x, y) } } /// A line of the form Ax + By + C = 0. #[derive(Debug, Copy, Clone, PartialEq)] pub(crate) struct Line { a: f64, b: f64, c: f64, } impl Line { /// Returns the `Line` that passes through p and q. pub fn from_points(p: Point, q: Point) -> Line { let a = p.y - q.y; let b = q.x - p.x; let c = p.x * q.y - q.x * p.y; Line { a, b, c } } /// Computes the shortest distance from this line to the given point. pub fn distance_from_point(&self, point: Point) -> f64 { let Line { a, b, c } = self; (a * point.x + b * point.y + c).abs() / (a.powf(2.0) + b.powf(2.)).sqrt() } } #[cfg(test)] mod tests { use super::*; #[test] fn line_from_points() { let p = Point::new(5.0, 7.0); let q = Point::new(10.0, 3.0); assert_eq!( Line::from_points(p, q), Line { a: 4.0, b: 5.0, c: -55.0 } ); } #[test] fn distance_between_line_and_point() { assert_approx_eq!( Line { a: 8.0, b: 7.0, c: 5.0 } .distance_from_point(Point::new(2.0, 3.0)), 3.9510276472, 1e-10 ); } } imageproc-0.25.0/src/property_testing.rs000064400000000000000000000066341046102023000164450ustar 00000000000000//! Utilities to help with writing property-based tests //! (e.g. [quickcheck] tests) for image processing functions. //! //! [quickcheck]: https://github.com/BurntSushi/quickcheck use crate::definitions::Image; use image::{GenericImage, ImageBuffer, Luma, Pixel, Primitive, Rgb}; use quickcheck::{Arbitrary, Gen}; use rand_distr::{Distribution, Standard}; use std::fmt; /// Wrapper for image buffers to allow us to write an Arbitrary instance. #[derive(Clone)] pub struct TestBuffer(pub Image); /// 8bpp grayscale `TestBuffer`. pub type GrayTestImage = TestBuffer>; /// 24bpp RGB `TestBuffer`. pub type RgbTestImage = TestBuffer>; impl Arbitrary for TestBuffer where ::Subpixel: Send, { fn arbitrary(g: &mut Gen) -> Self { let (width, height) = small_image_dimensions(g); let mut image = ImageBuffer::new(width, height); for y in 0..height { for x in 0..width { let pix: T = ArbitraryPixel::arbitrary(g); image.put_pixel(x, y, pix); } } TestBuffer(image) } fn shrink(&self) -> Box>> { Box::new(shrink(&self.0).map(TestBuffer)) } } /// Workaround for not being able to define Arbitrary instances for pixel types /// defines in other modules. pub trait ArbitraryPixel { /// Generate an arbitrary instance of this pixel type. fn arbitrary(g: &mut Gen) -> Self; } fn shrink(image: &I) -> Box>> where I: GenericImage, I::Pixel: 'static, { let mut subs = vec![]; let w = image.width(); let h = image.height(); if w > 0 { let left = copy_sub(image, 0, 0, w - 1, h); subs.push(left); let right = copy_sub(image, 1, 0, w - 1, h); subs.push(right); } if h > 0 { let top = copy_sub(image, 0, 0, w, h - 1); subs.push(top); let bottom = copy_sub(image, 0, 1, w, h - 1); subs.push(bottom); } Box::new(subs.into_iter()) } fn copy_sub(image: &I, x: u32, y: u32, width: u32, height: u32) -> Image where I: GenericImage, { let mut out = ImageBuffer::new(width, height); for dy in 0..height { let oy = y + dy; for dx in 0..width { let ox = x + dx; out.put_pixel(dx, dy, image.get_pixel(ox, oy)); } } out } impl fmt::Debug for TestBuffer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "width: {}, height: {}, data: {:?}", self.0.width(), self.0.height(), self.0.enumerate_pixels().collect::>() ) } } fn small_image_dimensions(g: &mut Gen) -> (u32, u32) { let dims: (u8, u8) = Arbitrary::arbitrary(g); ((dims.0 % 10) as u32, (dims.1 % 10) as u32) } impl ArbitraryPixel for Rgb where Standard: Distribution, { fn arbitrary(g: &mut Gen) -> Self { let red: T = T::arbitrary(g); let green: T = T::arbitrary(g); let blue: T = T::arbitrary(g); Rgb([red, green, blue]) } } impl ArbitraryPixel for Luma where Standard: Distribution, { fn arbitrary(g: &mut Gen) -> Self { let val: T = T::arbitrary(g); Luma([val]) } } imageproc-0.25.0/src/proptest_utils.rs000064400000000000000000000064761046102023000161300ustar 00000000000000use crate::definitions::Image; use image::Pixel; use proptest::{ arbitrary::{any, Arbitrary}, sample::SizeRange, strategy::{BoxedStrategy, Strategy}, }; use std::{fmt, ops::RangeInclusive}; /// Create a strategy to generate arbitrary images with dimensions selected /// within the specified ranges. pub(crate) fn arbitrary_image

( width_range: impl Into, height_range: impl Into, ) -> BoxedStrategy> where P: Pixel + fmt::Debug, P::Subpixel: Arbitrary + fmt::Debug, ::Strategy: Clone + 'static, { arbitrary_image_with(any::(), width_range, height_range) } /// Create a strategy to generate images with a given subpixel strategy and /// dimensions selected within the specified ranges. pub(crate) fn arbitrary_image_with

( subpixels: impl Strategy + Clone + 'static, width_range: impl Into, height_range: impl Into, ) -> BoxedStrategy> where P: Pixel + fmt::Debug, P::Subpixel: fmt::Debug, { dims(width_range, height_range) .prop_flat_map(move |(w, h)| fixed_image_with(subpixels.clone(), w, h)) .boxed() } fn fixed_image_with

( strategy: impl Strategy + 'static, width: u32, height: u32, ) -> BoxedStrategy> where P: Pixel + fmt::Debug, P::Subpixel: fmt::Debug, { let size = (width * height * P::CHANNEL_COUNT as u32) as usize; let vecs = proptest::collection::vec(strategy, size); vecs.prop_map(move |v| Image::from_vec(width, height, v).unwrap()) .boxed() } fn dims(width: impl Into, height: impl Into) -> BoxedStrategy<(u32, u32)> { let width = to_range(width); let height = to_range(height); width .prop_flat_map(move |w| height.clone().prop_map(move |h| (w, h))) .boxed() } fn to_range(range: impl Into) -> RangeInclusive { let range = range.into(); range.start() as u32..=range.end_incl() as u32 } #[cfg(test)] mod tests { use super::*; #[test] fn test_to_range() { macro_rules! to_range { ($range:expr) => { to_range($range).collect::>() }; } macro_rules! to_vec { ($range:expr) => { ($range).map(|x| x as u32).collect::>() }; } assert_eq!(to_range!(0), [0]); assert_eq!(to_range!(1), [1]); assert_eq!(to_range!(..2), to_vec!(0..2)); assert_eq!(to_range!(..=2), to_vec!(0..=2)); assert_eq!(to_range!(2..4), to_vec!(2..4)); assert_eq!(to_range!(2..=4), to_vec!(2..=4)); assert_eq!(to_range!(2..2), to_vec!(2..2)); assert_eq!(to_range!(2..=2), to_vec!(2..=2)); } } #[cfg(not(miri))] #[cfg(test)] mod proptests { use super::*; use image::{Luma, Rgb}; use proptest::prelude::*; proptest! { #[test] fn test_arbitrary_fixed_rgb(img in arbitrary_image::>(3, 7)) { assert_eq!(img.width(), 3); assert_eq!(img.height(), 7); } #[test] fn test_arbitrary_gray(img in arbitrary_image::>(1..30, 2..=150)) { assert!((1..30).contains(&img.width())); assert!((2..=150).contains(&img.height())); } } } imageproc-0.25.0/src/rect.rs000064400000000000000000000123621046102023000137540ustar 00000000000000//! Basic manipulation of rectangles. use std::cmp; /// A rectangular region of non-zero width and height. /// # Examples /// ``` /// use imageproc::rect::Rect; /// use imageproc::rect::Region; /// /// // Construct a rectangle with top-left corner at (4, 5), width 6 and height 7. /// let rect = Rect::at(4, 5).of_size(6, 7); /// /// // Contains top-left point: /// assert_eq!(rect.left(), 4); /// assert_eq!(rect.top(), 5); /// assert!(rect.contains(rect.left(), rect.top())); /// /// // Contains bottom-right point, at (left + width - 1, top + height - 1): /// assert_eq!(rect.right(), 9); /// assert_eq!(rect.bottom(), 11); /// assert!(rect.contains(rect.right(), rect.bottom())); /// ``` #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Rect { left: i32, top: i32, width: u32, height: u32, } /// A geometrical representation of a set of 2D points with coordinate type T. pub trait Region { /// Whether this region contains the given point. fn contains(&self, x: T, y: T) -> bool; } impl Rect { /// Reduces possibility of confusing coordinates and dimensions /// when specifying rects. /// /// See the [struct-level documentation](struct.Rect.html) for examples. pub fn at(x: i32, y: i32) -> RectPosition { RectPosition { left: x, top: y } } /// Smallest y-coordinate reached by rect. /// /// See the [struct-level documentation](struct.Rect.html) for examples. pub fn top(&self) -> i32 { self.top } /// Smallest x-coordinate reached by rect. /// /// See the [struct-level documentation](struct.Rect.html) for examples. pub fn left(&self) -> i32 { self.left } /// Greatest y-coordinate reached by rect. /// /// See the [struct-level documentation](struct.Rect.html) for examples. pub fn bottom(&self) -> i32 { self.top + (self.height as i32) - 1 } /// Greatest x-coordinate reached by rect. /// /// See the [struct-level documentation](struct.Rect.html) for examples. pub fn right(&self) -> i32 { self.left + (self.width as i32) - 1 } /// Width of rect. pub fn width(&self) -> u32 { self.width } /// Height of rect. pub fn height(&self) -> u32 { self.height } /// Returns the intersection of self and other, or none if they are are disjoint. /// /// # Examples /// ``` /// use imageproc::rect::Rect; /// use imageproc::rect::Region; /// /// // Intersecting a rectangle with itself /// let r = Rect::at(4, 5).of_size(6, 7); /// assert_eq!(r.intersect(r), Some(r)); /// /// // Intersecting overlapping but non-equal rectangles /// let r = Rect::at(0, 0).of_size(5, 5); /// let s = Rect::at(1, 4).of_size(10, 12); /// let i = Rect::at(1, 4).of_size(4, 1); /// assert_eq!(r.intersect(s), Some(i)); /// /// // Intersecting disjoint rectangles /// let r = Rect::at(0, 0).of_size(5, 5); /// let s = Rect::at(10, 10).of_size(100, 12); /// assert_eq!(r.intersect(s), None); /// ``` pub fn intersect(&self, other: Rect) -> Option { let left = cmp::max(self.left, other.left); let top = cmp::max(self.top, other.top); let right = cmp::min(self.right(), other.right()); let bottom = cmp::min(self.bottom(), other.bottom()); if right < left || bottom < top { return None; } Some(Rect { left, top, width: (right - left) as u32 + 1, height: (bottom - top) as u32 + 1, }) } } impl Region for Rect { fn contains(&self, x: i32, y: i32) -> bool { self.left <= x && x <= self.right() && self.top <= y && y <= self.bottom() } } impl Region for Rect { fn contains(&self, x: f32, y: f32) -> bool { self.left as f32 <= x && x <= self.right() as f32 && self.top as f32 <= y && y <= self.bottom() as f32 } } /// Position of the top left of a rectangle. /// Only used when building a [`Rect`](struct.Rect.html). #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct RectPosition { left: i32, top: i32, } impl RectPosition { /// Construct a rectangle from a position and size. Width and height /// are required to be strictly positive. /// /// See the [`Rect`](struct.Rect.html) documentation for examples. pub fn of_size(self, width: u32, height: u32) -> Rect { assert!(width > 0, "width must be strictly positive"); assert!(height > 0, "height must be strictly positive"); Rect { left: self.left, top: self.top, width, height, } } } #[cfg(test)] mod tests { use super::{Rect, Region}; #[test] #[should_panic] fn test_rejects_empty_rectangle() { Rect::at(1, 2).of_size(0, 1); } #[test] fn test_contains_i32() { let r = Rect::at(5, 5).of_size(6, 6); assert!(r.contains(5, 5)); assert!(r.contains(10, 10)); assert!(!r.contains(10, 11)); assert!(!r.contains(11, 10)); } #[test] fn test_contains_f32() { let r = Rect::at(5, 5).of_size(6, 6); assert!(r.contains(5f32, 5f32)); assert!(!r.contains(10.1f32, 10f32)); } } imageproc-0.25.0/src/region_labelling.rs000064400000000000000000000246501046102023000163160ustar 00000000000000//! Functions for finding and labelling connected components of an image. use std::cmp; use image::{GenericImage, GenericImageView, ImageBuffer, Luma}; use crate::definitions::Image; use crate::union_find::DisjointSetForest; /// Determines which neighbors of a pixel we consider /// to be connected to it. #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum Connectivity { /// A pixel is connected to its N, S, E and W neighbors. Four, /// A pixel is connected to all of its neighbors. Eight, } /// Returns an image of the same size as the input, where each pixel /// is labelled by the connected foreground component it belongs to, /// or 0 if it's in the background. Input pixels are treated as belonging /// to the background if and only if they are equal to the provided background pixel. /// /// # Panics /// Panics if the image contains 232 or more pixels. If this limitation causes you /// problems then open an issue and we can rewrite this function to support larger images. /// /// # Examples /// /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::Luma; /// use imageproc::region_labelling::{connected_components, Connectivity}; /// /// let background_color = Luma([0u8]); /// /// let image = gray_image!( /// 1, 0, 1, 1; /// 0, 1, 1, 0; /// 0, 0, 0, 0; /// 0, 0, 0, 1); /// /// // With four-way connectivity the foreground regions which /// // are only connected across diagonals belong to different /// // connected components. /// let components_four = gray_image!(type: u32, /// 1, 0, 2, 2; /// 0, 2, 2, 0; /// 0, 0, 0, 0; /// 0, 0, 0, 3); /// /// assert_pixels_eq!( /// connected_components(&image, Connectivity::Four, background_color), /// components_four); /// /// // With eight-way connectivity all foreground pixels in the top two rows /// // belong to the same connected component. /// let components_eight = gray_image!(type: u32, /// 1, 0, 1, 1; /// 0, 1, 1, 0; /// 0, 0, 0, 0; /// 0, 0, 0, 2); /// /// assert_pixels_eq!( /// connected_components(&image, Connectivity::Eight, background_color), /// components_eight); /// # } /// ``` /// /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// // This example is like the first, except that not all of the input foreground /// // pixels are the same color. Pixels of different color are never counted /// // as belonging to the same connected component. /// /// use image::Luma; /// use imageproc::region_labelling::{connected_components, Connectivity}; /// /// let background_color = Luma([0u8]); /// /// let image = gray_image!( /// 1, 0, 1, 1; /// 0, 1, 2, 0; /// 0, 0, 0, 0; /// 0, 0, 0, 1); /// /// let components_four = gray_image!(type: u32, /// 1, 0, 2, 2; /// 0, 3, 4, 0; /// 0, 0, 0, 0; /// 0, 0, 0, 5); /// /// assert_pixels_eq!( /// connected_components(&image, Connectivity::Four, background_color), /// components_four); /// /// // If this behaviour is not what you want then you can first /// // threshold the input image. /// use imageproc::contrast::{threshold, ThresholdType}; /// /// // Pixels equal to the threshold are treated as background. /// let thresholded = threshold(&image, 0,ThresholdType::Binary); /// /// let thresholded_components_four = gray_image!(type: u32, /// 1, 0, 2, 2; /// 0, 2, 2, 0; /// 0, 0, 0, 0; /// 0, 0, 0, 3); /// /// assert_pixels_eq!( /// connected_components(&thresholded, Connectivity::Four, background_color), /// thresholded_components_four); /// # } /// ``` pub fn connected_components( image: &I, conn: Connectivity, background: I::Pixel, ) -> Image> where I: GenericImage, I::Pixel: Eq, { let (width, height) = image.dimensions(); let image_size = width as usize * height as usize; if image_size >= 2usize.saturating_pow(32) { panic!("Images with 2^32 or more pixels are not supported"); } let mut out = ImageBuffer::new(width, height); // TODO: add macro to abandon early if either dimension is zero if width == 0 || height == 0 { return out; } let mut forest = DisjointSetForest::new(image_size); let mut adj_labels = [0u32; 4]; let mut next_label = 1; for y in 0..height { for x in 0..width { let current = unsafe { image.unsafe_get_pixel(x, y) }; if current == background { continue; } let mut num_adj = 0; if x > 0 { // West let pixel = unsafe { image.unsafe_get_pixel(x - 1, y) }; if pixel == current { let label = unsafe { out.unsafe_get_pixel(x - 1, y)[0] }; adj_labels[num_adj] = label; num_adj += 1; } } if y > 0 { // North let pixel = unsafe { image.unsafe_get_pixel(x, y - 1) }; if pixel == current { let label = unsafe { out.unsafe_get_pixel(x, y - 1)[0] }; adj_labels[num_adj] = label; num_adj += 1; } if conn == Connectivity::Eight { if x > 0 { // North West let pixel = unsafe { image.unsafe_get_pixel(x - 1, y - 1) }; if pixel == current { let label = unsafe { out.unsafe_get_pixel(x - 1, y - 1)[0] }; adj_labels[num_adj] = label; num_adj += 1; } } if x < width - 1 { // North East let pixel = unsafe { image.unsafe_get_pixel(x + 1, y - 1) }; if pixel == current { let label = unsafe { out.unsafe_get_pixel(x + 1, y - 1)[0] }; adj_labels[num_adj] = label; num_adj += 1; } } } } if num_adj == 0 { unsafe { out.unsafe_put_pixel(x, y, Luma([next_label])); } next_label += 1; } else { let mut min_label = u32::MAX; for n in 0..num_adj { min_label = cmp::min(min_label, adj_labels[n]); } unsafe { out.unsafe_put_pixel(x, y, Luma([min_label])); } for n in 0..num_adj { forest.union(min_label as usize, adj_labels[n] as usize); } } } } // Make components start at 1 let mut output_labels = vec![0u32; image_size]; let mut count = 1; unsafe { for y in 0..height { for x in 0..width { let label = { if image.unsafe_get_pixel(x, y) == background { continue; } out.unsafe_get_pixel(x, y)[0] }; let root = forest.root(label as usize); let mut output_label = *output_labels.get_unchecked(root); if output_label < 1 { output_label = count; count += 1; } *output_labels.get_unchecked_mut(root) = output_label; out.unsafe_put_pixel(x, y, Luma([output_label])); } } } out } #[cfg(test)] mod tests { extern crate wasm_bindgen_test; use image::{GrayImage, ImageBuffer, Luma}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; use crate::definitions::{HasBlack, HasWhite}; use super::connected_components; use super::Connectivity::{Eight, Four}; #[cfg_attr(not(target_arch = "wasm32"), test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn test_connected_components_eight_white_background() { let image = gray_image!( 1, 255, 2, 1; 255, 1, 1, 255; 255, 255, 255, 255; 255, 255, 255, 1); let expected = gray_image!(type: u32, 1, 0, 2, 1; 0, 1, 1, 0; 0, 0, 0, 0; 0, 0, 0, 3); let labelled = connected_components(&image, Eight, Luma::white()); assert_pixels_eq!(labelled, expected); } // One huge component with eight-way connectivity, loads of // isolated components with four-way connectivity. pub(super) fn chessboard(width: u32, height: u32) -> GrayImage { ImageBuffer::from_fn(width, height, |x, y| { if (x + y) % 2 == 0 { Luma([255u8]) } else { Luma([0u8]) } }) } #[cfg_attr(not(target_arch = "wasm32"), test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn test_connected_components_eight_chessboard() { let image = chessboard(30, 30); let components = connected_components(&image, Eight, Luma::black()); let max_component = components.pixels().map(|p| p[0]).max(); assert_eq!(max_component, Some(1u32)); } #[cfg_attr(not(target_arch = "wasm32"), test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn test_connected_components_four_chessboard() { let image = chessboard(30, 30); let components = connected_components(&image, Four, Luma::black()); let max_component = components.pixels().map(|p| p[0]).max(); assert_eq!(max_component, Some(450u32)); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::connected_components; use super::tests::chessboard; use super::Connectivity::{Eight, Four}; use crate::definitions::HasBlack; use ::test; use image::Luma; #[bench] fn bench_connected_components_eight_chessboard(b: &mut test::Bencher) { let image = chessboard(300, 300); b.iter(|| { let components = connected_components(&image, Eight, Luma::black()); test::black_box(components); }); } #[bench] fn bench_connected_components_four_chessboard(b: &mut test::Bencher) { let image = chessboard(300, 300); b.iter(|| { let components = connected_components(&image, Four, Luma::black()); test::black_box(components); }); } } imageproc-0.25.0/src/seam_carving.rs000064400000000000000000000151021046102023000154500ustar 00000000000000//! An implementation of [seam carving]. Currently in a pretty rough state. //! See examples/seam_carving.rs for an example. //! //! [seam carving]: https://en.wikipedia.org/wiki/Seam_carving use crate::definitions::{HasBlack, Image}; use crate::gradients::sobel_gradient_map; use crate::map::{map_colors, WithChannel}; use image::{GrayImage, Luma, Pixel, Rgb}; use std::cmp::min; /// An image seam connecting the bottom of an image to its top (in that order). pub struct VerticalSeam(Vec); /// Reduces the width of an image using seam carving. /// /// Warning: this is very slow! It implements the algorithm from /// , with some /// extra unnecessary allocations thrown in. Rather than attempting to optimise the implementation /// of this inherently slow algorithm, the planned next step is to switch to the algorithm from /// . pub fn shrink_width

(image: &Image

, target_width: u32) -> Image

// TODO: this is pretty silly! We should just be able to express that we want a pixel which is a slice of integral values where P: Pixel + WithChannel + WithChannel,

>::Pixel: HasBlack, { assert!( target_width <= image.width(), "target_width must be <= input image width" ); let iterations = image.width() - target_width; let mut result = image.clone(); for _ in 0..iterations { let seam = find_vertical_seam(&result); result = remove_vertical_seam(&result, &seam); } result } /// Computes an 8-connected path from the bottom of the image to the top whose sum of /// gradient magnitudes is minimal. pub fn find_vertical_seam

(image: &Image

) -> VerticalSeam where P: Pixel + WithChannel + WithChannel,

>::Pixel: HasBlack, { let (width, height) = image.dimensions(); assert!( image.width() >= 2, "Cannot find seams if image width is < 2" ); let mut gradients = sobel_gradient_map(image, |p| { let gradient_sum: u16 = p.channels().iter().sum(); let gradient_mean: u16 = gradient_sum / P::CHANNEL_COUNT as u16; Luma([gradient_mean as u32]) }); // Find the least energy path through the gradient image. for y in 1..height { for x in 0..width { set_path_energy(&mut gradients, x, y); } } // Retrace our steps to find the vertical seam. let mut min_x = 0; let mut min_energy = gradients.get_pixel(0, height - 1)[0]; for x in 1..width { let c = gradients.get_pixel(x, height - 1)[0]; if c < min_energy { min_x = x; min_energy = c; } } let mut seam = Vec::with_capacity(height as usize); seam.push(min_x); let mut last_x = min_x; for y in (1..height).rev() { let above = gradients.get_pixel(last_x, y - 1)[0]; if last_x > 0 { let left = gradients.get_pixel(last_x - 1, y - 1)[0]; if left < above { min_x = last_x - 1; min_energy = left; } } if last_x < width - 1 { let right = gradients.get_pixel(last_x + 1, y - 1)[0]; if right < min_energy { min_x = last_x + 1; min_energy = right; } } last_x = min_x; seam.push(min_x); } VerticalSeam(seam) } /// Assumes that the previous rows have all been processed. fn set_path_energy(path_energies: &mut Image>, x: u32, y: u32) { let above = path_energies.get_pixel(x, y - 1)[0]; let mut min_energy = above; if x > 0 { let above_left = path_energies.get_pixel(x - 1, y - 1)[0]; min_energy = min(above, above_left); } if x < path_energies.width() - 1 { let above_right = path_energies.get_pixel(x + 1, y - 1)[0]; min_energy = min(min_energy, above_right); } let current = path_energies.get_pixel(x, y)[0]; path_energies.put_pixel(x, y, Luma([min_energy + current])); } /// Returns the result of removing `seam` from `image`. // This should just mutate an image in place. The problem is that we don't have a // way of talking about views of ImageBuffer without devolving into supporting // arbitrary GenericImages. And a lot of other functions don't support those because // it would make them a lot slower. pub fn remove_vertical_seam

(image: &Image

, seam: &VerticalSeam) -> Image

where P: Pixel, { assert!( seam.0.len() as u32 == image.height(), "seam length does not match image height" ); let (width, height) = image.dimensions(); let mut out = Image::new(width - 1, height); for y in 0..height { let x_seam = seam.0[(height - y - 1) as usize]; for x in 0..x_seam { out.put_pixel(x, y, *image.get_pixel(x, y)); } for x in (x_seam + 1)..width { out.put_pixel(x - 1, y, *image.get_pixel(x, y)); } } out } /// Draws a series of `seams` on `image` in red. Assumes that the provided seams were /// removed in the given order from the input image. pub fn draw_vertical_seams(image: &GrayImage, seams: &[VerticalSeam]) -> Image> { let height = image.height(); let mut offsets = vec![vec![]; height as usize]; let mut out = map_colors(image, |p| p.to_rgb()); for seam in seams { for (y, x) in (0..height).rev().zip(&seam.0) { let mut x_original = *x; for o in &offsets[y as usize] { if *o < *x { x_original += 1; } } out.put_pixel(x_original, y, Rgb([255, 0, 0])); offsets[y as usize].push(x_original); } } out } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::utils::gray_bench_image; use test::{black_box, Bencher}; macro_rules! bench_shrink_width { ($name:ident, side: $s:expr, shrink_by: $m:expr) => { #[bench] fn $name(b: &mut Bencher) { let image = gray_bench_image($s, $s); b.iter(|| { let filtered = shrink_width(&image, $s - $m); black_box(filtered); }) } }; } bench_shrink_width!(bench_shrink_width_s100_r1, side: 100, shrink_by: 1); bench_shrink_width!(bench_shrink_width_s100_r4, side: 100, shrink_by: 4); bench_shrink_width!(bench_shrink_width_s100_r8, side: 100, shrink_by: 8); } imageproc-0.25.0/src/stats.rs000064400000000000000000000243251046102023000141570ustar 00000000000000//! Statistical properties of images. use crate::definitions::Image; use image::{GenericImageView, GrayImage, Pixel, Primitive}; use num::Bounded; /// A minimum and maximum value returned by [`min_max()`] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MinMax { /// The minimum value pub min: T, /// The maximum value pub max: T, } /// Returns the minimum and maximum values per channel in an image. pub fn min_max(image: &Image

) -> Vec> where P: Pixel, T: Ord + Copy, { if image.is_empty() { panic!("cannot find the range of an empty image"); } let mut ranges = vec![(None, None); P::CHANNEL_COUNT as usize]; for pix in image.pixels() { for (i, c) in pix.channels().iter().enumerate() { let (current_min, current_max) = &mut ranges[i]; if current_min.map_or(true, |x| c < x) { *current_min = Some(c); } if current_max.map_or(true, |x| c > x) { *current_max = Some(c); } } } ranges .into_iter() .map(|(min, max)| MinMax { min: *min.unwrap(), max: *max.unwrap(), }) .collect() } /// A set of per-channel histograms from an image with 8 bits per channel. pub struct ChannelHistogram { /// Per-channel histograms. pub channels: Vec<[u32; 256]>, } /// Returns a vector of per-channel histograms. pub fn histogram

(image: &Image

) -> ChannelHistogram where P: Pixel, { let mut hist = vec![[0u32; 256]; P::CHANNEL_COUNT as usize]; for pix in image.pixels() { for (i, c) in pix.channels().iter().enumerate() { hist[i][*c as usize] += 1; } } ChannelHistogram { channels: hist } } /// A set of per-channel cumulative histograms from an image with 8 bits per channel. pub struct CumulativeChannelHistogram { /// Per-channel cumulative histograms. pub channels: Vec<[u32; 256]>, } /// Returns per-channel cumulative histograms. pub fn cumulative_histogram

(image: &Image

) -> CumulativeChannelHistogram where P: Pixel, { let mut hist = histogram(image); for c in 0..hist.channels.len() { for i in 1..hist.channels[c].len() { hist.channels[c][i] += hist.channels[c][i - 1]; } } CumulativeChannelHistogram { channels: hist.channels, } } /// Returns the `p`th percentile of the pixel intensities in an image. /// /// We define the `p`th percentile intensity to be the least `x` such /// that at least `p`% of image pixels have intensity less than or /// equal to `x`. /// /// # Panics /// If `p > 100`. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use imageproc::stats::percentile; /// /// let image = gray_image!( /// 1, 2, 3, 4, 5; /// 6, 7, 8, 9, 10); /// /// // The 0th percentile is always 0 /// assert_eq!(percentile(&image, 0), 0); /// /// // Exactly 10% of pixels have intensity <= 1. /// assert_eq!(percentile(&image, 10), 1); /// /// // Fewer than 15% of pixels have intensity <=1, so the 15th percentile is 2. /// assert_eq!(percentile(&image, 15), 2); /// /// // All pixels have intensity <= 10. /// assert_eq!(percentile(&image, 100), 10); /// # } /// ``` pub fn percentile(image: &GrayImage, p: u8) -> u8 { assert!(p <= 100, "requested percentile must be <= 100"); let cum_hist = cumulative_histogram(image).channels[0]; let total = cum_hist[255] as u64; for i in 0..256 { if 100 * cum_hist[i] as u64 / total >= p as u64 { return i as u8; } } unreachable!(); } /// Returns the square root of the mean of the squares of differences /// between all subpixels in left and right. All channels are considered /// equally. If you do not want this (e.g. if using RGBA) then change /// image formats first. pub fn root_mean_squared_error(left: &I, right: &J) -> f64 where I: GenericImageView, J: GenericImageView, P: Pixel, P::Subpixel: Into, { mean_squared_error(left, right).sqrt() } /// Returns the peak signal to noise ratio for a clean image and its noisy /// approximation. All channels are considered equally. If you do not want this /// (e.g. if using RGBA) then change image formats first. /// See also [peak signal-to-noise ratio (wikipedia)](https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio). pub fn peak_signal_to_noise_ratio(original: &I, noisy: &J) -> f64 where I: GenericImageView, J: GenericImageView, P: Pixel, P::Subpixel: Into + Primitive, { let max: f64 = ::max_value().into(); let mse = mean_squared_error(original, noisy); 20f64 * max.log(10f64) - 10f64 * mse.log(10f64) } fn mean_squared_error(left: &I, right: &J) -> f64 where I: GenericImageView, J: GenericImageView, P: Pixel, P::Subpixel: Into, { assert_dimensions_match!(left, right); let mut sum_squared_diffs = 0f64; for (p, q) in left.pixels().zip(right.pixels()) { for (c, d) in p.2.channels().iter().zip(q.2.channels().iter()) { let fc: f64 = (*c).into(); let fd: f64 = (*d).into(); let diff = fc - fd; sum_squared_diffs += diff * diff; } } let count = (left.width() * left.height() * P::CHANNEL_COUNT as u32) as f64; sum_squared_diffs / count } #[cfg(test)] mod tests { use super::*; #[test] fn test_range() { let image = rgb_image!( [1u8, 10u8, 0u8], [2u8, 20u8, 3u8], [3u8, 30u8, 255u8], [2u8, 20u8, 7u8], [1u8, 10u8, 8u8] ); assert_eq!( min_max(&image), vec![ MinMax { min: 1, max: 3 }, MinMax { min: 10, max: 30 }, MinMax { min: 0, max: 255 } ] ) } #[test] fn test_cumulative_histogram() { let image = gray_image!(1u8, 2u8, 3u8, 2u8, 1u8); let hist = cumulative_histogram(&image).channels[0]; assert_eq!(hist[0..4], [0, 2, 4, 5]); assert!(hist.iter().skip(4).all(|x| *x == 5)); } #[test] fn test_cumulative_histogram_rgb() { let image = rgb_image!( [1u8, 10u8, 1u8], [2u8, 20u8, 2u8], [3u8, 30u8, 3u8], [2u8, 20u8, 2u8], [1u8, 10u8, 1u8] ); let hist = cumulative_histogram(&image); let r = hist.channels[0]; let g = hist.channels[1]; let b = hist.channels[2]; assert_eq!(r[0..4], [0, 2, 4, 5]); assert!(r.iter().skip(4).all(|x| *x == 5)); assert_eq!(g[0..10], [0; 10]); assert_eq!(g[10..20], [2; 10]); assert_eq!(g[20..30], [4; 10]); assert_eq!(g[30], 5); assert!(b.iter().skip(30).all(|x| *x == 5)); assert_eq!(b[0..4], [0, 2, 4, 5]); assert!(b.iter().skip(4).all(|x| *x == 5)); } #[test] fn test_histogram() { let image = gray_image!(1u8, 2u8, 3u8, 2u8, 1u8); let hist = histogram(&image).channels[0]; assert_eq!(hist[0..4], [0, 2, 2, 1]); } #[test] fn test_histogram_rgb() { let image = rgb_image!( [1u8, 10u8, 1u8], [2u8, 20u8, 2u8], [3u8, 30u8, 3u8], [2u8, 20u8, 2u8], [1u8, 10u8, 1u8] ); let hist = histogram(&image); let r = hist.channels[0]; let g = hist.channels[1]; let b = hist.channels[2]; assert_eq!(r[0..4], [0, 2, 2, 1]); assert!(r.iter().skip(4).all(|x| *x == 0)); assert_eq!(g[0..10], [0; 10]); assert_eq!(g[10], 2); assert_eq!(g[11..20], [0; 9]); assert_eq!(g[20], 2); assert_eq!(g[21..30], [0; 9]); assert_eq!(g[30], 1); assert!(b.iter().skip(30).all(|x| *x == 0)); assert_eq!(b[0..4], [0, 2, 2, 1]); assert!(b.iter().skip(4).all(|x| *x == 0)); } #[test] fn test_root_mean_squared_error_grayscale() { let left = gray_image!( 1, 2, 3; 4, 5, 6); let right = gray_image!( 8, 4, 7; 6, 9, 1); let rms = root_mean_squared_error(&left, &right); let expected = (114f64 / 6f64).sqrt(); assert_eq!(rms, expected); } #[test] fn test_root_mean_squared_error_rgb() { let left = rgb_image!([1, 2, 3], [4, 5, 6]); let right = rgb_image!([8, 4, 7], [6, 9, 1]); let rms = root_mean_squared_error(&left, &right); let expected = (114f64 / 6f64).sqrt(); assert_eq!(rms, expected); } #[test] #[should_panic] fn test_root_mean_squares_rejects_mismatched_dimensions() { let left = gray_image!(1, 2); let right = gray_image!(8; 4); let _ = root_mean_squared_error(&left, &right); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use image::{GrayImage, Luma, Rgb, RgbImage}; use test::Bencher; fn left_image_rgb(width: u32, height: u32) -> RgbImage { RgbImage::from_fn(width, height, |x, y| Rgb([x as u8, y as u8, (x + y) as u8])) } fn right_image_rgb(width: u32, height: u32) -> RgbImage { RgbImage::from_fn(width, height, |x, y| Rgb([(x + y) as u8, x as u8, y as u8])) } #[bench] fn bench_root_mean_squared_error_rgb(b: &mut Bencher) { let left = left_image_rgb(50, 50); let right = right_image_rgb(50, 50); b.iter(|| { let error = root_mean_squared_error(&left, &right); test::black_box(error); }); } fn left_image_gray(width: u32, height: u32) -> GrayImage { GrayImage::from_fn(width, height, |x, _| Luma([x as u8])) } fn right_image_gray(width: u32, height: u32) -> GrayImage { GrayImage::from_fn(width, height, |_, y| Luma([y as u8])) } #[bench] fn bench_root_mean_squared_error_gray(b: &mut Bencher) { let left = left_image_gray(50, 50); let right = right_image_gray(50, 50); b.iter(|| { let error = root_mean_squared_error(&left, &right); test::black_box(error); }); } } imageproc-0.25.0/src/suppress.rs000064400000000000000000000320271046102023000147030ustar 00000000000000//! Functions for suppressing non-maximal values. use crate::definitions::{Position, Score}; use image::{GenericImage, ImageBuffer, Luma, Primitive}; use std::cmp; /// Returned image has zeroes for all inputs pixels which do not have the greatest /// intensity in the (2 * radius + 1) square block centred on them. /// Ties are resolved lexicographically. pub fn suppress_non_maximum(image: &I, radius: u32) -> ImageBuffer, Vec> where I: GenericImage>, C: Primitive + Ord, { let (width, height) = image.dimensions(); let mut out: ImageBuffer, Vec> = ImageBuffer::new(width, height); if width == 0 || height == 0 { return out; } // We divide the image into a grid of blocks of size r * r. We find the maximum // value in each block, and then test whether this is in fact the maximum value // in the (2r + 1) * (2r + 1) block centered on it. Any pixel that's not maximal // within its r * r grid cell can't be a local maximum so we need only perform // the (2r + 1) * (2r + 1) search once per r * r grid cell (as opposed to once // per pixel in the naive implementation of this algorithm). for y in (0..height).step_by(radius as usize + 1) { for x in (0..width).step_by(radius as usize + 1) { let mut best_x = x; let mut best_y = y; let mut mi = image.get_pixel(x, y)[0]; // These mins are necessary for when radius > min(width, height) for cy in y..cmp::min(height, y + radius + 1) { for cx in x..cmp::min(width, x + radius + 1) { let ci = unsafe { image.unsafe_get_pixel(cx, cy)[0] }; if ci < mi { continue; } if ci > mi || (cx, cy) < (best_x, best_y) { best_x = cx; best_y = cy; mi = ci; } } } let x0 = if radius >= best_x { 0 } else { best_x - radius }; let x1 = x; let x2 = cmp::min(width, x + radius + 1); let x3 = cmp::min(width, best_x + radius + 1); let y0 = if radius >= best_y { 0 } else { best_y - radius }; let y1 = y; let y2 = cmp::min(height, y + radius + 1); let y3 = cmp::min(height, best_y + radius + 1); // Above initial r * r block let mut failed = contains_greater_value(image, best_x, best_y, mi, y0, y1, x0, x3); // Left of initial r * r block failed |= contains_greater_value(image, best_x, best_y, mi, y1, y2, x0, x1); // Right of initial r * r block failed |= contains_greater_value(image, best_x, best_y, mi, y1, y2, x2, x3); // Below initial r * r block failed |= contains_greater_value(image, best_x, best_y, mi, y2, y3, x0, x3); if !failed { unsafe { out.unsafe_put_pixel(best_x, best_y, Luma([mi])) }; } } } out } /// Returns true if the given block contains a larger value than /// the input, or contains an equal value with lexicographically /// lesser coordinates. fn contains_greater_value( image: &I, x: u32, y: u32, v: C, y_lower: u32, y_upper: u32, x_lower: u32, x_upper: u32, ) -> bool where I: GenericImage>, C: Primitive + Ord, { for cy in y_lower..y_upper { for cx in x_lower..x_upper { let ci = unsafe { image.unsafe_get_pixel(cx, cy)[0] }; if ci < v { continue; } if ci > v || (cx, cy) < (x, y) { return true; } } } false } /// Returns all items which have the highest score in the /// (2 * radius + 1) square block centred on them. Ties are resolved lexicographically. pub fn local_maxima(ts: &[T], radius: u32) -> Vec where T: Position + Score + Copy, { let mut ordered_ts = ts.to_vec(); ordered_ts.sort_by_key(|&c| (c.y(), c.x())); let height = match ordered_ts.last() { Some(t) => t.y(), None => 0, }; let mut ts_by_row = vec![vec![]; (height + 1) as usize]; for t in &ordered_ts { ts_by_row[t.y() as usize].push(t); } let mut max_ts = vec![]; for t in &ordered_ts { let cx = t.x(); let cy = t.y(); let cs = t.score(); let mut is_max = true; let row_lower = if radius > cy { 0 } else { cy - radius }; let row_upper = if cy + radius + 1 > height { height } else { cy + radius + 1 }; for y in row_lower..row_upper { for c in &ts_by_row[y as usize] { if c.x() + radius < cx { continue; } if c.x() > cx + radius { break; } if c.score() > cs { is_max = false; break; } if c.score() < cs { continue; } // Break tiebreaks lexicographically if (c.y(), c.x()) < (cy, cx) { is_max = false; break; } } if !is_max { break; } } if is_max { max_ts.push(*t); } } max_ts } #[cfg(test)] mod tests { use super::{local_maxima, suppress_non_maximum}; use crate::definitions::{Position, Score}; use crate::property_testing::GrayTestImage; use crate::utils::pixel_diff_summary; use image::{GenericImage, GrayImage, ImageBuffer, Luma, Primitive}; use quickcheck::{quickcheck, TestResult}; use std::cmp; #[derive(PartialEq, Debug, Copy, Clone)] pub(super) struct T { x: u32, y: u32, score: f32, } impl T { pub(super) fn new(x: u32, y: u32, score: f32) -> T { T { x, y, score } } } impl Position for T { fn x(&self) -> u32 { self.x } fn y(&self) -> u32 { self.y } } impl Score for T { fn score(&self) -> f32 { self.score } } #[test] fn test_local_maxima() { let ts = vec![ // Suppress vertically T::new(0, 0, 8f32), T::new(0, 3, 10f32), T::new(0, 6, 9f32), // Suppress horizontally T::new(5, 5, 10f32), T::new(7, 5, 15f32), // Tiebreak T::new(12, 20, 10f32), T::new(13, 20, 10f32), T::new(13, 21, 10f32), ]; let expected = vec![ T::new(0, 3, 10f32), T::new(7, 5, 15f32), T::new(12, 20, 10f32), ]; let max = local_maxima(&ts, 3); assert_eq!(max, expected); } #[test] fn test_suppress_non_maximum() { let mut image = GrayImage::new(25, 25); // Suppress vertically image.put_pixel(0, 0, Luma([8u8])); image.put_pixel(0, 3, Luma([10u8])); image.put_pixel(0, 6, Luma([9u8])); // Suppress horizontally image.put_pixel(5, 5, Luma([10u8])); image.put_pixel(7, 5, Luma([15u8])); // Tiebreak image.put_pixel(12, 20, Luma([10u8])); image.put_pixel(13, 20, Luma([10u8])); image.put_pixel(13, 21, Luma([10u8])); let mut expected = GrayImage::new(25, 25); expected.put_pixel(0, 3, Luma([10u8])); expected.put_pixel(7, 5, Luma([15u8])); expected.put_pixel(12, 20, Luma([10u8])); let actual = suppress_non_maximum(&image, 3); assert_pixels_eq!(actual, expected); } #[test] fn test_suppress_non_maximum_handles_radius_greater_than_image_side() { // Don't care about output pixels, just want to make sure that // we don't go out of bounds when radius exceeds width or height. let image = GrayImage::new(7, 3); let r = suppress_non_maximum(&image, 5); let image = GrayImage::new(3, 7); let s = suppress_non_maximum(&image, 5); // Use r and s to silence warnings about unused variables. assert!(r.width() == 7); assert!(s.width() == 3); } /// Reference implementation of suppress_non_maximum. Used to validate /// the (presumably faster) actual implementation. fn suppress_non_maximum_reference(image: &I, radius: u32) -> ImageBuffer, Vec> where I: GenericImage>, C: Primitive + Ord, { let (width, height) = image.dimensions(); let mut out = ImageBuffer::new(width, height); out.copy_from(image, 0, 0).unwrap(); let iradius = radius as i32; let iheight = height as i32; let iwidth = width as i32; // We update zero values from out as we go, so to check intensities // we need to read values from the input image. for y in 0..height { for x in 0..width { let intensity = image.get_pixel(x, y)[0]; let mut is_max = true; let y_lower = cmp::max(0, y as i32 - iradius); let y_upper = cmp::min(y as i32 + iradius + 1, iheight); let x_lower = cmp::max(0, x as i32 - iradius); let x_upper = cmp::min(x as i32 + iradius + 1, iwidth); for py in y_lower..y_upper { for px in x_lower..x_upper { let v = image.get_pixel(px as u32, py as u32)[0]; // Handle intensity tiebreaks lexicographically let candidate_is_lexically_earlier = (px as u32, py as u32) < (x, y); if v > intensity || (v == intensity && candidate_is_lexically_earlier) { is_max = false; break; } } } if !is_max { out.put_pixel(x, y, Luma([C::zero()])); } } } out } #[test] fn test_suppress_non_maximum_matches_reference_implementation() { fn prop(image: GrayTestImage) -> TestResult { let expected = suppress_non_maximum_reference(&image.0, 3); let actual = suppress_non_maximum(&image.0, 3); match pixel_diff_summary(&actual, &expected) { None => TestResult::passed(), Some(err) => TestResult::error(err), } } quickcheck(prop as fn(GrayTestImage) -> TestResult); } #[test] fn test_step() { assert_eq!((0u32..5).step_by(4).collect::>(), vec![0, 4]); assert_eq!((0u32..4).step_by(4).collect::>(), vec![0]); assert_eq!((4u32..4).step_by(4).collect::>(), vec![]); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::{local_maxima, suppress_non_maximum, tests::T}; use crate::noise::gaussian_noise_mut; use image::{GrayImage, ImageBuffer, Luma}; use test::Bencher; #[bench] fn bench_local_maxima_dense(b: &mut Bencher) { let mut ts = vec![]; for x in 0..20 { for y in 0..20 { let score = (x * y) % 15; ts.push(T::new(x, y, score as f32)); } } b.iter(|| local_maxima(&ts, 15)); } #[bench] fn bench_local_maxima_sparse(b: &mut Bencher) { let mut ts = vec![]; for x in 0..20 { for y in 0..20 { ts.push(T::new(50 * x, 50 * y, 50f32)); } } b.iter(|| local_maxima(&ts, 15)); } #[bench] fn bench_suppress_non_maximum_increasing_gradient(b: &mut Bencher) { // Increasing gradient in both directions. This can be a worst-case for // early-abort strategies. let img = ImageBuffer::from_fn(40, 20, |x, y| Luma([(x + y) as u8])); b.iter(|| suppress_non_maximum(&img, 7)); } #[bench] fn bench_suppress_non_maximum_decreasing_gradient(b: &mut Bencher) { let width = 40u32; let height = 20u32; let img = ImageBuffer::from_fn(width, height, |x, y| { Luma([((width - x) + (height - y)) as u8]) }); b.iter(|| suppress_non_maximum(&img, 7)); } #[bench] fn bench_suppress_non_maximum_noise_7(b: &mut Bencher) { let mut img: GrayImage = ImageBuffer::new(40, 20); gaussian_noise_mut(&mut img, 128f64, 30f64, 1); b.iter(|| suppress_non_maximum(&img, 7)); } #[bench] fn bench_suppress_non_maximum_noise_3(b: &mut Bencher) { let mut img: GrayImage = ImageBuffer::new(40, 20); gaussian_noise_mut(&mut img, 128f64, 30f64, 1); b.iter(|| suppress_non_maximum(&img, 3)); } #[bench] fn bench_suppress_non_maximum_noise_1(b: &mut Bencher) { let mut img: GrayImage = ImageBuffer::new(40, 20); gaussian_noise_mut(&mut img, 128f64, 30f64, 1); b.iter(|| suppress_non_maximum(&img, 1)); } } imageproc-0.25.0/src/template_matching.rs000064400000000000000000000740741046102023000165140ustar 00000000000000//! Functions for performing template matching. use crate::definitions::Image; use image::{GenericImageView, GrayImage, Luma, Primitive}; #[cfg_attr(feature = "katexit", katexit::katexit)] /// Scoring functions when comparing a template and an image region. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum MatchTemplateMethod { /// Sum of the squares of the difference between image and template pixel intensities. Smaller values indicate a better match. /// /// Without a mask: /// $$ /// \text{output}(x, y) = \sum_{x', y'} \left( \text{template}(x', y') - \text{image}(x+x', y+y') \right)^2 /// $$ /// /// With a mask: /// $$ /// \text{output}(x, y) = \sum_{x', y'} \left( (\text{template}(x', y') - \text{image}(x+x', y+y')) \cdot \text{mask}(x', y') \right)^2 /// $$ /// SumOfSquaredErrors, /// Divides the sum computed using `SumOfSquaredErrors` by a normalization term. Smaller values indicate a better match. /// /// Without a mask: /// $$ /// \text{output}(x, y) = \frac{\sum_{x', y'} \left( \text{template}(x', y') - \text{image}(x+x', y+y') \right)^2} /// {\sqrt{ \sum_{x', y'} {\text{template}(x', y')}^2 \cdot \sum_{x', y'} {\text{image}(x+x', y+y')}^2 }} /// $$ /// /// With a mask: /// $$ /// \text{output}(x, y) = \frac{\sum_{x', y'} \left( (\text{template}(x', y') - \text{image}(x+x', y+y')) \cdot \text{mask}(x', y') \right)^2} /// {\sqrt{ \sum_{x', y'}{(\text{template}(x', y') \cdot \text{mask}(x', y'))}^2 \cdot \sum_{x', y'}{(\text{image}(x+x', y+y') \cdot \text{mask}(x', y'))}^2 }} /// $$ SumOfSquaredErrorsNormalized, /// Cross Correlation. Larger values indicate a better match. /// /// Without a mask: /// $$ /// \text{output}(x, y) = \sum_{x', y'} \left( \text{template}(x', y') \cdot \text{image}(x+x', y+y') \right) /// $$ /// /// With a mask: /// $$ /// \text{output}(x, y) = \sum_{x', y'} \left( \text{template}(x', y') \cdot \text{image}(x+x', y+y') \cdot {\text{mask}(x', y')}^2 \right) /// $$ /// CrossCorrelation, /// Divides the sum computed using `CrossCorrelation` by a normalization term. Larger values indicate a better match. /// /// Without a mask: /// $$ /// \text{output}(x, y) = \frac{\sum_{x', y'} \left( \text{template}(x', y') \cdot \text{image}(x+x', y+y') \right)} /// {\sqrt{ \sum_{x', y'} {\text{template}(x', y')}^2 \cdot \sum_{x', y'} {\text{image}(x+x', y+y')}^2 }} /// $$ /// /// With a mask: /// $$ /// \text{output}(x, y) = \frac{\sum_{x', y'} \left( \text{template}(x', y') \cdot \text{image}(x+x', y+y') \cdot {\text{mask}(x', y')}^2 \right)} /// {\sqrt{ \sum_{x', y'}{(\text{template}(x', y') \cdot \text{mask}(x', y'))}^2 \cdot \sum_{x', y'}{(\text{image}(x+x', y+y') \cdot \text{mask}(x', y'))}^2 }} /// $$ /// CrossCorrelationNormalized, } /// Slides a `template` over an `image` and scores the match at each point using /// the requested `method`. /// /// The returned image has dimensions `image.width() - template.width() + 1` by /// `image.height() - template.height() + 1`. /// /// See [`MatchTemplateMethod`] for details of the matching methods. /// /// # Panics /// /// If either dimension of `template` is not strictly less than the corresponding dimension /// of `image`. pub fn match_template( image: &GrayImage, template: &GrayImage, method: MatchTemplateMethod, ) -> Image> { use MatchTemplateMethod as M; let input = &ImageTemplate::new(image, template); match method { M::SumOfSquaredErrors => methods::Sse::match_template(input), M::SumOfSquaredErrorsNormalized => methods::SseNormalized::match_template(input), M::CrossCorrelation => methods::Ccorr::match_template(input), M::CrossCorrelationNormalized => methods::CcorrNormalized::match_template(input), } } #[cfg(feature = "rayon")] /// Slides a `template` over an `image` and scores the match at each point using /// the requested `method`. This version uses rayon to parallelize the computation. /// /// The returned image has dimensions `image.width() - template.width() + 1` by /// `image.height() - template.height() + 1`. /// /// See [`MatchTemplateMethod`] for details of the matching methods. /// /// # Panics /// /// If either dimension of `template` is not strictly less than the corresponding dimension /// of `image`. pub fn match_template_parallel( image: &GrayImage, template: &GrayImage, method: MatchTemplateMethod, ) -> Image> { use MatchTemplateMethod as M; let input = &ImageTemplate::new(image, template); match method { M::SumOfSquaredErrors => methods::Sse::match_template_parallel(input), M::SumOfSquaredErrorsNormalized => methods::SseNormalized::match_template_parallel(input), M::CrossCorrelation => methods::Ccorr::match_template_parallel(input), M::CrossCorrelationNormalized => methods::CcorrNormalized::match_template_parallel(input), } } /// Slides a `template` and a `mask` over an `image` and scores the match at each point using /// the requested `method`. /// /// The returned image has dimensions `image.width() - template.width() + 1` by /// `image.height() - template.height() + 1`. /// /// See [`MatchTemplateMethod`] for details of the matching methods. /// /// # Panics /// /// - If either dimension of `template` is not strictly less than the corresponding dimension /// of `image`. /// - If `template.dimensions() != mask.dimensions()`. pub fn match_template_with_mask( image: &GrayImage, template: &GrayImage, method: MatchTemplateMethod, mask: &GrayImage, ) -> Image> { use MatchTemplateMethod as M; let input = &ImageTemplateMask::new(image, template, mask); match method { M::SumOfSquaredErrors => methods::SseWithMask::match_template(input), M::SumOfSquaredErrorsNormalized => methods::SseNormalizedWithMask::match_template(input), M::CrossCorrelation => methods::CcorrWithMask::match_template(input), M::CrossCorrelationNormalized => methods::CcorrNormalizedWithMask::match_template(input), } } #[cfg(feature = "rayon")] /// Slides a `template` and a `mask` over an `image` and scores the match at each point using /// the requested `method`. This version uses rayon to parallelize the computation. /// /// The returned image has dimensions `image.width() - template.width() + 1` by /// `image.height() - template.height() + 1`. /// /// See [`MatchTemplateMethod`] for details of the matching methods. /// /// # Panics /// - If either dimension of `template` is not strictly less than the corresponding dimension /// of `image`. /// - If `template.dimensions() != mask.dimensions()`. pub fn match_template_with_mask_parallel( image: &GrayImage, template: &GrayImage, method: MatchTemplateMethod, mask: &GrayImage, ) -> Image> { use MatchTemplateMethod as M; let input = &ImageTemplateMask::new(image, template, mask); match method { M::SumOfSquaredErrors => methods::SseWithMask::match_template_parallel(input), M::SumOfSquaredErrorsNormalized => { methods::SseNormalizedWithMask::match_template_parallel(input) } M::CrossCorrelation => methods::CcorrWithMask::match_template_parallel(input), M::CrossCorrelationNormalized => { methods::CcorrNormalizedWithMask::match_template_parallel(input) } } } trait MatchTemplate<'a> where Self: Sync + Sized, { type Input: Sync + OutputDims; fn init(input: &Self::Input) -> Self; fn score_at(&self, at: (u32, u32), input: &Self::Input) -> f32; fn match_template(input: &Self::Input) -> Image> { let method = Self::init(input); let (width, height) = input.output_dims(); Image::from_fn(width, height, |x, y| { let score = method.score_at((x, y), input); Luma([score]) }) } #[cfg(feature = "rayon")] fn match_template_parallel(input: &Self::Input) -> Image> { use rayon::prelude::*; let method = Self::init(input); let (width, height) = input.output_dims(); let rows = (0..height) .into_par_iter() .map(|y| { (0..width) .map(|x| method.score_at((x, y), input)) .collect::>() }) .collect::>(); Image::from_fn(width, height, |x, y| { let score = rows[y as usize][x as usize]; Luma([score]) }) } } trait OutputDims { fn output_dims(&self) -> (u32, u32); } mod methods { use super::*; pub struct Sse; impl<'a> MatchTemplate<'a> for Sse { type Input = ImageTemplate<'a>; fn init(_: &Self::Input) -> Self { Self } fn score_at(&self, at: (u32, u32), input: &Self::Input) -> f32 { let mut score = 0f32; unsafe { input.slide_window_at(at, |i, t| { score += (t - i).powi(2); }) }; score } } pub struct SseNormalized { template_squared_sum: f32, } impl<'a> MatchTemplate<'a> for SseNormalized { type Input = ImageTemplate<'a>; fn init(input: &Self::Input) -> Self { Self { template_squared_sum: square_sum(input.template), } } fn score_at(&self, at: (u32, u32), input: &Self::Input) -> f32 { let mut score = 0f32; let mut ii = 0f32; unsafe { input.slide_window_at(at, |i, t| { score += (t - i).powi(2); ii += i * i; }) }; let norm = (ii * self.template_squared_sum).sqrt(); if norm > 0.0 { score / norm } else { score } } } pub struct Ccorr; impl<'a> MatchTemplate<'a> for Ccorr { type Input = ImageTemplate<'a>; fn init(_: &Self::Input) -> Self { Self } fn score_at(&self, at: (u32, u32), input: &Self::Input) -> f32 { let mut score = 0f32; unsafe { input.slide_window_at(at, |i, t| { score += i * t; }) }; score } } pub struct CcorrNormalized { template_squared_sum: f32, } impl<'a> MatchTemplate<'a> for CcorrNormalized { type Input = ImageTemplate<'a>; fn init(input: &Self::Input) -> Self { Self { template_squared_sum: square_sum(input.template), } } fn score_at(&self, at: (u32, u32), input: &Self::Input) -> f32 { let mut score = 0f32; let mut ii = 0f32; unsafe { input.slide_window_at(at, |i, t| { score += i * t; ii += i * i; }) }; let norm = (ii * self.template_squared_sum).sqrt(); if norm > 0.0 { score / norm } else { score } } } pub struct SseWithMask; impl<'a> MatchTemplate<'a> for SseWithMask { type Input = ImageTemplateMask<'a>; fn init(_: &Self::Input) -> Self { Self } fn score_at(&self, at: (u32, u32), input: &Self::Input) -> f32 { let mut score = 0f32; unsafe { input.slide_window_at(at, |i, t, m| { score += ((t - i) * m).powi(2); }) }; score } } pub struct SseNormalizedWithMask { template_mask_squared_sum: f32, } impl<'a> MatchTemplate<'a> for SseNormalizedWithMask { type Input = ImageTemplateMask<'a>; fn init(input: &Self::Input) -> Self { let template_mask_squared_sum = mult_square_sum(input.inner.template, input.mask); Self { template_mask_squared_sum, } } fn score_at(&self, at: (u32, u32), input: &Self::Input) -> f32 { let mut score = 0f32; let mut im_im = 0f32; unsafe { input.slide_window_at(at, |i, t, m| { score += ((t - i) * m).powi(2); im_im += (i * m).powi(2); }) }; let norm = (self.template_mask_squared_sum * im_im).sqrt(); if norm > 0.0 { score / norm } else { score } } } pub struct CcorrWithMask; impl<'a> MatchTemplate<'a> for CcorrWithMask { type Input = ImageTemplateMask<'a>; fn init(_: &Self::Input) -> Self { Self } fn score_at(&self, at: (u32, u32), input: &Self::Input) -> f32 { let mut score = 0f32; unsafe { input.slide_window_at(at, |i, t, m| { score += t * i * m * m; }) }; score } } pub struct CcorrNormalizedWithMask { template_mask_squared_sum: f32, } impl<'a> MatchTemplate<'a> for CcorrNormalizedWithMask { type Input = ImageTemplateMask<'a>; fn init(input: &Self::Input) -> Self { let template_mask_squared_sum = mult_square_sum(input.inner.template, input.mask); Self { template_mask_squared_sum, } } fn score_at(&self, at: (u32, u32), input: &Self::Input) -> f32 { let mut score = 0f32; let mut im_im = 0f32; unsafe { input.slide_window_at(at, |i, t, m| { score += t * i * m * m; im_im += (i * m).powi(2); }) }; let norm = (self.template_mask_squared_sum * im_im).sqrt(); if norm > 0.0 { score / norm } else { score } } } fn square_sum(input: &GrayImage) -> f32 { input.iter().map(|&x| (x as f32 * x as f32)).sum() } fn mult_square_sum(a: &GrayImage, b: &GrayImage) -> f32 { a.iter() .zip(b.iter()) .map(|(&x, &y)| (x as f32 * y as f32).powi(2)) .sum() } } struct ImageTemplate<'a> { image: &'a GrayImage, template: &'a GrayImage, } impl<'a> ImageTemplate<'a> { fn new(image: &'a GrayImage, template: &'a GrayImage) -> Self { assert!( image.width() >= template.width(), "image width must be greater than or equal to template width" ); assert!( image.height() >= template.height(), "image height must be greater than or equal to template height" ); Self { image, template } } unsafe fn slide_window_at(&self, (x, y): (u32, u32), mut for_each: impl FnMut(f32, f32)) { let (image, template) = (self.image, self.template); debug_assert!(x + template.width() - 1 < image.width()); debug_assert!(y + template.height() - 1 < image.height()); for dy in 0..template.height() { for dx in 0..template.width() { let image_value = unsafe { image.unsafe_get_pixel(x + dx, y + dy)[0] as f32 }; let template_value = unsafe { template.unsafe_get_pixel(dx, dy)[0] as f32 }; for_each(image_value, template_value); } } } } impl<'a> OutputDims for ImageTemplate<'a> { fn output_dims(&self) -> (u32, u32) { let width = self.image.width() - self.template.width() + 1; let height = self.image.height() - self.template.height() + 1; (width, height) } } struct ImageTemplateMask<'a> { inner: ImageTemplate<'a>, mask: &'a GrayImage, } impl<'a> ImageTemplateMask<'a> { fn new(image: &'a GrayImage, template: &'a GrayImage, mask: &'a GrayImage) -> Self { assert_eq!( template.dimensions(), mask.dimensions(), "the template and mask must be the same size" ); let inner = ImageTemplate::new(image, template); Self { inner, mask } } unsafe fn slide_window_at(&self, (x, y): (u32, u32), mut for_each: impl FnMut(f32, f32, f32)) { let Self { mask, inner } = self; let (image, template) = (inner.image, inner.template); debug_assert!(x + template.width() - 1 < image.width()); debug_assert!(y + template.height() - 1 < image.height()); for dy in 0..template.height() { for dx in 0..template.width() { let image_value = unsafe { image.unsafe_get_pixel(x + dx, y + dy)[0] as f32 }; let template_value = unsafe { template.unsafe_get_pixel(dx, dy)[0] as f32 }; let mask_value = unsafe { mask.unsafe_get_pixel(dx, dy)[0] as f32 }; for_each(image_value, template_value, mask_value); } } } } impl<'a> OutputDims for ImageTemplateMask<'a> { fn output_dims(&self) -> (u32, u32) { self.inner.output_dims() } } /// The largest and smallest values in an image, /// together with their locations. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Extremes { /// The largest value in an image. pub max_value: T, /// The smallest value in an image. pub min_value: T, /// The coordinates of the largest value in an image. pub max_value_location: (u32, u32), /// The coordinates of the smallest value in an image. pub min_value_location: (u32, u32), } /// Finds the largest and smallest values in an image and their locations. /// If there are multiple such values then the lexicographically smallest is returned. pub fn find_extremes(image: &Image>) -> Extremes where T: Primitive, { assert!( image.width() > 0 && image.height() > 0, "image must be non-empty" ); let mut min_value = image.get_pixel(0, 0)[0]; let mut max_value = image.get_pixel(0, 0)[0]; let mut min_value_location = (0, 0); let mut max_value_location = (0, 0); for (x, y, p) in image.enumerate_pixels() { if p[0] < min_value { min_value = p[0]; min_value_location = (x, y); } if p[0] > max_value { max_value = p[0]; max_value_location = (x, y); } } Extremes { max_value, min_value, max_value_location, min_value_location, } } #[cfg(test)] mod tests { use super::*; use image::GrayImage; #[test] #[should_panic] fn match_template_panics_if_image_width_does_is_less_than_template_width() { let _ = match_template( &GrayImage::new(5, 5), &GrayImage::new(6, 5), MatchTemplateMethod::SumOfSquaredErrors, ); } #[test] #[should_panic] fn match_template_panics_if_image_height_is_less_than_template_height() { let _ = match_template( &GrayImage::new(5, 5), &GrayImage::new(5, 6), MatchTemplateMethod::SumOfSquaredErrors, ); } #[test] fn match_template_handles_template_of_same_size_as_image() { assert_pixels_eq!( match_template( &GrayImage::new(5, 5), &GrayImage::new(5, 5), MatchTemplateMethod::SumOfSquaredErrors ), gray_image!(type: f32, 0.0) ); } #[test] fn match_template_normalization_handles_zero_norm() { assert_pixels_eq!( match_template( &GrayImage::new(1, 1), &GrayImage::new(1, 1), MatchTemplateMethod::SumOfSquaredErrorsNormalized ), gray_image!(type: f32, 0.0) ); } #[test] fn match_template_sum_of_squared_errors() { let image = gray_image!( 1, 4, 2; 2, 1, 3; 3, 3, 4 ); let template = gray_image!( 1, 2; 3, 4 ); let actual = match_template(&image, &template, MatchTemplateMethod::SumOfSquaredErrors); let expected = gray_image!(type: f32, 14.0, 14.0; 3.0, 1.0 ); assert_pixels_eq!(actual, expected); } #[test] fn match_template_sum_of_squared_errors_normalized() { let image = gray_image!( 1, 4, 2; 2, 1, 3; 3, 3, 4 ); let template = gray_image!( 1, 2; 3, 4 ); let actual = match_template( &image, &template, MatchTemplateMethod::SumOfSquaredErrorsNormalized, ); let tss = 30f32; let expected = gray_image!(type: f32, 14.0 / (22.0 * tss).sqrt(), 14.0 / (30.0 * tss).sqrt(); 3.0 / (23.0 * tss).sqrt(), 1.0 / (35.0 * tss).sqrt() ); assert_pixels_eq!(actual, expected); } #[test] fn match_template_cross_correlation() { let image = gray_image!( 1, 4, 2; 2, 1, 3; 3, 3, 4 ); let template = gray_image!( 1, 2; 3, 4 ); let actual = match_template(&image, &template, MatchTemplateMethod::CrossCorrelation); let expected = gray_image!(type: f32, 19.0, 23.0; 25.0, 32.0 ); assert_pixels_eq!(actual, expected); } #[test] fn match_template_cross_correlation_normalized() { let image = gray_image!( 1, 4, 2; 2, 1, 3; 3, 3, 4 ); let template = gray_image!( 1, 2; 3, 4 ); let actual = match_template( &image, &template, MatchTemplateMethod::CrossCorrelationNormalized, ); let tss = 30f32; let expected = gray_image!(type: f32, 19.0 / (22.0 * tss).sqrt(), 23.0 / (30.0 * tss).sqrt(); 25.0 / (23.0 * tss).sqrt(), 32.0 / (35.0 * tss).sqrt() ); assert_pixels_eq!(actual, expected); } #[test] fn match_template_sum_of_squared_errors_with_mask() { let image = gray_image!( 1, 4, 2; 2, 1, 3; 3, 3, 4 ); let template = gray_image!( 1, 2; 3, 4 ); let mask = gray_image!( 0, 1; 2, 3 ); let expected = gray_image!(type: f32, 89., 25.; 10., 1. ); let actual = match_template_with_mask( &image, &template, MatchTemplateMethod::SumOfSquaredErrors, &mask, ); assert_pixels_eq!(actual, expected); #[cfg(feature = "rayon")] { let actual_parallel = match_template_with_mask_parallel( &image, &template, MatchTemplateMethod::SumOfSquaredErrors, &mask, ); assert_pixels_eq!(actual_parallel, expected); } } #[test] fn match_template_sum_of_squared_errors_normalized_with_mask() { let image = gray_image!( 1, 4, 2; 2, 1, 3; 3, 3, 4 ); let template = gray_image!( 1, 2; 3, 4 ); let mask = gray_image!( 0, 1; 2, 3 ); let expected = gray_image!(type: f32, 1.0246822 , 0.19536021; 0.067865655, 0.005362412 ); let actual = match_template_with_mask( &image, &template, MatchTemplateMethod::SumOfSquaredErrorsNormalized, &mask, ); assert_pixels_eq!(actual, expected); #[cfg(feature = "rayon")] { let actual_parallel = match_template_with_mask_parallel( &image, &template, MatchTemplateMethod::SumOfSquaredErrorsNormalized, &mask, ); assert_pixels_eq!(actual_parallel, expected); } } #[test] fn match_template_cross_correlation_with_mask() { let image = gray_image!( 1, 4, 2; 2, 1, 3; 3, 3, 4 ); let template = gray_image!( 1, 2; 3, 4 ); let mask = gray_image!( 0, 1; 2, 3 ); let expected = gray_image!(type: f32, 68., 124.; 146., 186. ); let actual = match_template_with_mask( &image, &template, MatchTemplateMethod::CrossCorrelation, &mask, ); assert_pixels_eq!(actual, expected); #[cfg(feature = "rayon")] { let actual_parallel = match_template_with_mask_parallel( &image, &template, MatchTemplateMethod::CrossCorrelation, &mask, ); assert_pixels_eq!(actual_parallel, expected); } } #[test] fn match_template_cross_correlation_normalized_with_mask() { let image = gray_image!( 1, 4, 2; 2, 1, 3; 3, 3, 4 ); let template = gray_image!( 1, 2; 3, 4 ); let mask = gray_image!( 0, 1; 2, 3 ); let expected = gray_image!(type: f32, 0.78290325, 0.96898663; 0.9908386, 0.9974086 ); let actual = match_template_with_mask( &image, &template, MatchTemplateMethod::CrossCorrelationNormalized, &mask, ); assert_pixels_eq!(actual, expected); #[cfg(feature = "rayon")] { let actual_parallel = match_template_with_mask_parallel( &image, &template, MatchTemplateMethod::CrossCorrelationNormalized, &mask, ); assert_pixels_eq!(actual_parallel, expected); } } #[test] fn test_find_extremes() { let image = gray_image!( 10, 7, 8, 1; 9, 15, 4, 2 ); let expected = Extremes { max_value: 15, min_value: 1, max_value_location: (1, 1), min_value_location: (3, 0), }; assert_eq!(find_extremes(&image), expected); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use crate::utils::gray_bench_image; use test::{black_box, Bencher}; macro_rules! bench_match_template { ($name:ident, image_size: $s:expr, template_size: $t:expr, method: $m:expr) => { #[bench] fn $name(b: &mut Bencher) { let image = gray_bench_image($s, $s); let template = gray_bench_image($t, $t); b.iter(|| { let result = match_template(&image, &template, MatchTemplateMethod::SumOfSquaredErrors); black_box(result); }) } }; } bench_match_template!( bench_match_template_s100_t1_sse, image_size: 100, template_size: 1, method: MatchTemplateMethod::SumOfSquaredErrors); bench_match_template!( bench_match_template_s100_t4_sse, image_size: 100, template_size: 4, method: MatchTemplateMethod::SumOfSquaredErrors); bench_match_template!( bench_match_template_s100_t16_sse, image_size: 100, template_size: 16, method: MatchTemplateMethod::SumOfSquaredErrors); bench_match_template!( bench_match_template_s100_t1_sse_norm, image_size: 100, template_size: 1, method: MatchTemplateMethod::SumOfSquaredErrorsNormalized); bench_match_template!( bench_match_template_s100_t4_sse_norm, image_size: 100, template_size: 4, method: MatchTemplateMethod::SumOfSquaredErrorsNormalized); bench_match_template!( bench_match_template_s100_t16_sse_norm, image_size: 100, template_size: 16, method: MatchTemplateMethod::SumOfSquaredErrorsNormalized); macro_rules! bench_match_template_with_mask { ($name:ident, image_size: $s:expr, template_size: $t:expr, method: $m:expr) => { #[bench] fn $name(b: &mut Bencher) { let image = gray_bench_image($s, $s); let template = gray_bench_image($t, $t); let mask = gray_bench_image($t, $t); b.iter(|| { let result = match_template_with_mask( &image, &template, MatchTemplateMethod::SumOfSquaredErrors, &mask, ); black_box(result); }) } }; } bench_match_template_with_mask!( bench_match_template_with_mask_s100_t1_sse, image_size: 100, template_size: 1, method: MatchTemplateMethod::SumOfSquaredErrors); bench_match_template_with_mask!( bench_match_template_with_mask_s100_t4_sse, image_size: 100, template_size: 4, method: MatchTemplateMethod::SumOfSquaredErrors); bench_match_template_with_mask!( bench_match_template_with_mask_s100_t16_sse, image_size: 100, template_size: 16, method: MatchTemplateMethod::SumOfSquaredErrors); bench_match_template_with_mask!( bench_match_template_with_mask_s100_t1_sse_norm, image_size: 100, template_size: 1, method: MatchTemplateMethod::SumOfSquaredErrorsNormalized); bench_match_template_with_mask!( bench_match_template_with_mask_s100_t4_sse_norm, image_size: 100, template_size: 4, method: MatchTemplateMethod::SumOfSquaredErrorsNormalized); bench_match_template_with_mask!( bench_match_template_with_mask_s100_t16_sse_norm, image_size: 100, template_size: 16, method: MatchTemplateMethod::SumOfSquaredErrorsNormalized); } imageproc-0.25.0/src/union_find.rs000064400000000000000000000136061046102023000151510ustar 00000000000000//! An implementation of disjoint set forests for union find. /// Data structure for efficient union find. pub struct DisjointSetForest { /// Number of forest elements. count: usize, /// parent[i] is the index of the parent /// of the element with index i. If parent[i] == i /// then i is a root. parent: Vec, /// tree_size[i] is the size of the tree rooted at i. tree_size: Vec, } impl DisjointSetForest { /// Constructs forest of singletons with count elements. pub fn new(count: usize) -> DisjointSetForest { let parent: Vec = (0..count).collect(); let tree_size = vec![1_usize; count]; DisjointSetForest { count, parent, tree_size, } } /// Returns the number of trees in the forest. pub fn num_trees(&self) -> usize { self.parent .iter() .enumerate() .fold(0, |acc, (i, p)| acc + if i == *p { 1 } else { 0 }) } /// Returns index of the root of the tree containing i. /// Needs mutable reference to self for path compression. pub fn root(&mut self, i: usize) -> usize { assert!(i < self.count); let mut j = i; loop { unsafe { let p = *self.parent.get_unchecked(j); *self.parent.get_unchecked_mut(j) = *self.parent.get_unchecked(p); if j == p { break; } j = p; } } j } /// Returns true if i and j are in the same tree. /// Need mutable reference to self for path compression. pub fn find(&mut self, i: usize, j: usize) -> bool { assert!(i < self.count && j < self.count); self.root(i) == self.root(j) } /// Unions the trees containing i and j. pub fn union(&mut self, i: usize, j: usize) { assert!(i < self.count && j < self.count); let p = self.root(i); let q = self.root(j); if p == q { return; } unsafe { let p_size = *self.tree_size.get_unchecked(p); let q_size = *self.tree_size.get_unchecked(q); if p_size < q_size { *self.parent.get_unchecked_mut(p) = q; *self.tree_size.get_unchecked_mut(q) = p_size + q_size; } else { *self.parent.get_unchecked_mut(q) = p; *self.tree_size.get_unchecked_mut(p) = p_size + q_size; } } } /// Returns the elements of each tree. pub fn trees(&mut self) -> Vec> { use std::collections::HashMap; // Maps a tree root to the index of the set // containing its children let mut root_sets: HashMap = HashMap::new(); let mut sets: Vec> = vec![]; for i in 0..self.count { let root = self.root(i); match root_sets.get(&root).cloned() { Some(set_idx) => { sets[set_idx].push(i); } None => { let idx = sets.len(); let set = vec![i]; sets.push(set); root_sets.insert(root, idx); } } } sets } } #[cfg(test)] mod tests { use super::DisjointSetForest; #[test] fn test_trees() { // 3 4 // | / \ // 1 5 7 // / \ | // 0 2 6 #[rustfmt::skip] let mut forest = DisjointSetForest { count: 8, // element: 0, 1, 2, 3, 4, 5, 6, 7 parent: vec![1, 3, 1, 3, 4, 4, 5, 4], tree_size: vec![1, 3, 1, 4, 4, 2, 1, 1], }; assert_eq!(forest.trees(), vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]]); } #[test] fn test_union_find_sequence() { let mut forest = DisjointSetForest::new(6); // 0 1 2 3 4 5 // 0, 1, 2, 3, 4, 5 assert_eq!(forest.parent, vec![0, 1, 2, 3, 4, 5]); assert_eq!(forest.num_trees(), 6); forest.union(0, 4); // 0 1 2 3 5 // | // 4 // 0, 1, 2, 3, 4, 5 assert_eq!(forest.parent, vec![0, 1, 2, 3, 0, 5]); assert_eq!(forest.num_trees(), 5); forest.union(1, 3); // 0 1 2 5 // | | // 4 3 // 0, 1, 2, 3, 4, 5 assert_eq!(forest.parent, vec![0, 1, 2, 1, 0, 5]); assert_eq!(forest.num_trees(), 4); forest.union(3, 2); // 0 1 5 // | / \ // 4 3 2 // 0, 1, 2, 3, 4, 5 assert_eq!(forest.parent, vec![0, 1, 1, 1, 0, 5]); assert_eq!(forest.num_trees(), 3); forest.union(2, 4); // 1 5 // / | \ // 0 3 2 // | // 4 // 0, 1, 2, 3, 4, 5 assert_eq!(forest.parent, vec![1, 1, 1, 1, 0, 5]); assert_eq!(forest.num_trees(), 2); } } #[cfg(not(miri))] #[cfg(test)] mod benches { use super::*; use rand::{rngs::StdRng, SeedableRng}; use rand_distr::{Distribution, Uniform}; #[bench] fn bench_disjoint_set_forest(b: &mut test::Bencher) { let num_nodes = 500; let num_edges = 20 * num_nodes; let mut rng: StdRng = SeedableRng::seed_from_u64(1); let uniform = Uniform::new(0, num_nodes); let mut forest = DisjointSetForest::new(num_nodes); b.iter(|| { let mut count = 0; while count < num_edges { let u = uniform.sample(&mut rng); let v = uniform.sample(&mut rng); forest.union(u, v); count += 1; } test::black_box(forest.num_trees()); }); } } imageproc-0.25.0/src/utils.rs000064400000000000000000000533341046102023000141630ustar 00000000000000//! Utils for testing and debugging. use image::{ open, DynamicImage, GenericImage, GenericImageView, GrayImage, Luma, Pixel, Rgb, RgbImage, }; use itertools::Itertools; use std::cmp::{max, min}; use std::collections::HashSet; use std::fmt; use std::fmt::Write; use std::path::Path; /// Helper for defining greyscale images. /// /// Columns are separated by commas and rows by semi-colons. /// By default a subpixel type of `u8` is used but this can be /// overridden, as shown in the examples. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::{GrayImage, ImageBuffer, Luma}; /// /// // An empty grayscale image with pixel type Luma /// let empty = gray_image!(); /// /// assert_pixels_eq!( /// empty, /// GrayImage::from_raw(0, 0, vec![]).unwrap() /// ); /// /// // A single pixel grayscale image with pixel type Luma /// let single_pixel = gray_image!(1); /// /// assert_pixels_eq!( /// single_pixel, /// GrayImage::from_raw(1, 1, vec![1]).unwrap() /// ); /// /// // A single row grayscale image with pixel type Luma /// let single_row = gray_image!(1, 2, 3); /// /// assert_pixels_eq!( /// single_row, /// GrayImage::from_raw(3, 1, vec![1, 2, 3]).unwrap() /// ); /// /// // A grayscale image with 2 rows and 3 columns /// let image = gray_image!( /// 1, 2, 3; /// 4, 5, 6); /// /// let equivalent = GrayImage::from_raw(3, 2, vec![ /// 1, 2, 3, /// 4, 5, 6 /// ]).unwrap(); /// /// // An empty grayscale image with pixel type Luma. /// let empty_i16 = gray_image!(type: i16); /// /// assert_pixels_eq!( /// empty_i16, /// ImageBuffer::, Vec>::from_raw(0, 0, vec![]).unwrap() /// ); /// /// // A grayscale image with 2 rows, 3 columns and pixel type Luma /// let image_i16 = gray_image!(type: i16, /// 1, 2, 3; /// 4, 5, 6); /// /// let expected_i16 = ImageBuffer::, Vec>::from_raw(3, 2, vec![ /// 1, 2, 3, /// 4, 5, 6]).unwrap(); /// /// assert_pixels_eq!(image_i16, expected_i16); /// # } /// ``` #[macro_export] macro_rules! gray_image { // Empty image with default channel type u8 () => { gray_image!(type: u8) }; // Empty image with the given channel type (type: $channel_type:ty) => { { use image::{ImageBuffer, Luma}; ImageBuffer::, Vec<$channel_type>>::new(0, 0) } }; // Non-empty image of default channel type u8 ($( $( $x: expr ),*);*) => { gray_image!(type: u8, $( $( $x ),*);*) }; // Non-empty image of given channel type (type: $channel_type:ty, $( $( $x: expr ),*);*) => { { use image::{ImageBuffer, Luma}; let nested_array = [ $( [ $($x),* ] ),* ]; let height = nested_array.len() as u32; let width = nested_array[0].len() as u32; let flat_array: Vec<_> = nested_array.iter() .flat_map(|row| row.into_iter()) .cloned() .collect(); ImageBuffer::, Vec<$channel_type>>::from_raw(width, height, flat_array) .unwrap() } } } /// Helper for defining RGB images. /// /// Pixels are delineated by square brackets, columns are /// separated by commas and rows are separated by semi-colons. /// By default a subpixel type of `u8` is used but this can be /// overridden, as shown in the examples. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::{ImageBuffer, Rgb, RgbImage}; /// /// // An empty image with pixel type Rgb /// let empty = rgb_image!(); /// /// assert_pixels_eq!( /// empty, /// RgbImage::from_raw(0, 0, vec![]).unwrap() /// ); /// /// // A single pixel image with pixel type Rgb /// let single_pixel = rgb_image!([1, 2, 3]); /// /// assert_pixels_eq!( /// single_pixel, /// RgbImage::from_raw(1, 1, vec![1, 2, 3]).unwrap() /// ); /// /// // A single row image with pixel type Rgb /// let single_row = rgb_image!([1, 2, 3], [4, 5, 6]); /// /// assert_pixels_eq!( /// single_row, /// RgbImage::from_raw(2, 1, vec![1, 2, 3, 4, 5, 6]).unwrap() /// ); /// /// // An image with 2 rows and 2 columns /// let image = rgb_image!( /// [1, 2, 3], [ 4, 5, 6]; /// [7, 8, 9], [10, 11, 12]); /// /// let equivalent = RgbImage::from_raw(2, 2, vec![ /// 1, 2, 3, 4, 5, 6, /// 7, 8, 9, 10, 11, 12 /// ]).unwrap(); /// /// assert_pixels_eq!(image, equivalent); /// /// // An empty image with pixel type Rgb. /// let empty_i16 = rgb_image!(type: i16); /// /// // An image with 2 rows, 3 columns and pixel type Rgb /// let image_i16 = rgb_image!(type: i16, /// [1, 2, 3], [4, 5, 6]; /// [7, 8, 9], [10, 11, 12]); /// /// let expected_i16 = ImageBuffer::, Vec>::from_raw(2, 2, vec![ /// 1, 2, 3, 4, 5, 6, /// 7, 8, 9, 10, 11, 12], /// ).unwrap(); /// # } /// ``` #[macro_export] macro_rules! rgb_image { // Empty image with default channel type u8 () => { rgb_image!(type: u8) }; // Empty image with the given channel type (type: $channel_type:ty) => { { use image::{ImageBuffer, Rgb}; ImageBuffer::, Vec<$channel_type>>::new(0, 0) } }; // Non-empty image of default channel type u8 ($( $( [$r: expr, $g: expr, $b: expr]),*);*) => { rgb_image!(type: u8, $( $( [$r, $g, $b]),*);*) }; // Non-empty image of given channel type (type: $channel_type:ty, $( $( [$r: expr, $g: expr, $b: expr]),*);*) => { { use image::{ImageBuffer, Rgb}; let nested_array = [$( [ $([$r, $g, $b]),*]),*]; let height = nested_array.len() as u32; let width = nested_array[0].len() as u32; let flat_array: Vec<_> = nested_array.iter() .flat_map(|row| row.into_iter().flat_map(|p| p.into_iter())) .cloned() .collect(); ImageBuffer::, Vec<$channel_type>>::from_raw(width, height, flat_array) .unwrap() } } } /// Helper for defining RGBA images. /// /// Pixels are delineated by square brackets, columns are /// separated by commas and rows are separated by semi-colons. /// By default a subpixel type of `u8` is used but this can be /// overridden, as shown in the examples. /// /// # Examples /// ``` /// # extern crate image; /// # #[macro_use] /// # extern crate imageproc; /// # fn main() { /// use image::{ImageBuffer, Rgba, RgbaImage}; /// /// // An empty image with pixel type Rgba /// let empty = rgba_image!(); /// /// assert_pixels_eq!( /// empty, /// RgbaImage::from_raw(0, 0, vec![]).unwrap() /// ); /// /// // A single pixel image with pixel type Rgba /// let single_pixel = rgba_image!([1, 2, 3, 4]); /// /// assert_pixels_eq!( /// single_pixel, /// RgbaImage::from_raw(1, 1, vec![1, 2, 3, 4]).unwrap() /// ); /// /// // A single row image with pixel type Rgba /// let single_row = rgba_image!([1, 2, 3, 10], [4, 5, 6, 20]); /// /// assert_pixels_eq!( /// single_row, /// RgbaImage::from_raw(2, 1, vec![1, 2, 3, 10, 4, 5, 6, 20]).unwrap() /// ); /// /// // An image with 2 rows and 2 columns /// let image = rgba_image!( /// [1, 2, 3, 10], [ 4, 5, 6, 20]; /// [7, 8, 9, 30], [10, 11, 12, 40]); /// /// let equivalent = RgbaImage::from_raw(2, 2, vec![ /// 1, 2, 3, 10, 4, 5, 6, 20, /// 7, 8, 9, 30, 10, 11, 12, 40 /// ]).unwrap(); /// /// assert_pixels_eq!(image, equivalent); /// /// // An empty image with pixel type Rgba. /// let empty_i16 = rgba_image!(type: i16); /// /// // An image with 2 rows, 3 columns and pixel type Rgba /// let image_i16 = rgba_image!(type: i16, /// [1, 2, 3, 10], [ 4, 5, 6, 20]; /// [7, 8, 9, 30], [10, 11, 12, 40]); /// /// let expected_i16 = ImageBuffer::, Vec>::from_raw(2, 2, vec![ /// 1, 2, 3, 10, 4, 5, 6, 20, /// 7, 8, 9, 30, 10, 11, 12, 40], /// ).unwrap(); /// # } /// ``` #[macro_export] macro_rules! rgba_image { // Empty image with default channel type u8 () => { rgba_image!(type: u8) }; // Empty image with the given channel type (type: $channel_type:ty) => { { use image::{ImageBuffer, Rgba}; ImageBuffer::, Vec<$channel_type>>::new(0, 0) } }; // Non-empty image of default channel type u8 ($( $( [$r: expr, $g: expr, $b: expr, $a:expr]),*);*) => { rgba_image!(type: u8, $( $( [$r, $g, $b, $a]),*);*) }; // Non-empty image of given channel type (type: $channel_type:ty, $( $( [$r: expr, $g: expr, $b: expr, $a: expr]),*);*) => { { use image::{ImageBuffer, Rgba}; let nested_array = [$( [ $([$r, $g, $b, $a]),*]),*]; let height = nested_array.len() as u32; let width = nested_array[0].len() as u32; let flat_array: Vec<_> = nested_array.iter() .flat_map(|row| row.into_iter().flat_map(|p| p.into_iter())) .cloned() .collect(); ImageBuffer::, Vec<$channel_type>>::from_raw(width, height, flat_array) .unwrap() } } } /// Human readable description of some of the pixels that differ /// between left and right, or None if all pixels match. pub fn pixel_diff_summary(actual: &I, expected: &J) -> Option where P: Pixel + PartialEq, P::Subpixel: fmt::Debug, I: GenericImage, J: GenericImage, { significant_pixel_diff_summary(actual, expected, |p, q| p != q) } /// Human readable description of some of the pixels that differ /// significantly (according to provided function) between left /// and right, or None if all pixels match. pub fn significant_pixel_diff_summary( actual: &I, expected: &J, is_significant_diff: F, ) -> Option where P: Pixel, P::Subpixel: fmt::Debug, I: GenericImage, J: GenericImage, F: Fn((u32, u32, I::Pixel), (u32, u32, J::Pixel)) -> bool, { if actual.dimensions() != expected.dimensions() { return Some(format!( "dimensions do not match. \ actual: {:?}, expected: {:?}", actual.dimensions(), expected.dimensions() )); } let diffs = pixel_diffs(actual, expected, is_significant_diff); if diffs.is_empty() { return None; } Some(describe_pixel_diffs(actual, expected, &diffs)) } /// Panics if any pixels differ between the two input images. #[macro_export] macro_rules! assert_pixels_eq { ($actual:expr, $expected:expr) => {{ $crate::assert_dimensions_match!($actual, $expected); match $crate::utils::pixel_diff_summary(&$actual, &$expected) { None => {} Some(err) => panic!("{}", err), }; }}; } /// Panics if any pixels differ between the two images by more than the /// given tolerance in a single channel. #[macro_export] macro_rules! assert_pixels_eq_within { ($actual:expr, $expected:expr, $channel_tolerance:expr) => {{ $crate::assert_dimensions_match!($actual, $expected); let diffs = $crate::utils::pixel_diffs(&$actual, &$expected, |p, q| { use image::Pixel; let cp = p.2.channels(); let cq = q.2.channels(); if cp.len() != cq.len() { panic!( "pixels have different channel counts. \ actual: {:?}, expected: {:?}", cp.len(), cq.len() ) } let mut large_diff = false; for i in 0..cp.len() { let sp = cp[i]; let sq = cq[i]; // Handle unsigned subpixels let diff = if sp > sq { sp - sq } else { sq - sp }; if diff > $channel_tolerance { large_diff = true; break; } } large_diff }); if !diffs.is_empty() { panic!( "{}", $crate::utils::describe_pixel_diffs(&$actual, &$expected, &diffs,) ) } }}; } /// Panics if image dimensions do not match. #[macro_export] macro_rules! assert_dimensions_match { ($actual:expr, $expected:expr) => {{ let actual_dim = $actual.dimensions(); let expected_dim = $expected.dimensions(); if actual_dim != expected_dim { panic!( "dimensions do not match. \ actual: {:?}, expected: {:?}", actual_dim, expected_dim ) } }}; } /// Lists pixels that differ between left and right images. pub fn pixel_diffs(actual: &I, expected: &J, is_diff: F) -> Vec> where P: Pixel, I: GenericImage, J: GenericImage, F: Fn((u32, u32, I::Pixel), (u32, u32, J::Pixel)) -> bool, { if is_empty(actual) || is_empty(expected) { return vec![]; } // Can't just call $image.pixels(), as that needn't hit the // trait pixels method - ImageBuffer defines its own pixels // method with a different signature GenericImageView::pixels(actual) .zip(GenericImageView::pixels(expected)) .filter(|&(p, q)| is_diff(p, q)) .map(|(p, q)| { assert!(p.0 == q.0 && p.1 == q.1, "Pixel locations do not match"); Diff { x: p.0, y: p.1, actual: p.2, expected: q.2, } }) .collect::>() } fn is_empty(image: &I) -> bool { image.width() == 0 || image.height() == 0 } /// A difference between two images pub struct Diff

{ /// x-coordinate of diff. pub x: u32, /// y-coordinate of diff. pub y: u32, /// Pixel value in expected image. pub expected: P, /// Pixel value in actual image. pub actual: P, } /// Gives a summary description of a list of pixel diffs for use in error messages. pub fn describe_pixel_diffs(actual: &I, expected: &J, diffs: &[Diff

]) -> String where P: Pixel, P::Subpixel: fmt::Debug, I: GenericImage, J: GenericImage, { let mut err = "pixels do not match.\n".to_owned(); // Find the boundaries of the region containing diffs let top_left = diffs.iter().fold((u32::MAX, u32::MAX), |acc, d| { (acc.0.min(d.x), acc.1.min(d.y)) }); let bottom_right = diffs .iter() .fold((0, 0), |acc, d| (acc.0.max(d.x), acc.1.max(d.y))); // If all the diffs are contained in a small region of the image then render all of this // region, with a small margin. if max(bottom_right.0 - top_left.0, bottom_right.1 - top_left.1) < 6 { let left = max(0, top_left.0 as i32 - 2) as u32; let top = max(0, top_left.1 as i32 - 2) as u32; let right = min(actual.width() as i32 - 1, bottom_right.0 as i32 + 2) as u32; let bottom = min(actual.height() as i32 - 1, bottom_right.1 as i32 + 2) as u32; let diff_locations = diffs.iter().map(|d| (d.x, d.y)).collect::>(); err.push_str(&colored("Actual:", Color::Red)); let actual_rendered = render_image_region(actual, left, top, right, bottom, |x, y| { if diff_locations.contains(&(x, y)) { Color::Red } else { Color::Cyan } }); err.push_str(&actual_rendered); err.push_str(&colored("Expected:", Color::Green)); let expected_rendered = render_image_region(expected, left, top, right, bottom, |x, y| { if diff_locations.contains(&(x, y)) { Color::Green } else { Color::Cyan } }); err.push_str(&expected_rendered); return err; } // Otherwise just list the first 5 diffs err.push_str( &(diffs .iter() .take(5) .map(|d| { format!( "\nlocation: {}, actual: {}, expected: {} ", colored(&format!("{:?}", (d.x, d.y)), Color::Yellow), colored(&render_pixel(d.actual), Color::Red), colored(&render_pixel(d.expected), Color::Green) ) }) .collect::>() .join("")), ); err } enum Color { Red, Green, Cyan, Yellow, } fn render_image_region( image: &I, left: u32, top: u32, right: u32, bottom: u32, color: C, ) -> String where P: Pixel, P::Subpixel: fmt::Debug, I: GenericImage, C: Fn(u32, u32) -> Color, { let mut rendered = String::new(); // Render all the pixels first, so that we can determine the column width let mut rendered_pixels = vec![]; for y in top..bottom + 1 { for x in left..right + 1 { let p = image.get_pixel(x, y); rendered_pixels.push(render_pixel(p)); } } // Width of a column containing rendered pixels let pixel_column_width = rendered_pixels.iter().map(|p| p.len()).max().unwrap() + 1; // Maximum number of digits required to display a row or column number let max_digits = (max(1, max(right, bottom)) as f64).log10().ceil() as usize; // Each pixel column is labelled with its column number let pixel_column_width = pixel_column_width.max(max_digits + 1); let num_columns = (right - left + 1) as usize; // First row contains the column numbers write!(rendered, "\n{}", " ".repeat(max_digits + 4)).unwrap(); for x in left..right + 1 { write!(rendered, "{x:>w$} ", x = x, w = pixel_column_width).unwrap(); } // +-------------- write!( rendered, "\n {}+{}", " ".repeat(max_digits), "-".repeat((pixel_column_width + 1) * num_columns + 1) ) .unwrap(); // row_number | write!(rendered, "\n {y:>w$}| ", y = " ", w = max_digits).unwrap(); let mut count = 0; for y in top..bottom + 1 { // Empty row, except for leading | separating row numbers from pixels write!(rendered, "\n {y:>w$}| ", y = y, w = max_digits).unwrap(); for x in left..right + 1 { // Pad pixel string to column width and right align let padded = format!( "{c:>w$}", c = rendered_pixels[count], w = pixel_column_width ); write!(rendered, "{} ", &colored(&padded, color(x, y))).unwrap(); count += 1; } // Empty row, except for leading | separating row numbers from pixels write!(rendered, "\n {y:>w$}| ", y = " ", w = max_digits).unwrap(); } rendered.push('\n'); rendered } fn render_pixel

(p: P) -> String where P: Pixel, P::Subpixel: fmt::Debug, { let cs = p.channels(); match cs.len() { 1 => format!("{:?}", cs[0]), _ => format!("[{}]", cs.iter().map(|c| format!("{:?}", c)).join(", ")), } } fn colored(s: &str, c: Color) -> String { let escape_sequence = match c { Color::Red => "\x1b[31m", Color::Green => "\x1b[32m", Color::Cyan => "\x1b[36m", Color::Yellow => "\x1b[33m", }; format!("{}{}\x1b[0m", escape_sequence, s) } /// Loads image at given path, panicking on failure. pub fn load_image_or_panic + fmt::Debug>(path: P) -> DynamicImage { open(path.as_ref()).expect(&format!("Could not load image at {:?}", path.as_ref())) } /// Gray image to use in benchmarks. This is neither noise nor /// similar to natural images - it's just a convenience method /// to produce an image that's not constant. pub fn gray_bench_image(width: u32, height: u32) -> GrayImage { let mut image = GrayImage::new(width, height); for y in 0..image.height() { for x in 0..image.width() { let intensity = (x % 7 + y % 6) as u8; image.put_pixel(x, y, Luma([intensity])); } } image } /// RGB image to use in benchmarks. See comment on `gray_bench_image`. pub fn rgb_bench_image(width: u32, height: u32) -> RgbImage { use std::cmp; let mut image = RgbImage::new(width, height); for y in 0..image.height() { for x in 0..image.width() { let r = (x % 7 + y % 6) as u8; let g = 255u8 - r; let b = cmp::min(r, g); image.put_pixel(x, y, Rgb([r, g, b])); } } image } #[cfg(test)] mod tests { use super::*; #[test] fn test_assert_pixels_eq_passes() { let image = gray_image!( 00, 01, 02; 10, 11, 12); assert_pixels_eq!(image, image); } #[test] #[should_panic] fn test_assert_pixels_eq_fails() { let image = gray_image!( 00, 01, 02; 10, 11, 12); let diff = gray_image!( 00, 11, 02; 10, 11, 12); assert_pixels_eq!(diff, image); } #[test] fn test_assert_pixels_eq_within_passes() { let image = gray_image!( 00, 01, 02; 10, 11, 12); let diff = gray_image!( 00, 02, 02; 10, 11, 12); assert_pixels_eq_within!(diff, image, 1); } #[test] #[should_panic] fn test_assert_pixels_eq_within_fails() { let image = gray_image!( 00, 01, 02; 10, 11, 12); let diff = gray_image!( 00, 03, 02; 10, 11, 12); assert_pixels_eq_within!(diff, image, 1); } #[test] fn test_pixel_diff_summary_handles_1x1_image() { let summary = pixel_diff_summary(&gray_image!(1), &gray_image!(0)); assert_eq!(&summary.unwrap()[0..19], "pixels do not match"); } } imageproc-0.25.0/src/window.rs000064400000000000000000000177521046102023000143360ustar 00000000000000//! Displays an image in a window created by sdl2. use image::{ buffer::ConvertBuffer, imageops::{resize, FilterType}, GenericImageView, RgbaImage, }; use sdl2::{ event::{Event, WindowEvent}, keyboard::Keycode, pixels::{Color, PixelFormatEnum}, rect::Rect, render::{Canvas, TextureCreator, WindowCanvas}, surface::Surface, video::{Window, WindowContext}, }; /// Displays the provided RGBA image in a new window. /// /// The minimum window width or height is 150 pixels - input values less than this /// will be rounded up to the minimum. pub fn display_image(title: &str, image: &I, window_width: u32, window_height: u32) where I: GenericImageView + ConvertBuffer, { display_multiple_images(title, &[image], window_width, window_height); } /// Displays the provided RGBA images in new windows. /// /// The minimum window width or height is 150 pixels - input values less than this /// will be rounded up to the minimum. pub fn display_multiple_images(title: &str, images: &[&I], window_width: u32, window_height: u32) where I: GenericImageView + ConvertBuffer, { if images.is_empty() { return; } // Enforce minimum window size const MIN_WINDOW_DIMENSION: u32 = 150; let window_width = window_width.max(MIN_WINDOW_DIMENSION); let window_height = window_height.max(MIN_WINDOW_DIMENSION); // Initialise sdl2 window let sdl = sdl2::init().expect("couldn't create sdl2 context"); let video_subsystem = sdl.video().expect("couldn't create video subsystem"); let mut windows: Vec = Vec::with_capacity(images.len()); let mut window_visibility: Vec = Vec::with_capacity(images.len()); for _ in 0..images.len() { let mut window = video_subsystem .window(title, window_width, window_height) .resizable() .allow_highdpi() .build() .expect("couldn't create window"); window .set_minimum_size(MIN_WINDOW_DIMENSION, MIN_WINDOW_DIMENSION) .expect("invalid minimum size for window"); windows.push(window); window_visibility.push(true); } { use sdl2::video::WindowPos::Positioned; let (base_position_x, base_position_y) = windows[0].position(); for (i, window) in windows.iter_mut().enumerate() { let multiplier = 1.0 + i as f32 / 20.0; window.set_position( Positioned((base_position_x as f32 * multiplier) as i32), Positioned(base_position_y), ); let (window_pos_x, _window_pos_y) = window.position(); let display_bounds = video_subsystem .display_bounds(0) .expect("No bounds found for that display."); let screen_width = display_bounds.w; if window_pos_x + window_width as i32 > screen_width { window.set_position( Positioned(screen_width - window_width as i32), Positioned(base_position_y), ); } } } let mut canvases: Vec = Vec::with_capacity(images.len()); for window in windows.into_iter() { let canvas = window .into_canvas() .software() .build() .expect("couldn't create canvas"); canvases.push(canvas); } let mut texture_creators: Vec> = Vec::with_capacity(images.len()); for canvas in canvases.iter() { let texture_creator = canvas.texture_creator(); texture_creators.push(texture_creator); } // Shrinks input image to fit if required and renders to the sdl canvas for (i, (canvas, texture_creator)) in canvases.iter_mut().zip(texture_creators.iter()).enumerate() { render_image_to_canvas( images[i], window_width, window_height, canvas, texture_creator, ); } let mut hidden_count = 0; // Create and start event loop to keep window open until Esc let mut event_pump = sdl.event_pump().unwrap(); event_pump.enable_event(sdl2::event::EventType::Window); 'running: loop { for event in event_pump.wait_iter() { match event { Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => break 'running, Event::KeyDown { keycode: Some(Keycode::Q), window_id, .. } | Event::Window { win_event: WindowEvent::Close, window_id, .. } => { for (i, canvas) in canvases.iter_mut().enumerate() { if window_id == canvas.window().id() { canvas.window_mut().hide(); window_visibility[i] = false; hidden_count += 1; } if hidden_count == images.len() { break 'running; } } } Event::Window { win_event: WindowEvent::Resized(w, h), window_id, .. } => { for (i, (canvas, texture_creator)) in canvases.iter_mut().zip(texture_creators.iter()).enumerate() { if window_id == canvas.window().id() { render_image_to_canvas( images[i], w as u32, h as u32, canvas, texture_creator, ); } } } _ => {} } } } } // Scale input image down if required so that it fits within a window of the given dimensions fn resize_to_fit(image: &I, window_width: u32, window_height: u32) -> RgbaImage where I: GenericImageView + ConvertBuffer, { let image = image.convert(); if image.height() < window_height && image.width() < window_width { return image; } let scale = { let width_scale = window_width as f32 / image.width() as f32; let height_scale = window_height as f32 / image.height() as f32; width_scale.min(height_scale) }; let height = (scale * image.height() as f32) as u32; let width = (scale * image.width() as f32) as u32; resize(&image, width, height, FilterType::Triangle) } fn render_image_to_canvas( image: &I, window_width: u32, window_height: u32, canvas: &mut Canvas, texture_creator: &TextureCreator, ) where I: GenericImageView + ConvertBuffer, { let scaled_image = resize_to_fit(image, window_width, window_height); let (image_width, image_height) = scaled_image.dimensions(); let mut buffer = scaled_image.into_raw(); const CHANNEL_COUNT: u32 = 4; let surface = Surface::from_data( &mut buffer, image_width, image_height, image_width * CHANNEL_COUNT, PixelFormatEnum::ABGR8888, // sdl2 expects bits from highest to lowest ) .expect("couldn't create surface"); let texture = texture_creator .create_texture_from_surface(surface) .expect("couldn't create texture from surface"); canvas.set_draw_color(Color::RGB(255, 255, 255)); canvas.clear(); let left = ((window_width - image_width) as f32 / 2f32) as i32; let top = ((window_height - image_height) as f32 / 2f32) as i32; canvas .copy( &texture, None, Rect::new(left, top, image_width, image_height), ) .unwrap(); canvas.present(); }