Combine image and enlargement of a section of that image

Han Oostdijk

2019/07/01

In this document we show how to create an image consisting of image and a subsection of that image. We use the R package magick. For details about these packages see the reference manual and vignette.

In Figure 1 we show the original input. We extract the animal shaped figure and position an enlarged version beside the original version in Figure 2. With an arrow and a selection box we visibly indicate what was done.

plot of chunk g1

Figure 1: full image

plot of chunk g2

Figure 2: image with an enlarged subsection

Utility functions

Because I want to use the pipe of the magrittr package but keep the code as clean as possible, I made a local copy of %>% . I also defined the cond_set function that creates a list from a given one by checking a list of names with corresponding default values. As an exemple:

  myoptions = list(dx=10,dy=5,sep=',')
  myparms = c('dx','dy', 'x','y')
  mydefs  = c(info$width,info$height,0,0)
  myoptions2 = cond_set(myparms, mydefs,myoptions)

Here the list list(dx=10,dy=5,x=0,y=0) is generated by using the default 0 for x and y and removing sep because that is not given in myparms.

Also the utility functions add_rect and add_arrows were defined as cover functions for the base R functions rect and arrows. These functions add a rectangle resp. arrows to a Magick image.

`%>%` = magrittr::`%>%`

cond_set <- function(myparms, mydefs, myoptions) {
	cond_set1 <- function(var,def,mystruct) {
		if (!is.null(mystruct[[var]])) {
			res = mystruct[[var]]
		} else {
			res = def
		}
		res
	}
	myoptions2 = purrr::map2(myparms, mydefs, ~ cond_set1(.x,.y, myoptions))
	names(myoptions2) = myparms
	myoptions2
}

add_rect <- function(im_object,
	xleft, ybottom, xright, ytop, density = NULL, angle = 45,
	col = NA, border = NULL, lty = par("lty"), lwd = par("lwd"),
	...)
{
	img <- magick::image_draw(im_object)
	rect(xleft, ybottom, xright, ytop, density, angle,
		col, border, lty , lwd,
		...	)
	d <- dev.off()
	img
}

add_arrows <- function(im_object,
	x0, y0, x1 = x0, y1 = y0, length = 0.25, angle = 30,
	code = 2, col = par("fg"), lty = par("lty"),
	lwd = par("lwd"), ...)
{
	img <- magick::image_draw(im_object)
	arrows(x0, y0, x1 , y1, length, angle ,
		code = 2, col, lty, lwd, ...)
	d <- dev.off()
	img
}

Main functions

Most functions defined here have a parameter list that is described by myparms and mydefs in the function. The offsets in comb_it have the format ‘+x+y’. The geometry in resize_it has the format ‘nnnx’, ‘xnnn’ or ‘mmmxnnn’ and in the border specification the format mxn with m and n integers.

The following functions are defined:

NB. as usual in image calculations the y-coordinate starts with 0 at the top of the image and increases when going to the bottom. In contrast, in mathematical plots the y-coordinate decreases when going to the bottom of the plot.

crop_it <- function (img,myoptions) {
  force(myoptions)
  info= magick::image_info(img)
  myparms = c('dx','dy', 'x','y')
  mydefs  = c(info$width,info$height,0,0)
  myoptions2 = cond_set(myparms, mydefs,myoptions)
  img %>%
    magick::image_crop(glue::glue_data(myoptions2,"{dx}x{dy}+{x}+{y}"))
}

show_it <- function (img,myoptions) {
	force(myoptions)
	info= magick::image_info(img)
	myparms = c('dx','dy', 'x','y',
	   'border', 'lty', 'lwd')
	mydefs  = list(info$width,info$height,0,0,NULL,par("lty"),par("lwd"))
	myoptions2 = cond_set(myparms, mydefs,myoptions)
	xleft   = myoptions2$x
	xright  = xleft + myoptions2$dx
	ytop    = myoptions2$y
	ybottom = ytop + myoptions2$dy
	xoords = c(	xleft, ybottom, xright, ytop)
	add_rect(img,
		xleft, ybottom, xright, ytop,border=myoptions2$border,
		lty = myoptions2$lty, lwd = myoptions2$lwd)
}

comb_it <- function(newspec, img1, offset1, img2, offset2) {
  # offsets given as '+x+y'
	force(newspec)
	myparms = c('width', 'height', 'color',
		'bordercol1',	'bordergem1',	'bordercol2',	'bordergem2')
	mydefs  = list(800, 400, 'white', 'lightgray', NA, 'lightgray', NA)
	newspec = cond_set(myparms, mydefs, newspec)
	imgcomb <-
		magick::image_blank(newspec$width, newspec$height, color = newspec$color)
	imgcomb = magick::image_composite(imgcomb, img1, offset = offset1)
	imgcomb = magick::image_composite(imgcomb, img2, offset = offset2)
	if (!is.na(newspec$bordergem1)) {
		imgcomb = border_it(imgcomb,
			color=newspec$bordercol1, geometry = newspec$bordergem1)
	}
	if (!is.na(newspec$bordergem2)) {
		imgcomb = border_it(imgcomb,
			color=newspec$bordercol2, geometry = newspec$bordergem2)
	}
	imgcomb
}

resize_it <- function(img,g) {
	magick::image_resize(img, geometry = g, filter = NULL)
}

border_it <- function(img, color='yellow', geometry = '1x1') {
	magick::image_border(img,	color = color, geometry = geometry)
}

Using the functions

Read the input file

Define the datasets for input and output

image_fn1 = "myart1.jpg" 
image_fn2 = "myart2.jpg"
img1 = magick::image_read(image_fn1)
magick::image_info(img1)
#> # A tibble: 1 x 7
#>   format width height colorspace matte filesize density
#>   <chr>  <int>  <int> <chr>      <lgl>    <int> <chr>  
#> 1 JPEG     800    400 sRGB       FALSE    22244 72x72

The 'print(img1)' statement would produce Figure 1.

Extract part of the image

With the list spad we define which part of the image we want to extract and how (later) we will draw a rectangle on this image.

spad = list(dx=180,dy=160,x=30,y=150,border='black',lwd=4)
img2 = crop_it(img1,spad)
magick::image_info(img2)
#> # A tibble: 1 x 7
#>   format width height colorspace matte filesize density
#>   <chr>  <int>  <int> <chr>      <lgl>    <int> <chr>  
#> 1 JPEG     180    160 sRGB       FALSE        0 72x72
img2

plot of chunk unnamed-chunk-7

Figure 3: cropped 'animal'

We resize the image to a width of about 400 pixels (keeping the aspect ratio intact) and give it a black border of 2 pixels on all sides:

 
img2 = resize_it(img2, '400x') 
img2 = border_it(img2, color='black', geometry = '2x2') 
magick::image_info(img2) 
#> # A tibble: 1 x 7
#>   format width height colorspace matte filesize density
#>   <chr>  <int>  <int> <chr>      <lgl>    <int> <chr>  
#> 1 JPEG     404    360 sRGB       FALSE        0 72x72
img2 

plot of chunk unnamed-chunk-8

Figure 4: enlarged 'animal' with yellow border

Indicate which part is extracted on the original image

We use the same spad list to show which part of the original image we did extract. After doing that we resize it so that we can combine it with Figure 4


img12 = show_it(img1,spad)
img12 = resize_it(img12,'300x')
img12 

plot of chunk unnamed-chunk-9

Figure 5: minimized original image with indicated extract

Combine Figure 4 and Figure 5

Now we combine the two images and add one arrow from the original image to the enlarged section. For the arrow we use two calls with different colors and line widths. The result is a yellow arrow with a black boundary.

imgcomb= comb_it(list(bordergem2='2x2',bordercol2='black'),img12,"+450+100",img2,"+10+020" )
imgcomb = add_arrows(imgcomb,
	460, 190, 420, 190,col='black',lwd=5)
imgcomb = add_arrows(imgcomb,
	460, 190, 420, 190,col='yellow',lwd=3)
imgcomb

plot of chunk unnamed-chunk-10

Figure 6: combined images

Write the image to an external file

Write the image to an external file. It will refuse to do so (without a warning) when the file already exists. Therefore first (try to) remove it.

invisible(base::file.remove(image_fn2))
magick::image_write(imgcomb,image_fn2)

Session Info

#> R version 3.6.0 (2019-04-26)
#> Platform: x86_64-w64-mingw32/x64 (64-bit)
#> Running under: Windows 10 x64 (build 18362)
#> 
#> Matrix products: default
#> 
#> locale:
#> [1] LC_COLLATE=English_United States.1252 
#> [2] LC_CTYPE=English_United States.1252   
#> [3] LC_MONETARY=English_United States.1252
#> [4] LC_NUMERIC=C                          
#> [5] LC_TIME=English_United States.1252    
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#>  [1] HOQCutil_0.1.10 jsonlite_1.6    glue_1.3.1      purrr_0.3.2    
#>  [5] xml2_1.2.2      ggspatial_1.0.3 ggplot2_3.2.1   sf_0.7-7       
#>  [9] dplyr_0.8.3     stringr_1.4.0   osmdata_0.1.1  
#> 
#> loaded via a namespace (and not attached):
#>  [1] tidyselect_0.2.5   xfun_0.8           lattice_0.20-38   
#>  [4] vctrs_0.2.0        colorspace_1.4-1   htmltools_0.3.6   
#>  [7] utf8_1.1.4         rlang_0.4.0        e1071_1.7-0       
#> [10] pillar_1.4.2       withr_2.1.2        DBI_1.0.0         
#> [13] sp_1.3-1           readxl_1.3.1       lifecycle_0.1.0   
#> [16] plyr_1.8.4         cellranger_1.1.0   munsell_0.5.0     
#> [19] blogdown_0.15      gtable_0.3.0       rvest_0.3.4       
#> [22] evaluate_0.14      knitr_1.24         prettymapr_0.2.2  
#> [25] curl_4.0           class_7.3-15       fansi_0.4.0       
#> [28] highr_0.8          Rcpp_1.0.2         KernSmooth_2.23-15
#> [31] backports_1.1.4    scales_1.0.0       classInt_0.3-3    
#> [34] magick_2.2         captioner_2.2.3    fs_1.3.1          
#> [37] png_0.1-7          digest_0.6.20      stringi_1.4.3     
#> [40] rosm_0.2.5         grid_3.6.0         cli_1.1.0         
#> [43] rgdal_1.4-4        tools_3.6.0        magrittr_1.5      
#> [46] lazyeval_0.2.1     tibble_2.1.3       zeallot_0.1.0     
#> [49] tidyr_1.0.0        crayon_1.3.4       pkgconfig_2.0.2   
#> [52] lubridate_1.7.4    assertthat_0.2.1   rmarkdown_1.15    
#> [55] httr_1.4.1         R6_2.4.0           units_0.6-2       
#> [58] compiler_3.6.0