--- title: "Images" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Images} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} # Without the suggested packages, the code is shown but not run knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = rlang::is_installed(c("dplyr", "magick", "yardstick")) ) old_options <- options(width = 100, pillar.sigfig = 3) ``` ```{r library} library(decisions) ``` ## Overview A decision model can look at an image. `decide()` takes images in the argument `image`, alone or with `text`. This vignette shows the image arguments on one task: does a woven pattern have a flaw? It runs three models: * Jev, from TypeSafe, with `decider_typesafe()`. * The Decisions API of OpenAI, with `decider_openai()`. Its default model is `gpt-6-luna`. * Clef-Flash, on a local SGLang server, with `decider_systemone()`. The results below come from live calls. The package saved them in the package folder `extdata`, so this vignette builds without a key and without a network. The code that made them is in `vignette_results.R` in the folder `data-raw` of the [GitHub repository](https://github.com/fmegahed/decisions). Microsoft-Decision-1 and Laya read text only. They are not in this vignette. ## Image inputs `image` takes one of these for each input: * a file path, * a URL, * a magick image, or a list of magick images. `text` and `image` can go together. Element `i` of `text` and element `i` of `image` make input `i`. The package needs the magick package to read images. It turns each image upright, converts it to sRGB and sends it as a JPEG. ## Data The package has the dataset `textiles`. It has 50 simulated images of a woven textile, 250 x 250 pixels. They are in the folder `system.file("extdata", "textiles", package = "decisions")`. Half of them show a flaw in the pattern. See `?textiles` for the details. Source: the images come from the R package spc4sts. The public repository fmegahed/qe_genai shares them (MIT license, ). Bui, A. T., & Apley, D. W. (2021). spc4sts: Statistical process control for stochastic textured surfaces in R. *Journal of Quality Technology, 53*(3), 219-242. The CRAN page of spc4sts, which links its reference manual: ```{r data, script = TRUE} textile_dir <- system.file("extdata", "textiles", package = "decisions") textiles <- textiles |> dplyr::mutate(path = file.path(textile_dir, file)) dplyr::count(textiles, role, class) ``` The column `role` splits the images. Four images are references, two for each class. The other 46 images are the inputs. The column `class` is the truth. ```{r saved, echo = FALSE} saved <- readRDS(system.file("extdata", "images.rds", package = "decisions")) stopifnot(identical(dplyr::filter(textiles, role == "input")$file, saved$inputs$file)) ``` The picture shows the four references. The top row has the two `nominal` images. The bottom row has the two `defective` images. ```{r montage, script = TRUE, fig.alt = "Four textile images in two rows. The top row shows two regular weaves. The bottom row shows two weaves with a flaw.", out.width = "50%"} reference_files <- textiles |> dplyr::filter(role == "reference") |> dplyr::arrange(class, reference_rank) reference_files$path |> magick::image_read() |> magick::image_montage(tile = "2x2", geometry = "250x250+4+4") ``` ## References A reference is an image that shows what an answer looks like. `references` is a named list. Each name is an answer of the question. Each element has one or more images. The package prepares each reference once and sends it with every input. ```{r references, script = TRUE} references <- split(reference_files$path, reference_files$class) inputs <- dplyr::filter(textiles, role == "input") ``` ## Question One choice question has two answers. The descriptions are short. They say what the model sees. ```{r question, script = TRUE} weave <- question_choice( "Does the woven pattern in `inspection_image` have a flaw?", c( nominal = "The weave is regular. The pattern repeats with no break.", defective = "The weave has a flaw. A part of the pattern is broken, or the whole pattern is distorted." ) ) ``` ## Image arguments Three arguments set how the package prepares an image. They apply to the inputs and to the references. | Argument | Meaning | |---|---| | `max_size` | The longest side in pixels after the resize. The default is 768. `NULL` keeps the size. | | `upscale` | `TRUE` enlarges an image that is smaller than `max_size`. The default is `FALSE`. | | `crop` | `"none"` (default), `"center"` (a centered square), a box, or a function. | A box is `c(x, y, width, height)` in pixels. The origin is the top left of the image. The crop happens before the resize. The textiles are 250 x 250 pixels. TypeSafe refuses images smaller than 280 pixels. So Jev needs `upscale = TRUE`. With `max_size = 768`, each image grows to 768 pixels. ## Crop function `crop` can be a function. It takes the magick image and returns a box. This is the hook for an object detection model. A detector would return the box of the object here, and the model would see the object only. The function below is simple: it trims a border of 12 pixels. ```{r trim, script = TRUE} trim_border <- function(image) { info <- magick::image_info(image) c(12, 12, info$width - 24, info$height - 24) } ``` ## Two hosted models `purrr::map()` runs `decide()` once for each hosted decider. Each call sends 46 images. Each request holds the four references and one input. ```{r decide, eval = FALSE, script = TRUE} deciders <- list(jev = decider_typesafe(), openai = decider_openai()) answers <- purrr::map(deciders, \(decider) { decide( decider, weave, image = inputs$path, references = references, max_size = 768, upscale = TRUE ) }) ``` ```{r show-decide, echo = FALSE} answers <- saved$answers ``` ## Clef-Flash Clef-Flash is a model of Cloudflare. It runs on your computer, on a GPU, and it costs nothing per call. The shell command below starts a server with Docker. It is not run here. The image `lmsysorg/sglang:dev-clef` holds SGLang. The first start downloads the model. ```{bash clef-server, eval = FALSE} docker run --gpus all --shm-size 8g --ipc=host -p 30000:30000 \ lmsysorg/sglang:dev-clef \ sglang serve --model-path Cloudflare/clef-flash --host 0.0.0.0 --port 30000 ``` When `GET http://localhost:30000/health` answers, the server is ready. Clef-Flash reads the images in a list, so `image_format = "images"`. The server needs no key. With a free model we can compare image settings: the same inputs with `max_size = 768`, with `max_size = 512`, and with the crop function. ```{r clef, eval = FALSE, script = TRUE} clef <- decider_systemone( "http://localhost:30000", "Cloudflare/clef-flash", image_format = "images" ) settings <- list( `max_size 768` = list(max_size = 768), `max_size 512` = list(max_size = 512), `crop trim` = list(max_size = 768, crop = trim_border) ) clef_answers <- purrr::map(settings, \(setting) { rlang::exec( decide, clef, weave, image = inputs$path, references = references, upscale = TRUE, !!!setting ) }) ``` ```{r show-clef, echo = FALSE} clef_answers <- saved$clef_answers ``` ## Performance We bind the tibbles and add the truth. Each tibble has 46 rows, in the order of the inputs. The levels of `truth` are `nominal` and `defective`. The event is the second level, so `roc_auc_vec()` gets `event_level = "second"`. ```{r results, script = TRUE} all_answers <- c(answers, clef_answers) results <- all_answers |> dplyr::bind_rows(.id = "model") |> dplyr::mutate( model = factor(model, levels = names(all_answers)), truth = rep(inputs$class, times = length(all_answers)) ) ``` The next table has one row for each model and setting. `roc_auc` is the area under the ROC curve, from `.pred_defective`. `errors` is the number of rows with an error. A row with an error has `NA` answers, and yardstick removes it. ```{r performance, script = TRUE} results |> dplyr::group_by(model) |> dplyr::summarise( roc_auc = yardstick::roc_auc_vec(truth, .pred_defective, event_level = "second"), errors = sum(!is.na(.error)), input_tokens = sum(.input_tokens, na.rm = TRUE), cost_usd = if (all(is.na(.cost))) NA_real_ else sum(.cost, na.rm = TRUE), median_seconds = median(.seconds, na.rm = TRUE) ) ``` Forty-six images give a rough number only. One image changes the share of a class by 4 points. Use more images before you choose a model for real work. ## Cost and time TypeSafe, OpenAI and Clef-Flash report no cost, so `.cost` is `NA`, and `decide()` prints one message about it. All three report the input tokens. Each input holds four reference images, so the input tokens are many. `.seconds` is the time of one response. The requests run in parallel, so these times do not add up to the time of the whole call. ## Settings The three Clef-Flash rows differ in one setting. The `roc_auc` of the three is close: 0.83, 0.83 and 0.85. But Clef-Flash answered `nominal` for all 46 images in each run. Its probabilities rank the images, but the most likely answer is never `defective`. So a cut-off below 0.5 would be needed for this task. Jev answered `defective` for 14 images, and OpenAI for 9. Half of the images, 23, are `defective`. A change of `max_size` changes the input tokens. The images at 512 pixels need about half the tokens of the images at 768 pixels, and the calls take about half the time. The `roc_auc` is the same to two digits. The crop function removes the border and changes the `roc_auc` by 0.02. With 46 images, we cannot say that this change is real. A setting that works for one model may not work for another. Test each setting on your own data, with images that you did not use to choose it. ```{r reset, include = FALSE} options(old_options) ```