├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── ci.yaml ├── .gitignore ├── imgs ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.png ├── 5.png ├── 1_bw.jpg ├── tom.jpg ├── bricks.png ├── masks │ ├── 1_tile.jpg │ ├── 2_target.jpg │ ├── 2_example.jpg │ ├── 3_inpaint.jpg │ └── 4_sample_mask.png ├── multiexample │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ └── 4.jpg └── docs │ └── video-thumbnail.jpg ├── .githooks ├── pre-push └── setup.sh ├── about.toml ├── Cargo.toml ├── lib ├── examples │ ├── 01_single_example_synthesis.rs │ ├── 06_inpaint_channel.rs │ ├── 09_sample_masks.rs │ ├── 04_style_transfer.rs │ ├── 07_tiling_texture.rs │ ├── 03_guided_synthesis.rs │ ├── 08_repeat_transform.rs │ ├── 02_multi_example_synthesis.rs │ └── 05_inpaint.rs ├── src │ ├── unsync.rs │ ├── img_pyramid.rs │ ├── errors.rs │ ├── utils.rs │ ├── session.rs │ └── lib.rs ├── Cargo.toml ├── tests │ └── diff.rs └── benches │ └── all-the-things.rs ├── .mergify.yml ├── LICENSE-MIT ├── cli ├── Cargo.toml └── src │ ├── repeat.rs │ ├── progress_window.rs │ └── main.rs ├── deny.toml ├── .ci └── util.sh ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md ├── LICENSE-APACHE ├── README.md └── Cargo.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @Jake-Shadle 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | cli/target 3 | lib/target 4 | **/*.rs.bk 5 | /out 6 | -------------------------------------------------------------------------------- /imgs/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/1.jpg -------------------------------------------------------------------------------- /imgs/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/2.jpg -------------------------------------------------------------------------------- /imgs/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/3.jpg -------------------------------------------------------------------------------- /imgs/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/4.png -------------------------------------------------------------------------------- /imgs/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/5.png -------------------------------------------------------------------------------- /imgs/1_bw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/1_bw.jpg -------------------------------------------------------------------------------- /imgs/tom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/tom.jpg -------------------------------------------------------------------------------- /imgs/bricks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/bricks.png -------------------------------------------------------------------------------- /imgs/masks/1_tile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/masks/1_tile.jpg -------------------------------------------------------------------------------- /imgs/masks/2_target.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/masks/2_target.jpg -------------------------------------------------------------------------------- /imgs/multiexample/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/multiexample/1.jpg -------------------------------------------------------------------------------- /imgs/multiexample/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/multiexample/2.jpg -------------------------------------------------------------------------------- /imgs/multiexample/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/multiexample/3.jpg -------------------------------------------------------------------------------- /imgs/multiexample/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/multiexample/4.jpg -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run rustfmt and fail if there is a diff 4 | cargo fmt -- --check 5 | -------------------------------------------------------------------------------- /imgs/masks/2_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/masks/2_example.jpg -------------------------------------------------------------------------------- /imgs/masks/3_inpaint.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/masks/3_inpaint.jpg -------------------------------------------------------------------------------- /imgs/masks/4_sample_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/masks/4_sample_mask.png -------------------------------------------------------------------------------- /imgs/docs/video-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmbarkStudios/texture-synthesis/HEAD/imgs/docs/video-thumbnail.jpg -------------------------------------------------------------------------------- /about.toml: -------------------------------------------------------------------------------- 1 | accepted = [ 2 | "Apache-2.0", 3 | "BSD-2-Clause", 4 | "BSD-3-Clause", 5 | "CC0-1.0", 6 | "ISC", 7 | "MIT", 8 | "Zlib", 9 | ] 10 | -------------------------------------------------------------------------------- /.githooks/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run this script from your shell (git bash if you're on windows!) 4 | # * pre-push - Fails to push if you haven't run `cargo fmt` 5 | 6 | git config core.hooksPath .githooks 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "lib", 4 | "cli", 5 | ] 6 | 7 | [patch.crates-io] 8 | # https://github.com/abonander/img_hash/pull/30 9 | # * Updates to use image 0.22 10 | # * Fixes a bug with to/from_base64 11 | img_hash = { git = "https://github.com/EmbarkStudios/img_hash.git", rev = "c40da78" } 12 | -------------------------------------------------------------------------------- /lib/examples/01_single_example_synthesis.rs: -------------------------------------------------------------------------------- 1 | use texture_synthesis as ts; 2 | 3 | fn main() -> Result<(), ts::Error> { 4 | //create a new session 5 | let texsynth = ts::Session::builder() 6 | //load a single example image 7 | .add_example(&"imgs/1.jpg") 8 | .build()?; 9 | 10 | //generate an image 11 | let generated = texsynth.run(None); 12 | 13 | //save the image to the disk 14 | generated.save("out/01.jpg") 15 | } 16 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge when CI passes and 1 reviews 3 | conditions: 4 | - "#approved-reviews-by>=1" 5 | - "#review-requested=0" 6 | - "#changes-requested-reviews-by=0" 7 | - "#commented-reviews-by=0" 8 | - base=main 9 | - label!=work-in-progress 10 | actions: 11 | merge: 12 | method: squash 13 | - name: delete head branch after merge 14 | conditions: [] 15 | actions: 16 | delete_head_branch: {} 17 | -------------------------------------------------------------------------------- /lib/examples/06_inpaint_channel.rs: -------------------------------------------------------------------------------- 1 | use texture_synthesis as ts; 2 | 3 | fn main() -> Result<(), ts::Error> { 4 | let texsynth = ts::Session::builder() 5 | // Let the generator know that it is using 6 | .inpaint_example_channel( 7 | ts::ChannelMask::A, 8 | &"imgs/bricks.png", 9 | ts::Dims::square(400), 10 | ) 11 | .build()?; 12 | 13 | let generated = texsynth.run(None); 14 | 15 | //save the result to the disk 16 | generated.save("out/06.jpg") 17 | } 18 | -------------------------------------------------------------------------------- /lib/examples/09_sample_masks.rs: -------------------------------------------------------------------------------- 1 | use texture_synthesis as ts; 2 | 3 | fn main() -> Result<(), ts::Error> { 4 | let session = ts::Session::builder() 5 | .add_example( 6 | ts::Example::builder(&"imgs/4.png").set_sample_method(ts::SampleMethod::Ignore), 7 | ) 8 | .add_example(ts::Example::builder(&"imgs/5.png").set_sample_method(ts::SampleMethod::All)) 9 | .seed(211) 10 | .output_size(ts::Dims::square(200)) 11 | .build()?; 12 | 13 | // generate an image 14 | let generated = session.run(None); 15 | 16 | // save the image to the disk 17 | generated.save("out/09.png") 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /lib/examples/04_style_transfer.rs: -------------------------------------------------------------------------------- 1 | use texture_synthesis as ts; 2 | 3 | fn main() -> Result<(), ts::Error> { 4 | let texsynth = ts::Session::builder() 5 | // load example which will serve as our style, note you can have more than 1! 6 | .add_examples(&[&"imgs/multiexample/4.jpg"]) 7 | // load target which will be the content 8 | // with style transfer, we do not need to provide example guides 9 | // they will be auto-generated if none were provided 10 | .load_target_guide(&"imgs/tom.jpg") 11 | .build()?; 12 | 13 | // generate an image that applies 'style' to "tom.jpg" 14 | let generated = texsynth.run(None); 15 | 16 | // save the result to the disk 17 | generated.save("out/04.jpg") 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Device:** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /lib/examples/07_tiling_texture.rs: -------------------------------------------------------------------------------- 1 | use texture_synthesis as ts; 2 | 3 | fn main() -> Result<(), ts::Error> { 4 | // Let's start layering some of the "verbs" of texture synthesis 5 | // if we just run tiling_mode(true) we will generate a completely new image from scratch (try it!) 6 | // but what if we want to tile an existing image? 7 | // we can use inpaint! 8 | 9 | let texsynth = ts::Session::builder() 10 | // load a mask that specifies borders of the image we can modify to make it tiling 11 | .inpaint_example( 12 | &"imgs/masks/1_tile.jpg", 13 | ts::Example::new(&"imgs/1.jpg"), 14 | ts::Dims::square(400), 15 | ) 16 | //turn on tiling mode! 17 | .tiling_mode(true) 18 | .build()?; 19 | 20 | let generated = texsynth.run(None); 21 | 22 | generated.save("out/07.jpg") 23 | } 24 | -------------------------------------------------------------------------------- /lib/examples/03_guided_synthesis.rs: -------------------------------------------------------------------------------- 1 | use texture_synthesis as ts; 2 | 3 | fn main() -> Result<(), ts::Error> { 4 | let texsynth = ts::Session::builder() 5 | // NOTE: it is important that example(s) and their corresponding guides have same size(s) 6 | // you can ensure that by overwriting the input images sizes with .resize_input() 7 | .add_example(ts::Example::builder(&"imgs/2.jpg").with_guide(&"imgs/masks/2_example.jpg")) 8 | // load target "heart" shape that we would like the generated image to look like 9 | // now the generator will take our target guide into account during synthesis 10 | .load_target_guide(&"imgs/masks/2_target.jpg") 11 | .build()?; 12 | 13 | let generated = texsynth.run(None); 14 | 15 | // save the image to the disk 16 | generated.save("out/03.jpg") 17 | 18 | // You can also do a more involved segmentation with guide maps with R G B annotating specific features of your examples 19 | } 20 | -------------------------------------------------------------------------------- /lib/examples/08_repeat_transform.rs: -------------------------------------------------------------------------------- 1 | use texture_synthesis as ts; 2 | 3 | fn main() -> Result<(), ts::Error> { 4 | // create a new session 5 | let texsynth = ts::Session::builder() 6 | //load a single example image 7 | .add_example(&"imgs/1.jpg") 8 | .build()?; 9 | 10 | // generate an image 11 | let generated = texsynth.run(None); 12 | 13 | // now we can apply the same transformation of the generated image 14 | // onto a new image (which can be used to ensure 1-1 mapping between multiple images) 15 | // NOTE: it is important to provide same number and image dimensions as the examples used for synthesis 16 | // otherwise, there will be coordinates mismatch 17 | let repeat_transform_img = generated 18 | .get_coordinate_transform() 19 | .apply(&["imgs/1_bw.jpg"])?; 20 | 21 | // save the image to the disk 22 | // 08 and 08_repeated images should match perfectly 23 | repeat_transform_img.save("out/08_repeated.jpg").unwrap(); 24 | generated.save("out/08.jpg") 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Embark Studios 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /lib/src/unsync.rs: -------------------------------------------------------------------------------- 1 | //! Unsynchronized plain old data vector and image for fast access from multiple threads 2 | 3 | use std::cell::UnsafeCell; 4 | 5 | pub struct UnsyncVec(UnsafeCell>); 6 | pub struct UnsyncRgbaImage(UnsafeCell); 7 | 8 | impl UnsyncVec { 9 | pub fn new(v: Vec) -> Self { 10 | Self(UnsafeCell::new(v)) 11 | } 12 | 13 | pub unsafe fn assign_at(&self, idx: usize, value: T) { 14 | self.0.get().as_mut().unwrap()[idx] = value; 15 | } 16 | 17 | pub fn as_ref(&self) -> &[T] { 18 | unsafe { self.0.get().as_ref() }.unwrap() 19 | } 20 | } 21 | 22 | impl UnsyncRgbaImage { 23 | pub fn new(img: image::RgbaImage) -> Self { 24 | Self(UnsafeCell::new(img)) 25 | } 26 | 27 | pub fn as_ref(&self) -> &image::RgbaImage { 28 | unsafe { self.0.get().as_ref() }.unwrap() 29 | } 30 | 31 | pub fn into_inner(self) -> image::RgbaImage { 32 | self.0.into_inner() 33 | } 34 | 35 | pub fn put_pixel(&self, x: u32, y: u32, pixel: image::Rgba) { 36 | unsafe { self.0.get().as_mut() } 37 | .unwrap() 38 | .put_pixel(x, y, pixel); 39 | } 40 | } 41 | 42 | unsafe impl Sync for UnsyncVec {} 43 | unsafe impl Sync for UnsyncRgbaImage {} 44 | -------------------------------------------------------------------------------- /lib/examples/02_multi_example_synthesis.rs: -------------------------------------------------------------------------------- 1 | use texture_synthesis as ts; 2 | 3 | fn main() -> Result<(), ts::Error> { 4 | // create a new session 5 | let texsynth = ts::Session::builder() 6 | // load multiple example image 7 | .add_examples(&[ 8 | &"imgs/multiexample/1.jpg", 9 | &"imgs/multiexample/2.jpg", 10 | &"imgs/multiexample/3.jpg", 11 | &"imgs/multiexample/4.jpg", 12 | ]) 13 | // we can ensure all of them come with same size 14 | // that is however optional, the generator doesnt care whether all images are same sizes 15 | // however, if you have guides or other additional maps, those have to be same size(s) as corresponding example(s) 16 | .resize_input(ts::Dims { 17 | width: 300, 18 | height: 300, 19 | }) 20 | // randomly initialize first 10 pixels 21 | .random_init(10) 22 | .seed(211) 23 | .build()?; 24 | 25 | // generate an image 26 | let generated = texsynth.run(None); 27 | 28 | // save the image to the disk 29 | generated.save("out/02.jpg")?; 30 | 31 | //save debug information to see "remixing" borders of different examples in map_id.jpg 32 | //different colors represent information coming from different maps 33 | generated.save_debug("out/") 34 | } 35 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "texture-synthesis-cli" 3 | description = "A CLI for texture-synthesis" 4 | repository = "https://github.com/EmbarkStudios/texture-synthesis" 5 | version = "0.8.3" 6 | authors = [ 7 | "Embark ", 8 | "Anastasia Opara ", 9 | "Tomasz Stachowiak ", 10 | ] 11 | edition = "2018" 12 | license = "MIT OR Apache-2.0" 13 | readme = "../README.md" 14 | documentation = "https://docs.rs/texture-synthesis" 15 | homepage = "https://github.com/EmbarkStudios/texture-synthesis" 16 | categories = ["multimedia::images"] 17 | keywords = ["texture", "synthesis", "procedural", "cli"] 18 | 19 | [badges.maintenance] 20 | status = "looking-for-maintainer" 21 | 22 | [[bin]] 23 | name = "texture-synthesis" 24 | path = "src/main.rs" 25 | bench = false 26 | 27 | [dependencies] 28 | structopt = "0.3" 29 | texture-synthesis = { version = "0.8.2", path = "../lib" } 30 | 31 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 32 | # We unfortunately can't use clicolors-control which is used by indicatif 33 | # because it only ever operates on stdout, even though we only ever print 34 | # to stderr. This is also why indicatif colors don't work if you pipe 35 | # the image output :( 36 | atty = "0.2" 37 | indicatif = "0.16" 38 | minifb = { version = "0.19", default-features = false, features = [ 39 | "x11", 40 | ], optional = true } 41 | 42 | [features] 43 | default = [] 44 | progress = ["minifb"] 45 | -------------------------------------------------------------------------------- /lib/src/img_pyramid.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone)] 2 | pub struct ImagePyramid { 3 | pub pyramid: Vec, 4 | } 5 | 6 | impl ImagePyramid { 7 | pub fn new(in_img: image::RgbaImage, levels: Option) -> Self { 8 | let lvls = levels.unwrap_or_else(|| { 9 | //auto-calculate max number of downsampling 10 | let (dimx, dimy) = in_img.dimensions(); 11 | (f64::from(dimx.max(dimy))).log2() as u32 // pow(2, x) ~ img => x ~ log2(img) 12 | }); 13 | 14 | Self { 15 | pyramid: Self::build_gaussian(lvls, in_img), 16 | } 17 | } 18 | 19 | //build gaussian pyramid by downsampling the image by 2 20 | fn build_gaussian(in_lvls: u32, in_img: image::RgbaImage) -> Vec { 21 | let mut imgs = Vec::new(); 22 | let (dimx, dimy) = in_img.dimensions(); 23 | 24 | //going from lowest to largest resolution (to match the texture synthesis generation order) 25 | for i in (1..in_lvls).rev() { 26 | let p = u32::pow(2, i); 27 | imgs.push(image::imageops::resize( 28 | &image::imageops::resize(&in_img, dimx / p, dimy / p, image::imageops::Gaussian), 29 | dimx, 30 | dimy, 31 | image::imageops::Gaussian, 32 | )); 33 | } 34 | 35 | imgs.push(in_img); 36 | imgs 37 | } 38 | 39 | pub fn bottom(&self) -> &image::RgbaImage { 40 | &self.pyramid[self.pyramid.len() - 1] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | targets = [ 2 | { triple = "x86_64-unknown-linux-gnu" }, 3 | { triple = "x86_64-unknown-linux-musl" }, 4 | { triple = "x86_64-apple-darwin" }, 5 | { triple = "x86_64-pc-windows-msvc" }, 6 | ] 7 | 8 | [advisories] 9 | unmaintained = "deny" 10 | ignore = [ 11 | "RUSTSEC-2022-0004", 12 | "RUSTSEC-2021-0127", 13 | ] 14 | 15 | [bans] 16 | multiple-versions = "allow" 17 | deny = [ 18 | # color-backtrace is nice but brings in too many dependencies and that are often outdated, so not worth it for us. 19 | { name = "color-backtrace" }, 20 | 21 | # dirs crate has a lot of dependencies and there are better alternatives 22 | { name = "dirs" }, 23 | { name = "dirs-sys" }, 24 | 25 | # deprecated 26 | { name = "quickersort" }, 27 | 28 | # term is not fully maintained, and termcolor is replacing it 29 | { name = "term" }, 30 | ] 31 | skip = [ 32 | ] 33 | 34 | [licenses] 35 | unlicensed = "deny" 36 | # We want really high confidence when inferring licenses from text 37 | confidence-threshold = 0.92 38 | allow = [ 39 | "Apache-2.0", 40 | "MIT", 41 | ] 42 | exceptions = [ 43 | { allow = ["BSD-3-Clause"], name = "bindgen" }, 44 | { allow = ["ISC"], name = "libloading" }, 45 | { allow = ["Zlib"], name = "adler32" }, 46 | { allow = ["Zlib"], name = "bytemuck" }, 47 | { allow = ["WTFPL"], name = "xkbcommon-sys" }, 48 | { allow = ["WTFPL"], name = "xkb" }, 49 | ] 50 | 51 | [sources] 52 | unknown-git = "deny" 53 | allow-git = [ 54 | "https://github.com/EmbarkStudios/img_hash.git", 55 | ] 56 | -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "texture-synthesis" 3 | description = "Multiresolution Stochastic Texture Synthesis, a non-parametric example-based algorithm for image generation" 4 | repository = "https://github.com/EmbarkStudios/texture-synthesis" 5 | version = "0.8.2" 6 | authors = [ 7 | "Embark ", 8 | "Anastasia Opara ", 9 | "Tomasz Stachowiak ", 10 | ] 11 | edition = "2018" 12 | license = "MIT OR Apache-2.0" 13 | readme = "../README.md" 14 | documentation = "https://docs.rs/texture-synthesis" 15 | homepage = "https://github.com/EmbarkStudios/texture-synthesis" 16 | categories = ["multimedia::images"] 17 | keywords = ["texture", "synthesis", "procedural"] 18 | 19 | # We don't want the example images in the publish crate 20 | exclude = ["/imgs"] 21 | 22 | [badges.maintenance] 23 | status = "looking-for-maintainer" 24 | 25 | [dependencies] 26 | num_cpus = "1.13" 27 | # avoid bringing in OS random gen that we don't use 28 | rand = { version = "0.8", default-features = false } 29 | rand_pcg = "0.3" 30 | rstar = "0.7" 31 | 32 | # Pinned to this version as there seems to be a change that affects output 33 | [dependencies.image] 34 | version = "=0.23.12" 35 | default-features = false 36 | features = ["jpeg", "png", "bmp"] 37 | 38 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 39 | crossbeam-utils = "0.8" 40 | 41 | [dev-dependencies] 42 | criterion = "0.3" 43 | img_hash = { version = "2.1.0", default-features = false } 44 | 45 | [lib] 46 | bench = false 47 | 48 | [[bench]] 49 | name = "all-the-things" 50 | harness = false 51 | -------------------------------------------------------------------------------- /lib/examples/05_inpaint.rs: -------------------------------------------------------------------------------- 1 | use texture_synthesis as ts; 2 | 3 | fn main() -> Result<(), ts::Error> { 4 | let texsynth = ts::Session::builder() 5 | // let the generator know which part we would like to fill in 6 | // if we had more examples, they would be additional information 7 | // the generator could use to inpaint 8 | .inpaint_example( 9 | &"imgs/masks/3_inpaint.jpg", 10 | // load a "corrupted" example with missing red information we would like to fill in 11 | ts::Example::builder(&"imgs/3.jpg") 12 | // we would also like to prevent sampling from "corrupted" red areas 13 | // otherwise, generator will treat that those as valid areas it can copy from in the example, 14 | // we could also use SampleMethod::Ignore to ignore the example altogether, but we 15 | // would then need at least 1 other example image to actually source from 16 | // example.set_sample_method(ts::SampleMethod::Ignore); 17 | .set_sample_method(&"imgs/masks/3_inpaint.jpg"), 18 | // Inpaint requires that inputs and outputs be the same size, so it's a required 19 | // parameter that overrides both `resize_input` and `output_size` 20 | ts::Dims::square(400), 21 | ) 22 | // Ignored 23 | .resize_input(ts::Dims::square(200)) 24 | // Ignored 25 | .output_size(ts::Dims::square(100)) 26 | .build()?; 27 | 28 | let generated = texsynth.run(None); 29 | 30 | //save the result to the disk 31 | generated.save("out/05.jpg") 32 | } 33 | -------------------------------------------------------------------------------- /.ci/util.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | name=$(basename "$0") 5 | root=$(realpath "$(dirname "$(dirname "$0")")") 6 | target_dir="$root/lib/target" 7 | 8 | if ! [ -x "$(command -v critcmp)" ]; then 9 | echo "Installing critcmp..." 10 | cargo install critcmp 11 | fi 12 | 13 | function sub_help() { 14 | printf "Usage: %s [options]\n" "$name" 15 | echo "Subcommands:" 16 | echo " export Exports the specified baselines to json in the benches dir" 17 | echo " cmp Compare two benchmarks by name" 18 | echo "" 19 | echo "For help with each subcommand run:" 20 | echo "$name -h | --help" 21 | echo "" 22 | } 23 | 24 | function sub_cmp() { 25 | critcmp --target-dir "$target_dir" "$@" 26 | } 27 | 28 | function sub_export() { 29 | critcmp --target-dir "$target_dir" --export "$1" > "$root/lib/benches/${2:-$1}.json" 30 | } 31 | 32 | function sub_run_examples() { 33 | prefix=${1:-0} 34 | 35 | cargo build --release 36 | target/release/texture-synthesis --out out/"${prefix}"1.jpg generate imgs/1.jpg 37 | target/release/texture-synthesis --rand-init 10 --seed 211 --in-size 300 -o out/"${prefix}"2.png generate imgs/multiexample/1.jpg imgs/multiexample/2.jpg imgs/multiexample/3.jpg imgs/multiexample/4.jpg 38 | target/release/texture-synthesis -o out/"${prefix}"3.png generate --target-guide imgs/masks/2_target.jpg --guides imgs/masks/2_example.jpg -- imgs/2.jpg 39 | target/release/texture-synthesis -o out/"${prefix}"4.png transfer-style --style imgs/multiexample/4.jpg --guide imgs/tom.jpg 40 | target/release/texture-synthesis --in-size 400 --out-size 400 --inpaint imgs/masks/3_inpaint.jpg -o out/"${prefix}"5.png generate imgs/3.jpg 41 | target/release/texture-synthesis --inpaint imgs/masks/1_tile.jpg --in-size 400 --out-size 400 --tiling -o out/"${prefix}"6.bmp generate imgs/1.jpg 42 | } 43 | 44 | subcommand=$1 45 | case $subcommand in 46 | "" | "-h" | "--help") 47 | sub_help 48 | ;; 49 | *) 50 | shift 51 | "sub_${subcommand}" "$@" 52 | if [ $? = 127 ]; then 53 | echo "Error: '$subcommand' is not a known subcommand." >&2 54 | echo " Run '$name --help' for a list of known subcommands." >&2 55 | exit 1 56 | fi 57 | ;; 58 | esac 59 | -------------------------------------------------------------------------------- /cli/src/repeat.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | }; 5 | use structopt::StructOpt; 6 | use texture_synthesis::{self as ts, Error, ImageSource}; 7 | 8 | #[derive(StructOpt)] 9 | #[structopt(rename_all = "kebab-case")] 10 | pub(crate) struct Args { 11 | /// Path to the transform to apply to the examples. Will default to 12 | /// `generate.tranform` in the current directory if not specified. 13 | #[structopt(long = "transform", parse(from_os_str))] 14 | transform: PathBuf, 15 | /// Path(s) to example images used to synthesize a new image 16 | #[structopt(parse(from_os_str))] 17 | examples: Vec, 18 | } 19 | 20 | pub(crate) fn cmd(args: &Args, global_opts: &crate::Opt) -> Result<(), Error> { 21 | let transform_path = &args.transform; 22 | 23 | let xform = { 24 | let mut transform = fs::File::open(&transform_path).map_err(|e| { 25 | eprintln!( 26 | "unable to open transform path '{}': {}", 27 | transform_path.display(), 28 | e 29 | ); 30 | e 31 | })?; 32 | 33 | ts::CoordinateTransform::read(&mut transform).map_err(|e| { 34 | eprintln!( 35 | "failed to load transform from '{}': {}", 36 | transform_path.display(), 37 | e 38 | ); 39 | e 40 | })? 41 | }; 42 | 43 | // Load the example images. We resize each of them to the dimensions of the 44 | // coordinate transform as each pixel can be sampled from any of them, at 45 | // any location within the dimensions of the transform 46 | let inputs = args 47 | .examples 48 | .iter() 49 | .map(|path| ImageSource::from_path(path)); 50 | 51 | let new_img = xform.apply(inputs)?; 52 | 53 | if global_opts.output_path.to_str() == Some("-") { 54 | let out = std::io::stdout(); 55 | let mut out = out.lock(); 56 | 57 | let dyn_img = ts::image::DynamicImage::ImageRgba8(new_img); 58 | dyn_img.write_to(&mut out, global_opts.stdout_fmt.clone())?; 59 | } else { 60 | new_img.save(&global_opts.output_path)?; 61 | } 62 | 63 | Ok(()) 64 | } 65 | 66 | pub(crate) fn save_coordinate_transform( 67 | generated: &ts::GeneratedImage, 68 | path: &Path, 69 | ) -> Result<(), Error> { 70 | if let Some(parent) = path.parent() { 71 | std::fs::create_dir_all(parent)?; 72 | } 73 | 74 | { 75 | let xform = generated.get_coordinate_transform(); 76 | let mut output = std::io::BufWriter::new(std::fs::File::create(path)?); 77 | xform.write(&mut output)?; 78 | } 79 | 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Embark Contributor Guidelines 2 | 3 | Welcome! This project is created by the team at [Embark Studios](https://embark.games). We're glad you're interested in contributing! We welcome contributions from people of all backgrounds who are interested in making great software with us. 4 | 5 | At Embark, we aspire to empower everyone to create interactive experiences. To do this, we're exploring and pushing the boundaries of new technologies, and sharing our learnings with the open source community. 6 | 7 | If you have any difficulties getting involved or finding answers to your questions, please don't hesitate to ask your questions in our [Discord server](https://discord.com/invite/8TW9nfF). 8 | 9 | If you have ideas for collaboration, email us at opensource@embark-studios.com. 10 | 11 | We're also hiring full-time engineers to work with us in Stockholm! Check out our current job postings [here](https://www.embark-studios.com/jobs). 12 | 13 | ## Issues 14 | 15 | ### Feature Requests 16 | 17 | If you have ideas or how to improve our projects, you can suggest features by opening a GitHub issue. Make sure to include details about the feature or change, and describe any uses cases it would enable. 18 | 19 | Feature requests will be tagged as `enhancement` and their status will be updated in the comments of the issue. 20 | 21 | ### Bugs 22 | 23 | When reporting a bug or unexpected behaviour in a project, make sure your issue descibes steps to reproduce the behaviour, including the platform you were using, what steps you took, and any error messages. 24 | 25 | Reproducible bugs will be tagged as `bug` and their status will be updated in the comments of the issue. 26 | 27 | ### Wontfix 28 | 29 | Issues will be closed and tagged as `wontfix` if we decide that we do not wish to implement it, usually due to being misaligned with the project vision or out of scope. We will comment on the issue with more detailed reasoning. 30 | 31 | ## Contribution Workflow 32 | 33 | ### Open Issues 34 | 35 | If you're ready to contribute, start by looking at our open issues tagged as [`help wanted`](../../issues?q=is%3Aopen+is%3Aissue+label%3A"help+wanted") or [`good first issue`](../../issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue"). 36 | 37 | You can comment on the issue to let others know you're interested in working on it or to ask questions. 38 | 39 | ### Making Changes 40 | 41 | 1. Fork the repository. 42 | 43 | 2. Create a new feature branch. 44 | 45 | 3. Make your changes. Ensure that there are no build errors by running the project with your changes locally. 46 | 47 | 4. Open a pull request with a name and description of what you did. You can read more about working with pull requests on GitHub [here](https://help.github.com/en/articles/creating-a-pull-request-from-a-fork). 48 | 49 | 5. A maintainer will review your pull request and may ask you to make changes. 50 | 51 | ## Code Guidelines 52 | 53 | You can read about our standards and recommendations for working with Rust [here](https://github.com/EmbarkStudios/rust-ecosystem/blob/main/guidelines.md). 54 | 55 | ## Licensing 56 | 57 | Unless otherwise specified, all Embark open source projects are licensed under a dual MIT OR Apache-2.0 license, allowing licensees to chose either at their option. You can read more in each project's respective README. 58 | 59 | ## Code of Conduct 60 | 61 | Please note that our projects are released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md) to ensure that they are welcoming places for everyone to contribute. By participating in any Embark open source project, you agree to abide by these terms. 62 | -------------------------------------------------------------------------------- /lib/src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug)] 4 | pub struct InvalidRange { 5 | pub(crate) min: f32, 6 | pub(crate) max: f32, 7 | pub(crate) value: f32, 8 | pub(crate) name: &'static str, 9 | } 10 | 11 | impl fmt::Display for InvalidRange { 12 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | write!( 14 | f, 15 | "parameter '{}' - value '{}' is outside the range of {}-{}", 16 | self.name, self.value, self.min, self.max 17 | ) 18 | } 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct SizeMismatch { 23 | pub(crate) input: (u32, u32), 24 | pub(crate) output: (u32, u32), 25 | } 26 | 27 | impl fmt::Display for SizeMismatch { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | write!( 30 | f, 31 | "the input size ({}x{}) must match the output size ({}x{}) when using an inpaint mask", 32 | self.input.0, self.input.1, self.output.0, self.output.1 33 | ) 34 | } 35 | } 36 | 37 | #[derive(Debug)] 38 | pub enum Error { 39 | /// An error in the image library occurred, eg failed to load/save 40 | Image(image::ImageError), 41 | /// An input parameter had an invalid range specified 42 | InvalidRange(InvalidRange), 43 | /// When using inpaint, the input and output sizes must match 44 | SizeMismatch(SizeMismatch), 45 | /// If more than 1 example guide is provided, then **all** examples must have 46 | /// a guide 47 | ExampleGuideMismatch(u32, u32), 48 | /// Io is notoriously error free with no problems, but we cover it just in case! 49 | Io(std::io::Error), 50 | /// The user specified an image format we don't support as the output 51 | UnsupportedOutputFormat(String), 52 | /// There are no examples to source pixels from, either because no examples 53 | /// were added, or all of them used SampleMethod::Ignore 54 | NoExamples, 55 | /// 56 | MapsCountMismatch(u32, u32), 57 | } 58 | 59 | impl std::error::Error for Error { 60 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 61 | match self { 62 | Self::Image(err) => Some(err), 63 | Self::Io(err) => Some(err), 64 | _ => None, 65 | } 66 | } 67 | } 68 | 69 | impl fmt::Display for Error { 70 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 71 | match self { 72 | Self::Image(ie) => write!(f, "{}", ie), 73 | Self::InvalidRange(ir) => write!(f, "{}", ir), 74 | Self::SizeMismatch(sm) => write!(f, "{}", sm), 75 | Self::ExampleGuideMismatch(examples, guides) => { 76 | if examples > guides { 77 | write!( 78 | f, 79 | "{} examples were provided, but only {} guides were", 80 | examples, guides 81 | ) 82 | } else { 83 | write!( 84 | f, 85 | "{} examples were provided, but {} guides were", 86 | examples, guides 87 | ) 88 | } 89 | } 90 | Self::Io(io) => write!(f, "{}", io), 91 | Self::UnsupportedOutputFormat(fmt) => { 92 | write!(f, "the output format '{}' is not supported", fmt) 93 | } 94 | Self::NoExamples => write!( 95 | f, 96 | "at least 1 example must be available as a sampling source" 97 | ), 98 | Self::MapsCountMismatch(input, required) => write!( 99 | f, 100 | "{} map(s) were provided, but {} is/are required", 101 | input, required 102 | ), 103 | } 104 | } 105 | } 106 | 107 | impl From for Error { 108 | fn from(ie: image::ImageError) -> Self { 109 | Self::Image(ie) 110 | } 111 | } 112 | 113 | impl From for Error { 114 | fn from(io: std::io::Error) -> Self { 115 | Self::Io(io) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /cli/src/progress_window.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "progress")] 2 | use texture_synthesis::{Dims, Error}; 3 | 4 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 5 | #[cfg(feature = "progress")] 6 | use minifb::Window; 7 | 8 | pub struct ProgressWindow { 9 | #[cfg(feature = "progress")] 10 | window: Option<(Window, std::time::Duration, std::time::Instant)>, 11 | 12 | total_pb: ProgressBar, 13 | stage_pb: ProgressBar, 14 | 15 | total_len: usize, 16 | stage_len: usize, 17 | stage_num: u32, 18 | } 19 | 20 | impl ProgressWindow { 21 | pub fn new() -> Self { 22 | let multi_pb = MultiProgress::new(); 23 | let sty = ProgressStyle::default_bar() 24 | .template("[{elapsed_precise}] {bar:40.cyan/blue} {percent}%") 25 | .progress_chars("##-"); 26 | 27 | let total_pb = multi_pb.add(ProgressBar::new(100)); 28 | total_pb.set_style(sty); 29 | 30 | let sty = ProgressStyle::default_bar() 31 | .template(" stage {msg:>3} {bar:40.cyan/blue} {percent}%") 32 | .progress_chars("##-"); 33 | let stage_pb = multi_pb.add(ProgressBar::new(100)); 34 | stage_pb.set_style(sty); 35 | 36 | std::thread::spawn(move || { 37 | let _ = multi_pb.join(); 38 | }); 39 | 40 | Self { 41 | #[cfg(feature = "progress")] 42 | window: None, 43 | total_pb, 44 | stage_pb, 45 | total_len: 100, 46 | stage_len: 100, 47 | stage_num: 0, 48 | } 49 | } 50 | 51 | #[cfg(feature = "progress")] 52 | pub fn with_preview( 53 | mut self, 54 | size: Dims, 55 | update_every: std::time::Duration, 56 | ) -> Result { 57 | let window = Window::new( 58 | "Texture Synthesis", 59 | size.width as usize, 60 | size.height as usize, 61 | minifb::WindowOptions::default(), 62 | ) 63 | .unwrap(); 64 | 65 | self.window = Some((window, update_every, std::time::Instant::now())); 66 | 67 | Ok(self) 68 | } 69 | } 70 | 71 | impl Drop for ProgressWindow { 72 | fn drop(&mut self) { 73 | self.total_pb.finish(); 74 | self.stage_pb.finish(); 75 | } 76 | } 77 | 78 | impl texture_synthesis::session::GeneratorProgress for ProgressWindow { 79 | fn update(&mut self, update: texture_synthesis::session::ProgressUpdate<'_>) { 80 | if update.total.total != self.total_len { 81 | self.total_len = update.total.total; 82 | self.total_pb.set_length(self.total_len as u64); 83 | } 84 | 85 | if update.stage.total != self.stage_len { 86 | self.stage_len = update.stage.total; 87 | self.stage_pb.set_length(self.stage_len as u64); 88 | self.stage_num += 1; 89 | self.stage_pb.set_message(self.stage_num.to_string()); 90 | } 91 | 92 | self.total_pb.set_position(update.total.current as u64); 93 | self.stage_pb.set_position(update.stage.current as u64); 94 | 95 | #[cfg(feature = "progress")] 96 | { 97 | if let Some((ref mut window, ref dur, ref mut last_update)) = self.window { 98 | let now = std::time::Instant::now(); 99 | 100 | if now - *last_update < *dur { 101 | return; 102 | } 103 | 104 | *last_update = now; 105 | 106 | if !window.is_open() { 107 | return; 108 | } 109 | 110 | let pixels = &update.image; 111 | if pixels.len() % 4 != 0 { 112 | return; 113 | } 114 | 115 | // The pixel channels are in a different order so the colors are 116 | // incorrect in the window, but at least the shape and unfilled pixels 117 | // are still apparent 118 | let pixels: &[u32] = unsafe { 119 | let raw_pixels: &[u8] = pixels; 120 | #[allow(clippy::cast_ptr_alignment, clippy::cast_slice_different_sizes)] 121 | &*(raw_pixels as *const [u8] as *const [u32]) 122 | }; 123 | 124 | // We don't particularly care if this fails 125 | let _ = window.update_with_buffer( 126 | pixels, 127 | update.image.width() as usize, 128 | update.image.height() as usize, 129 | ); 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{Dims, Error}; 2 | use std::path::Path; 3 | 4 | /// Helper type used to define the source of `ImageSource`'s data 5 | #[derive(Clone)] 6 | pub enum ImageSource<'a> { 7 | /// A raw buffer of image data, see `image::load_from_memory` for details 8 | /// on what is supported 9 | Memory(&'a [u8]), 10 | /// The path to an image to load from disk. The image format is inferred 11 | /// from the file extension, see `image::open` for details 12 | Path(&'a Path), 13 | /// An already loaded image that is passed directly to the generator 14 | Image(image::DynamicImage), 15 | } 16 | 17 | impl<'a> ImageSource<'a> { 18 | pub fn from_path(path: &'a Path) -> Self { 19 | Self::Path(path) 20 | } 21 | } 22 | 23 | impl<'a> From for ImageSource<'a> { 24 | fn from(img: image::DynamicImage) -> Self { 25 | Self::Image(img) 26 | } 27 | } 28 | 29 | impl<'a, S> From<&'a S> for ImageSource<'a> 30 | where 31 | S: AsRef + 'a, 32 | { 33 | fn from(path: &'a S) -> Self { 34 | Self::Path(path.as_ref()) 35 | } 36 | } 37 | 38 | pub fn load_dynamic_image(src: ImageSource<'_>) -> Result { 39 | match src { 40 | ImageSource::Memory(data) => image::load_from_memory(data), 41 | ImageSource::Path(path) => image::open(path), 42 | ImageSource::Image(img) => Ok(img), 43 | } 44 | } 45 | 46 | /// Helper type used to mask `ImageSource`'s channels 47 | #[derive(Clone, Copy)] 48 | pub enum ChannelMask { 49 | R, 50 | G, 51 | B, 52 | A, 53 | } 54 | 55 | pub(crate) fn load_image( 56 | src: ImageSource<'_>, 57 | resize: Option, 58 | ) -> Result { 59 | let img = load_dynamic_image(src)?; 60 | 61 | let img = match resize { 62 | None => img.to_rgba8(), 63 | Some(ref size) => { 64 | use image::GenericImageView; 65 | 66 | if img.width() != size.width || img.height() != size.height { 67 | image::imageops::resize( 68 | &img.to_rgba8(), 69 | size.width, 70 | size.height, 71 | image::imageops::CatmullRom, 72 | ) 73 | } else { 74 | img.to_rgba8() 75 | } 76 | } 77 | }; 78 | 79 | Ok(img) 80 | } 81 | 82 | pub(crate) fn apply_mask(mut image: image::RgbaImage, mask: ChannelMask) -> image::RgbaImage { 83 | let channel = match mask { 84 | ChannelMask::R => 0, 85 | ChannelMask::G => 1, 86 | ChannelMask::B => 2, 87 | ChannelMask::A => 3, 88 | }; 89 | 90 | for pixel_iter in image.enumerate_pixels_mut() { 91 | let pixel = pixel_iter.2; 92 | pixel[0] = pixel[channel]; 93 | pixel[1] = pixel[channel]; 94 | pixel[2] = pixel[channel]; 95 | pixel[3] = 255; 96 | } 97 | 98 | image 99 | } 100 | 101 | pub(crate) fn transform_to_guide_map( 102 | image: image::RgbaImage, 103 | size: Option, 104 | blur_sigma: f32, 105 | ) -> image::RgbaImage { 106 | use image::GenericImageView; 107 | let dyn_img = image::DynamicImage::ImageRgba8(image); 108 | 109 | if let Some(s) = size { 110 | if dyn_img.width() != s.width || dyn_img.height() != s.height { 111 | dyn_img.resize(s.width, s.height, image::imageops::Triangle); 112 | } 113 | } 114 | 115 | dyn_img.blur(blur_sigma).grayscale().to_rgba8() 116 | } 117 | 118 | pub(crate) fn get_histogram(img: &image::RgbaImage) -> Vec { 119 | let mut hist = vec![0; 256]; //0-255 incl 120 | 121 | let pixels = &img; 122 | 123 | //populate the hist 124 | for pixel_value in pixels 125 | .iter() 126 | .step_by(/*since RGBA image, we only care for 1st channel*/ 4) 127 | { 128 | hist[*pixel_value as usize] += 1; //increment histogram by 1 129 | } 130 | 131 | hist 132 | } 133 | 134 | //source will be modified to fit the target 135 | pub(crate) fn match_histograms(source: &mut image::RgbaImage, target: &image::RgbaImage) { 136 | let target_hist = get_histogram(target); 137 | let source_hist = get_histogram(source); 138 | 139 | //get commutative distrib 140 | let target_cdf = get_cdf(&target_hist); 141 | let source_cdf = get_cdf(&source_hist); 142 | 143 | //clone the source image, modify and return 144 | let (dx, dy) = source.dimensions(); 145 | 146 | for x in 0..dx { 147 | for y in 0..dy { 148 | let pixel_value = source.get_pixel(x, y)[0]; //we only care about the first channel 149 | let pixel_source_cdf = source_cdf[pixel_value as usize]; 150 | 151 | //now need to find by value similar cdf in the target 152 | let new_pixel_val = target_cdf 153 | .iter() 154 | .position(|cdf| *cdf > pixel_source_cdf) 155 | .unwrap_or((pixel_value + 1) as usize) as u8 156 | - 1; 157 | 158 | let new_color: image::Rgba = 159 | image::Rgba([new_pixel_val, new_pixel_val, new_pixel_val, 255]); 160 | source.put_pixel(x, y, new_color); 161 | } 162 | } 163 | } 164 | 165 | pub(crate) fn get_cdf(a: &[u32]) -> Vec { 166 | let mut cumm = vec![0.0; 256]; 167 | 168 | for i in 0..a.len() { 169 | if i != 0 { 170 | cumm[i] = cumm[i - 1] + (a[i] as f32); 171 | } else { 172 | cumm[i] = a[i] as f32; 173 | } 174 | } 175 | 176 | //normalize 177 | let max = cumm[255]; 178 | for i in cumm.iter_mut() { 179 | *i /= max; 180 | } 181 | 182 | cumm 183 | } 184 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | opensource@embark-studios.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | tags: 6 | - "*" 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | name: CI 14 | jobs: 15 | lint: 16 | name: Lint 17 | runs-on: ubuntu-20.04 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: stable 23 | override: true 24 | 25 | # make sure all code has been formatted with rustfmt 26 | - run: rustup component add rustfmt 27 | - name: check rustfmt 28 | run: cargo fmt -- --check --color always 29 | 30 | # run clippy to verify we have no warnings 31 | - run: cargo fetch 32 | - run: rustup component add clippy 33 | - name: cargo clippy 34 | run: cargo clippy --all-targets -- -D warnings 35 | 36 | test: 37 | name: Test 38 | strategy: 39 | matrix: 40 | os: [ubuntu-20.04, windows-latest, macOS-latest] 41 | runs-on: ${{ matrix.os }} 42 | steps: 43 | - uses: actions/checkout@v2 44 | - uses: actions-rs/toolchain@v1 45 | with: 46 | toolchain: stable 47 | override: true 48 | - name: Install build deps 49 | if: matrix.os == 'ubuntu-20.04' 50 | run: | 51 | sudo apt-get install -y libxkbcommon-x11-dev 52 | - run: cargo fetch 53 | - name: cargo test build 54 | run: cargo build --tests --release --all-features 55 | - name: cargo test 56 | run: cargo test --release --all-features 57 | 58 | build: 59 | name: Build WASM 60 | runs-on: ubuntu-20.04 61 | steps: 62 | - uses: actions/checkout@v2 63 | - uses: actions-rs/toolchain@v1 64 | with: 65 | toolchain: stable 66 | target: wasm32-wasi 67 | override: true 68 | - run: cargo fetch 69 | - name: cargo build 70 | run: cargo build --target wasm32-wasi 71 | 72 | deny-check: 73 | name: cargo-deny 74 | runs-on: ubuntu-20.04 75 | steps: 76 | - uses: actions/checkout@v2 77 | - uses: EmbarkStudios/cargo-deny-action@v1 78 | 79 | publish-check: 80 | name: Publish Check 81 | runs-on: ubuntu-20.04 82 | steps: 83 | - uses: actions/checkout@v2 84 | - uses: actions-rs/toolchain@v1 85 | with: 86 | toolchain: stable 87 | override: true 88 | - name: Install build deps 89 | run: | 90 | sudo apt-get install -y libxkbcommon-x11-dev 91 | - run: cargo fetch 92 | - name: copy README 93 | shell: bash 94 | run: | 95 | cp README.md lib 96 | cp README.md cli 97 | - name: cargo publish lib 98 | run: cargo publish --dry-run --allow-dirty --manifest-path lib/Cargo.toml 99 | 100 | release: 101 | name: Release 102 | needs: [test, deny-check] 103 | if: startsWith(github.ref, 'refs/tags/') 104 | strategy: 105 | matrix: 106 | include: 107 | - os: ubuntu-20.04 108 | rust: stable 109 | target: x86_64-unknown-linux-musl 110 | bin: texture-synthesis 111 | # We don't enable the progress feature when targeting 112 | # musl since there are some dependencies on shared libs 113 | features: "" 114 | - os: windows-latest 115 | rust: stable 116 | target: x86_64-pc-windows-msvc 117 | bin: texture-synthesis.exe 118 | features: --features=progress 119 | - os: macOS-latest 120 | rust: stable 121 | target: x86_64-apple-darwin 122 | bin: texture-synthesis 123 | features: --features=progress 124 | - os: macOS-latest 125 | rust: stable 126 | target: aarch64-apple-darwin 127 | bin: texture-synthesis 128 | features: --features=progress 129 | runs-on: ${{ matrix.os }} 130 | steps: 131 | - name: Install stable toolchain 132 | uses: actions-rs/toolchain@v1 133 | with: 134 | toolchain: ${{ matrix.rust }} 135 | override: true 136 | target: ${{ matrix.target }} 137 | - name: Install build deps 138 | if: matrix.os == 'ubuntu-20.04' 139 | run: | 140 | sudo apt-get install -y musl-tools libxkbcommon-x11-dev 141 | - name: Workaround xcode shenanigans 142 | if: matrix.target == 'aarch64-apple-darwin' 143 | # https://github.com/actions/virtual-environments/issues/2557#issuecomment-769611326 144 | run: | 145 | sudo xcode-select -s "/Applications/Xcode_12.3.app" 146 | sudo rm -Rf /Library/Developer/CommandLineTools/SDKs/* 147 | - name: Checkout 148 | uses: actions/checkout@v2 149 | - name: cargo fetch 150 | run: cargo fetch --target ${{ matrix.target }} 151 | - name: Release build 152 | run: cargo build --manifest-path cli/Cargo.toml --release --target ${{ matrix.target }} ${{ matrix.features }} 153 | - name: Package 154 | shell: bash 155 | run: | 156 | name=texture-synthesis 157 | tag=$(git describe --tags --abbrev=0) 158 | release_name="$name-$tag-${{ matrix.target }}" 159 | 160 | # Archive in .zip for Windows, .tar.gz for other platforms 161 | if [ "${{ matrix.os }}" == "windows-latest" ]; then 162 | release_archive="${release_name}.zip" 163 | else 164 | release_archive="${release_name}.tar.gz" 165 | fi 166 | mkdir "$release_name" 167 | 168 | if [ "${{ matrix.target }}" != "x86_64-pc-windows-msvc" ]; then 169 | strip "target/${{ matrix.target }}/release/${{ matrix.bin }}" 170 | fi 171 | 172 | cp "target/${{ matrix.target }}/release/${{ matrix.bin }}" "$release_name/" 173 | cp README.md LICENSE-APACHE LICENSE-MIT "$release_name/" 174 | 175 | # Archive in .zip for Windows, .tar.gz for other platforms 176 | if [ "${{ matrix.os }}" == "windows-latest" ]; then 177 | 7z a "$release_archive" "$release_name" 178 | else 179 | tar czvf "$release_archive" "$release_name" 180 | fi 181 | 182 | rm -r "$release_name" 183 | 184 | # Windows environments in github actions don't have the gnu coreutils installed, 185 | # which includes the shasum exe, so we just use powershell instead 186 | if [ "${{ matrix.os }}" == "windows-latest" ]; then 187 | echo "(Get-FileHash \"${release_archive}\" -Algorithm SHA256).Hash | Out-File -Encoding ASCII -NoNewline \"${release_archive}.sha256\"" | pwsh -c - 188 | else 189 | echo -n "$(shasum -ba 256 "${release_archive}" | cut -d " " -f 1)" > "${release_archive}.sha256" 190 | fi 191 | - name: Publish 192 | uses: softprops/action-gh-release@v1 193 | with: 194 | draft: true 195 | files: "texture-synthesis*" 196 | env: 197 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 198 | -------------------------------------------------------------------------------- /lib/tests/diff.rs: -------------------------------------------------------------------------------- 1 | use img_hash::{HashType, ImageHash}; 2 | use texture_synthesis as ts; 3 | use ts::Dims; 4 | 5 | // The tests below each run the different example code we have and 6 | // compare the image hash against a "known good" hash. The test 7 | // generation is run in a single thread, with the same seed and other 8 | // parameters to produce a consistent hash between runs to detect 9 | // regressions when making changes to the generator. If the hashes 10 | // don't match, the right hand side of the assertion message will 11 | // be the hash of the generated output, which you can copy and paste 12 | // into the second parameter of the test macro if you intended 13 | // to make an algorithm/parameter change that affects output, but 14 | // please don't just update solely to get the test to pass! 15 | 16 | // For example, if cargo test output this 17 | // 18 | // thread 'single_example' panicked at 'assertion failed: `(left == right)` 19 | // left: `"JKc2MqWo1iNWeJ856Ty6+a2M"`, 20 | // right: `"JKc2MqWo1iNWeJ856Ty6+a1M"`: images hashes differed by 0.014814815', lib/tests/diff.rs:46:1 21 | // 22 | // You would copy `JKc2MqWo1iNWeJ856Ty6+a1M` and paste it over the hash for `single_example` to 23 | // update the hash 24 | 25 | use image::{ 26 | imageops, imageops::FilterType, DynamicImage, GenericImageView, GrayImage, Pixel, RgbaImage, 27 | }; 28 | use img_hash::HashImage; 29 | const FILTER_TYPE: FilterType = FilterType::Nearest; 30 | 31 | struct MyPrecious(T); 32 | 33 | impl HashImage for MyPrecious { 34 | type Grayscale = MyPrecious; 35 | 36 | fn dimensions(&self) -> (u32, u32) { 37 | ::dimensions(&self.0) 38 | } 39 | 40 | fn resize(&self, width: u32, height: u32) -> Self { 41 | Self(self.0.resize(width, height, FILTER_TYPE)) 42 | } 43 | 44 | fn grayscale(&self) -> Self::Grayscale { 45 | MyPrecious(imageops::grayscale(&self.0)) 46 | } 47 | 48 | fn to_bytes(self) -> Vec { 49 | self.0.to_bytes() 50 | } 51 | 52 | fn channel_count() -> u8 { 53 | <::Pixel as Pixel>::CHANNEL_COUNT 54 | } 55 | 56 | fn foreach_pixel(&self, mut iter_fn: F) 57 | where 58 | F: FnMut(u32, u32, &[u8]), 59 | { 60 | for (x, y, px) in self.0.pixels() { 61 | iter_fn(x, y, px.channels()); 62 | } 63 | } 64 | } 65 | 66 | impl HashImage for MyPrecious { 67 | type Grayscale = MyPrecious; 68 | 69 | fn dimensions(&self) -> (u32, u32) { 70 | ::dimensions(&self.0) 71 | } 72 | 73 | fn resize(&self, width: u32, height: u32) -> Self { 74 | Self(imageops::resize(&self.0, width, height, FILTER_TYPE)) 75 | } 76 | 77 | fn grayscale(&self) -> Self::Grayscale { 78 | Self(imageops::grayscale(&self.0)) 79 | } 80 | 81 | fn to_bytes(self) -> Vec { 82 | self.0.into_raw() 83 | } 84 | 85 | fn channel_count() -> u8 { 86 | <::Pixel as Pixel>::CHANNEL_COUNT 87 | } 88 | 89 | fn foreach_pixel(&self, mut iter_fn: F) 90 | where 91 | F: FnMut(u32, u32, &[u8]), 92 | { 93 | for (x, y, px) in self.0.enumerate_pixels() { 94 | iter_fn(x, y, px.channels()); 95 | } 96 | } 97 | } 98 | 99 | impl HashImage for MyPrecious { 100 | type Grayscale = MyPrecious; 101 | 102 | fn dimensions(&self) -> (u32, u32) { 103 | ::dimensions(&self.0) 104 | } 105 | 106 | fn resize(&self, width: u32, height: u32) -> Self { 107 | Self(imageops::resize(&self.0, width, height, FILTER_TYPE)) 108 | } 109 | 110 | fn grayscale(&self) -> Self::Grayscale { 111 | MyPrecious(imageops::grayscale(&self.0)) 112 | } 113 | 114 | fn to_bytes(self) -> Vec { 115 | self.0.into_raw() 116 | } 117 | 118 | fn channel_count() -> u8 { 119 | <::Pixel as Pixel>::CHANNEL_COUNT 120 | } 121 | 122 | fn foreach_pixel(&self, mut iter_fn: F) 123 | where 124 | F: FnMut(u32, u32, &[u8]), 125 | { 126 | for (x, y, px) in self.0.enumerate_pixels() { 127 | iter_fn(x, y, px.channels()); 128 | } 129 | } 130 | } 131 | 132 | macro_rules! diff_hash { 133 | // $name - The name of the test 134 | // $expected - A base64 encoded string of our expected image hash 135 | // $gen - A session builder, note that the max_thread_count is always 136 | // set to 1 regardless 137 | ($name:ident, $expected:expr, $gen:expr) => { 138 | #[test] 139 | fn $name() { 140 | let expected_hash = ImageHash::from_base64($expected).expect("loaded hash"); 141 | 142 | let generated = $gen 143 | // We always use a single thread to ensure we get consistent results 144 | // across runs 145 | .max_thread_count(1) 146 | .build() 147 | .unwrap() 148 | .run(None); 149 | 150 | let gen_img = generated.into_image(); 151 | let gen_hash = ImageHash::hash(&MyPrecious(gen_img), 8, HashType::DoubleGradient); 152 | 153 | if gen_hash != expected_hash { 154 | let distance = expected_hash.dist_ratio(&gen_hash); 155 | let txt_gen = gen_hash.to_base64(); 156 | 157 | assert_eq!($expected, txt_gen, "images hashes differed by {}", distance); 158 | } 159 | } 160 | }; 161 | } 162 | 163 | diff_hash!(single_example, "JKc2MqWo1iNWeJ856Ty6+a1M", { 164 | ts::Session::builder() 165 | .add_example(&"../imgs/1.jpg") 166 | .seed(120) 167 | .output_size(Dims::square(100)) 168 | }); 169 | 170 | diff_hash!(multi_example, "JFCWyK1a4vJ1eWNTQkPOmdy2", { 171 | ts::Session::builder() 172 | .add_examples(&[ 173 | &"../imgs/multiexample/1.jpg", 174 | &"../imgs/multiexample/2.jpg", 175 | &"../imgs/multiexample/3.jpg", 176 | &"../imgs/multiexample/4.jpg", 177 | ]) 178 | .resize_input(Dims::square(100)) 179 | .random_init(10) 180 | .seed(211) 181 | .output_size(Dims::square(100)) 182 | }); 183 | 184 | diff_hash!(guided, "JBQFEQoXm5CCiWZUfHHBhweK", { 185 | ts::Session::builder() 186 | .add_example( 187 | ts::Example::builder(&"../imgs/2.jpg").with_guide(&"../imgs/masks/2_example.jpg"), 188 | ) 189 | .load_target_guide(&"../imgs/masks/2_target.jpg") 190 | .output_size(Dims::square(100)) 191 | }); 192 | 193 | diff_hash!(style_transfer, "JEMRDSUzJ4uhpHMes1Onenz0", { 194 | ts::Session::builder() 195 | .add_example(&"../imgs/multiexample/4.jpg") 196 | .load_target_guide(&"../imgs/tom.jpg") 197 | .output_size(Dims::square(100)) 198 | }); 199 | 200 | diff_hash!(inpaint, "JNG1tl5SaIkqauco1NEmtikk", { 201 | ts::Session::builder().inpaint_example( 202 | &"../imgs/masks/3_inpaint.jpg", 203 | ts::Example::builder(&"../imgs/3.jpg").set_sample_method(&"../imgs/masks/3_inpaint.jpg"), 204 | Dims { 205 | width: 100, 206 | height: 100, 207 | }, 208 | ) 209 | }); 210 | 211 | diff_hash!(inpaint_channel, "JOVF4dThzPKa2suWLo1OWrKk", { 212 | ts::Session::builder().inpaint_example_channel( 213 | ts::ChannelMask::A, 214 | &"../imgs/bricks.png", 215 | ts::Dims::square(400), 216 | ) 217 | }); 218 | 219 | diff_hash!(tiling, "JFSVUUmMaMzhWSttmlwojR1q", { 220 | ts::Session::builder() 221 | .inpaint_example( 222 | &"../imgs/masks/1_tile.jpg", 223 | ts::Example::new(&"../imgs/1.jpg"), 224 | Dims { 225 | width: 100, 226 | height: 100, 227 | }, 228 | ) 229 | .tiling_mode(true) 230 | }); 231 | 232 | diff_hash!(sample_masks, "JLO1hQBEpakECqIXDiCkqBME", { 233 | ts::Session::builder() 234 | .add_example( 235 | ts::Example::builder(&"../imgs/4.png") 236 | .set_sample_method(&"../imgs/masks/4_sample_mask.png"), 237 | ) 238 | .seed(211) 239 | .output_size(Dims::square(100)) 240 | }); 241 | 242 | diff_hash!(sample_masks_ignore, "JGgWBEwJiqCaKpEiAonGkQRE", { 243 | ts::Session::builder() 244 | .add_example( 245 | ts::Example::builder(&"../imgs/4.png").set_sample_method(ts::SampleMethod::Ignore), 246 | ) 247 | .add_example( 248 | ts::Example::builder(&"../imgs/5.png").set_sample_method(ts::SampleMethod::All), 249 | ) 250 | .seed(211) 251 | .output_size(Dims::square(200)) 252 | }); 253 | 254 | #[test] 255 | fn repeat_transform() { 256 | //create a new session 257 | let texsynth = ts::Session::builder() 258 | .add_example(&"../imgs/1.jpg") 259 | .build() 260 | .unwrap(); 261 | 262 | let generated = texsynth.run(None); 263 | 264 | let repeat_transform_img = generated 265 | .get_coordinate_transform() 266 | .apply(&["../imgs/1.jpg"]) 267 | .unwrap(); 268 | 269 | let hash_synthesized = ImageHash::hash( 270 | &MyPrecious(generated.into_image()), 271 | 8, 272 | HashType::DoubleGradient, 273 | ); 274 | let hash_repeated = ImageHash::hash( 275 | &MyPrecious(repeat_transform_img), 276 | 8, 277 | HashType::DoubleGradient, 278 | ); 279 | 280 | assert_eq!( 281 | hash_synthesized, hash_repeated, 282 | "repeated transform image hash differs from synthesized image" 283 | ); 284 | } 285 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Changelog 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 7 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | ### Changed 11 | - Both the lib and CLI have been marked as `looking-for-maintainer` as we have not been actively maintaining these crates for a while, and would be open to transferring this repo to someone else. 12 | 13 | ## [0.8.2] - 2021-03-26 14 | ### Changed 15 | - Updated dependencies 16 | 17 | ## [0.8.1] - 2020-11-17 18 | ### Added 19 | - [PR#154](https://github.com/EmbarkStudios/texture-synthesis/pull/154) added progress notification is now supported when compiling for `wasm32`, thanks [@bnjbvr](https://github.com/bnjbvr)! 20 | 21 | ## [0.8.0] - 2020-02-26 22 | ### Added 23 | - Added the [09_sample_masks](lib/examples/09_sample_masks.rs) example 24 | - CLI: Resolved [#38](https://github.com/EmbarkStudios/texture-synthesis/issues/38) by adding the `repeat` subcommand, which can be used to repeat transforms with different input images 25 | - [PR#91](https://github.com/EmbarkStudios/texture-synthesis/pull/91) Added support for compiling and running texture-synthesis as WASM. 26 | 27 | ### Fixed 28 | - [PR#69](https://github.com/EmbarkStudios/texture-synthesis/pull/69) Improved performance by 1x-2x, especially for smaller inputs. Thanks [@Mr4k](https://github.com/Mr4k)! 29 | - [PR#70](https://github.com/EmbarkStudios/texture-synthesis/pull/70) Improved performance by **another** 1x-2.7x, especially for larger numbers of threads. Thanks (again!) [@Mr4k](https://github.com/Mr4k)! 30 | - CLI: [PR#71](https://github.com/EmbarkStudios/texture-synthesis/pull/71) The prebuilt binary for Windows is now packaged in a zip file instead of a gzipped tarball to improve end user experience. Thanks [@moppius](https://github.com/moppius)! 31 | - [PR#98](https://github.com/EmbarkStudios/texture-synthesis/pull/98) fixed [#85](https://github.com/EmbarkStudios/texture-synthesis/issues/85), sample masks could hang image generation. Thanks [@Mr4k](https://github.com/Mr4k)! 32 | 33 | ## [0.7.1] - 2019-11-19 34 | ### Fixed 35 | - Update to fix broken CI script 36 | 37 | ## [0.7.0] - 2019-11-19 38 | ### Added 39 | - [PR#57](https://github.com/EmbarkStudios/texture-synthesis/pull/57) CLI: Added the `flip-and-rotate` subcommand which applies flip and rotation transforms to each example input and adds them as additional examples. Thanks [@JD557](https://github.com/JD557)! 40 | - [PR#60](https://github.com/EmbarkStudios/texture-synthesis/pull/60) added the ability to specify a channel to use as the inpaint mask instead of a separate image. Thanks [@khskarl](https://github.com/khskarl)! 41 | - Added `SessionBuilder::inpaint_example_channel` 42 | - CLI: Added `--inpaint-channel ` 43 | 44 | ### Changed 45 | - Replace [`failure`](https://crates.io/crates/failure) crate for error handling with just `std::error::Error` 46 | 47 | ### Fixed 48 | - Validate that the `--m-rand` / `random_sample_locations` parameter is > 0. [#45](https://github.com/EmbarkStudios/texture-synthesis/issues/45) 49 | 50 | ## [0.6.0] - 2019-09-23 51 | ### Added 52 | - Added support for the alpha channel during generation, which was previously ignored 53 | 54 | ### Changed 55 | - `SessionBuilder::inpaint_example` now requires a size be provided by which all inputs will be resized, as well setting the output image size. Previously, you had to manually specify matching `output_size` and `resize_input` otherwise you would get a parameter validation error. 56 | - All public methods/functions that took a size either as 2 u32's, or a tuple of them, now use a simple `Dims` struct for clarity. 57 | 58 | ### Fixed 59 | - [PR#36](https://github.com/EmbarkStudios/texture-synthesis/pull/36) Fixed undefined behavior in `Generator::update`. Thanks for reporting, [@ralfbiedert](https://github.com/ralfbiedert)! 60 | 61 | ## [0.5.0] - 2019-09-13 62 | ### Added 63 | - You can now specify the maximum number of threads that can be used at any one time via `SessionBuilder::max_thread_count` 64 | - CLI: You can now specify the maximum thread count via `-t | --threads` 65 | - Added `From` for `ImageSource` 66 | - Added integrations tests for different examples, to catch regressions in generation 67 | - Added criterion benchmarks for the different examples, to catch performance regressions 68 | 69 | ### Changed 70 | - `SampleMethod::From>` is now `SampleMethod::From>` 71 | - `Example::From>` is now `Example::From>` 72 | - CLI: Renamed the `--out-fmt` arg to `--stdout-fmt` to indicate it only works when using stdout via `--out -` 73 | 74 | ### Fixed 75 | - [PR#14](https://github.com/EmbarkStudios/texture-synthesis/pull/14) Vastly improve performance, all benchmarks are sped up from between **1.03** to **1.96**, almost twice as fast! Thanks [@austinjones](https://github.com/austinjones)! 76 | - Disabled unused `rand` default features (OS random number generator) 77 | 78 | ## [0.4.2] - 2019-09-05 79 | ### Added 80 | - Added `Error::UnsupportedOutputFormat` 81 | 82 | ### Changed 83 | - CLI: `--out` is now required. `-` can still be passed to write to stdout instead of a file. 84 | - CLI: The file extension for the `--out` path is now checked to see if it a supported format. 85 | 86 | ### Removed 87 | - Removed tga feature in image since it wasn't used 88 | 89 | ## [0.4.1] - 2019-09-04 90 | ### Fixed 91 | - Removed unused `lodepng` dependency 92 | 93 | ## [0.4.0] - 2019-09-04 94 | ### Changed 95 | - Use [`failure`](https://crates.io/crates/failure) for errors 96 | - CLI: Replaced piston_window with [`minifb`](https://crates.io/crates/minifb) 97 | - CLI: Due to how minifb works via X11, the progress window is now an optional feature not enabled when building for musl 98 | 99 | ### Removed 100 | - Removed several codec features from `image`, only `png`, `jpeg`, `bmp`, and `tga` are supported now 101 | 102 | ## [0.3.0] - 2019-09-03 103 | ### Added 104 | - Added [`Example`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/lib.rs#L247) and [`ExampleBuilder`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/lib.rs#L208) which can be used to manipulate an indidivual 105 | example input before being added to a [`SessionBuilder`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/lib.rs#L342) 106 | - Added [`SampleMethod`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/lib.rs#L158) used to specify how a particular example is sampled during generation 107 | - Added [`ImageSource`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/utils.rs#L6) which is a small enum that means image data for examples, guides, 108 | masks, etc, can be specified either as paths, raw byte slices, or already loaded `image::DynamicImage` 109 | - Added [`GeneratedImage`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/lib.rs#L103) which allows saving, streaming, and inspection of the image 110 | generated by [`Session::run()`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/lib.rs#L736) 111 | 112 | ### Changed 113 | - All usage of `&str` paths to load images from disk have been replaced with `ImageSource` 114 | - Moved all of the building functionality in `Session` into `SessionBuilder` 115 | - [`SessionBuilder::inpaint_example`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/lib.rs#L410) now specifies an ImageSource for the inpaint mask, along with an `Example` to be paired with, rather than the previous use of an index that the user had to keep track of 116 | - [`GeneratorProgress`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/lib.rs#L789) now gets the total progress and stage progress in addition to the current image 117 | - `Session::run()` can no longer fail, and consumes the `Session` 118 | - `Session::run()` now returns a `GeneratedImage` which can be used to get, save, or stream the generated image (and maybe the debug ones) 119 | - The CLI default values for the various available parameters now match the defaults in the `SessionBuilder`, which means the examples provided in the README.md match 120 | 121 | ### Removed 122 | - Replaced `load_examples`, `load_example`, `load_example_guides`, and `load_sampling_masks` with 123 | [`add_example`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/lib.rs#L366) and [`add_examples`](https://github.com/EmbarkStudios/texture-synthesis/blob/7e65b8abb9508841e7acf758cb79dd3f49aac28e/lib/src/lib.rs#L382) which work with `Example`(s) 124 | 125 | ### Fixed 126 | - The top-level README.md is now deployed with both `texture-synthesis` and `texture-synthesis-cli` on crates.io 127 | 128 | ## [0.2.0] - 2019-08-27 129 | ### Added 130 | - Split lib and cli into separate crates so CLI specific dependencies weren't pulled in 131 | - Add `GeneratorProgress` to allow progress of image generation to be reported to external callers 132 | 133 | ## [0.1.0] - 2019-08-27 134 | ### Added 135 | - Initial add of `texture-synthesis` 136 | 137 | [Unreleased]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.8.0...HEAD 138 | [0.8.0]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.7.0...0.7.1 139 | [0.7.1]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.7.0...0.7.1 140 | [0.7.0]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.6.0...0.7.0 141 | [0.6.0]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.5.0...0.6.0 142 | [0.5.0]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.4.2...0.5.0 143 | [0.4.2]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.4.1...0.4.2 144 | [0.4.1]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.4.0...0.4.1 145 | [0.4.0]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.3.0...0.4.0 146 | [0.3.0]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.2.0...0.3.0 147 | [0.2.0]: https://github.com/EmbarkStudios/texture-synthesis/compare/0.1.0...0.2.0 148 | [0.1.0]: https://github.com/EmbarkStudios/texture-synthesis/releases/tag/0.1.0 149 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/benches/all-the-things.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; 2 | use std::time::{Duration, Instant}; 3 | use texture_synthesis as ts; 4 | 5 | fn single_example(c: &mut Criterion) { 6 | static DIM: u32 = 25; 7 | 8 | // Load the example image once to reduce variation between runs, 9 | // though we still do a memcpy each run 10 | let example_img = ts::image::open("../imgs/1.jpg").unwrap(); 11 | 12 | let mut group = c.benchmark_group("single_example"); 13 | group.sample_size(10); 14 | 15 | for dim in [DIM, 2 * DIM, 4 * DIM, 8 * DIM, 16 * DIM].iter() { 16 | group.bench_with_input(BenchmarkId::from_parameter(dim), dim, |b, &dim| { 17 | b.iter_custom(|iters| { 18 | let mut total_elapsed = Duration::new(0, 0); 19 | for _i in 0..iters { 20 | let sess = ts::Session::builder() 21 | .add_example(example_img.clone()) 22 | .seed(120) 23 | .output_size(ts::Dims::square(dim)) 24 | .build() 25 | .unwrap(); 26 | 27 | let start = Instant::now(); 28 | black_box(sess.run(None)); 29 | total_elapsed += start.elapsed(); 30 | } 31 | 32 | total_elapsed 33 | }); 34 | }); 35 | } 36 | group.finish(); 37 | } 38 | 39 | fn multi_example(c: &mut Criterion) { 40 | static DIM: u32 = 25; 41 | 42 | // Load the example image once to reduce variation between runs, 43 | // though we still do a memcpy each run 44 | let example_imgs = [ 45 | ts::image::open("../imgs/multiexample/1.jpg").unwrap(), 46 | ts::image::open("../imgs/multiexample/2.jpg").unwrap(), 47 | ts::image::open("../imgs/multiexample/3.jpg").unwrap(), 48 | ts::image::open("../imgs/multiexample/4.jpg").unwrap(), 49 | ]; 50 | 51 | let mut group = c.benchmark_group("multi_example"); 52 | group.sample_size(10); 53 | 54 | for dim in [DIM, 2 * DIM, 4 * DIM, 8 * DIM, 16 * DIM].iter() { 55 | group.bench_with_input(BenchmarkId::from_parameter(dim), dim, |b, &dim| { 56 | b.iter_custom(|iters| { 57 | let mut total_elapsed = Duration::new(0, 0); 58 | for _i in 0..iters { 59 | let sess = ts::Session::builder() 60 | .add_examples(example_imgs.iter().cloned()) 61 | .resize_input(ts::Dims::square(dim)) 62 | //.random_init(10) 63 | .seed(211) 64 | .output_size(ts::Dims::square(dim)) 65 | .build() 66 | .unwrap(); 67 | 68 | let start = Instant::now(); 69 | black_box(sess.run(None)); 70 | total_elapsed += start.elapsed(); 71 | } 72 | 73 | total_elapsed 74 | }); 75 | }); 76 | } 77 | group.finish(); 78 | } 79 | 80 | fn guided(c: &mut Criterion) { 81 | static DIM: u32 = 25; 82 | 83 | // Load the example image once to reduce variation between runs, 84 | // though we still do a memcpy each run 85 | let example_img = ts::image::open("../imgs/2.jpg").unwrap(); 86 | let guide_img = ts::image::open("../imgs/masks/2_example.jpg").unwrap(); 87 | let target_img = ts::image::open("../imgs/masks/2_target.jpg").unwrap(); 88 | 89 | let mut group = c.benchmark_group("guided"); 90 | group.sample_size(10); 91 | 92 | for dim in [DIM, 2 * DIM, 4 * DIM, 8 * DIM, 16 * DIM].iter() { 93 | group.bench_with_input(BenchmarkId::from_parameter(dim), dim, |b, &dim| { 94 | b.iter_custom(|iters| { 95 | let mut total_elapsed = Duration::new(0, 0); 96 | for _i in 0..iters { 97 | let sess = ts::Session::builder() 98 | .add_example( 99 | ts::Example::builder(example_img.clone()).with_guide(guide_img.clone()), 100 | ) 101 | .load_target_guide(target_img.clone()) 102 | //.random_init(10) 103 | .seed(211) 104 | .output_size(ts::Dims::square(dim)) 105 | .build() 106 | .unwrap(); 107 | 108 | let start = Instant::now(); 109 | black_box(sess.run(None)); 110 | total_elapsed += start.elapsed(); 111 | } 112 | 113 | total_elapsed 114 | }); 115 | }); 116 | } 117 | group.finish(); 118 | } 119 | 120 | fn style_transfer(c: &mut Criterion) { 121 | static DIM: u32 = 25; 122 | 123 | // Load the example image once to reduce variation between runs, 124 | // though we still do a memcpy each run 125 | let example_img = ts::image::open("../imgs/multiexample/4.jpg").unwrap(); 126 | let target_img = ts::image::open("../imgs/tom.jpg").unwrap(); 127 | 128 | let mut group = c.benchmark_group("style_transfer"); 129 | group.sample_size(10); 130 | 131 | for dim in [DIM, 2 * DIM, 4 * DIM, 8 * DIM, 16 * DIM].iter() { 132 | group.bench_with_input(BenchmarkId::from_parameter(dim), dim, |b, &dim| { 133 | b.iter_custom(|iters| { 134 | let mut total_elapsed = Duration::new(0, 0); 135 | for _i in 0..iters { 136 | let sess = ts::Session::builder() 137 | .add_example(example_img.clone()) 138 | .load_target_guide(target_img.clone()) 139 | .output_size(ts::Dims::square(dim)) 140 | .build() 141 | .unwrap(); 142 | 143 | let start = Instant::now(); 144 | black_box(sess.run(None)); 145 | total_elapsed += start.elapsed(); 146 | } 147 | 148 | total_elapsed 149 | }); 150 | }); 151 | } 152 | group.finish(); 153 | } 154 | 155 | fn inpaint(c: &mut Criterion) { 156 | static DIM: u32 = 25; 157 | 158 | // Load the example image once to reduce variation between runs, 159 | // though we still do a memcpy each run 160 | let example_img = ts::image::open("../imgs/3.jpg").unwrap(); 161 | let inpaint_mask = ts::image::open("../imgs/masks/3_inpaint.jpg").unwrap(); 162 | 163 | let mut group = c.benchmark_group("inpaint"); 164 | group.sample_size(10); 165 | 166 | for dim in [DIM, 2 * DIM, 4 * DIM, 8 * DIM, 16 * DIM].iter() { 167 | group.bench_with_input(BenchmarkId::from_parameter(dim), dim, |b, &dim| { 168 | b.iter_custom(|iters| { 169 | let mut total_elapsed = Duration::new(0, 0); 170 | for _i in 0..iters { 171 | let sess = ts::Session::builder() 172 | .inpaint_example( 173 | inpaint_mask.clone(), 174 | ts::Example::builder(example_img.clone()) 175 | .set_sample_method(inpaint_mask.clone()), 176 | ts::Dims::square(dim), 177 | ) 178 | .build() 179 | .unwrap(); 180 | 181 | let start = Instant::now(); 182 | black_box(sess.run(None)); 183 | total_elapsed += start.elapsed(); 184 | } 185 | 186 | total_elapsed 187 | }); 188 | }); 189 | } 190 | group.finish(); 191 | } 192 | 193 | fn inpaint_channel(c: &mut Criterion) { 194 | static DIM: u32 = 25; 195 | 196 | // Load the example image once to reduce variation between runs, 197 | // though we still do a memcpy each run 198 | let example_img = ts::load_dynamic_image(ts::ImageSource::from(&"../imgs/bricks.png")).unwrap(); 199 | 200 | let mut group = c.benchmark_group("inpaint_channel"); 201 | group.sample_size(10); 202 | 203 | for dim in [DIM, 2 * DIM, 4 * DIM, 8 * DIM, 16 * DIM].iter() { 204 | group.bench_with_input(BenchmarkId::from_parameter(dim), dim, |b, &dim| { 205 | b.iter_custom(|iters| { 206 | let mut total_elapsed = Duration::new(0, 0); 207 | for _i in 0..iters { 208 | let sess = ts::Session::builder() 209 | .inpaint_example_channel( 210 | ts::ChannelMask::A, 211 | ts::Example::builder(example_img.clone()), 212 | ts::Dims::square(dim), 213 | ) 214 | .build() 215 | .unwrap(); 216 | 217 | let start = Instant::now(); 218 | black_box(sess.run(None)); 219 | total_elapsed += start.elapsed(); 220 | } 221 | 222 | total_elapsed 223 | }); 224 | }); 225 | } 226 | group.finish(); 227 | } 228 | 229 | fn tiling(c: &mut Criterion) { 230 | static DIM: u32 = 25; 231 | 232 | // Load the example image once to reduce variation between runs, 233 | // though we still do a memcpy each run 234 | let example_img = ts::image::open("../imgs/1.jpg").unwrap(); 235 | let inpaint_mask = ts::image::open("../imgs/masks/1_tile.jpg").unwrap(); 236 | 237 | let mut group = c.benchmark_group("tiling"); 238 | group.sample_size(10); 239 | 240 | for dim in [DIM, 2 * DIM, 4 * DIM, 8 * DIM, 16 * DIM].iter() { 241 | group.bench_with_input(BenchmarkId::from_parameter(dim), dim, |b, &dim| { 242 | b.iter_custom(|iters| { 243 | let mut total_elapsed = Duration::new(0, 0); 244 | for _i in 0..iters { 245 | let sess = ts::Session::builder() 246 | .inpaint_example( 247 | inpaint_mask.clone(), 248 | example_img.clone(), 249 | ts::Dims::square(dim), 250 | ) 251 | .tiling_mode(true) 252 | .build() 253 | .unwrap(); 254 | 255 | let start = Instant::now(); 256 | black_box(sess.run(None)); 257 | total_elapsed += start.elapsed(); 258 | } 259 | 260 | total_elapsed 261 | }); 262 | }); 263 | } 264 | group.finish(); 265 | } 266 | 267 | fn repeat(c: &mut Criterion) { 268 | static DIM: u32 = 25; 269 | 270 | // Load the example image once to reduce variation between runs, 271 | // though we still do a memcpy each run 272 | let example_img = ts::load_dynamic_image(ts::ImageSource::from(&"../imgs/bricks.png")).unwrap(); 273 | 274 | let mut group = c.benchmark_group("repeat"); 275 | group.sample_size(10); 276 | 277 | let mut gen = Vec::with_capacity(5); 278 | 279 | for dim in [DIM, 2 * DIM, 4 * DIM, 8 * DIM, 16 * DIM].iter() { 280 | let sess = ts::Session::builder() 281 | .add_example(example_img.clone()) 282 | .output_size(ts::Dims::square(*dim)) 283 | .build() 284 | .unwrap(); 285 | 286 | let genned = sess.run(None); 287 | gen.push(genned); 288 | } 289 | 290 | for (genned, dim) in gen 291 | .into_iter() 292 | .zip([DIM, 2 * DIM, 4 * DIM, 8 * DIM, 16 * DIM].iter()) 293 | { 294 | group.bench_with_input(BenchmarkId::from_parameter(dim), dim, |b, &_dim| { 295 | b.iter_custom(|iters| { 296 | let mut total_elapsed = Duration::new(0, 0); 297 | 298 | for _i in 0..iters { 299 | let img = example_img.clone(); 300 | let start = Instant::now(); 301 | black_box( 302 | genned 303 | .get_coordinate_transform() 304 | .apply(std::iter::once(img)) 305 | .unwrap(), 306 | ); 307 | total_elapsed += start.elapsed(); 308 | } 309 | 310 | total_elapsed 311 | }); 312 | }); 313 | } 314 | group.finish(); 315 | } 316 | 317 | criterion_group!( 318 | benches, 319 | single_example, 320 | multi_example, 321 | guided, 322 | style_transfer, 323 | inpaint, 324 | inpaint_channel, 325 | tiling, 326 | repeat, 327 | ); 328 | criterion_main!(benches); 329 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | // BEGIN - Embark standard lints v5 for Rust 1.55+ 2 | // do not change or add/remove here, but one can add exceptions after this section 3 | // for more info see: 4 | #![deny(unsafe_code)] 5 | #![warn( 6 | clippy::all, 7 | clippy::await_holding_lock, 8 | clippy::char_lit_as_u8, 9 | clippy::checked_conversions, 10 | clippy::dbg_macro, 11 | clippy::debug_assert_with_mut_call, 12 | clippy::doc_markdown, 13 | clippy::empty_enum, 14 | clippy::enum_glob_use, 15 | clippy::exit, 16 | clippy::expl_impl_clone_on_copy, 17 | clippy::explicit_deref_methods, 18 | clippy::explicit_into_iter_loop, 19 | clippy::fallible_impl_from, 20 | clippy::filter_map_next, 21 | clippy::flat_map_option, 22 | clippy::float_cmp_const, 23 | clippy::fn_params_excessive_bools, 24 | clippy::from_iter_instead_of_collect, 25 | clippy::if_let_mutex, 26 | clippy::implicit_clone, 27 | clippy::imprecise_flops, 28 | clippy::inefficient_to_string, 29 | clippy::invalid_upcast_comparisons, 30 | clippy::large_digit_groups, 31 | clippy::large_stack_arrays, 32 | clippy::large_types_passed_by_value, 33 | clippy::let_unit_value, 34 | clippy::linkedlist, 35 | clippy::lossy_float_literal, 36 | clippy::macro_use_imports, 37 | clippy::manual_ok_or, 38 | clippy::map_err_ignore, 39 | clippy::map_flatten, 40 | clippy::map_unwrap_or, 41 | clippy::match_on_vec_items, 42 | clippy::match_same_arms, 43 | clippy::match_wild_err_arm, 44 | clippy::match_wildcard_for_single_variants, 45 | clippy::mem_forget, 46 | clippy::mismatched_target_os, 47 | clippy::missing_enforced_import_renames, 48 | clippy::mut_mut, 49 | clippy::mutex_integer, 50 | clippy::needless_borrow, 51 | clippy::needless_continue, 52 | clippy::needless_for_each, 53 | clippy::option_option, 54 | clippy::path_buf_push_overwrite, 55 | clippy::ptr_as_ptr, 56 | clippy::rc_mutex, 57 | clippy::ref_option_ref, 58 | clippy::rest_pat_in_fully_bound_structs, 59 | clippy::same_functions_in_if_condition, 60 | clippy::semicolon_if_nothing_returned, 61 | clippy::single_match_else, 62 | clippy::string_add_assign, 63 | clippy::string_add, 64 | clippy::string_lit_as_bytes, 65 | clippy::string_to_string, 66 | clippy::todo, 67 | clippy::trait_duplication_in_bounds, 68 | clippy::unimplemented, 69 | clippy::unnested_or_patterns, 70 | clippy::unused_self, 71 | clippy::useless_transmute, 72 | clippy::verbose_file_reads, 73 | clippy::zero_sized_map_values, 74 | future_incompatible, 75 | nonstandard_style, 76 | rust_2018_idioms 77 | )] 78 | // crate-specific exceptions: 79 | #![allow(unsafe_code)] 80 | 81 | #[cfg(not(target_arch = "wasm32"))] 82 | mod progress_window; 83 | 84 | mod repeat; 85 | 86 | use structopt::StructOpt; 87 | 88 | use std::path::PathBuf; 89 | use texture_synthesis::{ 90 | image::ImageOutputFormat as ImgFmt, load_dynamic_image, ChannelMask, Dims, Error, Example, 91 | ImageSource, SampleMethod, Session, 92 | }; 93 | 94 | fn parse_size(input: &str) -> Result { 95 | let mut i = input.splitn(2, 'x'); 96 | 97 | let width: u32 = i.next().unwrap_or("").parse()?; 98 | let height: u32 = match i.next() { 99 | Some(num) => num.parse()?, 100 | None => width, 101 | }; 102 | 103 | Ok(Dims { width, height }) 104 | } 105 | 106 | fn parse_img_fmt(input: &str) -> Result { 107 | let fmt = match input { 108 | "png" => ImgFmt::Png, 109 | "jpg" => ImgFmt::Jpeg(75), 110 | "bmp" => ImgFmt::Bmp, 111 | other => { 112 | return Err(format!( 113 | "image format `{}` not one of: 'png', 'jpg', 'bmp'", 114 | other 115 | )) 116 | } 117 | }; 118 | 119 | Ok(fmt) 120 | } 121 | 122 | fn parse_mask(input: &str) -> Result { 123 | let mask = match &input.to_lowercase()[..] { 124 | "r" => ChannelMask::R, 125 | "g" => ChannelMask::G, 126 | "b" => ChannelMask::B, 127 | "a" => ChannelMask::A, 128 | mask => { 129 | return Err(format!( 130 | "unknown mask '{}', must be one of 'a', 'r', 'g', 'b'", 131 | mask 132 | )) 133 | } 134 | }; 135 | 136 | Ok(mask) 137 | } 138 | 139 | #[derive(StructOpt)] 140 | #[structopt(rename_all = "kebab-case")] 141 | struct Generate { 142 | /// A target guidance map 143 | #[structopt(long, parse(from_os_str))] 144 | target_guide: Option, 145 | /// Path(s) to guide maps for the example output. 146 | #[structopt(long = "guides", parse(from_os_str))] 147 | example_guides: Vec, 148 | /// Saves the transforms used to generate the final output image from the 149 | /// input examples. This can be used by the `repeat` subcommand to reapply 150 | /// the same transform to different examples to get a new output image. 151 | #[structopt(long = "save-transform")] 152 | save_transform: Option, 153 | /// Path(s) to example images used to synthesize a new image 154 | #[structopt(parse(from_os_str))] 155 | examples: Vec, 156 | } 157 | 158 | #[derive(StructOpt)] 159 | struct TransferStyle { 160 | /// The image from which the style will be be sourced 161 | #[structopt(long)] 162 | style: PathBuf, 163 | /// The image used as a guide for the generated output 164 | #[structopt(long)] 165 | guide: PathBuf, 166 | } 167 | 168 | #[derive(StructOpt)] 169 | #[structopt(rename_all = "kebab-case")] 170 | struct FlipAndRotate { 171 | /// Path(s) to example images used to synthesize a new image. Each example 172 | /// is rotated 4 times, and flipped once around each axis, resulting in a 173 | /// total of 7 example inputs per example, so it is recommended you only 174 | /// use 1 example input, even if you can pass as many as you like. 175 | #[structopt(parse(from_os_str))] 176 | examples: Vec, 177 | } 178 | 179 | #[derive(StructOpt)] 180 | enum Subcommand { 181 | /// Transfers the style from an example onto a target guide 182 | #[structopt(name = "transfer-style")] 183 | TransferStyle(TransferStyle), 184 | /// Generates a new image from 1 or more examples 185 | #[structopt(name = "generate")] 186 | Generate(Generate), 187 | /// Generates a new image from 1 or more examples, extended with their 188 | /// flipped and rotated versions 189 | #[structopt(name = "flip-and-rotate")] 190 | FlipAndRotate(FlipAndRotate), 191 | /// Repeats transforms from a previous generate command onto the provided 192 | /// inputs to generate a new output image 193 | #[structopt(name = "repeat")] 194 | Repeat(repeat::Args), 195 | } 196 | 197 | #[derive(StructOpt)] 198 | #[structopt(rename_all = "kebab-case")] 199 | struct Tweaks { 200 | /// The number of neighboring pixels each pixel is aware of during the generation, 201 | /// larger numbers means more global structures are captured. 202 | #[structopt(long = "k-neighs", default_value = "50")] 203 | k_neighbors: u32, 204 | /// The number of random locations that will be considered during a pixel resolution, 205 | /// apart from its immediate neighbors. If unsure of this parameter, keep as the same as k-neigh. 206 | #[structopt(long, default_value = "50")] 207 | m_rand: u64, 208 | /// The distribution dispersion used for picking best candidate (controls the distribution 'tail flatness'). 209 | /// Values close to 0.0 will produce 'harsh' borders between generated 'chunks'. 210 | /// Values closer to 1.0 will produce a smoother gradient on those borders. 211 | #[structopt(long, default_value = "1.0")] 212 | cauchy: f32, 213 | /// The percentage of pixels to be backtracked during each p_stage. Range (0.0, 1.0). 214 | #[structopt(long = "backtrack-pct", default_value = "0.5")] 215 | backtrack_percentage: f32, 216 | /// The number of backtracking stages. Backtracking prevents 'garbage' generation. 217 | #[structopt(long = "backtrack-stages", default_value = "5")] 218 | backtrack_stages: u32, 219 | #[structopt(long = "window")] 220 | #[cfg(feature = "progress")] 221 | #[cfg_attr(feature = "progress", structopt(long = "window"))] 222 | #[cfg_attr( 223 | feature = "progress", 224 | doc = "Show a window with the current progress of the generation" 225 | )] 226 | show_window: bool, 227 | /// Show a window with the current progress of the generation 228 | #[structopt(long)] 229 | #[cfg(not(target_arch = "wasm32"))] 230 | no_progress: bool, 231 | /// A seed value for the random generator to give pseudo-deterministic result. 232 | /// Smaller details will be different from generation to generation due to the 233 | /// non-deterministic nature of multi-threading 234 | #[structopt(long)] 235 | seed: Option, 236 | /// Alpha parameter controls the 'importance' of the user guide maps. If you want 237 | /// to preserve more details from the example map, make sure the number < 1.0. Range (0.0 - 1.0) 238 | #[structopt(long, default_value = "0.8")] 239 | alpha: f32, 240 | /// The number of randomly initialized pixels before the main resolve loop starts 241 | #[structopt(long)] 242 | rand_init: Option, 243 | /// Enables tiling of the output image 244 | #[structopt(long = "tiling")] 245 | enable_tiling: bool, 246 | } 247 | 248 | #[derive(StructOpt)] 249 | #[structopt( 250 | name = "texture-synthesis", 251 | about = "Synthesizes images based on example images", 252 | rename_all = "kebab-case" 253 | )] 254 | struct Opt { 255 | /// Path(s) to sample masks used to determine which pixels in an example can be used as inputs 256 | /// during generation, any example that doesn't have a mask, or uses `ALL`, will consider 257 | /// all pixels in the example. If `IGNORE` is specified, then the example image won't be used 258 | /// at all, which is useful with `--inpaint`. 259 | /// 260 | /// The sample masks must be specified in the same order as the examples 261 | #[structopt(long = "sample-masks")] 262 | sample_masks: Vec, 263 | /// Path to an inpaint map image, where black pixels are resolved, and white pixels are kept 264 | #[structopt(long, parse(from_os_str))] 265 | inpaint: Option, 266 | /// Flag to extract inpaint from one of the example's channels 267 | #[structopt(long, parse(try_from_str = parse_mask), conflicts_with = "inpaint")] 268 | inpaint_channel: Option, 269 | /// Size of the generated image, in `width x height`, or a single number for both dimensions 270 | #[structopt( 271 | long, 272 | default_value = "500", 273 | parse(try_from_str = parse_size) 274 | )] 275 | out_size: Dims, 276 | /// Output format detection when writing to a file is based on the extension, but when 277 | /// writing to stdout by passing `-` you must specify the format if you want something 278 | /// other than the default. 279 | #[structopt( 280 | long, 281 | default_value = "png", 282 | parse(try_from_str = parse_img_fmt) 283 | )] 284 | stdout_fmt: ImgFmt, 285 | /// Resize input example map(s), in `width x height`, or a single number for both dimensions 286 | #[structopt(long, parse(try_from_str = parse_size))] 287 | in_size: Option, 288 | /// The path to save the generated image to, the file extensions of the path determines 289 | /// the image format used. You may use `-` for stdout. 290 | #[structopt(long = "out", short, parse(from_os_str))] 291 | output_path: PathBuf, 292 | /// A directory into which debug images are also saved. 293 | /// 294 | /// * `patch_id.png` - Map of the `copy islands` from an example 295 | /// * `map_id.png` - Map of ids of which example was the source of a pixel 296 | /// * `uncertainty.png` - Map of pixels the generator was uncertain of 297 | #[structopt(long, parse(from_os_str))] 298 | debug_out_dir: Option, 299 | /// The maximum number of worker threads that can be active at any one time 300 | /// while synthesizing images. Defaults to the logical core count. 301 | /// 302 | /// Note that setting this to `1` will allow you to generate 100% 303 | /// deterministic output images (considering all other inputs are 304 | /// the same) 305 | #[structopt(short = "t", long = "threads")] 306 | max_threads: Option, 307 | #[structopt(flatten)] 308 | tweaks: Tweaks, 309 | #[structopt(subcommand)] 310 | cmd: Subcommand, 311 | } 312 | 313 | fn main() { 314 | if let Err(e) = real_main() { 315 | eprintln!("error: {}", e); 316 | #[allow(clippy::exit)] 317 | std::process::exit(1); 318 | } 319 | } 320 | 321 | fn real_main() -> Result<(), Error> { 322 | let args = Opt::from_args(); 323 | 324 | // Check that the output format or extension for the path supplied by the user is one of the ones we support 325 | { 326 | if args.output_path.to_str() != Some("-") { 327 | match args.output_path.extension().and_then(|ext| ext.to_str()) { 328 | Some("png" | "jpg" | "bmp") => {} 329 | Some(other) => return Err(Error::UnsupportedOutputFormat(other.to_owned())), 330 | None => return Err(Error::UnsupportedOutputFormat(String::new())), 331 | } 332 | } 333 | } 334 | 335 | let (mut examples, target_guide) = match &args.cmd { 336 | Subcommand::Generate(gen) => { 337 | let mut examples: Vec<_> = gen.examples.iter().map(Example::new).collect(); 338 | if !gen.example_guides.is_empty() { 339 | for (ex, guide) in examples.iter_mut().zip(gen.example_guides.iter()) { 340 | ex.with_guide(guide); 341 | } 342 | } 343 | 344 | (examples, gen.target_guide.as_ref()) 345 | } 346 | Subcommand::FlipAndRotate(fr) => { 347 | let example_imgs = fr 348 | .examples 349 | .iter() 350 | .map(|path| load_dynamic_image(ImageSource::Path(path))) 351 | .collect::, _>>()?; 352 | 353 | let mut transformed: Vec> = Vec::with_capacity(example_imgs.len() * 7); 354 | for img in &example_imgs { 355 | transformed.push(Example::new(img.fliph())); 356 | transformed.push(Example::new(img.rotate90())); 357 | transformed.push(Example::new(img.fliph().rotate90())); 358 | transformed.push(Example::new(img.rotate180())); 359 | transformed.push(Example::new(img.fliph().rotate180())); 360 | transformed.push(Example::new(img.rotate270())); 361 | transformed.push(Example::new(img.fliph().rotate270())); 362 | } 363 | 364 | let mut examples: Vec<_> = example_imgs.into_iter().map(Example::new).collect(); 365 | examples.append(&mut transformed); 366 | 367 | (examples, None) 368 | } 369 | Subcommand::TransferStyle(ts) => (vec![Example::new(&ts.style)], Some(&ts.guide)), 370 | Subcommand::Repeat(rep) => { 371 | return repeat::cmd(rep, &args); 372 | } 373 | }; 374 | 375 | if !args.sample_masks.is_empty() { 376 | for (i, mask) in args.sample_masks.iter().enumerate() { 377 | // Just ignore sample masks that don't have a corresponding example, 378 | // though we could also just error out 379 | if i == examples.len() { 380 | break; 381 | } 382 | 383 | let example = &mut examples[i]; 384 | match mask.as_str() { 385 | "ALL" => example.set_sample_method(SampleMethod::All), 386 | "IGNORE" => example.set_sample_method(SampleMethod::Ignore), 387 | path => example.set_sample_method(SampleMethod::Image(ImageSource::from_path( 388 | std::path::Path::new(path), 389 | ))), 390 | }; 391 | } 392 | } 393 | 394 | let mut sb = Session::builder(); 395 | 396 | // TODO: Make inpaint work with multiple examples 397 | match (args.inpaint_channel, &args.inpaint) { 398 | (Some(channel), None) => { 399 | let inpaint_example = examples.remove(0); 400 | 401 | sb = sb.inpaint_example_channel(channel, inpaint_example, args.out_size); 402 | } 403 | (None, Some(inpaint)) => { 404 | let mut inpaint_example = examples.remove(0); 405 | 406 | // If the user hasn't explicitly specified sample masks, assume they 407 | // want to use the same mask 408 | if args.sample_masks.is_empty() { 409 | inpaint_example.set_sample_method(inpaint); 410 | } 411 | 412 | sb = sb.inpaint_example(inpaint, inpaint_example, args.out_size); 413 | } 414 | (None, None) => {} 415 | (Some(_), Some(_)) => unreachable!("we prevent this combination with 'conflicts_with'"), 416 | } 417 | 418 | sb = sb 419 | .add_examples(examples) 420 | .output_size(args.out_size) 421 | .seed(args.tweaks.seed.unwrap_or_default()) 422 | .nearest_neighbors(args.tweaks.k_neighbors) 423 | .random_sample_locations(args.tweaks.m_rand) 424 | .cauchy_dispersion(args.tweaks.cauchy) 425 | .backtrack_percent(args.tweaks.backtrack_percentage) 426 | .backtrack_stages(args.tweaks.backtrack_stages) 427 | .guide_alpha(args.tweaks.alpha) 428 | .tiling_mode(args.tweaks.enable_tiling); 429 | 430 | if let Some(mt) = args.max_threads { 431 | sb = sb.max_thread_count(mt); 432 | } 433 | 434 | if let Some(tg) = target_guide { 435 | sb = sb.load_target_guide(tg); 436 | } 437 | 438 | if let Some(rand_init) = args.tweaks.rand_init { 439 | sb = sb.random_init(rand_init); 440 | } 441 | 442 | if let Some(insize) = args.in_size { 443 | sb = sb.resize_input(insize); 444 | } 445 | 446 | let session = sb.build()?; 447 | 448 | #[cfg(not(target_arch = "wasm32"))] 449 | let progress: Option> = 450 | if !args.tweaks.no_progress { 451 | let progress = progress_window::ProgressWindow::new(); 452 | 453 | #[cfg(feature = "progress")] 454 | let progress = { 455 | if args.tweaks.show_window { 456 | progress.with_preview(args.out_size, std::time::Duration::from_millis(100))? 457 | } else { 458 | progress 459 | } 460 | }; 461 | 462 | Some(Box::new(progress)) 463 | } else { 464 | None 465 | }; 466 | 467 | #[cfg(target_arch = "wasm32")] 468 | let progress = None; 469 | 470 | let generated = session.run(progress); 471 | 472 | if let Some(ref dir) = args.debug_out_dir { 473 | generated.save_debug(dir)?; 474 | } 475 | 476 | if let Subcommand::Generate(gen) = args.cmd { 477 | if let Some(ref st_path) = gen.save_transform { 478 | if let Err(e) = repeat::save_coordinate_transform(&generated, st_path) { 479 | // Continue going, presumably the user will be ok with this 480 | // failing if they can at least get the actual generated image 481 | eprintln!( 482 | "unable to save coordinate transform to '{}': {}", 483 | st_path.display(), 484 | e 485 | ); 486 | } 487 | } 488 | } 489 | 490 | if args.output_path.to_str() == Some("-") { 491 | let out = std::io::stdout(); 492 | let mut out = out.lock(); 493 | generated.write(&mut out, args.stdout_fmt)?; 494 | } else { 495 | generated.save(&args.output_path)?; 496 | } 497 | 498 | Ok(()) 499 | } 500 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # `🎨 texture-synthesis` 4 | 5 | [![Embark](https://img.shields.io/badge/embark-open%20source-blueviolet.svg)](http://embark.games) 6 | [![Embark](https://img.shields.io/badge/discord-ark-%237289da.svg?logo=discord)](https://discord.gg/dAuKfZS) 7 | [![Crates.io](https://img.shields.io/crates/v/texture-synthesis.svg)](https://crates.io/crates/texture-synthesis) 8 | [![Docs](https://docs.rs/texture-synthesis/badge.svg)](https://docs.rs/texture-synthesis) 9 | [![dependency status](https://deps.rs/repo/github/EmbarkStudios/texture-synthesis/status.svg)](https://deps.rs/repo/github/EmbarkStudios/texture-synthesis) 10 | [![Build Status](https://github.com/EmbarkStudios/texture-synthesis/workflows/CI/badge.svg)](https://github.com/EmbarkStudios/texture-synthesis/actions?workflow=CI) 11 | 12 | A light Rust API for _Multiresolution Stochastic Texture Synthesis_ [1], a non-parametric example-based algorithm for image generation. 13 | 14 | The repo also includes multiple code examples to get you started (along with test images), and you can find a compiled binary with a command line interface under the release tab. 15 | 16 | Also see our talk [_More Like This, Please! Texture Synthesis and Remixing from a Single Example_](https://youtu.be/fMbK7PYQux4) which explains this technique and the background more in-depth: 17 | 18 | [![Video thumbnail](imgs/docs/video-thumbnail.jpg)](https://www.youtube.com/watch?v=fMbK7PYQux4&t=6m57s) 19 | 20 |
21 | 22 | ## Maintenance note 23 | 24 | We at Embark are not actively using or developing these crates and would be open to transferring them to a maintainer or maintainers that would be more active. See [#166](https://github.com/EmbarkStudios/texture-synthesis/issues/166). 25 | 26 | ## Features and examples 27 | 28 | ### 1. Single example generation 29 | 30 | ![Imgur](https://i.imgur.com/CsZoSPS.jpg) 31 | 32 | Generate similar-looking images from a single example. 33 | 34 | #### API - [01_single_example_synthesis](lib/examples/01_single_example_synthesis.rs) 35 | 36 | ```rust 37 | use texture_synthesis as ts; 38 | 39 | fn main() -> Result<(), ts::Error> { 40 | //create a new session 41 | let texsynth = ts::Session::builder() 42 | //load a single example image 43 | .add_example(&"imgs/1.jpg") 44 | .build()?; 45 | 46 | //generate an image 47 | let generated = texsynth.run(None); 48 | 49 | //save the image to the disk 50 | generated.save("out/01.jpg") 51 | } 52 | ``` 53 | 54 | #### CLI 55 | 56 | `cargo run --release -- --out out/01.jpg generate imgs/1.jpg` 57 | 58 | You should get the following result with the images provided in this repo: 59 | 60 | 61 | 62 | ### 2. Multi example generation 63 | 64 | ![Imgur](https://i.imgur.com/rYaae2w.jpg) 65 | 66 | We can also provide multiple example images and the algorithm will "remix" them into a new image. 67 | 68 | #### API - [02_multi_example_synthesis](lib/examples/02_multi_example_synthesis.rs) 69 | 70 | ```rust 71 | use texture_synthesis as ts; 72 | 73 | fn main() -> Result<(), ts::Error> { 74 | // create a new session 75 | let texsynth = ts::Session::builder() 76 | // load multiple example image 77 | .add_examples(&[ 78 | &"imgs/multiexample/1.jpg", 79 | &"imgs/multiexample/2.jpg", 80 | &"imgs/multiexample/3.jpg", 81 | &"imgs/multiexample/4.jpg", 82 | ]) 83 | // we can ensure all of them come with same size 84 | // that is however optional, the generator doesnt care whether all images are same sizes 85 | // however, if you have guides or other additional maps, those have to be same size(s) as corresponding example(s) 86 | .resize_input(ts::Dims { 87 | width: 300, 88 | height: 300, 89 | }) 90 | // randomly initialize first 10 pixels 91 | .random_init(10) 92 | .seed(211) 93 | .build()?; 94 | 95 | // generate an image 96 | let generated = texsynth.run(None); 97 | 98 | // save the image to the disk 99 | generated.save("out/02.jpg")?; 100 | 101 | //save debug information to see "remixing" borders of different examples in map_id.jpg 102 | //different colors represent information coming from different maps 103 | generated.save_debug("out/") 104 | } 105 | ``` 106 | 107 | #### CLI 108 | 109 | `cargo run --release -- --rand-init 10 --seed 211 --in-size 300x300 -o out/02.png --debug-out-dir out generate imgs/multiexample/1.jpg imgs/multiexample/2.jpg imgs/multiexample/3.jpg imgs/multiexample/4.jpg` 110 | 111 | You should get the following result with the images provided in this repo: 112 | 113 | 114 | 115 | ### 3. Guided Synthesis 116 | 117 | ![Imgur](https://i.imgur.com/eAiNZBg.jpg) 118 | 119 | We can also guide the generation by providing a transformation "FROM"-"TO" in a form of guide maps 120 | 121 | #### API - [03_guided_synthesis](lib/examples/03_guided_synthesis.rs) 122 | 123 | ```rust 124 | use texture_synthesis as ts; 125 | 126 | fn main() -> Result<(), ts::Error> { 127 | let texsynth = ts::Session::builder() 128 | // NOTE: it is important that example(s) and their corresponding guides have same size(s) 129 | // you can ensure that by overwriting the input images sizes with .resize_input() 130 | .add_example(ts::Example::builder(&"imgs/2.jpg").with_guide(&"imgs/masks/2_example.jpg")) 131 | // load target "heart" shape that we would like the generated image to look like 132 | // now the generator will take our target guide into account during synthesis 133 | .load_target_guide(&"imgs/masks/2_target.jpg") 134 | .build()?; 135 | 136 | let generated = texsynth.run(None); 137 | 138 | // save the image to the disk 139 | generated.save("out/03.jpg") 140 | } 141 | ``` 142 | 143 | #### CLI 144 | 145 | `cargo run --release -- -o out/03.png generate --target-guide imgs/masks/2_target.jpg --guides imgs/masks/2_example.jpg -- imgs/2.jpg` 146 | 147 | **NOTE:** Note the use of `--` to delimit the path to the example `imgs/2.jpg`, if you don't specify `--`, the path 148 | to the example will be used as another guide path and there won't be any examples. 149 | 150 | You should get the following result with the images provided in this repo: 151 | 152 | 153 | 154 | ### 4. Style Transfer 155 | 156 | ![Imgur](https://i.imgur.com/o9UxFGO.jpg) 157 | 158 | Texture synthesis API supports auto-generation of example guide maps, which produces a style transfer-like effect. 159 | 160 | #### API - [04_style_transfer](lib/examples/04_style_transfer.rs) 161 | 162 | ```rust 163 | use texture_synthesis as ts; 164 | 165 | fn main() -> Result<(), ts::Error> { 166 | let texsynth = ts::Session::builder() 167 | // load example which will serve as our style, note you can have more than 1! 168 | .add_examples(&[&"imgs/multiexample/4.jpg"]) 169 | // load target which will be the content 170 | // with style transfer, we do not need to provide example guides 171 | // they will be auto-generated if none were provided 172 | .load_target_guide(&"imgs/tom.jpg") 173 | .guide_alpha(0.8) 174 | .build()?; 175 | 176 | // generate an image that applies 'style' to "tom.jpg" 177 | let generated = texsynth.run(None); 178 | 179 | // save the result to the disk 180 | generated.save("out/04.jpg") 181 | } 182 | ``` 183 | 184 | #### CLI 185 | 186 | `cargo run --release -- --alpha 0.8 -o out/04.png transfer-style --style imgs/multiexample/4.jpg --guide imgs/tom.jpg` 187 | 188 | You should get the following result with the images provided in this repo: 189 | 190 | 191 | 192 | ### 5. Inpaint 193 | 194 | ![Imgur](https://i.imgur.com/FqvV651.jpg) 195 | 196 | We can also fill-in missing information with inpaint. By changing the seed, we will get different version of the 'fillment'. 197 | 198 | #### API - [05_inpaint](lib/examples/05_inpaint.rs) 199 | 200 | ```rust 201 | use texture_synthesis as ts; 202 | 203 | fn main() -> Result<(), ts::Error> { 204 | let texsynth = ts::Session::builder() 205 | // let the generator know which part we would like to fill in 206 | // if we had more examples, they would be additional information 207 | // the generator could use to inpaint 208 | .inpaint_example( 209 | &"imgs/masks/3_inpaint.jpg", 210 | // load a "corrupted" example with missing red information we would like to fill in 211 | ts::Example::builder(&"imgs/3.jpg") 212 | // we would also like to prevent sampling from "corrupted" red areas 213 | // otherwise, generator will treat that those as valid areas it can copy from in the example, 214 | // we could also use SampleMethod::Ignore to ignore the example altogether, but we 215 | // would then need at least 1 other example image to actually source from 216 | // example.set_sample_method(ts::SampleMethod::Ignore); 217 | .set_sample_method(&"imgs/masks/3_inpaint.jpg"), 218 | // Inpaint requires that inputs and outputs be the same size, so it's a required 219 | // parameter that overrides both `resize_input` and `output_size` 220 | ts::Dims::square(400), 221 | ) 222 | // Ignored 223 | .resize_input(ts::Dims::square(200)) 224 | // Ignored 225 | .output_size(ts::Dims::square(100)) 226 | .build()?; 227 | 228 | let generated = texsynth.run(None); 229 | 230 | //save the result to the disk 231 | generated.save("out/05.jpg") 232 | } 233 | ``` 234 | 235 | #### CLI 236 | 237 | Note that the `--out-size` parameter determines the size for all inputs and outputs when using inpaint! 238 | 239 | `cargo run --release -- --out-size 400 --inpaint imgs/masks/3_inpaint.jpg -o out/05.png generate imgs/3.jpg` 240 | 241 | You should get the following result with the images provided in this repo: 242 | 243 | 244 | 245 | ### 6. Inpaint Channel 246 | 247 | ![bricks](imgs/bricks.png) 248 | 249 | Instead of using a separate image for our inpaint mask, we can instead obtain the information from a specific 250 | channel. In this example, the alpha channel is a circle directly in the middle of the image. 251 | 252 | #### API - [06_inpaint_channel](lib/examples/06_inpaint_channel.rs) 253 | 254 | ```rust 255 | use texture_synthesis as ts; 256 | 257 | fn main() -> Result<(), ts::Error> { 258 | let texsynth = ts::Session::builder() 259 | // Let the generator know that it is using 260 | .inpaint_example_channel( 261 | ts::ChannelMask::A, 262 | &"imgs/bricks.png", 263 | ts::Dims::square(400), 264 | ) 265 | .build()?; 266 | 267 | let generated = texsynth.run(None); 268 | 269 | //save the result to the disk 270 | generated.save("out/06.jpg") 271 | } 272 | ``` 273 | 274 | #### CLI 275 | 276 | `cargo run --release -- --inpaint-channel a -o out/06.png generate imgs/bricks.jpg` 277 | 278 | You should get the following result with the images provided in this repo: 279 | 280 | 281 | 282 | ### 7. Tiling texture 283 | 284 | ![](https://i.imgur.com/nFpCFzy.jpg) 285 | 286 | We can make the generated image tile (meaning it will not have seams if you put multiple images together side-by-side). By invoking inpaint mode together with tiling, we can make an existing image tile. 287 | 288 | #### API - [07_tiling_texture](lib/examples/07_tiling_texture.rs) 289 | 290 | ```rust 291 | use texture_synthesis as ts; 292 | 293 | fn main() -> Result<(), ts::Error> { 294 | // Let's start layering some of the "verbs" of texture synthesis 295 | // if we just run tiling_mode(true) we will generate a completely new image from scratch (try it!) 296 | // but what if we want to tile an existing image? 297 | // we can use inpaint! 298 | 299 | let texsynth = ts::Session::builder() 300 | // load a mask that specifies borders of the image we can modify to make it tiling 301 | .inpaint_example( 302 | &"imgs/masks/1_tile.jpg", 303 | ts::Example::new(&"imgs/1.jpg"), 304 | ts::Dims::square(400), 305 | ) 306 | //turn on tiling mode! 307 | .tiling_mode(true) 308 | .build()?; 309 | 310 | let generated = texsynth.run(None); 311 | 312 | generated.save("out/07.jpg") 313 | } 314 | ``` 315 | 316 | #### CLI 317 | 318 | `cargo run --release -- --inpaint imgs/masks/1_tile.jpg --out-size 400 --tiling -o out/07.bmp generate imgs/1.jpg` 319 | 320 | You should get the following result with the images provided in this repo: 321 | 322 | 323 | 324 | ### 8. Repeat texture synthesis transform on a new image 325 | 326 | ![](https://i.imgur.com/WEf6iir.jpg) 327 | 328 | We can re-apply the coordinate transformation performed by texture synthesis onto a new image. 329 | 330 | #### API - [08_repeat_transform](lib/examples/08_repeat_transform.rs) 331 | 332 | ```rust 333 | use texture_synthesis as ts; 334 | 335 | fn main() -> Result<(), ts::Error> { 336 | // create a new session 337 | let texsynth = ts::Session::builder() 338 | //load a single example image 339 | .add_example(&"imgs/1.jpg") 340 | .build()?; 341 | 342 | // generate an image 343 | let generated = texsynth.run(None); 344 | 345 | // now we can apply the same transformation of the generated image 346 | // onto a new image (which can be used to ensure 1-1 mapping between multiple images) 347 | // NOTE: it is important to provide same number of input images as the 348 | // otherwise, there will be coordinates mismatch 349 | let repeat_transform_img = generated 350 | .get_coordinate_transform() 351 | .apply(&["imgs/1_bw.jpg"])?; 352 | 353 | // save the image to the disk 354 | // 08 and 08_repeated images should match perfectly 355 | repeat_transform_img.save("out/08_repeated.jpg").unwrap(); 356 | generated.save("out/08.jpg") 357 | } 358 | ``` 359 | 360 | #### CLI 361 | 362 | 1. First, we need to create a transform that can be reused 363 | 364 | The notable bit here is the `--save-transform out/multi.xform` which creates the 365 | file that can be used to generate new outputs with. 366 | 367 | `cargo run --release -- --rand-init 10 --seed 211 --in-size 300x300 -o 368 | out/02.png generate --save-transform out/multi.xform imgs/multiexample/1.jpg imgs/multiexample/2.jpg imgs/multiexample/3.jpg 369 | imgs/multiexample/4.jpg` 370 | 371 | 2. Next, we use the `repeat` subcommand to repeat transform with different 372 | inputs 373 | 374 | The important bits here are the use of the `repeat` subcommand instead of 375 | `generate`, and `--transform out/multi.xform` which tells what transform to 376 | apply to the inputs. The only restriction is that the number of images you 377 | specify must match the original number of examples **exactly**. If the input 378 | images have different dimensions than the example images, they will be 379 | automatically resized for you. 380 | 381 | `cargo run --release -- -o out/02-repeated.png repeat --transform 382 | out/multi.xform imgs/multiexample/1.jpg imgs/multiexample/2.jpg 383 | imgs/multiexample/4.jpg imgs/multiexample/3.jpg` 384 | 385 | Also note that the normal parameters that are used with `generate` don't apply 386 | to the `repeat` subcommand and will be ignored. 387 | 388 | ### 9. Sample masks 389 | 390 | Sample masks allow you to specify how an example image is sampled during generation. 391 | 392 | #### API - [09_sample_masks](lib/examples/09_sample_masks.rs) 393 | 394 | ```rust 395 | use texture_synthesis as ts; 396 | 397 | fn main() -> Result<(), ts::Error> { 398 | let session = ts::Session::builder() 399 | .add_example( 400 | ts::Example::builder(&"imgs/4.png").set_sample_method(ts::SampleMethod::Ignore), 401 | ) 402 | .add_example(ts::Example::builder(&"imgs/5.png").set_sample_method(ts::SampleMethod::All)) 403 | .seed(211) 404 | .output_size(ts::Dims::square(200)) 405 | .build()?; 406 | 407 | // generate an image 408 | let generated = session.run(None); 409 | 410 | // save the image to the disk 411 | generated.save("out/09.png") 412 | } 413 | ``` 414 | 415 | #### CLI 416 | 417 | `cargo run --release -- --seed 211 --out-size 200 --sample-masks IGNORE ALL --out 09_sample_masks.png generate imgs/4.png imgs/5.png` 418 | 419 | You should get the following result with the images provided in this repo: 420 | 421 | 422 | 423 | ### 10. Combining texture synthesis 'verbs' 424 | 425 | We can also combine multiple modes together. For example, multi-example guided synthesis: 426 | 427 | ![](https://i.imgur.com/By64UXG.jpg) 428 | 429 | Or chaining multiple stages of generation together: 430 | 431 | ![](https://i.imgur.com/FzZW3sl.jpg) 432 | 433 | For more use cases and examples, please refer to the presentation ["More Like This, Please! Texture Synthesis and Remixing from a Single Example"](https://youtu.be/fMbK7PYQux4) 434 | 435 | ### Additional CLI functionality 436 | 437 | Some functionality is only exposed through the CLI and not built into the library. 438 | 439 | #### `flip-and-rotate` 440 | 441 | This subcommand takes each example and performs flip and rotation transformations to it to generate additional example inputs for generation. This subcommand doesn't support target or example guides. 442 | 443 | Example: `cargo run --release -- -o out/output.png flip-and-rotate imgs/1.jpg` 444 | 445 | ## Command line binary 446 | 447 | * [Download the binary](https://github.com/EmbarkStudios/texture-synthesis/releases) for your OS. 448 | * **Or** Install it from source. 449 | * [Install Rust](https://www.rust-lang.org/tools/install) - The minimum required version is `1.37.0` 450 | * [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) 451 | * In a terminal `cd` to the directory you cloned this repository into 452 | * Run `cargo install --path=cli` 453 | * **Or** if you wish to see the texture as it is being synthesized `cargo install --path=cli --features="progress"` 454 | * Open a terminal 455 | * Navigate to the directory where you downloaded the binary, if you didn't just `cargo install` it 456 | * Run `texture_synthesis --help` to get a list of all of the options and commands you can run 457 | * Refer to the examples section in this readme for examples of running the binary 458 | 459 | ## Notes 460 | 461 | * By default, generating output will use all of your logical cores 462 | * When using multiple threads for generation, the output image is not guaranteed to be deterministic with the same inputs. To have 100% determinism, you must use a thread count of one, which can by done via 463 | * CLI - `texture-synthesis --threads 1` 464 | * API - `SessionBuilder::max_thread_count(1)` 465 | 466 | ## Limitations 467 | 468 | * Struggles with complex semantics beyond pixel color (unless you guide it) 469 | * Not great with regular textures (seams can become obvious) 470 | * Cannot infer new information from existing information (only operates on what’s already there) 471 | * Designed for single exemplars or very small datasets (unlike Deep Learning based approaches) 472 | 473 | ## Additional Dependencies 474 | 475 | If you're compiling for Linux, you'll need to have `libxkbcommon` development libraries installed. For ubuntu this is `libxkbcommon-x11-dev`. 476 | 477 | ## Links/references 478 | 479 | [1] [Opara & Stachowiak] ["More Like This, Please! Texture Synthesis and Remixing from a Single Example"](https://youtu.be/fMbK7PYQux4) 480 | 481 | [2] [Harrison] Image Texture Tools 482 | 483 | [3] [Ashikhmin] Synthesizing Natural Textures 484 | 485 | [4] [Efros & Leung] Texture Synthesis by Non-parametric Sampling 486 | 487 | [5] [Wey & Levoy] Fast Texture Synthesis using Tree-structured Vector Quantization 488 | 489 | [6] [De Bonet] Multiresolution Sampling Procedure for Analysis and Synthesis of Texture Images 490 | 491 | [7] All the test images in this repo are from [Unsplash](https://unsplash.com/) 492 | 493 | ## Contributing 494 | 495 | [![Contributor Covenant](https://img.shields.io/badge/contributor%20covenant-v1.4-ff69b4.svg)](../CODE_OF_CONDUCT.md) 496 | 497 | We welcome community contributions to this project. 498 | 499 | Please read our [Contributor Guide](CONTRIBUTING.md) for more information on how to get started. 500 | 501 | ## License 502 | 503 | Licensed under either of 504 | 505 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) 506 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 507 | 508 | at your option. 509 | 510 | ### Contribution 511 | 512 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 513 | -------------------------------------------------------------------------------- /lib/src/session.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// Texture synthesis session. 4 | /// 5 | /// Calling `run()` will generate a new image and return it, consuming the 6 | /// session in the process. You can provide a `GeneratorProgress` implementation 7 | /// to periodically get updates with the currently generated image and the 8 | /// number of pixels that have been resolved both in the current stage and 9 | /// globally. 10 | /// 11 | /// # Example 12 | /// ```no_run 13 | /// let tex_synth = texture_synthesis::Session::builder() 14 | /// .seed(10) 15 | /// .tiling_mode(true) 16 | /// .add_example(&"imgs/1.jpg") 17 | /// .build().expect("failed to build session"); 18 | /// 19 | /// let generated_img = tex_synth.run(None); 20 | /// generated_img.save("my_generated_img.jpg").expect("failed to save image"); 21 | /// ``` 22 | pub struct Session { 23 | examples: Vec, 24 | guides: Option, 25 | sampling_methods: Vec, 26 | generator: Generator, 27 | params: Parameters, 28 | } 29 | 30 | impl Session { 31 | /// Creates a new session with default parameters. 32 | pub fn builder<'a>() -> SessionBuilder<'a> { 33 | SessionBuilder::default() 34 | } 35 | 36 | /// Runs the generator and outputs a generated image. 37 | pub fn run(mut self, progress: Option>) -> GeneratedImage { 38 | // random resolve 39 | // TODO: Instead of consuming the generator, we could instead make the 40 | // seed and random_resolve parameters, so that you could rerun the 41 | // generator with the same inputs 42 | if let Some(count) = self.params.random_resolve { 43 | let lvl = self.examples[0].pyramid.len(); 44 | let imgs: Vec<_> = self 45 | .examples 46 | .iter() 47 | .map(|a| ImageBuffer::from(&a.pyramid[lvl - 1])) //take the blurriest image 48 | .collect(); 49 | 50 | self.generator 51 | .resolve_random_batch(count as usize, &imgs, self.params.seed); 52 | } 53 | 54 | // run generator 55 | self.generator.resolve( 56 | &self.params.to_generator_params(), 57 | &self.examples, 58 | progress, 59 | &self.guides, 60 | &self.sampling_methods, 61 | ); 62 | 63 | GeneratedImage { 64 | inner: self.generator, 65 | } 66 | } 67 | } 68 | 69 | /// Builds a session by setting parameters and adding input images, calling 70 | /// `build` will check all of the provided inputs to verify that texture 71 | /// synthesis will provide valid output 72 | #[derive(Default)] 73 | pub struct SessionBuilder<'a> { 74 | examples: Vec>, 75 | target_guide: Option>, 76 | inpaint_mask: Option>, 77 | params: Parameters, 78 | } 79 | 80 | impl<'a> SessionBuilder<'a> { 81 | /// Creates a new `SessionBuilder`, can also be created via 82 | /// `Session::builder()` 83 | pub fn new() -> Self { 84 | Self::default() 85 | } 86 | 87 | /// Adds an `Example` from which a generator will synthesize a new image. 88 | /// 89 | /// See [`examples/01_single_example_synthesis`](https://github.com/EmbarkStudios/texture-synthesis/tree/main/lib/examples/01_single_example_synthesis.rs) 90 | /// 91 | /// # Examples 92 | /// 93 | /// ```no_run 94 | /// let tex_synth = texture_synthesis::Session::builder() 95 | /// .add_example(&"imgs/1.jpg") 96 | /// .build().expect("failed to build session"); 97 | /// ``` 98 | pub fn add_example>>(mut self, example: E) -> Self { 99 | self.examples.push(example.into()); 100 | self 101 | } 102 | 103 | /// Adds Examples from which a generator will synthesize a new image. 104 | /// 105 | /// See [`examples/02_multi_example_synthesis`](https://github.com/EmbarkStudios/texture-synthesis/tree/main/lib/examples/02_multi_example_synthesis.rs) 106 | /// 107 | /// # Examples 108 | /// 109 | /// ```no_run 110 | /// let tex_synth = texture_synthesis::Session::builder() 111 | /// .add_examples(&[&"imgs/1.jpg", &"imgs/2.jpg"]) 112 | /// .build().expect("failed to build session"); 113 | /// ``` 114 | pub fn add_examples>, I: IntoIterator>( 115 | mut self, 116 | examples: I, 117 | ) -> Self { 118 | self.examples.extend(examples.into_iter().map(|e| e.into())); 119 | self 120 | } 121 | 122 | /// Inpaints an example. Due to how inpainting works, a size must also be 123 | /// provided, as all examples, as well as the inpaint mask, must be the same 124 | /// size as each other, as well as the final output image. Using 125 | /// `resize_input` or `output_size` is ignored if this method is called. 126 | /// 127 | /// To prevent sampling from the example, you can specify 128 | /// `SamplingMethod::Ignore` with `Example::set_sample_method`. 129 | /// 130 | /// See [`examples/05_inpaint`](https://github.com/EmbarkStudios/texture-synthesis/tree/main/lib/examples/05_inpaint.rs) 131 | /// 132 | /// # Examples 133 | /// 134 | /// ```no_run 135 | /// let tex_synth = texture_synthesis::Session::builder() 136 | /// .add_examples(&[&"imgs/1.jpg", &"imgs/3.jpg"]) 137 | /// .inpaint_example( 138 | /// &"masks/inpaint.jpg", 139 | /// // This will prevent sampling from the imgs/2.jpg, note that 140 | /// // we *MUST* provide at least one example to source from! 141 | /// texture_synthesis::Example::builder(&"imgs/2.jpg") 142 | /// .set_sample_method(texture_synthesis::SampleMethod::Ignore), 143 | /// texture_synthesis::Dims::square(400) 144 | /// ) 145 | /// .build().expect("failed to build session"); 146 | /// ``` 147 | pub fn inpaint_example>, E: Into>>( 148 | mut self, 149 | inpaint_mask: I, 150 | example: E, 151 | size: Dims, 152 | ) -> Self { 153 | self.inpaint_mask = Some(InpaintMask { 154 | src: MaskOrImg::ImageSource(inpaint_mask.into()), 155 | example_index: self.examples.len(), 156 | dims: size, 157 | }); 158 | self.examples.push(example.into()); 159 | self 160 | } 161 | 162 | /// Inpaints an example, using a specific channel in the example image as 163 | /// the inpaint mask 164 | /// 165 | /// # Examples 166 | /// 167 | /// ```no_run 168 | /// let tex_synth = texture_synthesis::Session::builder() 169 | /// .inpaint_example_channel( 170 | /// // Let's use inpaint the alpha channel 171 | /// texture_synthesis::ChannelMask::A, 172 | /// &"imgs/bricks.png", 173 | /// texture_synthesis::Dims::square(400) 174 | /// ) 175 | /// .build().expect("failed to build session"); 176 | /// ``` 177 | pub fn inpaint_example_channel>>( 178 | mut self, 179 | mask: utils::ChannelMask, 180 | example: E, 181 | size: Dims, 182 | ) -> Self { 183 | self.inpaint_mask = Some(InpaintMask { 184 | src: MaskOrImg::Mask(mask), 185 | example_index: self.examples.len(), 186 | dims: size, 187 | }); 188 | self.examples.push(example.into()); 189 | self 190 | } 191 | 192 | /// Loads a target guide map. 193 | /// 194 | /// If no `Example` guide maps are provided, this will produce a style 195 | /// transfer effect, where the Examples are styles and the target guide is 196 | /// content. 197 | /// 198 | /// See [`examples/03_guided_synthesis`](https://github.com/EmbarkStudios/texture-synthesis/tree/main/lib/examples/03_guided_synthesis.rs), 199 | /// or [`examples/04_style_transfer`](https://github.com/EmbarkStudios/texture-synthesis/tree/main/lib/examples/04_style_transfer.rs), 200 | pub fn load_target_guide>>(mut self, guide: I) -> Self { 201 | self.target_guide = Some(guide.into()); 202 | self 203 | } 204 | 205 | /// Overwrite incoming images sizes 206 | pub fn resize_input(mut self, dims: Dims) -> Self { 207 | self.params.resize_input = Some(dims); 208 | self 209 | } 210 | 211 | /// Changes pseudo-deterministic seed. 212 | /// 213 | /// Global structures will stay same, if the same seed is provided, but 214 | /// smaller details may change due to undeterministic nature of 215 | /// multithreading. 216 | pub fn seed(mut self, value: u64) -> Self { 217 | self.params.seed = value; 218 | self 219 | } 220 | 221 | /// Makes the generator output tiling image. 222 | /// 223 | /// Default: false. 224 | pub fn tiling_mode(mut self, is_tiling: bool) -> Self { 225 | self.params.tiling_mode = is_tiling; 226 | self 227 | } 228 | 229 | /// How many neighboring pixels each pixel is aware of during generation. 230 | /// 231 | /// A larger number means more global structures are captured. 232 | /// 233 | /// Default: 50 234 | pub fn nearest_neighbors(mut self, count: u32) -> Self { 235 | self.params.nearest_neighbors = count; 236 | self 237 | } 238 | 239 | /// The number of random locations that will be considered during a pixel 240 | /// resolution apart from its immediate neighbors. 241 | /// 242 | /// If unsure, keep same as nearest neighbors. 243 | /// 244 | /// Default: 50 245 | pub fn random_sample_locations(mut self, count: u64) -> Self { 246 | self.params.random_sample_locations = count; 247 | self 248 | } 249 | 250 | /// Forces the first `n` pixels to be randomly resolved, and prevents them 251 | /// from being overwritten. 252 | /// 253 | /// Can be an enforcing factor of remixing multiple images together. 254 | pub fn random_init(mut self, count: u64) -> Self { 255 | self.params.random_resolve = Some(count); 256 | self 257 | } 258 | 259 | /// The distribution dispersion used for picking best candidate (controls 260 | /// the distribution 'tail flatness'). 261 | /// 262 | /// Values close to 0.0 will produce 'harsh' borders between generated 263 | /// 'chunks'. Values closer to 1.0 will produce a smoother gradient on those 264 | /// borders. 265 | /// 266 | /// For futher reading, check out P.Harrison's "Image Texture Tools". 267 | /// 268 | /// Default: 1.0 269 | pub fn cauchy_dispersion(mut self, value: f32) -> Self { 270 | self.params.cauchy_dispersion = value; 271 | self 272 | } 273 | 274 | /// Controls the trade-off between guide and example maps. 275 | /// 276 | /// If doing style transfer, set to about 0.8-0.6 to allow for more global 277 | /// structures of the style. 278 | /// 279 | /// If you'd like the guide maps to be considered through all generation 280 | /// stages, set to 1.0, which will prevent guide maps weight "decay" during 281 | /// the score calculation. 282 | /// 283 | /// Default: 0.8 284 | pub fn guide_alpha(mut self, value: f32) -> Self { 285 | self.params.guide_alpha = value; 286 | self 287 | } 288 | 289 | /// The percentage of pixels to be backtracked during each `p_stage`. 290 | /// Range (0,1). 291 | /// 292 | /// Default: 0.5 293 | pub fn backtrack_percent(mut self, value: f32) -> Self { 294 | self.params.backtrack_percent = value; 295 | self 296 | } 297 | 298 | /// Controls the number of backtracking stages. 299 | /// 300 | /// Backtracking prevents 'garbage' generation. Right now, the depth of the 301 | /// image pyramid for multiresolution synthesis depends on this parameter as 302 | /// well. 303 | /// 304 | /// Default: 5 305 | pub fn backtrack_stages(mut self, stages: u32) -> Self { 306 | self.params.backtrack_stages = stages; 307 | self 308 | } 309 | 310 | /// Specify size of the generated image. 311 | /// 312 | /// Default: 500x500 313 | pub fn output_size(mut self, dims: Dims) -> Self { 314 | self.params.output_size = dims; 315 | self 316 | } 317 | 318 | /// Controls the maximum number of threads that will be spawned at any one 319 | /// time in parallel. 320 | /// 321 | /// This number is allowed to exceed the number of logical cores on the 322 | /// system, but it should generally be kept at or below that number. 323 | /// 324 | /// Setting this number to `1` will result in completely deterministic 325 | /// image generation, meaning that redoing generation with the same inputs 326 | /// will always give you the same outputs. 327 | /// 328 | /// Default: The number of logical cores on this system. 329 | pub fn max_thread_count(mut self, count: usize) -> Self { 330 | self.params.max_thread_count = Some(count); 331 | self 332 | } 333 | 334 | /// Creates a `Session`, or returns an error if invalid parameters or input 335 | /// images were specified. 336 | pub fn build(mut self) -> Result { 337 | self.check_parameters_validity()?; 338 | self.check_images_validity()?; 339 | 340 | struct InpaintExample { 341 | inpaint_mask: image::RgbaImage, 342 | color_map: image::RgbaImage, 343 | example_index: usize, 344 | } 345 | 346 | let (inpaint, out_size, in_size) = match self.inpaint_mask { 347 | Some(inpaint_mask) => { 348 | let dims = inpaint_mask.dims; 349 | let inpaint_img = match inpaint_mask.src { 350 | MaskOrImg::ImageSource(img) => load_image(img, Some(dims))?, 351 | MaskOrImg::Mask(mask) => { 352 | let example_img = &mut self.examples[inpaint_mask.example_index].img; 353 | 354 | let dynamic_img = utils::load_dynamic_image(example_img.clone())?; 355 | let inpaint_src = ImageSource::Image(dynamic_img.clone()); 356 | 357 | // Replace the example image source so we don't load it twice 358 | *example_img = ImageSource::Image(dynamic_img); 359 | 360 | let inpaint_mask = load_image(inpaint_src, Some(dims))?; 361 | 362 | utils::apply_mask(inpaint_mask, mask) 363 | } 364 | }; 365 | 366 | let color_map = load_image( 367 | self.examples[inpaint_mask.example_index].img.clone(), 368 | Some(dims), 369 | )?; 370 | 371 | ( 372 | Some(InpaintExample { 373 | inpaint_mask: inpaint_img, 374 | color_map, 375 | example_index: inpaint_mask.example_index, 376 | }), 377 | dims, 378 | Some(dims), 379 | ) 380 | } 381 | None => (None, self.params.output_size, self.params.resize_input), 382 | }; 383 | 384 | let target_guide = match self.target_guide { 385 | Some(tg) => { 386 | let tg_img = load_image(tg, Some(out_size))?; 387 | 388 | let num_guides = self.examples.iter().filter(|ex| ex.guide.is_some()).count(); 389 | let tg_img = if num_guides == 0 { 390 | transform_to_guide_map(tg_img, None, 2.0) 391 | } else { 392 | tg_img 393 | }; 394 | 395 | Some(ImagePyramid::new( 396 | tg_img, 397 | Some(self.params.backtrack_stages as u32), 398 | )) 399 | } 400 | None => None, 401 | }; 402 | 403 | let example_len = self.examples.len(); 404 | 405 | let mut examples = Vec::with_capacity(example_len); 406 | let mut guides = if target_guide.is_some() { 407 | Vec::with_capacity(example_len) 408 | } else { 409 | Vec::new() 410 | }; 411 | let mut methods = Vec::with_capacity(example_len); 412 | 413 | for example in self.examples { 414 | let resolved = example.resolve(self.params.backtrack_stages, in_size, &target_guide)?; 415 | 416 | examples.push(resolved.image); 417 | 418 | if let Some(guide) = resolved.guide { 419 | guides.push(guide); 420 | } 421 | 422 | methods.push(resolved.method); 423 | } 424 | 425 | // Initialize generator based on availability of an inpaint_mask. 426 | let generator = match inpaint { 427 | None => Generator::new(out_size), 428 | Some(inpaint) => Generator::new_from_inpaint( 429 | out_size, 430 | inpaint.inpaint_mask, 431 | inpaint.color_map, 432 | inpaint.example_index, 433 | ), 434 | }; 435 | 436 | let session = Session { 437 | examples, 438 | guides: target_guide.map(|tg| GuidesPyramidStruct { 439 | target_guide: tg, 440 | example_guides: guides, 441 | }), 442 | sampling_methods: methods, 443 | params: self.params, 444 | generator, 445 | }; 446 | 447 | Ok(session) 448 | } 449 | 450 | fn check_parameters_validity(&self) -> Result<(), Error> { 451 | if self.params.cauchy_dispersion < 0.0 || self.params.cauchy_dispersion > 1.0 { 452 | return Err(Error::InvalidRange(errors::InvalidRange { 453 | min: 0.0, 454 | max: 1.0, 455 | value: self.params.cauchy_dispersion, 456 | name: "cauchy-dispersion", 457 | })); 458 | } 459 | 460 | if self.params.backtrack_percent < 0.0 || self.params.backtrack_percent > 1.0 { 461 | return Err(Error::InvalidRange(errors::InvalidRange { 462 | min: 0.0, 463 | max: 1.0, 464 | value: self.params.backtrack_percent, 465 | name: "backtrack-percent", 466 | })); 467 | } 468 | 469 | if self.params.guide_alpha < 0.0 || self.params.guide_alpha > 1.0 { 470 | return Err(Error::InvalidRange(errors::InvalidRange { 471 | min: 0.0, 472 | max: 1.0, 473 | value: self.params.guide_alpha, 474 | name: "guide-alpha", 475 | })); 476 | } 477 | 478 | if let Some(max_count) = self.params.max_thread_count { 479 | if max_count == 0 { 480 | return Err(Error::InvalidRange(errors::InvalidRange { 481 | min: 1.0, 482 | max: 1024.0, 483 | value: max_count as f32, 484 | name: "max-thread-count", 485 | })); 486 | } 487 | } 488 | 489 | if self.params.random_sample_locations == 0 { 490 | return Err(Error::InvalidRange(errors::InvalidRange { 491 | min: 1.0, 492 | max: 1024.0, 493 | value: self.params.random_sample_locations as f32, 494 | name: "m-rand", 495 | })); 496 | } 497 | 498 | Ok(()) 499 | } 500 | 501 | fn check_images_validity(&self) -> Result<(), Error> { 502 | // We must have at least one example image to source pixels from 503 | let input_count = self 504 | .examples 505 | .iter() 506 | .filter(|ex| !ex.sample_method.is_ignore()) 507 | .count(); 508 | 509 | if input_count == 0 { 510 | return Err(Error::NoExamples); 511 | } 512 | 513 | // If we have more than one example guide, then *every* example 514 | // needs a guide 515 | let num_guides = self.examples.iter().filter(|ex| ex.guide.is_some()).count(); 516 | if num_guides != 0 && self.examples.len() != num_guides { 517 | return Err(Error::ExampleGuideMismatch( 518 | self.examples.len() as u32, 519 | num_guides as u32, 520 | )); 521 | } 522 | 523 | Ok(()) 524 | } 525 | } 526 | 527 | /// Helper struct for passing progress information to external callers 528 | pub struct ProgressStat { 529 | /// The current amount of work that has been done 530 | pub current: usize, 531 | /// The total amount of work to do 532 | pub total: usize, 533 | } 534 | 535 | /// The current state of the image generator 536 | pub struct ProgressUpdate<'a> { 537 | /// The currenty resolved image 538 | pub image: &'a image::RgbaImage, 539 | /// The total progress for the final image 540 | pub total: ProgressStat, 541 | /// The progress for the current stage 542 | pub stage: ProgressStat, 543 | } 544 | 545 | /// Allows the generator to update external callers with the current 546 | /// progress of the image synthesis 547 | pub trait GeneratorProgress { 548 | fn update(&mut self, info: ProgressUpdate<'_>); 549 | } 550 | 551 | impl GeneratorProgress for G 552 | where 553 | G: FnMut(ProgressUpdate<'_>) + Send, 554 | { 555 | fn update(&mut self, info: ProgressUpdate<'_>) { 556 | self(info); 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | // BEGIN - Embark standard lints v5 for Rust 1.55+ 2 | // do not change or add/remove here, but one can add exceptions after this section 3 | // for more info see: 4 | #![deny(unsafe_code)] 5 | #![warn( 6 | clippy::all, 7 | clippy::await_holding_lock, 8 | clippy::char_lit_as_u8, 9 | clippy::checked_conversions, 10 | clippy::dbg_macro, 11 | clippy::debug_assert_with_mut_call, 12 | clippy::doc_markdown, 13 | clippy::empty_enum, 14 | clippy::enum_glob_use, 15 | clippy::exit, 16 | clippy::expl_impl_clone_on_copy, 17 | clippy::explicit_deref_methods, 18 | clippy::explicit_into_iter_loop, 19 | clippy::fallible_impl_from, 20 | clippy::filter_map_next, 21 | clippy::flat_map_option, 22 | clippy::float_cmp_const, 23 | clippy::fn_params_excessive_bools, 24 | clippy::from_iter_instead_of_collect, 25 | clippy::if_let_mutex, 26 | clippy::implicit_clone, 27 | clippy::imprecise_flops, 28 | clippy::inefficient_to_string, 29 | clippy::invalid_upcast_comparisons, 30 | clippy::large_digit_groups, 31 | clippy::large_stack_arrays, 32 | clippy::large_types_passed_by_value, 33 | clippy::let_unit_value, 34 | clippy::linkedlist, 35 | clippy::lossy_float_literal, 36 | clippy::macro_use_imports, 37 | clippy::manual_ok_or, 38 | clippy::map_err_ignore, 39 | clippy::map_flatten, 40 | clippy::map_unwrap_or, 41 | clippy::match_on_vec_items, 42 | clippy::match_same_arms, 43 | clippy::match_wild_err_arm, 44 | clippy::match_wildcard_for_single_variants, 45 | clippy::mem_forget, 46 | clippy::mismatched_target_os, 47 | clippy::missing_enforced_import_renames, 48 | clippy::mut_mut, 49 | clippy::mutex_integer, 50 | clippy::needless_borrow, 51 | clippy::needless_continue, 52 | clippy::needless_for_each, 53 | clippy::option_option, 54 | clippy::path_buf_push_overwrite, 55 | clippy::ptr_as_ptr, 56 | clippy::rc_mutex, 57 | clippy::ref_option_ref, 58 | clippy::rest_pat_in_fully_bound_structs, 59 | clippy::same_functions_in_if_condition, 60 | clippy::semicolon_if_nothing_returned, 61 | clippy::single_match_else, 62 | clippy::string_add_assign, 63 | clippy::string_add, 64 | clippy::string_lit_as_bytes, 65 | clippy::string_to_string, 66 | clippy::todo, 67 | clippy::trait_duplication_in_bounds, 68 | clippy::unimplemented, 69 | clippy::unnested_or_patterns, 70 | clippy::unused_self, 71 | clippy::useless_transmute, 72 | clippy::verbose_file_reads, 73 | clippy::zero_sized_map_values, 74 | future_incompatible, 75 | nonstandard_style, 76 | rust_2018_idioms 77 | )] 78 | #![allow(unsafe_code)] 79 | 80 | //! `texture-synthesis` is a light API for Multiresolution Stochastic Texture Synthesis, 81 | //! a non-parametric example-based algorithm for image generation. 82 | //! 83 | //! First, you build a `Session` via a `SessionBuilder`, which follows the builder pattern. Calling 84 | //! `build` on the `SessionBuilder` loads all of the input images and checks for various errors. 85 | //! 86 | //! `Session` has a `run()` method that takes all of the parameters and inputs added in the session 87 | //! builder to generated an image, which is returned as a `GeneratedImage`. 88 | //! 89 | //! You can save, stream, or inspect the image from `GeneratedImage`. 90 | //! 91 | //! ## Features 92 | //! 93 | //! 1. Single example generation 94 | //! 2. Multi example generation 95 | //! 3. Guided synthesis 96 | //! 4. Style transfer 97 | //! 5. Inpainting 98 | //! 6. Tiling textures 99 | //! 100 | //! Please, refer to the examples folder in the [repository](https://github.com/EmbarkStudios/texture-synthesis) for the features usage examples. 101 | //! 102 | //! ## Usage 103 | //! Session follows a "builder pattern" for defining parameters, meaning you chain functions together. 104 | //! 105 | //! ```no_run 106 | //! // Create a new session with default parameters 107 | //! let session = texture_synthesis::Session::builder() 108 | //! // Set some parameters 109 | //! .seed(10) 110 | //! .nearest_neighbors(20) 111 | //! // Specify example images 112 | //! .add_example(&"imgs/1.jpg") 113 | //! // Build the session 114 | //! .build().expect("failed to build session"); 115 | //! 116 | //! // Generate a new image 117 | //! let generated_img = session.run(None); 118 | //! 119 | //! // Save the generated image to disk 120 | //! generated_img.save("my_generated_img.jpg").expect("failed to save generated image"); 121 | //! ``` 122 | mod errors; 123 | mod img_pyramid; 124 | use img_pyramid::*; 125 | mod utils; 126 | use utils::*; 127 | mod ms; 128 | use ms::*; 129 | pub mod session; 130 | mod unsync; 131 | 132 | pub use image; 133 | use std::path::Path; 134 | 135 | pub use errors::Error; 136 | pub use session::{Session, SessionBuilder}; 137 | pub use utils::{load_dynamic_image, ChannelMask, ImageSource}; 138 | 139 | /// Simple dimensions struct 140 | #[derive(Copy, Clone)] 141 | #[cfg_attr(test, derive(Debug, PartialEq))] 142 | pub struct Dims { 143 | pub width: u32, 144 | pub height: u32, 145 | } 146 | 147 | impl Dims { 148 | pub fn square(size: u32) -> Self { 149 | Self { 150 | width: size, 151 | height: size, 152 | } 153 | } 154 | pub fn new(width: u32, height: u32) -> Self { 155 | Self { width, height } 156 | } 157 | } 158 | 159 | /// A buffer of transforms that were used to generate an image from a set of 160 | /// examples, which can be applied to a different set of input images to get 161 | /// a different output image. 162 | pub struct CoordinateTransform { 163 | buffer: Vec, 164 | pub output_size: Dims, 165 | original_maps: Vec, 166 | } 167 | 168 | const TRANSFORM_MAGIC: u32 = 0x1234_0001; 169 | 170 | impl<'a> CoordinateTransform { 171 | /// Applies the coordinate transformation from new source images. This 172 | /// method will fail if the the provided source images aren't the same 173 | /// number of example images that generated the transform. 174 | /// 175 | /// The input images are automatically resized to the dimensions of the 176 | /// original example images used in the generation of this coordinate 177 | /// transform 178 | pub fn apply(&self, source: I) -> Result 179 | where 180 | I: IntoIterator, 181 | E: Into>, 182 | { 183 | let ref_maps: Vec = source 184 | .into_iter() 185 | .zip(self.original_maps.iter()) 186 | .map(|(is, dims)| load_image(is.into(), Some(*dims))) 187 | .collect::, Error>>()?; 188 | 189 | // Ensure the number of inputs match the number in that generated this 190 | // transform, otherwise we would get weird results 191 | if ref_maps.len() != self.original_maps.len() { 192 | return Err(Error::MapsCountMismatch( 193 | ref_maps.len() as u32, 194 | self.original_maps.len() as u32, 195 | )); 196 | } 197 | 198 | let mut img = image::RgbaImage::new(self.output_size.width, self.output_size.height); 199 | 200 | // Populate with pixels from ref maps 201 | for (i, pix) in img.pixels_mut().enumerate() { 202 | let x = self.buffer[i * 3]; 203 | let y = self.buffer[i * 3 + 1]; 204 | let map = self.buffer[i * 3 + 2]; 205 | 206 | *pix = *ref_maps[map as usize].get_pixel(x, y); 207 | } 208 | 209 | Ok(img) 210 | } 211 | 212 | pub fn write(&self, w: &mut W) -> std::io::Result { 213 | use std::mem; 214 | let mut written = 0; 215 | 216 | // Sanity check that that buffer length corresponds correctly with the 217 | // supposed dimensions 218 | if self.buffer.len() 219 | != self.output_size.width as usize * self.output_size.height as usize * 3 220 | { 221 | return Err(std::io::Error::new( 222 | std::io::ErrorKind::InvalidInput, 223 | "buffer length doesn't match dimensions", 224 | )); 225 | } 226 | 227 | let header = [ 228 | TRANSFORM_MAGIC, 229 | self.output_size.width, 230 | self.output_size.height, 231 | self.original_maps.len() as u32, 232 | ]; 233 | 234 | fn cast(ina: &[u32]) -> &[u8] { 235 | unsafe { 236 | let p = ina.as_ptr(); 237 | let len = ina.len(); 238 | 239 | std::slice::from_raw_parts(p.cast::(), len * mem::size_of::()) 240 | } 241 | } 242 | 243 | w.write_all(cast(&header))?; 244 | written += mem::size_of_val(&header); 245 | 246 | for om in &self.original_maps { 247 | let dims = [om.width, om.height]; 248 | w.write_all(cast(&dims))?; 249 | written += mem::size_of_val(&dims); 250 | } 251 | 252 | w.write_all(cast(&self.buffer))?; 253 | written += 4 * self.buffer.len(); 254 | 255 | Ok(written) 256 | } 257 | 258 | pub fn read(r: &mut R) -> std::io::Result { 259 | use std::{ 260 | io::{Error, ErrorKind, Read}, 261 | mem, 262 | }; 263 | 264 | fn do_read(r: &mut R, buf: &mut [u32]) -> std::io::Result<()> { 265 | unsafe { 266 | let p = buf.as_mut_ptr(); 267 | let len = buf.len(); 268 | 269 | let slice = 270 | std::slice::from_raw_parts_mut(p.cast::(), len * mem::size_of::()); 271 | 272 | r.read(slice).map(|_| ()) 273 | } 274 | } 275 | 276 | let mut magic = [0u32]; 277 | do_read(r, &mut magic)?; 278 | 279 | if magic[0] >> 16 != 0x1234 { 280 | return Err(Error::new(ErrorKind::InvalidData, "invalid magic")); 281 | } 282 | 283 | let (output_size, original_maps) = match magic[0] & 0x0000_ffff { 284 | 0x1 => { 285 | let mut header = [0u32; 3]; 286 | do_read(r, &mut header)?; 287 | 288 | let mut omaps = Vec::with_capacity(header[2] as usize); 289 | for _ in 0..header[2] { 290 | let mut dims = [0u32; 2]; 291 | do_read(r, &mut dims)?; 292 | omaps.push(Dims { 293 | width: dims[0], 294 | height: dims[1], 295 | }); 296 | } 297 | 298 | ( 299 | Dims { 300 | width: header[0], 301 | height: header[1], 302 | }, 303 | omaps, 304 | ) 305 | } 306 | _ => return Err(Error::new(ErrorKind::InvalidData, "invalid version")), 307 | }; 308 | 309 | #[allow(clippy::uninit_vec)] 310 | let buffer = unsafe { 311 | let len = output_size.width as usize * output_size.height as usize * 3; 312 | let mut buffer = Vec::with_capacity(len); 313 | buffer.set_len(len); 314 | 315 | do_read(r, &mut buffer)?; 316 | buffer 317 | }; 318 | 319 | Ok(Self { 320 | buffer, 321 | output_size, 322 | original_maps, 323 | }) 324 | } 325 | } 326 | 327 | struct Parameters { 328 | tiling_mode: bool, 329 | nearest_neighbors: u32, 330 | random_sample_locations: u64, 331 | cauchy_dispersion: f32, 332 | backtrack_percent: f32, 333 | backtrack_stages: u32, 334 | resize_input: Option, 335 | output_size: Dims, 336 | guide_alpha: f32, 337 | random_resolve: Option, 338 | max_thread_count: Option, 339 | seed: u64, 340 | } 341 | 342 | impl Default for Parameters { 343 | fn default() -> Self { 344 | Self { 345 | tiling_mode: false, 346 | nearest_neighbors: 50, 347 | random_sample_locations: 50, 348 | cauchy_dispersion: 1.0, 349 | backtrack_percent: 0.5, 350 | backtrack_stages: 5, 351 | resize_input: None, 352 | output_size: Dims::square(500), 353 | guide_alpha: 0.8, 354 | random_resolve: None, 355 | max_thread_count: None, 356 | seed: 0, 357 | } 358 | } 359 | } 360 | 361 | impl Parameters { 362 | fn to_generator_params(&self) -> GeneratorParams { 363 | GeneratorParams { 364 | nearest_neighbors: self.nearest_neighbors, 365 | random_sample_locations: self.random_sample_locations, 366 | cauchy_dispersion: self.cauchy_dispersion, 367 | p: self.backtrack_percent, 368 | p_stages: self.backtrack_stages as i32, 369 | seed: self.seed, 370 | alpha: self.guide_alpha, 371 | max_thread_count: self.max_thread_count.unwrap_or_else(num_cpus::get), 372 | tiling_mode: self.tiling_mode, 373 | } 374 | } 375 | } 376 | 377 | /// An image generated by a `Session::run()` 378 | pub struct GeneratedImage { 379 | inner: ms::Generator, 380 | } 381 | 382 | impl GeneratedImage { 383 | /// Saves the generated image to the specified path 384 | pub fn save>(&self, path: P) -> Result<(), Error> { 385 | let path = path.as_ref(); 386 | if let Some(parent_path) = path.parent() { 387 | std::fs::create_dir_all(&parent_path)?; 388 | } 389 | 390 | self.inner.color_map.as_ref().save(&path)?; 391 | Ok(()) 392 | } 393 | 394 | /// Writes the generated image to the specified stream 395 | pub fn write( 396 | self, 397 | writer: &mut W, 398 | fmt: image::ImageOutputFormat, 399 | ) -> Result<(), Error> { 400 | let dyn_img = self.into_image(); 401 | Ok(dyn_img.write_to(writer, fmt)?) 402 | } 403 | 404 | /// Saves debug information such as copied patches ids, map ids (if you have 405 | /// multi example generation) and a map indicating generated pixels the 406 | /// generator was "uncertain" of. 407 | pub fn save_debug>(&self, dir: P) -> Result<(), Error> { 408 | let dir = dir.as_ref(); 409 | std::fs::create_dir_all(&dir)?; 410 | 411 | self.inner 412 | .get_uncertainty_map() 413 | .save(&dir.join("uncertainty.png"))?; 414 | let id_maps = self.inner.get_id_maps(); 415 | id_maps[0].save(&dir.join("patch_id.png"))?; 416 | id_maps[1].save(&dir.join("map_id.png"))?; 417 | 418 | Ok(()) 419 | } 420 | 421 | /// Get the coordinate transform of this generated image, which can be 422 | /// applied to new example images to get a different output image. 423 | /// 424 | /// ```no_run 425 | /// use texture_synthesis as ts; 426 | /// 427 | /// // create a new session 428 | /// let texsynth = ts::Session::builder() 429 | /// //load a single example image 430 | /// .add_example(&"imgs/1.jpg") 431 | /// .build().unwrap(); 432 | /// 433 | /// // generate an image 434 | /// let generated = texsynth.run(None); 435 | /// 436 | /// // now we can repeat the same transformation on a different image 437 | /// let repeated_transform_image = generated 438 | /// .get_coordinate_transform() 439 | /// .apply(&["imgs/2.jpg"]); 440 | /// ``` 441 | pub fn get_coordinate_transform(&self) -> CoordinateTransform { 442 | self.inner.get_coord_transform() 443 | } 444 | 445 | /// Returns the generated output image 446 | pub fn into_image(self) -> image::DynamicImage { 447 | image::DynamicImage::ImageRgba8(self.inner.color_map.into_inner()) 448 | } 449 | } 450 | 451 | impl AsRef for GeneratedImage { 452 | fn as_ref(&self) -> &image::RgbaImage { 453 | self.inner.color_map.as_ref() 454 | } 455 | } 456 | 457 | /// Method used for sampling an example image. 458 | pub enum GenericSampleMethod { 459 | /// All pixels in the example image can be sampled. 460 | All, 461 | /// No pixels in the example image will be sampled. 462 | Ignore, 463 | /// Pixels are selectively sampled based on an image. 464 | Image(Img), 465 | } 466 | 467 | pub type SampleMethod<'a> = GenericSampleMethod>; 468 | pub type SamplingMethod = GenericSampleMethod; 469 | 470 | impl GenericSampleMethod { 471 | #[inline] 472 | fn is_ignore(&self) -> bool { 473 | matches!(self, Self::Ignore) 474 | } 475 | } 476 | 477 | impl<'a, IS> From for SampleMethod<'a> 478 | where 479 | IS: Into>, 480 | { 481 | fn from(is: IS) -> Self { 482 | SampleMethod::Image(is.into()) 483 | } 484 | } 485 | 486 | /// A builder for an `Example` 487 | pub struct ExampleBuilder<'a> { 488 | img: ImageSource<'a>, 489 | guide: Option>, 490 | sample_method: SampleMethod<'a>, 491 | } 492 | 493 | impl<'a> ExampleBuilder<'a> { 494 | /// Creates a new example builder from the specified image source 495 | pub fn new>>(img: I) -> Self { 496 | Self { 497 | img: img.into(), 498 | guide: None, 499 | sample_method: SampleMethod::All, 500 | } 501 | } 502 | 503 | /// Use a guide map that describe a 'FROM' transformation. 504 | /// 505 | /// Note: If any one example has a guide, then they **all** must have 506 | /// a guide, otherwise a session will not be created. 507 | pub fn with_guide>>(mut self, guide: G) -> Self { 508 | self.guide = Some(guide.into()); 509 | self 510 | } 511 | 512 | /// Specify how the example image is sampled during texture generation. 513 | /// 514 | /// By default, all pixels in the example can be sampled. 515 | pub fn set_sample_method>>(mut self, method: M) -> Self { 516 | self.sample_method = method.into(); 517 | self 518 | } 519 | } 520 | 521 | /// An example to be used in texture generation 522 | pub struct Example<'a> { 523 | img: ImageSource<'a>, 524 | guide: Option>, 525 | sample_method: SampleMethod<'a>, 526 | } 527 | 528 | impl<'a> Example<'a> { 529 | /// Creates a new example builder from the specified image source 530 | pub fn builder>>(img: I) -> ExampleBuilder<'a> { 531 | ExampleBuilder::new(img) 532 | } 533 | 534 | pub fn image_source(&self) -> &ImageSource<'a> { 535 | &self.img 536 | } 537 | 538 | /// Creates a new example input from the specified image source 539 | pub fn new>>(img: I) -> Self { 540 | Self { 541 | img: img.into(), 542 | guide: None, 543 | sample_method: SampleMethod::All, 544 | } 545 | } 546 | 547 | /// Use a guide map that describe a 'FROM' transformation. 548 | /// 549 | /// Note: If any one example has a guide, then they **all** must have 550 | /// a guide, otherwise a session will not be created. 551 | pub fn with_guide>>(&mut self, guide: G) -> &mut Self { 552 | self.guide = Some(guide.into()); 553 | self 554 | } 555 | 556 | /// Specify how the example image is sampled during texture generation. 557 | /// 558 | /// By default, all pixels in the example can be sampled. 559 | pub fn set_sample_method>>(&mut self, method: M) -> &mut Self { 560 | self.sample_method = method.into(); 561 | self 562 | } 563 | 564 | fn resolve( 565 | self, 566 | backtracks: u32, 567 | resize: Option, 568 | target_guide: &Option, 569 | ) -> Result { 570 | let image = ImagePyramid::new(load_image(self.img, resize)?, Some(backtracks)); 571 | 572 | let guide = if let Some(tg) = target_guide { 573 | let guide = if let Some(exguide) = self.guide { 574 | let exguide = load_image(exguide, resize)?; 575 | ImagePyramid::new(exguide, Some(backtracks)) 576 | } else { 577 | // if we do not have an example guide, create it as a b/w maps of the example 578 | let mut gm = transform_to_guide_map(image.bottom().clone(), resize, 2.0); 579 | match_histograms(&mut gm, tg.bottom()); 580 | 581 | ImagePyramid::new(gm, Some(backtracks)) 582 | }; 583 | 584 | Some(guide) 585 | } else { 586 | None 587 | }; 588 | 589 | let method = match self.sample_method { 590 | SampleMethod::All => SamplingMethod::All, 591 | SampleMethod::Ignore => SamplingMethod::Ignore, 592 | SampleMethod::Image(src) => { 593 | let img = load_image(src, resize)?; 594 | SamplingMethod::Image(img) 595 | } 596 | }; 597 | 598 | Ok(ResolvedExample { 599 | image, 600 | guide, 601 | method, 602 | }) 603 | } 604 | } 605 | 606 | impl<'a> From> for Example<'a> { 607 | fn from(eb: ExampleBuilder<'a>) -> Self { 608 | Self { 609 | img: eb.img, 610 | guide: eb.guide, 611 | sample_method: eb.sample_method, 612 | } 613 | } 614 | } 615 | 616 | impl<'a, IS> From for Example<'a> 617 | where 618 | IS: Into>, 619 | { 620 | fn from(is: IS) -> Self { 621 | Example::new(is) 622 | } 623 | } 624 | 625 | enum MaskOrImg<'a> { 626 | Mask(utils::ChannelMask), 627 | ImageSource(ImageSource<'a>), 628 | } 629 | 630 | struct InpaintMask<'a> { 631 | src: MaskOrImg<'a>, 632 | example_index: usize, 633 | dims: Dims, 634 | } 635 | 636 | struct ResolvedExample { 637 | image: ImagePyramid, 638 | guide: Option, 639 | method: SamplingMethod, 640 | } 641 | 642 | #[cfg(test)] 643 | mod test { 644 | #[test] 645 | fn coord_tx_serde() { 646 | use super::CoordinateTransform as CT; 647 | 648 | let fake_buffer = vec![1, 2, 3, 4, 5, 6]; 649 | 650 | let input = CT { 651 | buffer: fake_buffer.clone(), 652 | output_size: super::Dims { 653 | width: 2, 654 | height: 1, 655 | }, 656 | original_maps: vec![ 657 | super::Dims { 658 | width: 9001, 659 | height: 9002, 660 | }, 661 | super::Dims { 662 | width: 20, 663 | height: 5, 664 | }, 665 | ], 666 | }; 667 | 668 | let mut buffer = Vec::new(); 669 | input.write(&mut buffer).unwrap(); 670 | 671 | let mut cursor = std::io::Cursor::new(&buffer); 672 | let deserialized = CT::read(&mut cursor).unwrap(); 673 | 674 | assert_eq!(deserialized.buffer, fake_buffer); 675 | assert_eq!(deserialized.output_size.width, 2); 676 | assert_eq!(deserialized.output_size.height, 1); 677 | 678 | assert_eq!( 679 | super::Dims { 680 | width: 9001, 681 | height: 9002, 682 | }, 683 | deserialized.original_maps[0] 684 | ); 685 | assert_eq!( 686 | super::Dims { 687 | width: 20, 688 | height: 5, 689 | }, 690 | deserialized.original_maps[1] 691 | ); 692 | } 693 | } 694 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler32" 7 | version = "1.2.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "0.7.18" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "ansi_term" 22 | version = "0.12.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 25 | dependencies = [ 26 | "winapi", 27 | ] 28 | 29 | [[package]] 30 | name = "atty" 31 | version = "0.2.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 34 | dependencies = [ 35 | "hermit-abi", 36 | "libc", 37 | "winapi", 38 | ] 39 | 40 | [[package]] 41 | name = "autocfg" 42 | version = "1.1.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 45 | 46 | [[package]] 47 | name = "bindgen" 48 | version = "0.56.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" 51 | dependencies = [ 52 | "bitflags", 53 | "cexpr", 54 | "clang-sys", 55 | "clap", 56 | "env_logger", 57 | "lazy_static", 58 | "lazycell", 59 | "log", 60 | "peeking_take_while", 61 | "proc-macro2", 62 | "quote", 63 | "regex", 64 | "rustc-hash", 65 | "shlex", 66 | "which", 67 | ] 68 | 69 | [[package]] 70 | name = "bit-vec" 71 | version = "0.6.3" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 74 | 75 | [[package]] 76 | name = "bitflags" 77 | version = "1.3.2" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 80 | 81 | [[package]] 82 | name = "bstr" 83 | version = "0.2.17" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 86 | dependencies = [ 87 | "lazy_static", 88 | "memchr", 89 | "regex-automata", 90 | "serde", 91 | ] 92 | 93 | [[package]] 94 | name = "bumpalo" 95 | version = "3.12.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 98 | 99 | [[package]] 100 | name = "bytemuck" 101 | version = "1.7.3" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" 104 | 105 | [[package]] 106 | name = "byteorder" 107 | version = "1.4.3" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 110 | 111 | [[package]] 112 | name = "cast" 113 | version = "0.2.7" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a" 116 | dependencies = [ 117 | "rustc_version", 118 | ] 119 | 120 | [[package]] 121 | name = "cc" 122 | version = "1.0.73" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 125 | 126 | [[package]] 127 | name = "cexpr" 128 | version = "0.4.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" 131 | dependencies = [ 132 | "nom", 133 | ] 134 | 135 | [[package]] 136 | name = "cfg-if" 137 | version = "1.0.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 140 | 141 | [[package]] 142 | name = "clang-sys" 143 | version = "1.3.1" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" 146 | dependencies = [ 147 | "glob", 148 | "libc", 149 | "libloading", 150 | ] 151 | 152 | [[package]] 153 | name = "clap" 154 | version = "2.34.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 157 | dependencies = [ 158 | "ansi_term", 159 | "atty", 160 | "bitflags", 161 | "strsim", 162 | "textwrap", 163 | "unicode-width", 164 | "vec_map", 165 | ] 166 | 167 | [[package]] 168 | name = "cmake" 169 | version = "0.1.48" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a" 172 | dependencies = [ 173 | "cc", 174 | ] 175 | 176 | [[package]] 177 | name = "color_quant" 178 | version = "1.1.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 181 | 182 | [[package]] 183 | name = "console" 184 | version = "0.15.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" 187 | dependencies = [ 188 | "encode_unicode", 189 | "libc", 190 | "once_cell", 191 | "terminal_size", 192 | "winapi", 193 | ] 194 | 195 | [[package]] 196 | name = "crc32fast" 197 | version = "1.3.2" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 200 | dependencies = [ 201 | "cfg-if", 202 | ] 203 | 204 | [[package]] 205 | name = "criterion" 206 | version = "0.3.5" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10" 209 | dependencies = [ 210 | "atty", 211 | "cast", 212 | "clap", 213 | "criterion-plot", 214 | "csv", 215 | "itertools", 216 | "lazy_static", 217 | "num-traits", 218 | "oorandom", 219 | "plotters", 220 | "rayon", 221 | "regex", 222 | "serde", 223 | "serde_cbor", 224 | "serde_derive", 225 | "serde_json", 226 | "tinytemplate", 227 | "walkdir", 228 | ] 229 | 230 | [[package]] 231 | name = "criterion-plot" 232 | version = "0.4.4" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "d00996de9f2f7559f7f4dc286073197f83e92256a59ed395f9aac01fe717da57" 235 | dependencies = [ 236 | "cast", 237 | "itertools", 238 | ] 239 | 240 | [[package]] 241 | name = "crossbeam-channel" 242 | version = "0.5.2" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" 245 | dependencies = [ 246 | "cfg-if", 247 | "crossbeam-utils", 248 | ] 249 | 250 | [[package]] 251 | name = "crossbeam-deque" 252 | version = "0.8.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" 255 | dependencies = [ 256 | "cfg-if", 257 | "crossbeam-epoch", 258 | "crossbeam-utils", 259 | ] 260 | 261 | [[package]] 262 | name = "crossbeam-epoch" 263 | version = "0.9.7" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" 266 | dependencies = [ 267 | "cfg-if", 268 | "crossbeam-utils", 269 | "lazy_static", 270 | "memoffset", 271 | "scopeguard", 272 | ] 273 | 274 | [[package]] 275 | name = "crossbeam-utils" 276 | version = "0.8.7" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" 279 | dependencies = [ 280 | "cfg-if", 281 | "lazy_static", 282 | ] 283 | 284 | [[package]] 285 | name = "csv" 286 | version = "1.1.6" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" 289 | dependencies = [ 290 | "bstr", 291 | "csv-core", 292 | "itoa 0.4.8", 293 | "ryu", 294 | "serde", 295 | ] 296 | 297 | [[package]] 298 | name = "csv-core" 299 | version = "0.1.10" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" 302 | dependencies = [ 303 | "memchr", 304 | ] 305 | 306 | [[package]] 307 | name = "cty" 308 | version = "0.2.2" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" 311 | 312 | [[package]] 313 | name = "deflate" 314 | version = "0.8.6" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" 317 | dependencies = [ 318 | "adler32", 319 | "byteorder", 320 | ] 321 | 322 | [[package]] 323 | name = "either" 324 | version = "1.6.1" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 327 | 328 | [[package]] 329 | name = "encode_unicode" 330 | version = "0.3.6" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 333 | 334 | [[package]] 335 | name = "env_logger" 336 | version = "0.8.4" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" 339 | dependencies = [ 340 | "atty", 341 | "humantime", 342 | "log", 343 | "regex", 344 | "termcolor", 345 | ] 346 | 347 | [[package]] 348 | name = "glob" 349 | version = "0.3.0" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 352 | 353 | [[package]] 354 | name = "half" 355 | version = "1.8.2" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" 358 | 359 | [[package]] 360 | name = "heck" 361 | version = "0.3.3" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 364 | dependencies = [ 365 | "unicode-segmentation", 366 | ] 367 | 368 | [[package]] 369 | name = "hermit-abi" 370 | version = "0.1.19" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 373 | dependencies = [ 374 | "libc", 375 | ] 376 | 377 | [[package]] 378 | name = "humantime" 379 | version = "2.1.0" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 382 | 383 | [[package]] 384 | name = "image" 385 | version = "0.23.12" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "7ce04077ead78e39ae8610ad26216aed811996b043d47beed5090db674f9e9b5" 388 | dependencies = [ 389 | "bytemuck", 390 | "byteorder", 391 | "color_quant", 392 | "jpeg-decoder", 393 | "num-iter", 394 | "num-rational", 395 | "num-traits", 396 | "png", 397 | ] 398 | 399 | [[package]] 400 | name = "img_hash" 401 | version = "2.1.0" 402 | source = "git+https://github.com/EmbarkStudios/img_hash.git?rev=c40da78#c40da78946840c810543e6ec53967afb7adafd74" 403 | dependencies = [ 404 | "bit-vec", 405 | "rustc-serialize", 406 | ] 407 | 408 | [[package]] 409 | name = "indicatif" 410 | version = "0.16.2" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" 413 | dependencies = [ 414 | "console", 415 | "lazy_static", 416 | "number_prefix", 417 | "regex", 418 | ] 419 | 420 | [[package]] 421 | name = "itertools" 422 | version = "0.10.3" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 425 | dependencies = [ 426 | "either", 427 | ] 428 | 429 | [[package]] 430 | name = "itoa" 431 | version = "0.4.8" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 434 | 435 | [[package]] 436 | name = "itoa" 437 | version = "1.0.1" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 440 | 441 | [[package]] 442 | name = "jpeg-decoder" 443 | version = "0.1.22" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" 446 | 447 | [[package]] 448 | name = "js-sys" 449 | version = "0.3.56" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" 452 | dependencies = [ 453 | "wasm-bindgen", 454 | ] 455 | 456 | [[package]] 457 | name = "lazy_static" 458 | version = "1.4.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 461 | 462 | [[package]] 463 | name = "lazycell" 464 | version = "1.3.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 467 | 468 | [[package]] 469 | name = "libc" 470 | version = "0.2.119" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" 473 | 474 | [[package]] 475 | name = "libloading" 476 | version = "0.7.3" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" 479 | dependencies = [ 480 | "cfg-if", 481 | "winapi", 482 | ] 483 | 484 | [[package]] 485 | name = "log" 486 | version = "0.4.14" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 489 | dependencies = [ 490 | "cfg-if", 491 | ] 492 | 493 | [[package]] 494 | name = "memchr" 495 | version = "2.4.1" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 498 | 499 | [[package]] 500 | name = "memoffset" 501 | version = "0.6.5" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 504 | dependencies = [ 505 | "autocfg", 506 | ] 507 | 508 | [[package]] 509 | name = "minifb" 510 | version = "0.19.3" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "7b6e41119d1667465608d36488fa5dcd228057a26c156e25f17f492f38435124" 513 | dependencies = [ 514 | "cc", 515 | "orbclient", 516 | "raw-window-handle 0.3.4", 517 | "winapi", 518 | "x11-dl", 519 | "xkb", 520 | ] 521 | 522 | [[package]] 523 | name = "miniz_oxide" 524 | version = "0.3.7" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" 527 | dependencies = [ 528 | "adler32", 529 | ] 530 | 531 | [[package]] 532 | name = "nom" 533 | version = "5.1.2" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" 536 | dependencies = [ 537 | "memchr", 538 | "version_check", 539 | ] 540 | 541 | [[package]] 542 | name = "num-integer" 543 | version = "0.1.44" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 546 | dependencies = [ 547 | "autocfg", 548 | "num-traits", 549 | ] 550 | 551 | [[package]] 552 | name = "num-iter" 553 | version = "0.1.42" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" 556 | dependencies = [ 557 | "autocfg", 558 | "num-integer", 559 | "num-traits", 560 | ] 561 | 562 | [[package]] 563 | name = "num-rational" 564 | version = "0.3.2" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" 567 | dependencies = [ 568 | "autocfg", 569 | "num-integer", 570 | "num-traits", 571 | ] 572 | 573 | [[package]] 574 | name = "num-traits" 575 | version = "0.2.14" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 578 | dependencies = [ 579 | "autocfg", 580 | ] 581 | 582 | [[package]] 583 | name = "num_cpus" 584 | version = "1.13.1" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 587 | dependencies = [ 588 | "hermit-abi", 589 | "libc", 590 | ] 591 | 592 | [[package]] 593 | name = "number_prefix" 594 | version = "0.4.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 597 | 598 | [[package]] 599 | name = "once_cell" 600 | version = "1.9.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" 603 | 604 | [[package]] 605 | name = "oorandom" 606 | version = "11.1.3" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" 609 | 610 | [[package]] 611 | name = "orbclient" 612 | version = "0.3.32" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "2d3aa1482d3a9cb7547932f54a20910090073e81b3b7b236277c91698a10f83e" 615 | dependencies = [ 616 | "libc", 617 | "raw-window-handle 0.3.4", 618 | "redox_syscall", 619 | "sdl2", 620 | "sdl2-sys", 621 | "wasm-bindgen", 622 | "web-sys", 623 | ] 624 | 625 | [[package]] 626 | name = "pdqselect" 627 | version = "0.1.1" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "7778906d9321dd56cde1d1ffa69a73e59dcf5fda6d366f62727adf2bd4193aee" 630 | 631 | [[package]] 632 | name = "peeking_take_while" 633 | version = "0.1.2" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" 636 | 637 | [[package]] 638 | name = "pkg-config" 639 | version = "0.3.24" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" 642 | 643 | [[package]] 644 | name = "plotters" 645 | version = "0.3.1" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" 648 | dependencies = [ 649 | "num-traits", 650 | "plotters-backend", 651 | "plotters-svg", 652 | "wasm-bindgen", 653 | "web-sys", 654 | ] 655 | 656 | [[package]] 657 | name = "plotters-backend" 658 | version = "0.3.2" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c" 661 | 662 | [[package]] 663 | name = "plotters-svg" 664 | version = "0.3.1" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9" 667 | dependencies = [ 668 | "plotters-backend", 669 | ] 670 | 671 | [[package]] 672 | name = "png" 673 | version = "0.16.8" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" 676 | dependencies = [ 677 | "bitflags", 678 | "crc32fast", 679 | "deflate", 680 | "miniz_oxide", 681 | ] 682 | 683 | [[package]] 684 | name = "proc-macro-error" 685 | version = "1.0.4" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 688 | dependencies = [ 689 | "proc-macro-error-attr", 690 | "proc-macro2", 691 | "quote", 692 | "syn", 693 | "version_check", 694 | ] 695 | 696 | [[package]] 697 | name = "proc-macro-error-attr" 698 | version = "1.0.4" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 701 | dependencies = [ 702 | "proc-macro2", 703 | "quote", 704 | "version_check", 705 | ] 706 | 707 | [[package]] 708 | name = "proc-macro2" 709 | version = "1.0.36" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 712 | dependencies = [ 713 | "unicode-xid", 714 | ] 715 | 716 | [[package]] 717 | name = "quote" 718 | version = "1.0.15" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" 721 | dependencies = [ 722 | "proc-macro2", 723 | ] 724 | 725 | [[package]] 726 | name = "rand" 727 | version = "0.8.5" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 730 | dependencies = [ 731 | "rand_core", 732 | ] 733 | 734 | [[package]] 735 | name = "rand_core" 736 | version = "0.6.3" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 739 | 740 | [[package]] 741 | name = "rand_pcg" 742 | version = "0.3.1" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" 745 | dependencies = [ 746 | "rand_core", 747 | ] 748 | 749 | [[package]] 750 | name = "raw-window-handle" 751 | version = "0.3.4" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "e28f55143d0548dad60bb4fbdc835a3d7ac6acc3324506450c5fdd6e42903a76" 754 | dependencies = [ 755 | "libc", 756 | "raw-window-handle 0.4.2", 757 | ] 758 | 759 | [[package]] 760 | name = "raw-window-handle" 761 | version = "0.4.2" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "fba75eee94a9d5273a68c9e1e105d9cffe1ef700532325788389e5a83e2522b7" 764 | dependencies = [ 765 | "cty", 766 | ] 767 | 768 | [[package]] 769 | name = "rayon" 770 | version = "1.5.1" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" 773 | dependencies = [ 774 | "autocfg", 775 | "crossbeam-deque", 776 | "either", 777 | "rayon-core", 778 | ] 779 | 780 | [[package]] 781 | name = "rayon-core" 782 | version = "1.9.1" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" 785 | dependencies = [ 786 | "crossbeam-channel", 787 | "crossbeam-deque", 788 | "crossbeam-utils", 789 | "lazy_static", 790 | "num_cpus", 791 | ] 792 | 793 | [[package]] 794 | name = "redox_syscall" 795 | version = "0.2.10" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 798 | dependencies = [ 799 | "bitflags", 800 | ] 801 | 802 | [[package]] 803 | name = "regex" 804 | version = "1.5.6" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 807 | dependencies = [ 808 | "aho-corasick", 809 | "memchr", 810 | "regex-syntax", 811 | ] 812 | 813 | [[package]] 814 | name = "regex-automata" 815 | version = "0.1.10" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 818 | 819 | [[package]] 820 | name = "regex-syntax" 821 | version = "0.6.26" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 824 | 825 | [[package]] 826 | name = "rstar" 827 | version = "0.7.1" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "0650eaaa56cbd1726fd671150fce8ac6ed9d9a25d1624430d7ee9d196052f6b6" 830 | dependencies = [ 831 | "num-traits", 832 | "pdqselect", 833 | ] 834 | 835 | [[package]] 836 | name = "rustc-hash" 837 | version = "1.1.0" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 840 | 841 | [[package]] 842 | name = "rustc-serialize" 843 | version = "0.3.24" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" 846 | 847 | [[package]] 848 | name = "rustc_version" 849 | version = "0.4.0" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 852 | dependencies = [ 853 | "semver", 854 | ] 855 | 856 | [[package]] 857 | name = "ryu" 858 | version = "1.0.9" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" 861 | 862 | [[package]] 863 | name = "same-file" 864 | version = "1.0.6" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 867 | dependencies = [ 868 | "winapi-util", 869 | ] 870 | 871 | [[package]] 872 | name = "scopeguard" 873 | version = "1.1.0" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 876 | 877 | [[package]] 878 | name = "sdl2" 879 | version = "0.35.2" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" 882 | dependencies = [ 883 | "bitflags", 884 | "lazy_static", 885 | "libc", 886 | "raw-window-handle 0.4.2", 887 | "sdl2-sys", 888 | ] 889 | 890 | [[package]] 891 | name = "sdl2-sys" 892 | version = "0.35.2" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" 895 | dependencies = [ 896 | "cfg-if", 897 | "cmake", 898 | "libc", 899 | "version-compare", 900 | ] 901 | 902 | [[package]] 903 | name = "semver" 904 | version = "1.0.6" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d" 907 | 908 | [[package]] 909 | name = "serde" 910 | version = "1.0.136" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" 913 | 914 | [[package]] 915 | name = "serde_cbor" 916 | version = "0.11.2" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" 919 | dependencies = [ 920 | "half", 921 | "serde", 922 | ] 923 | 924 | [[package]] 925 | name = "serde_derive" 926 | version = "1.0.136" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" 929 | dependencies = [ 930 | "proc-macro2", 931 | "quote", 932 | "syn", 933 | ] 934 | 935 | [[package]] 936 | name = "serde_json" 937 | version = "1.0.79" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" 940 | dependencies = [ 941 | "itoa 1.0.1", 942 | "ryu", 943 | "serde", 944 | ] 945 | 946 | [[package]] 947 | name = "shlex" 948 | version = "0.1.1" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" 951 | 952 | [[package]] 953 | name = "strsim" 954 | version = "0.8.0" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 957 | 958 | [[package]] 959 | name = "structopt" 960 | version = "0.3.26" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 963 | dependencies = [ 964 | "clap", 965 | "lazy_static", 966 | "structopt-derive", 967 | ] 968 | 969 | [[package]] 970 | name = "structopt-derive" 971 | version = "0.4.18" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 974 | dependencies = [ 975 | "heck", 976 | "proc-macro-error", 977 | "proc-macro2", 978 | "quote", 979 | "syn", 980 | ] 981 | 982 | [[package]] 983 | name = "syn" 984 | version = "1.0.86" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" 987 | dependencies = [ 988 | "proc-macro2", 989 | "quote", 990 | "unicode-xid", 991 | ] 992 | 993 | [[package]] 994 | name = "termcolor" 995 | version = "1.1.2" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 998 | dependencies = [ 999 | "winapi-util", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "terminal_size" 1004 | version = "0.1.17" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" 1007 | dependencies = [ 1008 | "libc", 1009 | "winapi", 1010 | ] 1011 | 1012 | [[package]] 1013 | name = "texture-synthesis" 1014 | version = "0.8.2" 1015 | dependencies = [ 1016 | "criterion", 1017 | "crossbeam-utils", 1018 | "image", 1019 | "img_hash", 1020 | "num_cpus", 1021 | "rand", 1022 | "rand_pcg", 1023 | "rstar", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "texture-synthesis-cli" 1028 | version = "0.8.3" 1029 | dependencies = [ 1030 | "atty", 1031 | "indicatif", 1032 | "minifb", 1033 | "structopt", 1034 | "texture-synthesis", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "textwrap" 1039 | version = "0.11.0" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1042 | dependencies = [ 1043 | "unicode-width", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "tinytemplate" 1048 | version = "1.2.1" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 1051 | dependencies = [ 1052 | "serde", 1053 | "serde_json", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "unicode-segmentation" 1058 | version = "1.9.0" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" 1061 | 1062 | [[package]] 1063 | name = "unicode-width" 1064 | version = "0.1.9" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 1067 | 1068 | [[package]] 1069 | name = "unicode-xid" 1070 | version = "0.2.2" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 1073 | 1074 | [[package]] 1075 | name = "vec_map" 1076 | version = "0.8.2" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1079 | 1080 | [[package]] 1081 | name = "version-compare" 1082 | version = "0.1.0" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" 1085 | 1086 | [[package]] 1087 | name = "version_check" 1088 | version = "0.9.4" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1091 | 1092 | [[package]] 1093 | name = "walkdir" 1094 | version = "2.3.2" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 1097 | dependencies = [ 1098 | "same-file", 1099 | "winapi", 1100 | "winapi-util", 1101 | ] 1102 | 1103 | [[package]] 1104 | name = "wasm-bindgen" 1105 | version = "0.2.79" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" 1108 | dependencies = [ 1109 | "cfg-if", 1110 | "wasm-bindgen-macro", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "wasm-bindgen-backend" 1115 | version = "0.2.79" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" 1118 | dependencies = [ 1119 | "bumpalo", 1120 | "lazy_static", 1121 | "log", 1122 | "proc-macro2", 1123 | "quote", 1124 | "syn", 1125 | "wasm-bindgen-shared", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "wasm-bindgen-macro" 1130 | version = "0.2.79" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" 1133 | dependencies = [ 1134 | "quote", 1135 | "wasm-bindgen-macro-support", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "wasm-bindgen-macro-support" 1140 | version = "0.2.79" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" 1143 | dependencies = [ 1144 | "proc-macro2", 1145 | "quote", 1146 | "syn", 1147 | "wasm-bindgen-backend", 1148 | "wasm-bindgen-shared", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "wasm-bindgen-shared" 1153 | version = "0.2.79" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" 1156 | 1157 | [[package]] 1158 | name = "web-sys" 1159 | version = "0.3.56" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" 1162 | dependencies = [ 1163 | "js-sys", 1164 | "wasm-bindgen", 1165 | ] 1166 | 1167 | [[package]] 1168 | name = "which" 1169 | version = "3.1.1" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" 1172 | dependencies = [ 1173 | "libc", 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "winapi" 1178 | version = "0.3.9" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1181 | dependencies = [ 1182 | "winapi-i686-pc-windows-gnu", 1183 | "winapi-x86_64-pc-windows-gnu", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "winapi-i686-pc-windows-gnu" 1188 | version = "0.4.0" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1191 | 1192 | [[package]] 1193 | name = "winapi-util" 1194 | version = "0.1.5" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1197 | dependencies = [ 1198 | "winapi", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "winapi-x86_64-pc-windows-gnu" 1203 | version = "0.4.0" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1206 | 1207 | [[package]] 1208 | name = "x11-dl" 1209 | version = "2.19.1" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" 1212 | dependencies = [ 1213 | "lazy_static", 1214 | "libc", 1215 | "pkg-config", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "xkb" 1220 | version = "0.2.1" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "aec02bc5de902aa579f3d2f2c522edaf40fa42963cbaffe645b058ddcc68fdb2" 1223 | dependencies = [ 1224 | "bitflags", 1225 | "libc", 1226 | "xkbcommon-sys", 1227 | ] 1228 | 1229 | [[package]] 1230 | name = "xkbcommon-sys" 1231 | version = "0.7.5" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "59a001b79d45b0b4541c228a501177f2b35db976bf7ee3f7fce8fa2381554ab5" 1234 | dependencies = [ 1235 | "bindgen", 1236 | "libc", 1237 | "pkg-config", 1238 | ] 1239 | --------------------------------------------------------------------------------