├── LICENSE ├── .gitignore ├── data └── face.rda ├── notes ├── face.png ├── cat_900.jpg ├── sketch.png ├── bird_1400.jpg ├── bird_1800.jpg ├── face3_900.jpg ├── shadow_900.jpg ├── contrast_900.jpg ├── gallery_cake.jpg ├── gallery_soba.jpg ├── others2_2000.jpg ├── others2_900.jpg ├── face_demo_2000.jpg ├── face_demo_900.jpg ├── sketcher_face.jpg ├── style_line_1200.jpg ├── gallery_container.jpg ├── gallery_tokyo_st.jpg ├── line_smooth_1500.jpg └── line_smooth_2100.jpg ├── .Rbuildignore ├── NEWS.md ├── R ├── data.R └── utils.R ├── NAMESPACE ├── sketcher.Rproj ├── man ├── face.Rd ├── plot.nimg.Rd ├── im_load.Rd ├── im_save.Rd ├── sketch.Rd └── survey.Rd ├── DESCRIPTION ├── cran-comments.md ├── LICENSE.md ├── README.Rmd └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2020 2 | COPYRIGHT HOLDER: Hiroyuki Tsuda 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | inst/doc 3 | .Rhistory 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /data/face.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/data/face.rda -------------------------------------------------------------------------------- /notes/face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/face.png -------------------------------------------------------------------------------- /notes/cat_900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/cat_900.jpg -------------------------------------------------------------------------------- /notes/sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/sketch.png -------------------------------------------------------------------------------- /notes/bird_1400.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/bird_1400.jpg -------------------------------------------------------------------------------- /notes/bird_1800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/bird_1800.jpg -------------------------------------------------------------------------------- /notes/face3_900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/face3_900.jpg -------------------------------------------------------------------------------- /notes/shadow_900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/shadow_900.jpg -------------------------------------------------------------------------------- /notes/contrast_900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/contrast_900.jpg -------------------------------------------------------------------------------- /notes/gallery_cake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/gallery_cake.jpg -------------------------------------------------------------------------------- /notes/gallery_soba.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/gallery_soba.jpg -------------------------------------------------------------------------------- /notes/others2_2000.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/others2_2000.jpg -------------------------------------------------------------------------------- /notes/others2_900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/others2_900.jpg -------------------------------------------------------------------------------- /notes/face_demo_2000.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/face_demo_2000.jpg -------------------------------------------------------------------------------- /notes/face_demo_900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/face_demo_900.jpg -------------------------------------------------------------------------------- /notes/sketcher_face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/sketcher_face.jpg -------------------------------------------------------------------------------- /notes/style_line_1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/style_line_1200.jpg -------------------------------------------------------------------------------- /notes/gallery_container.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/gallery_container.jpg -------------------------------------------------------------------------------- /notes/gallery_tokyo_st.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/gallery_tokyo_st.jpg -------------------------------------------------------------------------------- /notes/line_smooth_1500.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/line_smooth_1500.jpg -------------------------------------------------------------------------------- /notes/line_smooth_2100.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsuda16k/sketcher/HEAD/notes/line_smooth_2100.jpg -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^sketcher\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^README\.md$ 6 | ^cran-comments.md$ 7 | .git 8 | .gitignore 9 | ^notes$ 10 | ^CRAN-RELEASE$ 11 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # sketcher 0.1.2 2 | 3 | Improvements: 4 | 5 | - sketch(): added gain argument. 6 | 7 | Bug fixes: 8 | 9 | - im_load(): appropriate handling for png format image with alpha channel. 10 | 11 | # sketcher 0.1.1 12 | 13 | * initial release 14 | -------------------------------------------------------------------------------- /R/data.R: -------------------------------------------------------------------------------- 1 | #' A face image. 2 | #' 3 | #' A photograph obtained from a free stock photos site. 4 | #' pexels.com/photo/man-about-to-touch-his-face-wearing-blue-suit-718261/ 5 | #' 6 | #' @format An array with 600 x 460 * 3 dimensions. 7 | #' Each dimension represents y-coordinate, x-coordinate, and color channel. 8 | #' @examples 9 | #' plot(face) 10 | "face" 11 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(plot,nimg) 4 | S3method(print,nimg) 5 | export(im_load) 6 | export(im_save) 7 | export(sketch) 8 | export(survey) 9 | importFrom(graphics,plot) 10 | importFrom(magrittr,"%>%") 11 | importFrom(magrittr,mod) 12 | importFrom(stats,median) 13 | importFrom(stats,runif) 14 | importFrom(stringr,str_match) 15 | importFrom(stringr,str_split) 16 | importFrom(stringr,str_sub) 17 | -------------------------------------------------------------------------------- /sketcher.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /man/face.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/data.R 3 | \docType{data} 4 | \name{face} 5 | \alias{face} 6 | \title{A face image.} 7 | \format{ 8 | An array with 600 x 460 * 3 dimensions. 9 | Each dimension represents y-coordinate, x-coordinate, and color channel. 10 | } 11 | \usage{ 12 | face 13 | } 14 | \description{ 15 | A photograph obtained from a free stock photos site. 16 | pexels.com/photo/man-about-to-touch-his-face-wearing-blue-suit-718261/ 17 | } 18 | \examples{ 19 | plot(face) 20 | } 21 | \keyword{datasets} 22 | -------------------------------------------------------------------------------- /man/plot.nimg.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{plot.nimg} 4 | \alias{plot.nimg} 5 | \title{Display an image} 6 | \usage{ 7 | \method{plot}{nimg}(x, rescale = FALSE, ...) 8 | } 9 | \arguments{ 10 | \item{x}{an image} 11 | 12 | \item{rescale}{logical. if true, then pixel value is rescaled to range between 0 and 1.} 13 | 14 | \item{...}{other parameters to be passed to plot.default} 15 | } 16 | \value{ 17 | No return value, called for side effects. 18 | } 19 | \description{ 20 | Display an image 21 | } 22 | \examples{ 23 | plot(face) 24 | } 25 | -------------------------------------------------------------------------------- /man/im_load.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{im_load} 4 | \alias{im_load} 5 | \title{Load image from file or URL} 6 | \usage{ 7 | im_load(file, name) 8 | } 9 | \arguments{ 10 | \item{file}{path to file or URL} 11 | 12 | \item{name}{a string for name attribute. if missing, inferred from the file argument.} 13 | } 14 | \value{ 15 | an array of image data 16 | } 17 | \description{ 18 | Load image from file or URL 19 | } 20 | \examples{ 21 | \dontrun{ 22 | # load an image from disk 23 | im = im_load("path/to/your/image.jpg") 24 | plot(im) 25 | # load an image from URL 26 | im = im_load("http://placehold.jp/150x150.png") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /man/im_save.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{im_save} 4 | \alias{im_save} 5 | \title{Save an image to disk} 6 | \usage{ 7 | im_save(im, name, path, format = "png", quality = 0.95) 8 | } 9 | \arguments{ 10 | \item{im}{An image.} 11 | 12 | \item{name}{Name of the image file.} 13 | 14 | \item{path}{Path to file.} 15 | 16 | \item{format}{Image format. Either "jpg", "png", "tiff", or "bmp". Default is "png".} 17 | 18 | \item{quality}{(jpg only) default is 0.95. Higher quality means less compression.} 19 | } 20 | \value{ 21 | No return value, called for side effects. 22 | } 23 | \description{ 24 | Save an image to disk 25 | } 26 | \examples{ 27 | \dontrun{ 28 | im = sketch(face) 29 | 30 | # im.png is saved to the current working directory 31 | im_save( im, name = "im", path = getwd() ) 32 | 33 | # myimage.jpg is saved to a specified directory 34 | im_save( im, name = "myimage", path = "path/to/image", format = "jpg" ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: sketcher 2 | Title: Pencil Sketch Effect 3 | Version: 0.1.3 4 | Authors@R: 5 | person(given = "Hiroyuki", family = "Tsuda", role = c("aut", "cre"), 6 | email = "tsuda16k@gmail.com", 7 | comment = c(ORCID = "0000-0001-9396-5327")) 8 | Description: An implementation of image processing effects that convert a photo into a line drawing image. 9 | For details, please refer to Tsuda, H. (2020). sketcher: An R package for converting a photo into a sketch style image. 10 | . 11 | URL: https://htsuda.net/sketcher/ 12 | BugReports: https://github.com/tsuda16k/sketcher/issues/ 13 | License: MIT + file LICENSE 14 | Encoding: UTF-8 15 | LazyData: true 16 | Roxygen: list(markdown = TRUE) 17 | RoxygenNote: 7.1.0 18 | Imports: 19 | jpeg, 20 | png, 21 | readbitmap, 22 | downloader, 23 | imager, 24 | magrittr, 25 | methods, 26 | stringr, 27 | dplyr 28 | Depends: 29 | R (>= 2.10) 30 | Suggests: 31 | knitr, 32 | rmarkdown 33 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Resubmission 2 | This is a resubmission. 3 | 4 | CRAN maintainers suggestion: 5 | 6 | - Please only write/save files if the user has specified a directory in the function themselves. 7 | - Therefore please omit any default path = getwd() in writing functions. 8 | 9 | So I removed any getwd() from default function parameters. 10 | 11 | ## Test environments 12 | - Local: darwin15.6.0, R 3.6.1 13 | - R-hub fedora-clang-devel (r-devel) 14 | - R-hub windows-x86_64-devel (r-devel) 15 | - R-hub ubuntu-gcc-release (r-release) 16 | 17 | ## R CMD check results 18 | 0 errors | 0 warnings | 1 notes 19 | 1 note should be the first time submission note. 20 | 21 | Also, r-hub may report a note, saying "Possibly mis-spelled words in DESCRIPTION: Tsuda (9:34)." 22 | But the spelling is OK. 23 | 24 | When I ran rhub::check_for_cran(), there was an error, saying "there is no package called 'tiff'". 25 | But when I ran rhub::check_for_cran(env_vars=c(R_COMPILE_AND_INSTALL_PACKAGES = "always")), no error was reported. 26 | 27 | ## Downstream dependencies 28 | There are currently no downstream dependencies for this package. 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Hiroyuki Tsuda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /man/sketch.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{sketch} 4 | \alias{sketch} 5 | \title{Apply the sketch effect on an image} 6 | \usage{ 7 | sketch( 8 | im, 9 | style = 1, 10 | lineweight = 1, 11 | smooth = ceiling(lineweight), 12 | gain = 0.02, 13 | contrast = NULL, 14 | shadow = 0, 15 | max.size = 2048 16 | ) 17 | } 18 | \arguments{ 19 | \item{im}{an image (array).} 20 | 21 | \item{style}{a numeric (integer). Either 1 or 2.} 22 | 23 | \item{lineweight}{a numeric. Strength of lines.} 24 | 25 | \item{smooth}{a numeric (integer). Smoothness of image texture.} 26 | 27 | \item{gain}{a numeric between 0 and 1. Can be used to reduce noise in dim regions.} 28 | 29 | \item{contrast}{a numeric (integer). Adjusts the image contrast.} 30 | 31 | \item{shadow}{a numeric between 0 and 1} 32 | 33 | \item{max.size}{maximum image resolution (width or height) of the output image} 34 | } 35 | \value{ 36 | an image. 37 | } 38 | \description{ 39 | Apply the sketch effect on an image 40 | } 41 | \examples{ 42 | im = sketch(face) 43 | plot(im) 44 | 45 | \dontrun{ 46 | im = im_load("path/to/your/image.jpg") 47 | plot(im) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /man/survey.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{survey} 4 | \alias{survey} 5 | \title{Create multiple sketches at once and combine them into a single image} 6 | \usage{ 7 | survey( 8 | im, 9 | style = 1, 10 | weight_levels = c(1, 2, 4), 11 | smooth_levels = c(1, 2, 4), 12 | gain = 0.02, 13 | contrast = NULL, 14 | shadow = 0, 15 | verbose = TRUE 16 | ) 17 | } 18 | \arguments{ 19 | \item{im}{an image.} 20 | 21 | \item{style}{numeric (integer). Either 1 (edge-focused) or 2 (smooth gradient)} 22 | 23 | \item{weight_levels}{numeric (integer). a vector of lineweight values} 24 | 25 | \item{smooth_levels}{numeric (integer). a vector of smooth values} 26 | 27 | \item{gain}{a numeric between 0 and 1. Can be used to reduce noise in dim regions.} 28 | 29 | \item{contrast}{numeric (integer). Adjusts the image contrast.} 30 | 31 | \item{shadow}{a numeric between 0 and 1} 32 | 33 | \item{verbose}{If TRUE (default), progress information is displayed in the Console.} 34 | } 35 | \value{ 36 | an array of the sketched image. 37 | } 38 | \description{ 39 | It is often necessary to find optimal sketch style parameters for your task. 40 | With this function, you can easily compare the effects of different style parameters. 41 | } 42 | \examples{ 43 | \donttest{ 44 | im = survey(face, style = 1, weight_levels = c(1, 3), smooth_levels = c(1, 3), shadow = 0.3) 45 | plot(im) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "notes/figures/", 12 | out.width = "100%", 13 | dpi=130 14 | ) 15 | ``` 16 | 17 | # sketcher 18 | 19 | 20 | [![CRAN_Status_Badge](https://www.r-pkg.org/badges/version/sketcher)](https://cran.r-project.org/package=sketcher) 21 | [![CRAN_time_from_release](https://www.r-pkg.org/badges/ago/sketcher)](https://cran.r-project.org/package=sketcher) 22 | [![CRAN_latest_release_date](https://www.r-pkg.org/badges/last-release/sketcher)](https://cran.r-project.org/package=sketcher) 23 | [![metacran downloads](https://cranlogs.r-pkg.org/badges/grand-total/sketcher)](https://cran.r-project.org/package=sketcher) 24 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://choosealicense.com/licenses/mit/) 25 | 26 | 27 | By using the ```sketcher``` package, you can convert a photo into a line drawing image. 28 | Drawing style (lineweight and inclusion/exclusion of shadow/shading) can be controlled. 29 | Some examples (source photos and generated sketches) are shown below. 30 | 31 |

32 | 33 |

34 | 35 | ## Paper 36 | 37 | Details of this package are described in the article below: 38 | 39 | Tsuda, H. (2020). sketcher: An R package for converting a photo into a sketch style image. 40 | https://psyarxiv.com/svmw5 41 | 42 | 43 | ## Dependencies 44 | 45 | Mac OS X users need to install XQuartz (https://www.xquartz.org/). 46 | 47 | ## Installation 48 | 49 | The package is available on CRAN. It can be installed with: 50 | 51 | ``` r 52 | install.packages("sketcher") 53 | ``` 54 | 55 | Then, attach the package. 56 | 57 | ```{r} 58 | library(sketcher) 59 | ``` 60 | 61 | ### Installation via GitHub 62 | 63 | To install the development version of the ```sketcher``` package via GitHub, you can use the ```devtools``` package. 64 | 65 | __NOTE:__ 66 | - On Windows, Rtools needs to be installed to install a package from GitHub. 67 | - On Mac, XCode may be needed to be installed to install a package from GitHub. 68 | 69 | ``` r 70 | devtools::install_github("tsuda16k/sketcher") 71 | ``` 72 | 73 | ## Example image 74 | 75 | The ```sketcher``` package has a built-in image, which is useful when you want to try sketch effects right away. The image data is named ```face```. 76 | 77 | Internally, this is just a numeric array of size 600 x 460 x 3 [y-coordinate, x-coordinate, color channel]. Each element of the array represents a pixel value, which can range between 0 and 1. 78 | 79 | ```{r} 80 | dim(face) 81 | ``` 82 | 83 | To plot an image, use the ```plot()``` function. 84 | 85 | ```{r, eval=FALSE, echo=TRUE} 86 | plot(face) 87 | ``` 88 | 89 | 90 | 91 | (In examples below, I actually used this image, which has higher resolution than the image provided by the package. Sketch results will be similar, but in general aesthetically more pleasing results can be obtained with images of higher resolution, at the cost of processing time.) 92 | 93 | ## Load an image 94 | 95 | To load your own image, use the ```im_load()``` function. 96 | 97 | ```{r, eval=FALSE, echo=TRUE} 98 | im = im_load("path/to/your/image.jpg") 99 | plot(im) 100 | ``` 101 | 102 | The jpg, png, and bmp formats are supported. 103 | 104 | You can load an image from the web (URL). For example, 105 | 106 | ```{r, eval=FALSE, echo=TRUE} 107 | im = im_load("https://raw.githubusercontent.com/tsuda16k/sketcher/master/notes/sketcher_face.jpg") 108 | ``` 109 | 110 | will load a high resolution version of the face image noted above (compare ```dim(face)``` and ```dim(im)```). 111 | 112 | ## Apply the sketch effect 113 | 114 | The built-in face image is used in the following examples. 115 | For consistency purposes, the ```face``` image is labeled as ```im```. 116 | 117 | ```{r} 118 | im = face 119 | ``` 120 | 121 | Use the ```sketch()``` function to apply the sketch effect to an image. 122 | 123 | ```{r, eval=FALSE, echo=TRUE} 124 | im2 = sketch(im) # may take some seconds 125 | plot(im2) 126 | ``` 127 | 128 |

129 | 130 | That's all. 131 | 132 | The ```sketch()``` function has several parameters to control the style of sketch. 133 | 134 | ## Arguments of the sketch() function 135 | 136 | A table of arguments of the ```sketch()``` function: 137 | 138 | ```{r echo = F, results = 'asis'} 139 | Argument = c("im", "style", "lineweight", "smooth", "gain", "contrast", "shadow", "max.size") 140 | Meaning = c( "An input image", "Sketch style", "Strength of lines", 141 | "Smoothness of texture", "Gain parameter", "Contrast parameter", "Shadow threshold", 142 | "Max resolution of output" ) 143 | Value = c( "image", "1 or 2", "a numeric, >= 0.3", "an integer, >= 0", "a numeric betw 0 and 1", "a numeric, >= 0", 144 | "a numeric betw 0 and 1", "an integer, > 0") 145 | Default = c( "", "1", "1", "1", "0.02", "20 (for style1) or 4 (for style2)", "0.0", "2048" ) 146 | doc_sketch = data.frame( Argument, Meaning, Value, Default, stringsAsFactors = F ) 147 | library(knitr) 148 | kable(doc_sketch) 149 | ``` 150 | 151 | The default is ```sketch(im, style = 1, lineweight = 1, smooth = ceiling(lineweight), gain = .02, contrast = NULL, shadow = 0, max.size = 2048)```. 152 | 153 | - im: an image, obtained by using the ```im_load()``` function. 154 | - style: while style 1 focuses on edges, style 2 also retains shading. 155 | - lineweight: as the name suggests. set a numeric value equal to or larger than 0.3. 156 | - smooth: noise/blob smoother. set an integer value equal to or larger than 0. 157 | - gain: this parameter may be useful for noise reduction in dim region. 158 | - contrast: contrast of the sketch image is adjusted by this parameter. 159 | - shadow: if given a value larger than 0 (e.g., 0.3), shadows are added to sketch. 160 | - max.size: the size (image resolution) of output sketch is constrained by this parameter. if the input image has a very high resolution, such as 20000 x 10000 pixels, sketch processing will take a long time. In such cases, the algorithm first downscales the input image to 2048 x 1024 pixels, in this case, and then apply the sketch effect. 161 | 162 | The effects of these parameters on sketch appearances are described in detail below. 163 | 164 | ## Saving the image 165 | 166 | Use the ```im_save()``` function to save an image. 167 | 168 | ```{r, eval=F, echo=T} 169 | im = face 170 | im2 = sketch(im, style = 1, lineweight = 1, shadow = 0.3) 171 | 172 | # myimage.png is saved in the current working directory 173 | im_save(im2, name = "myimage", path = getwd()) 174 | 175 | # newimg.jpg is saved to a specified directory 176 | im_save(im2, name = "newimg", path = "set/your/path", format = "jpg", quality = .95) 177 | ``` 178 | 179 | By default, an image is saved in the png format. 180 | When using ```format = "jpg"```, you can set the quality of jpg compression (default is 0.95). 181 | 182 | ## The effects of sketch parameters 183 | 184 | ### - style and lineweight 185 | 186 | The most important parameters of the ```sketch()``` function are ```style``` and ```lineweight```. 187 | 188 | While style 1 is good at extracting fine edges, style 2 retains shading, as shown in the figure. Note that the shading gets more salient when ```lineweight``` is given a larger value. 189 | 190 |

191 | 192 | ### - smooth 193 | 194 | The ```smooth``` parameter controls the degree of smoothness of image texture. By increasing the ```smooth``` value, fine-scale edges, noises, and blobs are eliminated (see the figure below). 195 | 196 | In most cases, aesthetically pleasing results will be obtained when ```smooth``` value is equal to or larger than ```lineweight```. If ```smooth``` is not given in the ```sketch()``` function, ```smooth``` is assigned with the same value as ```lineweight``` (actually, ```smooth = ceiling(lineweight)```). 197 | 198 |

199 | 200 | ### - gain 201 | 202 | A constant value (gain) is added to an input image before the extraction of edges to produce a sketch. A sketch can be very noisy when an input image has dark/dim regions. In such cases, increasing the gain, such as ```gain = 0.2``` or ```gain = 0.3```, may reduce the noise. In most cases, however, you don't have to care about this parameter. 203 | 204 | ### - contrast 205 | 206 | By increasing the ```contrast``` parameter, a sketch is darkened. When a sketch appears whitish, you may need to increase the ```contrast``` value. 207 | 208 |

209 | 210 | ### - shadow 211 | 212 | Shadow can be added to a sketch by using the ```shadow``` parameter. 213 | By default, the sketch function does not include shadow (shadow = 0). In many cases, however, adding shadow will be needed to produce a reasonable result (described later). 214 | 215 |

216 | 217 | ## Tips for successful sketching 218 | 219 | For some images, good results may be obtained with the default parameters of the sketch function. However, in many cases, the default sketch will produce an unsatisfactory result. Here I show some cases where the default sketch fails, and how to fix it. 220 | 221 | ### Case 1 222 | 223 | Outline is missing and texture is lacking in the default sketch. By using style 2 and adding shadow, ```sketch(im, style = 2, shadow = 0.4)```, the problems are largely solved. In addition, by setting the ```smooth``` parameter to 0, ```sketch(im, style = 2, lineweight = 1, smooth = 0, shadow = 0.4)```, the sketch was successful in representing the fine texture of bird body. 224 | 225 |

226 | 227 | ### Case 2 228 | 229 | Due to the lack of edges in the dark region of the face, the default sketch, ```sketch(im)```, produced a weird result. This can be fixed by adding shadow, such as ```sketch(im, shadow = 0.4)```. Finding the right value of the shadow parameter usually requires trial and error, but usually a value between 0.3 and 0.6 will produce a reasonable result. 230 | 231 |

232 | 233 | ### Case 3 234 | 235 | The sketch algorithm detects edges in an image. If objects have unclear edges/outlines, sketching will fail. This typically happens when a photo contains 1) out of focus region or 2) objects with unclear outlines such as furry animals. 236 | 237 | A cat image below is such an example. The background of the photo and the lower body of the cat is out of focus, and the outline of the cat is blurry. The default sketch, ```sketch(im)```, is unsuccessful in representing the cat body. Zero smoothing, ```sketch(im, smooth = 0)```, can capture hairs/textures of the cat to some extent, at the cost of increased noise in background, and still the body and legs of the cat is not represented well. Unfortunately, there is no simple solution to the difficulty with blurred/defocused images. 238 | 239 |

240 | 241 | ## Gallery 242 | 243 | Here are some sketches produced by the sketcher package. 244 | 245 |

246 |

247 |

248 |

249 | 250 | 251 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # sketcher 5 | 6 | 7 | 8 | [![CRAN\_Status\_Badge](https://www.r-pkg.org/badges/version/sketcher)](https://cran.r-project.org/package=sketcher) 9 | [![CRAN\_time\_from\_release](https://www.r-pkg.org/badges/ago/sketcher)](https://cran.r-project.org/package=sketcher) 10 | [![CRAN\_latest\_release\_date](https://www.r-pkg.org/badges/last-release/sketcher)](https://cran.r-project.org/package=sketcher) 11 | [![metacran 12 | downloads](https://cranlogs.r-pkg.org/badges/grand-total/sketcher)](https://cran.r-project.org/package=sketcher) 13 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://choosealicense.com/licenses/mit/) 14 | 15 | 16 | By using the `sketcher` package, you can convert a photo into a line 17 | drawing image. 18 | Drawing style (lineweight and inclusion/exclusion of shadow/shading) can 19 | be controlled. 20 | Some examples (source photos and generated sketches) are shown below. 21 | 22 |

23 | 24 |

25 |

26 | 27 |

28 | 29 | ## Paper 30 | 31 | Details of this package are described in the article below: 32 | 33 | Tsuda, H. (2020). sketcher: An R package for converting a photo into a 34 | sketch style image. 35 | 36 | 37 | ## Dependencies 38 | 39 | Mac OS X users need to install XQuartz (). 40 | 41 | ## Installation 42 | 43 | The package is available on CRAN. It can be installed with: 44 | 45 | ``` r 46 | install.packages("sketcher") 47 | ``` 48 | 49 | Then, attach the package. 50 | 51 | ``` r 52 | library(sketcher) 53 | ``` 54 | 55 | ### Installation via GitHub 56 | 57 | To install the development version of the `sketcher` package via GitHub, 58 | you can use the `devtools` package. 59 | 60 | **NOTE:** 61 | - On Windows, 62 | Rtools 63 | needs to be installed to install a package from GitHub. 64 | - On Mac, XCode may be needed to be installed to install a package from 65 | GitHub. 66 | 67 | ``` r 68 | devtools::install_github("tsuda16k/sketcher") 69 | ``` 70 | 71 | ## Example image 72 | 73 | The `sketcher` package has a built-in image, which is useful when you 74 | want to try sketch effects right away. The image data is named `face`. 75 | 76 | Internally, this is just a numeric array of size 600 x 460 x 3 77 | \[y-coordinate, x-coordinate, color channel\]. Each element of the array 78 | represents a pixel value, which can range between 0 and 1. 79 | 80 | ``` r 81 | dim(face) 82 | #> [1] 600 460 3 83 | ``` 84 | 85 | To plot an image, use the `plot()` function. 86 | 87 | ``` r 88 | plot(face) 89 | ``` 90 | 91 | 92 | 93 | (In examples below, I actually used 94 | this image, which has higher 95 | resolution than the image provided by the package. Sketch results will 96 | be similar, but in general aesthetically more pleasing results can be 97 | obtained with images of higher resolution, at the cost of processing 98 | time.) 99 | 100 | ## Load an image 101 | 102 | To load your own image, use the `im_load()` function. 103 | 104 | ``` r 105 | im = im_load("path/to/your/image.jpg") 106 | plot(im) 107 | ``` 108 | 109 | The jpg, png, and bmp formats are supported. 110 | 111 | You can load an image from the web (URL). For example, 112 | 113 | ``` r 114 | im = im_load("https://raw.githubusercontent.com/tsuda16k/sketcher/master/notes/sketcher_face.jpg") 115 | ``` 116 | 117 | will load a high resolution version of the face image noted above 118 | (compare `dim(face)` and `dim(im)`). 119 | 120 | ## Apply the sketch effect 121 | 122 | The built-in face image is used in the following examples. 123 | For consistency purposes, the `face` image is labeled as `im`. 124 | 125 | ``` r 126 | im = face 127 | ``` 128 | 129 | Use the `sketch()` function to apply the sketch effect to an image. 130 | 131 | ``` r 132 | im2 = sketch(im) # may take some seconds 133 | plot(im2) 134 | ``` 135 | 136 |

137 | 138 |

139 | 140 | That’s all. 141 | 142 | The `sketch()` function has several parameters to control the style of 143 | sketch. 144 | 145 | ## Arguments of the sketch() function 146 | 147 | A table of arguments of the `sketch()` function: 148 | 149 | | Argument | Meaning | Value | Default | 150 | |:-----------|:-------------------------|:-----------------------|:----------------------------------| 151 | | im | An input image | image | | 152 | | style | Sketch style | 1 or 2 | 1 | 153 | | lineweight | Strength of lines | a numeric, >= 0.3 | 1 | 154 | | smooth | Smoothness of texture | an integer, >= 0 | 1 | 155 | | gain | Gain parameter | a numeric betw 0 and 1 | 0.02 | 156 | | contrast | Contrast parameter | a numeric, >= 0 | 20 (for style1) or 4 (for style2) | 157 | | shadow | Shadow threshold | a numeric betw 0 and 1 | 0.0 | 158 | | max.size | Max resolution of output | an integer, > 0 | 2048 | 159 | 160 | The default is 161 | `sketch(im, style = 1, lineweight = 1, smooth = ceiling(lineweight), gain = .02, contrast = NULL, shadow = 0, max.size = 2048)`. 162 | 163 | - im: an image, obtained by using the `im_load()` function. 164 | - style: while style 1 focuses on edges, style 2 also retains shading. 165 | - lineweight: as the name suggests. set a numeric value equal to or 166 | larger than 0.3. 167 | - smooth: noise/blob smoother. set an integer value equal to or larger 168 | than 0. 169 | - gain: this parameter may be useful for noise reduction in dim 170 | region. 171 | - contrast: contrast of the sketch image is adjusted by this 172 | parameter. 173 | - shadow: if given a value larger than 0 (e.g., 0.3), shadows are 174 | added to sketch. 175 | - max.size: the size (image resolution) of output sketch is 176 | constrained by this parameter. if the input image has a very high 177 | resolution, such as 20000 x 10000 pixels, sketch processing will 178 | take a long time. In such cases, the algorithm first downscales the 179 | input image to 2048 x 1024 pixels, in this case, and then apply the 180 | sketch effect. 181 | 182 | The effects of these parameters on sketch appearances are described in 183 | detail below. 184 | 185 | ## Saving the image 186 | 187 | Use the `im_save()` function to save an image. 188 | 189 | ``` r 190 | im = face 191 | im2 = sketch(im, style = 1, lineweight = 1, shadow = 0.3) 192 | 193 | # myimage.png is saved in the current working directory 194 | im_save(im2, name = "myimage", path = getwd()) 195 | 196 | # newimg.jpg is saved to a specified directory 197 | im_save(im2, name = "newimg", path = "set/your/path", format = "jpg", quality = .95) 198 | ``` 199 | 200 | By default, an image is saved in the png format. 201 | When using `format = "jpg"`, you can set the quality of jpg compression 202 | (default is 0.95). 203 | 204 | ## The effects of sketch parameters 205 | 206 | ### - style and lineweight 207 | 208 | The most important parameters of the `sketch()` function are `style` and 209 | `lineweight`. 210 | 211 | While style 1 is good at extracting fine edges, style 2 retains shading, 212 | as shown in the figure. Note that the shading gets more salient when 213 | `lineweight` is given a larger value. 214 | 215 |

216 | 217 |

218 | 219 | ### - smooth 220 | 221 | The `smooth` parameter controls the degree of smoothness of image 222 | texture. By increasing the `smooth` value, fine-scale edges, noises, and 223 | blobs are eliminated (see the figure below). 224 | 225 | In most cases, aesthetically pleasing results will be obtained when 226 | `smooth` value is equal to or larger than `lineweight`. If `smooth` is 227 | not given in the `sketch()` function, `smooth` is assigned with the same 228 | value as `lineweight` (actually, `smooth = ceiling(lineweight)`). 229 | 230 |

231 | 232 |

233 | 234 | ### - gain 235 | 236 | A constant value (gain) is added to an input image before the extraction 237 | of edges to produce a sketch. A sketch can be very noisy when an input 238 | image has dark/dim regions. In such cases, increasing the gain, such as 239 | `gain = 0.2` or `gain = 0.3`, may reduce the noise. In most cases, 240 | however, you don’t have to care about this parameter. 241 | 242 | ### - contrast 243 | 244 | By increasing the `contrast` parameter, a sketch is darkened. When a 245 | sketch appears whitish, you may need to increase the `contrast` value. 246 | 247 |

248 | 249 |

250 | 251 | ### - shadow 252 | 253 | Shadow can be added to a sketch by using the `shadow` parameter. 254 | By default, the sketch function does not include shadow (shadow = 0). In 255 | many cases, however, adding shadow will be needed to produce a 256 | reasonable result (described later). 257 | 258 |

259 | 260 |

261 | 262 | ## Tips for successful sketching 263 | 264 | For some images, good results may be obtained with the default 265 | parameters of the sketch function. However, in many cases, the default 266 | sketch will produce an unsatisfactory result. Here I show some cases 267 | where the default sketch fails, and how to fix it. 268 | 269 | ### Case 1 270 | 271 | Outline is missing and texture is lacking in the default sketch. By 272 | using style 2 and adding shadow, `sketch(im, style = 2, shadow = 0.4)`, 273 | the problems are largely solved. In addition, by setting the `smooth` 274 | parameter to 0, 275 | `sketch(im, style = 2, lineweight = 1, smooth = 0, shadow = 0.4)`, the 276 | sketch was successful in representing the fine texture of bird body. 277 | 278 |

279 | 280 |

281 | 282 | ### Case 2 283 | 284 | Due to the lack of edges in the dark region of the face, the default 285 | sketch, `sketch(im)`, produced a weird result. This can be fixed by 286 | adding shadow, such as `sketch(im, shadow = 0.4)`. Finding the right 287 | value of the shadow parameter usually requires trial and error, but 288 | usually a value between 0.3 and 0.6 will produce a reasonable result. 289 | 290 |

291 | 292 |

293 | 294 | ### Case 3 295 | 296 | The sketch algorithm detects edges in an image. If objects have unclear 297 | edges/outlines, sketching will fail. This typically happens when a photo 298 | contains 1) out of focus region or 2) objects with unclear outlines such 299 | as furry animals. 300 | 301 | A cat image below is such an example. The background of the photo and 302 | the lower body of the cat is out of focus, and the outline of the cat is 303 | blurry. The default sketch, `sketch(im)`, is unsuccessful in 304 | representing the cat body. Zero smoothing, `sketch(im, smooth = 0)`, can 305 | capture hairs/textures of the cat to some extent, at the cost of 306 | increased noise in background, and still the body and legs of the cat is 307 | not represented well. Unfortunately, there is no simple solution to the 308 | difficulty with blurred/defocused images. 309 | 310 |

311 | 312 |

313 | 314 | ## Gallery 315 | 316 | Here are some sketches produced by the sketcher package. 317 | 318 |

319 | 320 |

321 |

322 | 323 |

324 |

325 | 326 |

327 |

328 | 329 |

330 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | #' @importFrom stats runif median 4 | #' @importFrom stringr str_match str_split str_sub 5 | #' @importFrom graphics plot 6 | #' @importFrom magrittr mod "%>%" 7 | NULL 8 | 9 | 10 | # CRAN sometimes issues spurious warnings about undefined variables 11 | utils::globalVariables( c( ".", "%>%", "x", "y", "c", "value" ) ) 12 | 13 | 14 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 15 | # nimg class ---- 16 | 17 | 18 | nimg = function( im, name ){ 19 | if( is.logical( im ) | is.integer( im ) ){ 20 | im = im + 0.0 21 | } 22 | if( length( dim( im ) ) == 2 ){ # gray-scale image 23 | dim( im ) = c( dim( im ), 1 ) 24 | } 25 | class( im ) = c( "nimg", "numeric" ) 26 | if( ! base::missing( name ) ){ 27 | attr( im, "name" ) = name 28 | } else if( is.null( attr( im, "name" ) ) ){ 29 | attr( im, "name" ) = "" 30 | } 31 | im 32 | } 33 | 34 | 35 | ##' @export 36 | print.nimg = function( x, ... ){ 37 | d = dim( x ) 38 | if( attr( x, "name" ) == "" || attr( x, "name" ) == "-" || is.null( attr( x, "name" ) ) ){ 39 | name = "image" 40 | } else { 41 | name = attr( x, "name" ) 42 | } 43 | cat( sprintf( "%s: %i [height] x %i [width] x %i [colour channels]\n", name, d[1], d[2], d[3] ) ) 44 | # cat( sprintf( "image: %i [height] x %i [width] x %i [colour channels]\n", d[1], d[2], d[3] ) ) 45 | invisible( x ) 46 | } 47 | 48 | 49 | #' Display an image 50 | #' @param x an image 51 | #' @param rescale logical. if true, then pixel value is rescaled to range between 0 and 1. 52 | #' @param ... other parameters to be passed to plot.default 53 | #' @return No return value, called for side effects. 54 | #' @examples 55 | #' plot(face) 56 | #' @export 57 | plot.nimg = function( x, rescale = FALSE, ... ){ 58 | old.par = graphics::par( no.readonly = TRUE ) 59 | on.exit( graphics::par( old.par ), add = TRUE ) 60 | if( im_npix( x ) == 0 ){ 61 | stop( "The image is empty." ) 62 | } 63 | 64 | if( im_nc( x ) == 1 ){ 65 | # a raster array must have exactly 3 or 4 planes 66 | x = im_rep( x, 3 ) 67 | } 68 | im = x[ ,,, drop = FALSE ] 69 | if( rescale ){ 70 | im = rescaling01( im ) 71 | } else if( max( im ) > 1 || min( im ) < 0 ){ 72 | # warning( paste0( "Pixcel value exceeds the range [0,1], and hence it was clamped when plotting.\n", 73 | # "min = ", min( im ), ", max = ", max( im ) ) ) 74 | im = clamping( im ) 75 | } 76 | graphics::par( mar = c( 0, 0, 0, 0 ) ) 77 | graphics::plot.new() 78 | graphics::plot.window( 79 | xlim = c(1,im_width(x)), ylim = c(im_height(x),1), asp = 1, xaxs = "i", yaxs = "i", ...) 80 | rst = grDevices::as.raster( 81 | im, rescale = FALSE, colorscale = NULL, colourscale = NULL, col.na = grDevices::rgb(0,0,0,0) ) 82 | graphics::rasterImage( rst, 1, nrow( rst ), ncol( rst ), 1, interpolate = FALSE ) 83 | invisible( x ) 84 | } 85 | 86 | 87 | #' Load image from file or URL 88 | #' @param file path to file or URL 89 | #' @param name a string for name attribute. if missing, inferred from the file argument. 90 | #' @return an array of image data 91 | #' @examples 92 | #' \dontrun{ 93 | #' # load an image from disk 94 | #' im = im_load("path/to/your/image.jpg") 95 | #' plot(im) 96 | #' # load an image from URL 97 | #' im = im_load("http://placehold.jp/150x150.png") 98 | #' } 99 | #' @export 100 | im_load = function( file, name ){ 101 | if( grepl("^(http|ftp)s?://", file) ){ # if URL 102 | url = file 103 | ext = stringr::str_extract_all( url, "\\.([A-Za-z0-9]+$)" )[[ 1 ]] 104 | if( length( ext ) > 0 ){ 105 | file = tempfile( fileext = ext ) 106 | } else { 107 | file = tempfile() 108 | } 109 | downloader::download( url, file, mode = "wb" ) 110 | im = im_load( file, get_image_name_from_file( url ) ) 111 | unlink( file ) 112 | return( im ) 113 | } 114 | ext = sub( ".*\\.([^.]{3,4})$", "\\1", file ) %>% tolower 115 | if( ext %in% c( "png", "bmp", "jpg", "jpeg" ) ){ 116 | tryCatch({ 117 | im = readbitmap::read.bitmap( file ) 118 | }, 119 | error = function(e) { 120 | stop( paste0( e, "Note: im_load() fails for binary (black/white) bmp image." ) ) 121 | }) 122 | # im = readbitmap::read.bitmap( file ) 123 | dim( im ) 124 | if( ! is.null( attr( im, "header" ) ) ){ 125 | im = im / 255 126 | } 127 | if( length( dim( im ) ) == 2 ){ # gray-scale image 128 | dim( im ) = c( dim( im ), 1 ) 129 | } else if( length( dim( im ) ) == 3 ){ # multiple channels 130 | if( dim( im )[ 3 ] %in% c( 2, 4 ) ){ 131 | # remove alpha channel if it is uninformative 132 | if( min( im[ , , dim( im )[ 3 ] ] ) == max( im[ , , dim( im )[ 3 ] ] ) ){ 133 | im = im[ , , 1:( dim( im )[ 3 ] - 1 ), drop = FALSE ] 134 | } 135 | } 136 | } 137 | im = nimg( im, ifelse( base::missing( name ), get_image_name_from_file( file ), name ) ) 138 | return( im ) 139 | } else { 140 | stop( "Only jpg, png, and bmp formats are supported." ) 141 | } 142 | } 143 | 144 | 145 | get_image_name_from_file = function( file ){ 146 | tryCatch({ 147 | name = stringr::str_split( file, "/" )[[ 1 ]] 148 | name = name[ length( name ) ] 149 | name = stringr::str_split( name, "[.]" )[[ 1 ]] 150 | return( name[ 1 ] ) 151 | }, 152 | error = function(e) { 153 | return( "-" ) 154 | }) 155 | } 156 | 157 | 158 | #' Save an image to disk 159 | #' @param im An image. 160 | #' @param name Name of the image file. 161 | #' @param path Path to file. 162 | #' @param format Image format. Either "jpg", "png", "tiff", or "bmp". Default is "png". 163 | #' @param quality (jpg only) default is 0.95. Higher quality means less compression. 164 | #' @return No return value, called for side effects. 165 | #' @examples 166 | #' \dontrun{ 167 | #' im = sketch(face) 168 | #' 169 | #' # im.png is saved to the current working directory 170 | #' im_save( im, name = "im", path = getwd() ) 171 | #' 172 | #' # myimage.jpg is saved to a specified directory 173 | #' im_save( im, name = "myimage", path = "path/to/image", format = "jpg" ) 174 | #' } 175 | #' @export 176 | im_save = function( im, name, path, format = "png", quality = .95 ){ 177 | if( ! format %in% c( "jpg", "png" ) ){ 178 | warning( "Incorrect imaeg format. Use either jpg or png." ) 179 | return() 180 | } 181 | if( base::missing( name ) ){ 182 | name = deparse( substitute( im ) ) 183 | } 184 | if( im_nc( im ) == 1 ){ 185 | im = im_rep( im, 3 ) 186 | } 187 | if( stringr::str_sub( path, stringr::str_length( path ) ) == "/" ){ 188 | path = stringr::str_sub( path, end = stringr::str_length( path ) - 1 ) 189 | } 190 | if( max( im ) > 1 || min( im ) < 0 ){ 191 | # warning( "Pixcel value exceeds the range [0,1], and hence it was clamped when saving.") 192 | im = clamping( im ) 193 | } 194 | base::dir.create( path, showWarnings = FALSE, recursive = TRUE ) 195 | file = paste0( path, "/", name, ".", format ) 196 | if( format == "png" ){ 197 | png::writePNG( im, file ) 198 | } else if ( format == "jpeg" | format == "jpg" ){ 199 | jpeg::writeJPEG( im, file, quality = quality ) 200 | } 201 | } 202 | 203 | 204 | cimg2nimg = function( im ){ 205 | if( is.list( im ) ){ 206 | im = lapply( im, function( x ){ 207 | if( "nimg" %in% class( x ) ){ 208 | x 209 | } else { 210 | cimg2nimg( x ) 211 | } 212 | }) 213 | return( im ) 214 | } else if( any( c( "cimg", "pixset" ) %in% class( im ) ) ){ 215 | im = aperm( im, c( 2, 1, 4, 3 ) ) # (x, y, z, cc) to (y, x, cc, z) 216 | return( nimg( im[,,,1] ) ) 217 | } else if( "nimg" %in% class( im ) ){ 218 | return( im ) 219 | } else { 220 | return( nimg( im ) ) 221 | } 222 | } 223 | 224 | 225 | nimg2cimg = function( im ){ 226 | if( is.list( im ) ){ 227 | im = lapply( im, function(x){ 228 | if( any( c( "cimg", "pixset" ) %in% class( x ) ) ){ 229 | x 230 | } else { 231 | nimg2cimg( x ) 232 | } 233 | }) 234 | return( im ) 235 | } else { 236 | if( any( c( "cimg", "pixset" ) %in% class( im ) ) ) { 237 | return( im ) 238 | } else if( length( dim( im ) ) == 2 ){ # (y, x) to (x, y) 239 | return( imager::as.cimg( t( im ) ) ) 240 | } else if( length( dim( im ) ) == 4 ){ # (y, x, cc, z) to (x, y, z, cc) 241 | return( imager::as.cimg( aperm( im, c( 2, 1, 4, 3 ) ) ) ) 242 | } else if( length( dim( im ) ) == 3 ){ # (y, x, cc) to (x, y, cc) 243 | im = aperm( im, c( 2, 1, 3 ) ) 244 | im2 = array( 0, dim = c( dim( im )[ 1 ], dim( im )[ 2 ], 1, dim( im )[ 3 ] ) ) 245 | im2[,,1,] = im 246 | return( imager::as.cimg( im2 ) ) 247 | } 248 | } 249 | } 250 | 251 | 252 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 253 | # color space ---- 254 | 255 | 256 | sRGB2RGB = function( im ){ 257 | mask = im < 0.04045 258 | im[ mask ] = im[ mask ] / 12.92 259 | im[ !mask ] = ( ( im[ !mask ] + 0.055 ) / 1.055 )^2.4 260 | return( im ) 261 | } 262 | 263 | 264 | RGB2sRGB = function( im ){ 265 | mask = im < 0.0031308 266 | im[ mask ] = im[ mask ] * 12.92 267 | im[ !mask ] = 1.055 * im[ !mask ]^( 1 / 2.4 ) - 0.055 268 | return( im ) 269 | } 270 | 271 | 272 | RGB2XYZ = function( im, use.D65 = TRUE ){ 273 | if( use.D65 ){ 274 | X = 0.4124564 * get_R( im ) + 0.3575761 * get_G( im ) + 0.1804375 * get_B( im ) 275 | Y = 0.2126729 * get_R( im ) + 0.7151522 * get_G( im ) + 0.0721750 * get_B( im ) 276 | Z = 0.0193339 * get_R( im ) + 0.1191920 * get_G( im ) + 0.9503041 * get_B( im ) 277 | } else { 278 | X = 0.4360747 * get_R( im ) + 0.3850649 * get_G( im ) + 0.1430804 * get_B( im ) 279 | Y = 0.2225045 * get_R( im ) + 0.7168786 * get_G( im ) + 0.0606169 * get_B( im ) 280 | Z = 0.0139322 * get_R( im ) + 0.0971045 * get_G( im ) + 0.7141733 * get_B( im ) 281 | } 282 | return( merge_color( list( X, Y, Z ) ) ) 283 | } 284 | 285 | 286 | XYZ2RGB = function( im, use.D65 = TRUE ){ 287 | if( use.D65 ){ 288 | R = 3.24045484 * get_R( im ) - 1.5371389 * get_G( im ) - 0.49853155 * get_B( im ) 289 | G = -0.96926639 * get_R( im ) + 1.8760109 * get_G( im ) + 0.04155608 * get_B( im ) 290 | B = 0.05564342 * get_R( im ) - 0.2040259 * get_G( im ) + 1.05722516 * get_B( im ) 291 | } else { 292 | R = 3.13385637 * get_R( im ) - 1.6168668 * get_G( im ) - 0.49061477 * get_B( im ) 293 | G = -0.97876856 * get_R( im ) + 1.9161416 * get_G( im ) + 0.03345412 * get_B( im ) 294 | B = 0.07194517 * get_R( im ) - 0.2289913 * get_G( im ) + 1.40524267 * get_B( im ) 295 | } 296 | return( merge_color( list( R, G, B ) ) ) 297 | } 298 | 299 | 300 | sRGB2XYZ = function( im, use.D65 = TRUE ){ 301 | im %>% sRGB2RGB %>% RGB2XYZ( use.D65 ) 302 | } 303 | 304 | 305 | XYZ2sRGB = function( im, use.D65 = TRUE ){ 306 | im %>% XYZ2RGB( use.D65 ) %>% RGB2sRGB 307 | } 308 | 309 | 310 | XYZ2Lab = function( im, use.D65 = TRUE ){ 311 | # reference white 312 | if( use.D65 ){ 313 | white = c( 0.95047, 1, 1.08883 ) 314 | } else { 315 | white = c( 0.96420, 1, 0.82491 ) 316 | } 317 | im[ ,,1 ] = im[ ,,1, drop = FALSE ] / white[ 1 ] 318 | im[ ,,3 ] = im[ ,,3, drop = FALSE ] / white[ 3 ] 319 | # 320 | mask = 24389 * im > 216 321 | im[ mask ] = im[ mask ]^( 1 / 3 ) 322 | im[ !mask ] = ( 24389 * im[ !mask ] / 27 + 16 ) / 116 323 | fx = im[ ,,1, drop = FALSE ] 324 | fy = im[ ,,2, drop = FALSE ] 325 | fz = im[ ,,3, drop = FALSE ] 326 | # 327 | L = ( 116 * fy - 16 ) 328 | a = 500 * ( fx - fy ) 329 | b = 200 * ( fy - fz ) 330 | return( merge_color( list( L, a, b ) ) ) 331 | } 332 | 333 | 334 | Lab2XYZ = function( im, use.D65 = TRUE ){ 335 | eta = 216 / 24389 336 | kappa = 24389 / 27 337 | # 338 | fy = ( im[,,1, drop = FALSE ] + 16 ) / 116 339 | fx = 0.002 * im[,,2, drop = FALSE ] + fy 340 | fz = fy - 0.005 * im[,,3, drop = FALSE ] 341 | # x = fx^3 > eta ? fx^3 : ( 116 * fx - 16 ) / kappa 342 | mask = fx^3 > eta 343 | fx[ mask ] = fx[ mask ]^3 344 | fx[ !mask ] = ( 116 * fx[ !mask ] - 16 ) / kappa 345 | # y = L > 8 ? ( ( L + 16 ) / 116 )^3 : L / kappa 346 | L = im[,,1, drop = FALSE ] 347 | mask = L > 8 348 | L[ mask ] = ( ( L[ mask ] + 16 ) / 116 )^3 349 | L[ !mask ] = L[ !mask ] / kappa 350 | # z = fz^3 > eta ? fz^3 : ( 116 * fz - 16 ) / kappa 351 | mask = fz^3 > eta 352 | fz[ mask ] = fz[ mask ]^3 353 | fz[ !mask ] = ( 116 * fz[ !mask ] - 16 ) / kappa 354 | # reference white 355 | if( use.D65 ){ 356 | white = c( 0.95047, 1, 1.08883 ) 357 | } else { 358 | white = c( 0.96420, 1, 0.82491 ) 359 | } 360 | fx = fx * white[ 1 ] 361 | fz = fz * white[ 3 ] 362 | return( merge_color( list( fx, L, fz ) ) ) 363 | } 364 | 365 | 366 | sRGB2Lab = function( im, use.D65 = TRUE ){ 367 | XYZ2Lab( sRGB2XYZ( im, use.D65 ), use.D65 ) 368 | } 369 | 370 | 371 | Lab2sRGB = function( im, use.D65 = TRUE ){ 372 | XYZ2sRGB( Lab2XYZ( im, use.D65 ), use.D65 ) 373 | } 374 | 375 | 376 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 377 | # math ---- 378 | 379 | 380 | rescaling01 = function( x ){ 381 | if( max( x ) == min( x ) ){ 382 | return( x ) 383 | } else { 384 | return( ( x - min( x ) ) / ( max( x ) - min( x ) ) ) 385 | } 386 | } 387 | 388 | 389 | clamping = function( x, min = 0, max = 1 ){ 390 | x[ x < min ] = min 391 | x[ x > max ] = max 392 | return( x ) 393 | } 394 | 395 | 396 | cubic_spline = function( x, low = 0, high = 1 ){ 397 | if( low == high ){ 398 | warning( "low and high must be different!" ) 399 | } else if( low > high ){ 400 | return( 1 - ( cubic_spline( x, high, low ) ) ) 401 | } 402 | x2 = x 403 | t = x[ x > low & x < high ] 404 | t = ( t - low ) / ( high - low ) 405 | x2[ x > low & x < high ] = t^2 * ( 3 - 2 * t ) 406 | x2[ x <= low ] = 0 407 | x2[ x >= high ] = 1 408 | return( x2 ) 409 | } 410 | 411 | 412 | ramp_threshold = function( x, eta, phi ){ 413 | y = x 414 | y[ x >= eta ] = 1 415 | y[ x < eta ] = 1 + tanh( phi * ( y[ x < eta ] - eta ) ) 416 | return( y ) 417 | } 418 | 419 | 420 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 421 | # image info ---- 422 | 423 | 424 | im_height = function( im ){ 425 | dim( im )[ 1 ] 426 | } 427 | 428 | 429 | im_width = function( im ){ 430 | dim( im )[ 2 ] 431 | } 432 | 433 | 434 | im_size = function( im ){ 435 | unname( dim( im )[ 1:2 ] ) 436 | } 437 | 438 | 439 | im_npix = function( im ){ 440 | prod( dim( im ) ) 441 | } 442 | 443 | 444 | im_nc = function( im ){ 445 | dim( im )[ 3 ] 446 | } 447 | 448 | 449 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 450 | # image slicing ---- 451 | 452 | 453 | force_channel_label_to_num = function( x ){ 454 | if( is.numeric( x ) ){ 455 | return( x ) 456 | } 457 | y = c() 458 | for( i in 1:length( x ) ){ 459 | if( x[ i ] %in% c( "R", "r", "L", "l" ) ){ 460 | y = c( y, 1 ) 461 | } else if( x[ i ] %in% c( "G", "g", "a" ) ){ 462 | y = c( y, 2 ) 463 | } else if( x[ i ] %in% c( "B", "b" ) ){ 464 | y = c( y, 3 ) 465 | } else if( x[ i ] %in% c( "A", "alpha", "Alpha" ) ){ 466 | y = c( y, 4 ) 467 | } else { 468 | y = c( y, 0 ) 469 | } 470 | } 471 | return( y ) 472 | } 473 | 474 | 475 | get_channel = function( im, channel ){ 476 | if( length( dim( im ) ) == 2 ){ 477 | return( im ) 478 | } else { 479 | return( nimg( im[ , , force_channel_label_to_num( channel ), drop = FALSE ] ) ) 480 | } 481 | } 482 | 483 | 484 | get_R = function( im ){ 485 | return( get_channel( im, 1 ) ) 486 | } 487 | 488 | 489 | get_G = function( im ){ 490 | return( get_channel( im, 2 ) ) 491 | } 492 | 493 | 494 | get_B = function( im ){ 495 | return( get_channel( im, 3 ) ) 496 | } 497 | 498 | 499 | split_color = function( im ){ 500 | ls = list() 501 | for( i in 1:dim( im )[ 3 ] ){ 502 | ls = c( ls, list( nimg( im[ , , i, drop = FALSE ] ) ) ) 503 | } 504 | return( ls ) 505 | } 506 | 507 | 508 | merge_color = function( imlist ){ 509 | imdim = dim( imlist[[ 1 ]] ) 510 | im = array( 0, c( imdim[ 1 ], imdim[ 2 ], length( imlist ) ) ) 511 | for( i in 1:length( imlist ) ){ 512 | im[,,i] = imlist[[ i ]] 513 | } 514 | return( nimg( im ) ) 515 | } 516 | 517 | 518 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 519 | # image transform ---- 520 | 521 | 522 | im_rep = function( im, n = 3, channel = 1 ){ 523 | nimg( array( get_channel( im, channel ), c( im_height( im ), im_width( im ), n ) ) ) 524 | } 525 | 526 | 527 | im_pad = function( im, n, method = "mirror" ){ 528 | if( n == 0 ) return( im ) 529 | 530 | w = im_width( im ) 531 | h = im_height( im ) 532 | 533 | if( any( n > c( w, h ) ) ){ 534 | warning( "n must be equal or smaller than image width (and height)." ) 535 | return( im ) 536 | } 537 | 538 | # create an empty matrix 539 | x = ifelse( is.numeric( method ), method, ifelse( method == "mean", mean( im ), 0 ) ) 540 | mat = array( x, c( h + 2 * n, w + 2 * n, dim( im )[ 3 ] ) ) 541 | 542 | # put the image 543 | mat[ ( n + 1 ):( n + h ), ( n + 1 ):( n + w ), ] = im 544 | 545 | # padding 546 | if( method == "zero" || method == "mean" || is.numeric( method ) ){ 547 | # do nothing 548 | } else if( method == "repeat" ){ 549 | # top left 550 | mat[ 1:n, 1:n, ] = im[ (h-n+1):h, (w-n+1):w, ] 551 | # top 552 | mat[ 1:n, (n+1):(n+w), ] = im[ (h-n+1):h, 1:w, ] 553 | # top right 554 | mat[ 1:n, (n+w+1):(2*n+w), ] = im[ (h-n+1):h, 1:n, ] 555 | # left 556 | mat[ (n+1):(n+h), 1:n, ] = im[ 1:h, (w-n+1):w, ] 557 | # right 558 | mat[ (n+1):(n+h), (n+w+1):(2*n+w), ] = im[ 1:h, 1:n, ] 559 | # bottom left 560 | mat[ (n+h+1):(2*n+h), 1:n, ] = im[ 1:n, (w-n+1):w, ] 561 | # bottom 562 | mat[ (n+h+1):(2*n+h), (n+1):(n+w), ] = im[ 1:n, 1:w, ] 563 | # bottom right 564 | mat[ (n+h+1):(2*n+h), (n+w+1):(2*n+w), ] = im[ 1:n, 1:n, ] 565 | } else if( method == "mirror" ){ 566 | # top left 567 | mat[ 1:n, 1:n, ] = im[ n:1, n:1, ] 568 | # top 569 | mat[ 1:n, (n+1):(n+w), ] = im[ n:1, 1:w, ] 570 | # top right 571 | mat[ 1:n, (n+w+1):(2*n+w), ] = im[ n:1, w:(w-n+1), ] 572 | # left 573 | mat[ (n+1):(n+h), 1:n, ] = im[ 1:h, n:1, ] 574 | # right 575 | mat[ (n+1):(n+h), (n+w+1):(2*n+w), ] = im[ 1:h, w:(w-n+1), ] 576 | # bottom left 577 | mat[ (n+h+1):(2*n+h), 1:n, ] = im[ h:(h-n+1), n:1, ] 578 | # bottom 579 | mat[ (n+h+1):(2*n+h), (n+1):(n+w), ] = im[ h:(h-n+1), 1:w, ] 580 | # bottom right 581 | mat[ (n+h+1):(2*n+h), (n+w+1):(2*n+w), ] = im[ h:(h-n+1), w:(w-n+1), ] 582 | } 583 | 584 | im = nimg( mat ) 585 | return( im ) 586 | } 587 | 588 | 589 | im_crop = function( im, margin ){ 590 | if( length( margin ) == 1 ){ 591 | top = bottom = left = right = margin 592 | } else if( length( margin ) == 2 ){ 593 | top = bottom = margin[ 1 ] 594 | left = right = margin[ 2 ] 595 | } else if( length( margin ) == 3 ){ 596 | warning( "margin length must be 1, 2, or 4!" ) 597 | } else if( length( margin ) == 4 ){ 598 | top = margin[ 1 ] 599 | right = margin[ 2 ] 600 | bottom = margin[ 3 ] 601 | left = margin[ 4 ] 602 | } 603 | im = im[ (1 + top):(im_height( im ) - bottom), (1 + left):(im_width( im ) - right), , drop = FALSE ] 604 | return( nimg( im ) ) 605 | } 606 | 607 | 608 | im_crop_square = function( im, position = 0.5 ){ 609 | position = clamping( position ) 610 | diff = im_width( im ) - im_height( im ) 611 | position = 2 * position - 1 # range [-1,1] 612 | size = min( im_size( im ) ) 613 | erode = abs( diff ) / 2 614 | center = max( im_size( im ) ) / 2 615 | start = floor( center - size / 2 + erode * position ) 616 | if( start < 1 ) start = 1 617 | end = start + size - 1 618 | if( diff > 0 ){ # wide 619 | im = im_crop( im, c( 0, im_width( im ) - end, 0, start - 1 ) ) 620 | } else { # tall 621 | im = im_crop( im, c( start - 1, 0, im_height( im ) - end, 0 ) ) 622 | } 623 | return( nimg( im ) ) 624 | } 625 | 626 | 627 | im_resize = function( im, height, width, interpolation = 1 ){ 628 | itype = 1 + 2 * interpolation # 0->1, 1->3, 2->5 629 | if( base::missing( width ) ){ # scale to height 630 | width = round( im_width( im ) * ( height / im_height( im ) ) ) 631 | } else if( base::missing( height ) ){ # scale to width 632 | height = round( im_height( im ) * ( width / im_width( im ) ) ) 633 | } 634 | im = imager::resize( nimg2cimg( im ), size_x = width, size_y = height, interpolation_type = itype ) 635 | return( cimg2nimg( im ) ) 636 | } 637 | 638 | 639 | im_resize_limit = function( im, bound, interpolation = 1 ){ 640 | if( max( im_size( im ) ) < bound ){ 641 | return( im ) 642 | } 643 | if( im_width( im ) > im_height( im ) ){ 644 | im_resize( im, width = bound, interpolation = interpolation ) 645 | } else { 646 | im_resize( im, height = bound, interpolation = interpolation ) 647 | } 648 | } 649 | 650 | 651 | im_resize_scale = function( im, scale = 1, interpolation = 1 ){ 652 | itype = 1 + 2 * interpolation # 0->1, 1->3, 2->5 653 | im = imager::imresize( nimg2cimg( im ), scale, itype ) 654 | return( cimg2nimg( im ) ) 655 | } 656 | 657 | 658 | im_combine = function( im1, im2, y = 0, x = 0, alpha = FALSE, background = 1 ){ 659 | cc = max( im_nc( im1 ), im_nc( im2 ) ) 660 | h = max( im_height( im1 ), y + im_height( im2 ), im_height( im2 ), - y + im_height( im1 ) ) 661 | w = max( im_width( im1 ), x + im_width( im2 ), im_width( im2 ), - x + im_width( im1 ) ) 662 | im = array( rep( background, each = h * w, times = cc ), dim = c( h, w, cc ) ) 663 | 664 | y1 = ifelse( y < 0, -y, 0 ) + 1 665 | y2 = ifelse( y < 0, 0, y ) + 1 666 | x1 = ifelse( x < 0, -x, 0 ) + 1 667 | x2 = ifelse( x < 0, 0, x ) + 1 668 | im[ y1:( y1 + im_height( im1 ) - 1 ), x1:( x1 + im_width( im1 ) - 1 ), 1:cc ] = im1 669 | im[ y2:( y2 + im_height( im2 ) - 1 ), x2:( x2 + im_width( im2 ) - 1 ), 1:cc ] = im2 670 | if( ! alpha ){ 671 | return( nimg( im ) ) 672 | } else { 673 | A = array( 0, dim = c( h, w, 1 ) ) 674 | A[ y1:( y1 + im_height( im1 ) - 1 ), x1:( x1 + im_width( im1 ) - 1 ), 1 ] = 1 675 | A[ y2:( y2 + im_height( im2 ) - 1 ), x2:( x2 + im_width( im2 ) - 1 ), 1 ] = 1 676 | return( merge_color( c( split_color( im ), list( A ) ) ) ) 677 | } 678 | } 679 | 680 | 681 | im_raise = function( im, intercept ){ 682 | intercept + ( 1 - intercept ) * im 683 | } 684 | 685 | 686 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 687 | # luminance ---- 688 | 689 | 690 | im_gray = function( im, tricolored = FALSE ){ 691 | if( im_nc( im ) < 2 ){ 692 | return( im ) 693 | } 694 | lab = sRGB2Lab( im ) 695 | L = get_R( lab ) 696 | C0 = array( 0, dim = dim( L ) ) 697 | im = merge_color( list( L, C0, C0 ) ) %>% Lab2sRGB 698 | if( ! tricolored ){ 699 | im = get_R( im ) 700 | } 701 | return( im ) 702 | } 703 | 704 | 705 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 706 | # spatial filtering ---- 707 | 708 | 709 | box_blur = function( im, radius ){ 710 | if( radius < 1 ){ 711 | warning( "radius should be equal to or larger than 1.") 712 | return( im ) 713 | } 714 | r = radius 715 | if( im_nc( im ) != 1 ){ 716 | imlist = list() 717 | for( i in 1:im_nc( im ) ){ 718 | imlist = c( imlist, list( box_blur( get_channel( im, i ), r ) ) ) 719 | } 720 | return( merge_color( imlist ) ) 721 | } 722 | L = 2 * r + 1 723 | width = im_width( im ) 724 | height = im_height( im ) 725 | im = im_pad( im, r, method = "mirror" ) 726 | out = array( 0.0, dim( im ) ) 727 | cumsum = rowSums( im[ , 1:(2*r), ] ) 728 | # i = r + 1 729 | cumsum = cumsum + im[ ,r + 1 + r, ] 730 | out[ , r + 1, ] = cumsum / L 731 | for( i in ( r + 2 ):( width + r ) ){ 732 | cumsum = cumsum + im[ ,i + r, ] - im[ ,i - r - 1, ] 733 | out[ , i, ] = cumsum / L 734 | } 735 | im = out 736 | cumsum = colSums( im[ 1:(2*r), , ] ) 737 | cumsum = cumsum + im[ r + 1 + r, , ] 738 | out[ r + 1, , ] = cumsum / L 739 | for( i in ( r + 2 ):( height + r ) ){ 740 | cumsum = cumsum + im[ i + r, , ] - im[ i - r - 1, , ] 741 | out[ i, , ] = cumsum / L 742 | } 743 | out = im_crop( out, r ) 744 | return( out ) 745 | } 746 | 747 | 748 | box_variance = function( im, radius ){ 749 | box_blur( im^2, radius ) - box_blur( im, radius )^2 750 | } 751 | 752 | 753 | gauss_kernel = function( sd, radius = round( 2.5 * sd ) ){ 754 | if( sd < 0.2 ){ 755 | warning( "sd must be equal to or larger than 0.2") 756 | return( NULL ) 757 | } 758 | L = 2 * radius + 1 759 | matx = matrix( stats::dnorm( 1:L, mean = radius + 1, sd = sd ), nrow = L, ncol = L, byrow = FALSE ) 760 | maty = matrix( stats::dnorm( 1:L, mean = radius + 1, sd = sd ), nrow = L, ncol = L, byrow = TRUE ) 761 | mat = matx * maty 762 | mat = mat / sum( mat ) 763 | return( nimg( array( mat, c( L, L, 1 ) ) ) ) 764 | } 765 | 766 | 767 | guided_filter = function( p, radius, epsilon = 0.1, I = p ){ 768 | if( radius < 1 ){ 769 | warning( "radius should be equal to or larger than 1.") 770 | return( p ) 771 | } 772 | 773 | I_mean = box_blur( I, radius ) 774 | I_var = box_variance( I, radius ) 775 | p_mean = box_blur( p, radius ) 776 | 777 | a = ( box_blur( I * p, radius ) - I_mean * p_mean ) / ( I_var + epsilon ) 778 | b = p_mean - a * I_mean 779 | 780 | a_mean = box_blur( a, radius ) 781 | b_mean = box_blur( b, radius ) 782 | 783 | q = a_mean * I + b_mean 784 | return( q ) 785 | } 786 | 787 | 788 | stat_filter = function( im, radius, FUN, pad.method = "mirror" ){ 789 | if( radius < 1 ){ 790 | warning( "radius should be equal to or larger than 1.") 791 | return( im ) 792 | } 793 | 794 | if( im_nc( im ) > 1 ){ 795 | imlist = list() 796 | for( i in 1:im_nc( im ) ){ 797 | imlist = c( imlist, list( stat_filter( get_channel( im, i ), radius, FUN, pad.method ) ) ) 798 | } 799 | return( merge_color( imlist ) ) 800 | } 801 | 802 | im = im_pad( im, radius, method = pad.method )[,,] 803 | im2 = im 804 | for( cy in ( 1 + radius ):( im_height( im ) - radius ) ){ 805 | for( cx in ( 1 + radius ):( im_width( im ) - radius ) ){ 806 | im2[ cy, cx ] = FUN( 807 | as.vector( im[ ( cy - radius ):( cy + radius ), ( cx - radius ):( cx + radius ) ] ) 808 | ) 809 | } 810 | } 811 | im2 = im_crop( nimg( im2 ), radius ) 812 | return( im2 ) 813 | } 814 | 815 | 816 | DOG = function( im, sigma, k = 1.6 ){ 817 | im_conv( im, gauss_kernel( sigma ) ) - im_conv( im, gauss_kernel( k * sigma ) ) 818 | } 819 | 820 | 821 | XDOG = function( im, sigma, k = 1.6, p = 20 ){ 822 | ( 1 + p ) * im_conv( im, gauss_kernel(sigma) ) - p * im_conv( im, gauss_kernel(k * sigma) ) 823 | } 824 | 825 | 826 | im_conv = function( im, kernel, pad.method = "mirror" ){ 827 | if( is.null( kernel ) ){ 828 | return( im ) 829 | } 830 | if( im_nc( im ) > 1 ){ 831 | imlist = list() 832 | for( i in 1:im_nc( im ) ){ 833 | imlist = c( imlist, list( im_conv( get_channel( im, i ), kernel, pad.method ) ) ) 834 | } 835 | return( merge_color( imlist ) ) 836 | } 837 | npad = floor( max( dim( kernel )[ 1:2 ] ) / 2 ) 838 | im = im_pad( im, n = npad, method = pad.method ) 839 | im = imager::convolve( nimg2cimg( im ), nimg2cimg( kernel ) ) 840 | im = imager::crop.borders( im, nPix = npad ) 841 | return( cimg2nimg( im ) ) 842 | } 843 | 844 | 845 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 846 | # material editing ---- 847 | 848 | 849 | gf_decompose = function( im, log_epsilon = 0.0001, filter_epsilon = 0.01 ){ 850 | if( im_nc( im ) == 2 || im_nc( im ) > 3 ){ 851 | warning( "The number of color channel must be either 1 or 3.") 852 | return( NULL ) 853 | } 854 | if( im_nc( im ) == 3 ){ 855 | lab = sRGB2Lab( im ) 856 | dec = gf_decompose( get_channel( lab, 1 ) / 100 ) 857 | dec = c( dec, list( a = get_channel( lab, 2 ), b = get_channel( lab, 3 ) ) ) 858 | dec$n.color = 3 859 | return( dec ) 860 | } 861 | 862 | dec = gf_decompose_scale( im, log_epsilon, filter_epsilon ) 863 | dec = gf_decompose_parts( dec ) 864 | 865 | return( dec ) 866 | } 867 | 868 | 869 | gf_decompose_scale = function( im, depth = NULL, log_epsilon = 0.0001, filter_epsilon = 0.01 ){ 870 | im = im_gray( im ) 871 | if( is.null( depth ) ){ 872 | depth = floor( log2( min( im_size( im ) ) ) ) 873 | } 874 | L = log( im + log_epsilon ) 875 | 876 | if( depth == 0 ) { 877 | N = 0 878 | D = list( residual = L ) 879 | } else { 880 | # L0 = L 881 | # Lk = guided_filter( Lk-1, filter_epsilon, 2^k ) (k=1~n) 882 | # Dk = Lk-1 - Lk 883 | # recon = ∑(Dk)[k=1~n] + Ln 884 | N = min( depth, floor( log2( min( im_size( L ) ) ) ) ) 885 | L_k_minus_1 = guided_filter( L, 2^1, filter_epsilon ) # L1 886 | D_k = L - L_k_minus_1 # D1 887 | D = list( D_k ) 888 | if( N > 1 ){ 889 | for( k in 2:N ){ 890 | L_k = guided_filter( L_k_minus_1, 2^k, filter_epsilon ) 891 | D_k = L_k_minus_1 - L_k 892 | D = c( D, list( D_k ) ) 893 | if( k == N ){ 894 | names( D ) = paste0( "D", sprintf( paste0( "%0", nchar( N ), "d" ), 1:N ) ) 895 | # add residual 896 | D = c( D, list( residual = L_k ) ) 897 | } else { 898 | L_k_minus_1 = L_k 899 | } 900 | } 901 | } else if( N == 1 ) { 902 | names( D ) = paste0( "D", sprintf( paste0( "%0", nchar( N ), "d" ), 1:N ) ) 903 | D = c( D, list( residual = L_k_minus_1 ) ) 904 | } 905 | } 906 | 907 | dec = list( 908 | size = im_size( im ), 909 | depth = N, 910 | n.color = 1, 911 | log_epsilon = log_epsilon, 912 | filter_epsilon = filter_epsilon, 913 | L = D 914 | ) 915 | return( dec ) 916 | } 917 | 918 | 919 | gf_decompose_parts = function( dec ){ 920 | L = dec$L 921 | residual = L$residual 922 | L$residual = NULL 923 | L = lapply( L, function( im ){ 924 | blur_range = 0.2 925 | range_lo = 1 - blur_range 926 | range_hi = 1 + blur_range 927 | sigma = stats::sd( im ) 928 | hi = 929 | im * cubic_spline( im, range_lo * sigma, range_hi * sigma ) + 930 | im * cubic_spline( im, -range_lo * sigma, -range_hi * sigma ) 931 | lo = 932 | im * pmin( cubic_spline( im, -range_hi * sigma, -range_lo * sigma ), 933 | cubic_spline( im, range_hi * sigma, range_lo * sigma ) ) 934 | hip = hi 935 | hip[ hi < 0 ] = 0 936 | hin = hi 937 | hin[ hi > 0 ] = 0 938 | lop = lo 939 | lop[ lo < 0 ] = 0 940 | lon = lo 941 | lon[ lo > 0 ] = 0 942 | return( list( highamp_posi = hip, highamp_nega = hin, lowamp_posi = lop, lowamp_nega = lon ) ) 943 | } ) 944 | L = c( L, list( residual = residual ) ) 945 | dec$L = L 946 | return( dec ) 947 | } 948 | 949 | 950 | gf_reconstruct = function( dec, scales, ind, include.residual = TRUE ){ 951 | if( base::missing( scales ) ){ 952 | scales = 1:dec$depth 953 | } 954 | if( base::missing( ind ) ){ 955 | ind = 1:4 956 | } 957 | 958 | recon = array( 0, c( dec$size, 1 ) ) 959 | if( ! any( 0 == scales ) && length( dec$L ) > 1 ){ 960 | for( i in scales ){ 961 | if( "nimg" %in% class( dec$L[[ i ]] ) ){ 962 | # scale-only decomposition 963 | recon = recon + dec$L[[ i ]] 964 | } else { 965 | # scale and parts decomposition 966 | for( j in ind ){ 967 | recon = recon + dec$L[[ i ]][[ j ]] 968 | } 969 | } 970 | } 971 | } 972 | if( include.residual ){ 973 | recon = recon + dec$L$residual 974 | } 975 | recon = exp( recon ) - dec$log_epsilon 976 | 977 | if( dec$n.color == 3 ){ 978 | recon = Lab2sRGB( merge_color( list( recon * 100, dec$a, dec$b ) ) ) 979 | } 980 | return( recon ) 981 | } 982 | 983 | 984 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 985 | # sketcher ---- 986 | 987 | 988 | #' Apply the sketch effect on an image 989 | #' @param im an image (array). 990 | #' @param style a numeric (integer). Either 1 or 2. 991 | #' @param lineweight a numeric. Strength of lines. 992 | #' @param smooth a numeric (integer). Smoothness of image texture. 993 | #' @param gain a numeric between 0 and 1. Can be used to reduce noise in dim regions. 994 | #' @param contrast a numeric (integer). Adjusts the image contrast. 995 | #' @param shadow a numeric between 0 and 1 996 | #' @param max.size maximum image resolution (width or height) of the output image 997 | #' @return an image. 998 | #' @examples 999 | #' im = sketch(face) 1000 | #' plot(im) 1001 | #' 1002 | #' \dontrun{ 1003 | #' im = im_load("path/to/your/image.jpg") 1004 | #' plot(im) 1005 | #' } 1006 | #' @export 1007 | sketch = function( im, style = 1, lineweight = 1, smooth = ceiling(lineweight), gain = .02, contrast = NULL, 1008 | shadow = 0, max.size = 2048 ){ 1009 | if( is.null( contrast ) ){ 1010 | contrast = ifelse( style == 1, 20, 4 ) 1011 | } 1012 | 1013 | im = im %>% im_resize_limit( max.size ) %>% im_gray() 1014 | 1015 | if( shadow ){ 1016 | shadow.smooth = -10 * shadow + 11 1017 | tone = sketch_XDOG( im, smooth, sigma = 0.5, k = 1.6, p = 20, eta = shadow, phi = shadow.smooth ) 1018 | } 1019 | 1020 | im = im_raise( im, gain ) 1021 | N = floor( log2( min( im_size( im ) ) ) ) 1022 | if( smooth > N ){ 1023 | warning( paste0( "smooth exceeded the maximum possible value for the input image. smooth = ", 1024 | N, " was used instead.") ) 1025 | smooth = N 1026 | } 1027 | 1028 | if( smooth >= 1 ){ 1029 | im = im %>% gf_decompose_scale( smooth ) %>% gf_reconstruct( scales = 0 ) 1030 | } 1031 | 1032 | if( style == 1 ){ 1033 | temp = im_conv( im, gauss_kernel( max( 0.3, lineweight ) ), pad.method = "mirror" ) 1034 | } else if( style == 2 ){ 1035 | temp = stat_filter( im, max( 1, lineweight ), max, pad.method = "mirror" ) 1036 | } 1037 | 1038 | im2 = clamping( im / temp ) 1039 | im2 = im2^contrast 1040 | if( shadow ){ 1041 | im2 = pmin( im2, ( 1 - ( 1 - tone ) * 0.87 ) ) 1042 | } 1043 | return( im2 ) 1044 | } 1045 | 1046 | 1047 | sketch_XDOG = function( im, smooth = 1, sigma = 0.5, k = 1.6, p = 20, eta = 0.5, phi = 6 ){ 1048 | im %>% gf_decompose_scale( smooth ) %>% gf_reconstruct( scales = 0 ) %>% 1049 | XDOG( sigma, k, p ) %>% ramp_threshold( eta, phi ) %>% clamping 1050 | } 1051 | 1052 | 1053 | #' Create multiple sketches at once and combine them into a single image 1054 | #' 1055 | #' It is often necessary to find optimal sketch style parameters for your task. 1056 | #' With this function, you can easily compare the effects of different style parameters. 1057 | #' 1058 | #' @param im an image. 1059 | #' @param style numeric (integer). Either 1 (edge-focused) or 2 (smooth gradient) 1060 | #' @param weight_levels numeric (integer). a vector of lineweight values 1061 | #' @param smooth_levels numeric (integer). a vector of smooth values 1062 | #' @param gain a numeric between 0 and 1. Can be used to reduce noise in dim regions. 1063 | #' @param contrast numeric (integer). Adjusts the image contrast. 1064 | #' @param shadow a numeric between 0 and 1 1065 | #' @param verbose If TRUE (default), progress information is displayed in the Console. 1066 | #' @return an array of the sketched image. 1067 | #' @export 1068 | #' @examples 1069 | #' \donttest{ 1070 | #' im = survey(face, style = 1, weight_levels = c(1, 3), smooth_levels = c(1, 3), shadow = 0.3) 1071 | #' plot(im) 1072 | #' } 1073 | survey = function( im, style = 1, weight_levels = c(1, 2, 4), smooth_levels = c(1, 2, 4), 1074 | gain = .02, contrast = NULL, shadow = 0, verbose = TRUE ){ 1075 | if( is.null( contrast ) ){ 1076 | contrast = ifelse( style == 1, 20, 4 ) 1077 | } 1078 | if( verbose ){ 1079 | N = length( smooth_levels ) * length( weight_levels ) 1080 | n = 1 1081 | cat( paste0( "Sketching ", N, " images: ") ) 1082 | } 1083 | 1084 | imgs = NULL 1085 | for( s in 1:length( smooth_levels ) ){ 1086 | for( t in 1:length( weight_levels ) ){ 1087 | if( verbose ){ 1088 | cat( paste0( n, " ") ) 1089 | } 1090 | im2 = sketch( im, style, weight_levels[ t ], smooth_levels[ s ], gain, contrast, shadow ) 1091 | if( is.null( imgs ) ){ 1092 | imgs = im2 1093 | } else { 1094 | y = ( s - 1 ) * im_height( im ) 1095 | x = ( t - 1 ) * im_width( im ) 1096 | imgs = im_combine( imgs, im2, y, x ) 1097 | } 1098 | if( verbose ){ 1099 | n = n + 1 1100 | } 1101 | } 1102 | } 1103 | if( verbose ){ 1104 | cat( "done.\n" ) 1105 | } 1106 | 1107 | return( imgs ) 1108 | } 1109 | 1110 | 1111 | 1112 | --------------------------------------------------------------------------------