|
a |
|
b/R/annotation.R |
|
|
1 |
#### |
|
|
2 |
# Main Shiny App #### |
|
|
3 |
#### |
|
|
4 |
|
|
|
5 |
#' annotateSpatialData |
|
|
6 |
#' |
|
|
7 |
#' A mini shiny app to for annotating spatial points |
|
|
8 |
#' |
|
|
9 |
#' @param object a VoltRon object |
|
|
10 |
#' @param label the name of the new metadata column (default: annotation) annotating spatial points by selected polygons |
|
|
11 |
#' @param assay assay name (exp: Assay1) or assay class (exp: Visium, Xenium), see \link{SampleMetadata}. |
|
|
12 |
#' if NULL, the default assay will be used, see \link{vrMainAssay}. |
|
|
13 |
#' @param annotation_assay name of the annotation assay () |
|
|
14 |
#' @param use.image.only if TRUE, use only the image |
|
|
15 |
#' @param shiny.options a list of shiny options (launch.browser, host, port etc.) passed \code{options} arguement of \link{shinyApp}. For more information, see \link{runApp} |
|
|
16 |
#' @param image_name the name/key of the image |
|
|
17 |
#' @param channel the name of the main channel |
|
|
18 |
#' @param ... additional parameters passed to \link{vrSpatialPlot}. |
|
|
19 |
#' |
|
|
20 |
#' @import shiny |
|
|
21 |
#' @importFrom shinyjs useShinyjs show hide |
|
|
22 |
#' @importFrom stats median |
|
|
23 |
#' @importFrom sp point.in.polygon |
|
|
24 |
#' @import ggplot2 |
|
|
25 |
#' |
|
|
26 |
#' @export |
|
|
27 |
#' |
|
|
28 |
#' @examples |
|
|
29 |
#' \dontrun{ |
|
|
30 |
#' # Annotate based on images |
|
|
31 |
#' visium_data <- annotateSpatialData(visium_data, use.image.only = TRUE) |
|
|
32 |
#' |
|
|
33 |
#' # Annotate based on spatial plot |
|
|
34 |
#' xenium_data <- annotateSpatialData(xenium_data, group.by = "clusters") |
|
|
35 |
#' } |
|
|
36 |
annotateSpatialData <- function(object, label = "annotation", assay = NULL, annotation_assay = "ROIAnnotation", use.image.only = FALSE, |
|
|
37 |
shiny.options = list(launch.browser = getOption("shiny.launch.browser", interactive())), |
|
|
38 |
image_name = NULL, channel = NULL, ...) { |
|
|
39 |
|
|
|
40 |
if(!inherits(object, "VoltRon")) |
|
|
41 |
stop("Please provide a VoltRon object!") |
|
|
42 |
|
|
|
43 |
## Importing images #### |
|
|
44 |
|
|
|
45 |
# get assay names, and always get a single assay |
|
|
46 |
assay_names <- vrAssayNames(object, assay = assay) |
|
|
47 |
if(length(assay_names) > 0) |
|
|
48 |
assay <- assay_names[1] |
|
|
49 |
|
|
|
50 |
# get image name and channel |
|
|
51 |
if(is.null(image_name)){ |
|
|
52 |
image_name <- vrMainSpatial(object[[assay]]) |
|
|
53 |
} |
|
|
54 |
|
|
|
55 |
# get image |
|
|
56 |
img <- vrImages(object[[assay]], name = image_name, channel = channel, as.raster = TRUE) |
|
|
57 |
if(!inherits(img, "Image_Array")){ |
|
|
58 |
if(!requireNamespace("ImageArray")){ |
|
|
59 |
message("Please install ImageArray package to speed up visualization") |
|
|
60 |
img <- magick::image_read(img) |
|
|
61 |
} else{ |
|
|
62 |
img <- ImageArray::createImageArray(img) |
|
|
63 |
} |
|
|
64 |
} |
|
|
65 |
if(!use.image.only){ |
|
|
66 |
# get spatial plot |
|
|
67 |
g_spatial <- vrSpatialPlot(object, assay = assay, spatial = image_name, channel = channel, scale.image = FALSE, ...) |
|
|
68 |
g_spatial <- g_spatial$layers[[2]] |
|
|
69 |
} |
|
|
70 |
|
|
|
71 |
# get image info |
|
|
72 |
max.pixel.size <- 1200 |
|
|
73 |
imginfo <- getImageInfo(img) |
|
|
74 |
|
|
|
75 |
## Get previous annotation #### |
|
|
76 |
|
|
|
77 |
# set label names |
|
|
78 |
sample_metadata <- SampleMetadata(object) |
|
|
79 |
metadata <- Metadata(object, assay = sample_metadata[assay, "Assay"]) |
|
|
80 |
coords <- vrCoordinates(object, assay = assay) |
|
|
81 |
if(label %in% colnames(metadata)){ |
|
|
82 |
unique_names <- make.unique(c(colnames(metadata)[grepl(paste0("^", label), colnames(metadata))], label)) |
|
|
83 |
label <- unique_names[length(unique_names)] |
|
|
84 |
} |
|
|
85 |
|
|
|
86 |
# get segmentations (if exists) from the same layer |
|
|
87 |
if(!is.null(annotation_assay)){ |
|
|
88 |
layer_metadata <- sample_metadata[sample_metadata$Layer == sample_metadata[assay, "Layer"] & sample_metadata$Sample == sample_metadata[assay, "Sample"],] |
|
|
89 |
if(annotation_assay %in% layer_metadata$Assay){ |
|
|
90 |
|
|
|
91 |
# get segments |
|
|
92 |
segments <- vrSegments(object, assay = annotation_assay) |
|
|
93 |
segments <- lapply(segments, function(seg) seg[,colnames(seg)[colnames(seg) != "id"]]) |
|
|
94 |
segment_names <- gsub("_Assay[0-9]+$", "", names(segments)) |
|
|
95 |
|
|
|
96 |
# remove the latest annotation |
|
|
97 |
all_assay_names <- vrAssayNames(object, assay = "all") |
|
|
98 |
object <- subsetVoltRon(object, assays = all_assay_names[!all_assay_names %in% rownames(layer_metadata)[layer_metadata$Assay == annotation_assay]]) |
|
|
99 |
|
|
|
100 |
} else { |
|
|
101 |
segments <- list() |
|
|
102 |
segment_names <- c() |
|
|
103 |
} |
|
|
104 |
} |
|
|
105 |
|
|
|
106 |
## UI and Server #### |
|
|
107 |
|
|
|
108 |
# Define UI for the application |
|
|
109 |
ui <- fluidPage( |
|
|
110 |
sidebarLayout(position = "left", |
|
|
111 |
|
|
|
112 |
sidebarPanel( |
|
|
113 |
|
|
|
114 |
# margin settings |
|
|
115 |
tags$style(make_css(list('.well', 'margin', '7%'))), |
|
|
116 |
|
|
|
117 |
# # specific settings for dealing with simultaneous click and brush events |
|
|
118 |
# # https://jokergoo.github.io/2021/02/20/differentiate-brush-and-click-event-in-shiny/ |
|
|
119 |
tags$script(HTML(" |
|
|
120 |
$('#plot').mousedown(function(e) { |
|
|
121 |
var parentOffset = $(this).offset(); |
|
|
122 |
var relX = e.pageX - parentOffset.left; |
|
|
123 |
var relY = e.pageY - parentOffset.top; |
|
|
124 |
Shiny.setInputValue('x1', relX); |
|
|
125 |
Shiny.setInputValue('y1', relY); |
|
|
126 |
}).mouseup(function(e) { |
|
|
127 |
var parentOffset = $(this).offset(); |
|
|
128 |
var relX = e.pageX - parentOffset.left; |
|
|
129 |
var relY = e.pageY - parentOffset.top; |
|
|
130 |
Shiny.setInputValue('x2', relX); |
|
|
131 |
Shiny.setInputValue('y2', relY); |
|
|
132 |
Shiny.setInputValue('action', Math.random()); |
|
|
133 |
}); |
|
|
134 |
")), |
|
|
135 |
|
|
|
136 |
# Interface |
|
|
137 |
fluidRow( |
|
|
138 |
column(12,h4("Spatial Annotation")), |
|
|
139 |
column(12,shiny::actionButton("reset_btn", "Reset Points ")), |
|
|
140 |
column(12,shiny::actionButton("rmvlast_btn", "Remove Last Point")), |
|
|
141 |
column(12,shiny::actionButton("addregion_btn", "Add Region ")), |
|
|
142 |
), |
|
|
143 |
br(), |
|
|
144 |
|
|
|
145 |
fluidRow( |
|
|
146 |
column(6,shiny::selectInput("region_type", label = "Region Type", choices = c("Polygon", "Circle"), selected = "Polygon")), |
|
|
147 |
column(6,shiny::sliderInput("alpha", "Transparency", min = 0, max = 1, value = 0.2)), |
|
|
148 |
), |
|
|
149 |
|
|
|
150 |
fluidRow( |
|
|
151 |
column(6,shiny::sliderInput("label_size", label = "Label Size", min = 1, max = 5, step = 0.5, value = 4)), |
|
|
152 |
), |
|
|
153 |
|
|
|
154 |
# instructions |
|
|
155 |
h4("How to use"), |
|
|
156 |
p(style="font-size: 12px;", strong("Single-L-click"), " to select polygon or circle points"), |
|
|
157 |
p(style="font-size: 12px;", strong("Add Region"), " to set points as a new region"), |
|
|
158 |
p(style="font-size: 12px;", strong("Circles"), " require only 2 points"), |
|
|
159 |
p(style="font-size: 12px;", strong("Polygons"), " require at least 4 points"), |
|
|
160 |
br(), |
|
|
161 |
|
|
|
162 |
# Subsets |
|
|
163 |
fluidRow( |
|
|
164 |
column(12,h4("Selected Regions")), |
|
|
165 |
br(), |
|
|
166 |
column(12,shiny::actionButton("done", "Done")), |
|
|
167 |
br() |
|
|
168 |
), |
|
|
169 |
|
|
|
170 |
# Subsets |
|
|
171 |
fluidRow( |
|
|
172 |
br(), |
|
|
173 |
uiOutput("textbox_ui"), |
|
|
174 |
br() |
|
|
175 |
), |
|
|
176 |
|
|
|
177 |
width = 4 |
|
|
178 |
), |
|
|
179 |
mainPanel( |
|
|
180 |
shinyjs::useShinyjs(), |
|
|
181 |
plotOutput("image_plot", |
|
|
182 |
height = "1000px", |
|
|
183 |
click = "plot_click", |
|
|
184 |
dblclick = "plot_dblclick", |
|
|
185 |
brush = brushOpts( |
|
|
186 |
id = "plot_brush", fill = "green", |
|
|
187 |
resetOnNew = TRUE |
|
|
188 |
)), |
|
|
189 |
width = 8 |
|
|
190 |
) |
|
|
191 |
) |
|
|
192 |
) |
|
|
193 |
|
|
|
194 |
# Define server logic required to create, add, and remove textboxes |
|
|
195 |
server <- function(input, output, session) { |
|
|
196 |
|
|
|
197 |
# Reactive values #### |
|
|
198 |
selected_corners_list <- reactiveVal(segments) |
|
|
199 |
selected_corners <- reactiveVal(data.frame(x = numeric(0), y = numeric(0))) |
|
|
200 |
ranges <- reactiveValues(x = c(0, imginfo$width), y = c(0, imginfo$height)) |
|
|
201 |
|
|
|
202 |
# Zoom Event #### |
|
|
203 |
manageImageBrush(img, ranges, max.pixel.size, input, output, session) |
|
|
204 |
|
|
|
205 |
# Corner Events #### |
|
|
206 |
observeEvent(input$reset_btn, { |
|
|
207 |
selected_corners(data.frame(x = numeric(0), y = numeric(0))) |
|
|
208 |
}) |
|
|
209 |
observeEvent(input$rmvlast_btn, { |
|
|
210 |
selected_corners(selected_corners()[-nrow(selected_corners()),]) |
|
|
211 |
}) |
|
|
212 |
manageSelectedCorners(selected_corners, img, ranges, max.pixel.size, input, output, session) |
|
|
213 |
|
|
|
214 |
# Region Events #### |
|
|
215 |
n <- length(segments) |
|
|
216 |
textboxes <- reactiveVal(if (n > 0) seq_len(n) else numeric(0)) |
|
|
217 |
if (n > 0) { |
|
|
218 |
segment_names <- as.list(segment_names) |
|
|
219 |
names(segment_names) <- paste0("region", seq_len(n)) |
|
|
220 |
textbox_values <- do.call("reactiveValues", segment_names) |
|
|
221 |
} else { |
|
|
222 |
textbox_values <- reactiveValues() |
|
|
223 |
} |
|
|
224 |
|
|
|
225 |
# Dynamically generate UI for textboxes and remove buttons |
|
|
226 |
output$textbox_ui <- renderUI({ |
|
|
227 |
lapply(rev(textboxes()), function(i) { |
|
|
228 |
column(12, |
|
|
229 |
textInputwithButton(textinputId = paste0("region", i), label = paste0("Region ", i), |
|
|
230 |
buttoninputId = paste0("remove", i), value = isolate(textbox_values[[paste0("region", i)]]), |
|
|
231 |
onclick = sprintf('Shiny.setInputValue("remove", %d)', i)) |
|
|
232 |
|
|
|
233 |
) |
|
|
234 |
}) |
|
|
235 |
}) |
|
|
236 |
|
|
|
237 |
### update Region text #### |
|
|
238 |
|
|
|
239 |
observe({ |
|
|
240 |
lapply(textboxes(), function(i) { |
|
|
241 |
observeEvent(input[[paste0("region", i)]], { |
|
|
242 |
textbox_values[[paste0("region", i)]] <- isolate(input[[paste0("region", i)]]) |
|
|
243 |
}, ignoreNULL = FALSE) |
|
|
244 |
}) |
|
|
245 |
}) |
|
|
246 |
|
|
|
247 |
### remove a Region #### |
|
|
248 |
|
|
|
249 |
# Observe event to remove textbox when the button is clicked |
|
|
250 |
observeEvent(input$remove, { |
|
|
251 |
|
|
|
252 |
# remove one point |
|
|
253 |
selected_corners_list(selected_corners_list()[!(textboxes() == as.numeric(isolate(input$remove)))]) |
|
|
254 |
|
|
|
255 |
# Update the reactive value to remove the textbox |
|
|
256 |
textboxes(setdiff(textboxes(), as.numeric(isolate(input$remove)))) |
|
|
257 |
|
|
|
258 |
# Remove the value from textbox_values |
|
|
259 |
textbox_values[[paste0("region", as.numeric(input$remove))]] <- NULL |
|
|
260 |
|
|
|
261 |
}, ignoreInit = TRUE) |
|
|
262 |
|
|
|
263 |
### add a Region #### |
|
|
264 |
|
|
|
265 |
# Observe event to add a new textbox |
|
|
266 |
observeEvent(input$addregion_btn, { |
|
|
267 |
|
|
|
268 |
# Polygon selection |
|
|
269 |
if(isolate(input$region_type == "Polygon")){ |
|
|
270 |
if(nrow(selected_corners()) > 3){ |
|
|
271 |
|
|
|
272 |
# add to region list |
|
|
273 |
selected_corners_list(c(selected_corners_list(), list(selected_corners()))) |
|
|
274 |
|
|
|
275 |
# remove selected points |
|
|
276 |
selected_corners(data.frame(x = numeric(0), y = numeric(0))) |
|
|
277 |
|
|
|
278 |
# add buttons |
|
|
279 |
new_id <- if (length(textboxes()) == 0) 1 else max(textboxes()) + 1 |
|
|
280 |
textboxes(c(textboxes(), new_id)) |
|
|
281 |
textbox_values[[paste0("region", new_id)]] <- "" |
|
|
282 |
} else { |
|
|
283 |
showNotification("You must selected at least 4 points for each polygon!") |
|
|
284 |
} |
|
|
285 |
} |
|
|
286 |
|
|
|
287 |
# Circle selection |
|
|
288 |
if(isolate(input$region_type == "Circle")){ |
|
|
289 |
if(nrow(selected_corners()) == 2){ |
|
|
290 |
|
|
|
291 |
# add to region list |
|
|
292 |
circle <- makeCircleData(selected_corners()) |
|
|
293 |
selected_corners_list(c(selected_corners_list(), list(circle))) |
|
|
294 |
|
|
|
295 |
# remove selected points |
|
|
296 |
selected_corners(data.frame(x = numeric(0), y = numeric(0))) |
|
|
297 |
|
|
|
298 |
# add buttons |
|
|
299 |
new_id <- if (length(textboxes()) == 0) 1 else max(textboxes()) + 1 |
|
|
300 |
textboxes(c(textboxes(), new_id)) |
|
|
301 |
textbox_values[[paste0("region", new_id)]] <- "" |
|
|
302 |
} else { |
|
|
303 |
showNotification("You must selected only 2 points for each circle!") |
|
|
304 |
} |
|
|
305 |
} |
|
|
306 |
}) |
|
|
307 |
# Image output #### |
|
|
308 |
output$image_plot <- renderPlot({ |
|
|
309 |
|
|
|
310 |
## get image #### |
|
|
311 |
zoom_info <- FromBoxToCrop(cbind(ranges$x, ranges$y), imageinfo = imginfo) |
|
|
312 |
img <- cropImage(img, zoom_info) |
|
|
313 |
g <- plotImage(img, max.pixel.size = max.pixel.size) + labs(title = "") |
|
|
314 |
if(!use.image.only){ |
|
|
315 |
g_spatial_clone <- cloneLayer(g_spatial) |
|
|
316 |
g <- g + transformSpatialLayer(g_spatial_clone, img, ranges, max.pixel.size) |
|
|
317 |
} |
|
|
318 |
|
|
|
319 |
# visualize currently selected corners #### |
|
|
320 |
transformed_corners <- transformSelectedCorners(selected_corners(), img, ranges, max.pixel.size) |
|
|
321 |
if(nrow(transformed_corners) > 0){ |
|
|
322 |
if(isolate(input$region_type == "Polygon")){ |
|
|
323 |
g <- g + |
|
|
324 |
ggplot2::geom_polygon(aes(x = x, y = y), data = transformed_corners, alpha = input$alpha, color = "red") |
|
|
325 |
} else { |
|
|
326 |
circle <- makeCircleData(transformed_corners) |
|
|
327 |
g <- g + |
|
|
328 |
ggforce::geom_ellipse(aes(x0 = as.numeric(x), y0 = as.numeric(y), a = as.numeric(rx), b = as.numeric(ry), angle = 0), |
|
|
329 |
data = circle, alpha = input$alpha, color = "red", fill = "red") |
|
|
330 |
} |
|
|
331 |
g <- g + |
|
|
332 |
ggplot2::geom_point(aes(x = x, y = y), data = transformed_corners, color = "red", shape = 16) |
|
|
333 |
} |
|
|
334 |
|
|
|
335 |
# visualize already selected regions #### |
|
|
336 |
transformed_corners_list <- lapply(selected_corners_list(), transformSelectedCorners, img, ranges, max.pixel.size) |
|
|
337 |
if(length(transformed_corners_list) > 0){ |
|
|
338 |
for (i in seq_len(length(transformed_corners_list))){ |
|
|
339 |
cur_corners <- transformed_corners_list[[i]] |
|
|
340 |
|
|
|
341 |
# visualize regions |
|
|
342 |
if(ncol(cur_corners) == 2){ |
|
|
343 |
g <- g + |
|
|
344 |
ggplot2::geom_polygon(aes(x = x, y = y, group = "region"), data = cur_corners, alpha = input$alpha, color = "red") |
|
|
345 |
} else { |
|
|
346 |
g <- g + |
|
|
347 |
ggforce::geom_ellipse(aes(x0 = as.numeric(x), y0 = as.numeric(y), a = as.numeric(rx), b = as.numeric(ry), angle = 0), |
|
|
348 |
data = cur_corners, alpha = input$alpha, color = "red", fill = "red") |
|
|
349 |
} |
|
|
350 |
|
|
|
351 |
# visualize labels |
|
|
352 |
if(ncol(cur_corners) == 2){ |
|
|
353 |
cur_corners <- data.frame(x = mean(cur_corners[,1]), y = max(cur_corners[,2]), region = paste("Region ", isolate(textboxes()[i]))) |
|
|
354 |
} else { |
|
|
355 |
cur_corners <- data.frame(x = cur_corners[,1], y = cur_corners[,2] + cur_corners[,3], region = paste("Region ", isolate(textboxes()[i]))) |
|
|
356 |
} |
|
|
357 |
g <- g + |
|
|
358 |
ggrepel::geom_label_repel(mapping = aes(x = x, y = y, label = region), data = cur_corners, |
|
|
359 |
size = input$label_size, direction = "y", nudge_y = 6, box.padding = 0, label.padding = input$label_size*0.1, seed = 1, color = "red") |
|
|
360 |
|
|
|
361 |
} |
|
|
362 |
} |
|
|
363 |
|
|
|
364 |
# return graph |
|
|
365 |
g |
|
|
366 |
}) |
|
|
367 |
|
|
|
368 |
# Return values for the shiny app #### |
|
|
369 |
observe({ |
|
|
370 |
if(length(selected_corners_list()) > 0){ |
|
|
371 |
shinyjs::show(id = "done") |
|
|
372 |
} else { |
|
|
373 |
shinyjs::hide(id = "done") |
|
|
374 |
} |
|
|
375 |
}) |
|
|
376 |
observeEvent(input$done, { |
|
|
377 |
|
|
|
378 |
# selected list |
|
|
379 |
selected_polygon_list <- selected_corners_list() |
|
|
380 |
|
|
|
381 |
# collect labels |
|
|
382 |
selected_label_list <- vapply(seq_len(length(selected_polygon_list)), function(i) input[[paste0("region",i)]], character(1)) |
|
|
383 |
|
|
|
384 |
if(length(selected_corners_list()) == 0){ |
|
|
385 |
showNotification("You have not annotated the data yet!") |
|
|
386 |
} else if(any(selected_label_list == "")) { |
|
|
387 |
showNotification("Some regions have blank annotations (empty labels!)") |
|
|
388 |
} else { |
|
|
389 |
|
|
|
390 |
### annotate spatial points #### |
|
|
391 |
if(inherits(metadata, "data.table")){ |
|
|
392 |
spatialpoints <- as.vector(metadata$id) |
|
|
393 |
} else { |
|
|
394 |
spatialpoints <- rownames(metadata) |
|
|
395 |
} |
|
|
396 |
|
|
|
397 |
new_label <- rep("undefined", length(spatialpoints)) |
|
|
398 |
names(new_label) <- spatialpoints |
|
|
399 |
result_list <- list() |
|
|
400 |
for(i in seq_len(length(selected_polygon_list))){ |
|
|
401 |
cur_poly <- selected_polygon_list[[i]] |
|
|
402 |
if(ncol(cur_poly) > 2){ |
|
|
403 |
in.list <- point.in.circle(coords[,1], coords[,2], cur_poly[,1], cur_poly[,2], cur_poly[,3]) |
|
|
404 |
} else { |
|
|
405 |
in.list <- sp::point.in.polygon(coords[,1], coords[,2], cur_poly[,1], cur_poly[,2]) |
|
|
406 |
} |
|
|
407 |
new_label[rownames(coords)[!!in.list]] <- selected_label_list[i] |
|
|
408 |
} |
|
|
409 |
|
|
|
410 |
# place annotation to metadata |
|
|
411 |
metadata[[label]] <- new_label |
|
|
412 |
Metadata(object, assay = sample_metadata[assay, "Assay"]) <- metadata |
|
|
413 |
|
|
|
414 |
# add polygons to a new assay #### |
|
|
415 |
segments <- list() |
|
|
416 |
for(i in seq_len(length(selected_label_list))){ |
|
|
417 |
segments[[selected_label_list[i]]] <- data.frame(id = i, selected_polygon_list[[i]]) |
|
|
418 |
} |
|
|
419 |
coords <- t(vapply(segments, function(seg){ |
|
|
420 |
apply(seg[,c("x", "y")], 2, mean) |
|
|
421 |
}, numeric(2))) |
|
|
422 |
new_assay <- formAssay(coords = coords, |
|
|
423 |
segments = segments, |
|
|
424 |
type = "ROI", |
|
|
425 |
image = vrImages(object, assay = assay), |
|
|
426 |
main_image = vrMainImage(object[[assay]]), |
|
|
427 |
name = assay) |
|
|
428 |
metadata <- data.frame(check.rows = FALSE, row.names = rownames(coords), selected_label_list) |
|
|
429 |
colnames(metadata) <- label |
|
|
430 |
object <- addAssayVoltRon(object, |
|
|
431 |
assay = new_assay, |
|
|
432 |
metadata = metadata, |
|
|
433 |
assay_name = annotation_assay, |
|
|
434 |
sample = sample_metadata[assay, "Sample"], |
|
|
435 |
layer = sample_metadata[assay, "Layer"]) |
|
|
436 |
|
|
|
437 |
# stop app and return |
|
|
438 |
stopApp(object) |
|
|
439 |
} |
|
|
440 |
}) |
|
|
441 |
} |
|
|
442 |
|
|
|
443 |
# Run App #### |
|
|
444 |
shiny.options <- configure_shiny_options(shiny.options) |
|
|
445 |
shiny::runApp( |
|
|
446 |
shiny::shinyApp(ui, server, options = list(host = shiny.options[["host"]], port = shiny.options[["port"]], launch.browser = shiny.options[["launch.browser"]]), |
|
|
447 |
onStart = function() { |
|
|
448 |
onStop(function() { |
|
|
449 |
}) |
|
|
450 |
}) |
|
|
451 |
) |
|
|
452 |
} |
|
|
453 |
|
|
|
454 |
#### |
|
|
455 |
# Annotation Utilities #### |
|
|
456 |
#### |
|
|
457 |
|
|
|
458 |
manageImageBrush <- function(image, ranges, max.pixel.size, input, output, session){ |
|
|
459 |
imginfo <- getImageInfo(image) |
|
|
460 |
observeEvent(input$plot_dblclick, { |
|
|
461 |
brush <- isolate(input$plot_brush) |
|
|
462 |
if (!is.null(brush)) { |
|
|
463 |
|
|
|
464 |
# get brush |
|
|
465 |
brush_mat <- data.frame(x = c(brush$xmin, brush$xmax), |
|
|
466 |
y = c(brush$ymin, brush$ymax)) |
|
|
467 |
|
|
|
468 |
# if width is large, then correct the brush event for the downsize (scaling) effect |
|
|
469 |
limits <- data.frame(x = ranges$x, y = ranges$y) |
|
|
470 |
width <- limits[2,1]-limits[1,1] |
|
|
471 |
height <- limits[2,2]-limits[1,2] |
|
|
472 |
if(max(height,width) > max.pixel.size){ |
|
|
473 |
if(inherits(image, "Image_Array")){ |
|
|
474 |
n.series <- ImageArray::len(image) |
|
|
475 |
cur_width <- width |
|
|
476 |
cur_height <- height |
|
|
477 |
for(ii in 2:n.series){ |
|
|
478 |
cur_width <- width/(2^(ii-1)) |
|
|
479 |
cur_height <- height/(2^(ii-1)) |
|
|
480 |
if(max(cur_height, cur_width) <= max.pixel.size){ |
|
|
481 |
break |
|
|
482 |
} |
|
|
483 |
} |
|
|
484 |
brush_mat <- brush_mat*width/ceiling(cur_width) |
|
|
485 |
} else { |
|
|
486 |
brush_mat <- brush_mat*width/max.pixel.size |
|
|
487 |
} |
|
|
488 |
} |
|
|
489 |
|
|
|
490 |
# correct brush for the zoom effect |
|
|
491 |
brush_mat[,1] <- brush_mat[,1] + limits[1,1] |
|
|
492 |
brush_mat[,2] <- brush_mat[,2] + limits[1,2] |
|
|
493 |
brush_mat[1,1] <- floor(brush_mat[1,1]) |
|
|
494 |
brush_mat[1,2] <- floor(brush_mat[1,2]) |
|
|
495 |
brush_mat[2,1] <- ceiling(brush_mat[2,1]) |
|
|
496 |
brush_mat[2,2] <- ceiling(brush_mat[2,2]) |
|
|
497 |
|
|
|
498 |
# update ranges |
|
|
499 |
ranges$x <- brush_mat[,1] |
|
|
500 |
ranges$y <- brush_mat[,2] |
|
|
501 |
|
|
|
502 |
} else { |
|
|
503 |
ranges$x <- c(0, imginfo$width) |
|
|
504 |
ranges$y <- c(0, imginfo$height) |
|
|
505 |
} |
|
|
506 |
}) |
|
|
507 |
} |
|
|
508 |
|
|
|
509 |
manageSelectedCorners <- function(selected_corners, image, ranges, max.pixel.size, input, output, session){ |
|
|
510 |
observeEvent(input$plot_click, { |
|
|
511 |
brush <- isolate(input$plot_brush) |
|
|
512 |
if(is.null(brush)) { |
|
|
513 |
|
|
|
514 |
# if a circle, dont allow more than two points |
|
|
515 |
if(isolate(input$region_type == "Circle")){ |
|
|
516 |
if(nrow(selected_corners()) == 2){ |
|
|
517 |
selected_corners(data.frame(x = numeric(0), y = numeric(0))) |
|
|
518 |
} |
|
|
519 |
} |
|
|
520 |
|
|
|
521 |
# get points |
|
|
522 |
click <- input$plot_click |
|
|
523 |
pts <- c(click$x, click$y) |
|
|
524 |
|
|
|
525 |
# correct for zoom effect |
|
|
526 |
limits <- data.frame(x = ranges$x, y = ranges$y) |
|
|
527 |
width <- limits[2,1]-limits[1,1] |
|
|
528 |
height <- limits[2,2]-limits[1,2] |
|
|
529 |
if(max(height,width) > max.pixel.size){ |
|
|
530 |
if(inherits(image, c("Image_Array"))){ |
|
|
531 |
n.series <- ImageArray::len(image) |
|
|
532 |
cur_width <- width |
|
|
533 |
cur_height <- height |
|
|
534 |
for(ii in 2:n.series){ |
|
|
535 |
cur_width <- width/(2^(ii-1)) |
|
|
536 |
cur_height <- height/(2^(ii-1)) |
|
|
537 |
if(max(cur_height, cur_width) <= max.pixel.size){ |
|
|
538 |
break |
|
|
539 |
} |
|
|
540 |
} |
|
|
541 |
pts <- pts*width/ceiling(cur_width) |
|
|
542 |
} else { |
|
|
543 |
pts <- pts*width/max.pixel.size |
|
|
544 |
} |
|
|
545 |
} |
|
|
546 |
|
|
|
547 |
# correct for offset effect |
|
|
548 |
pts[1] <- pts[1] + limits[1,1] |
|
|
549 |
pts[2] <- pts[2] + limits[1,2] |
|
|
550 |
|
|
|
551 |
# Append new point to the data frame |
|
|
552 |
new_point <- data.frame(x = pts[1], y = pts[2]) |
|
|
553 |
|
|
|
554 |
# add new point |
|
|
555 |
selected_corners(rbind(selected_corners(), new_point)) |
|
|
556 |
} |
|
|
557 |
}) |
|
|
558 |
} |
|
|
559 |
|
|
|
560 |
transformSelectedCorners <- function(selected_corners, image, ranges, max.pixel.size){ |
|
|
561 |
|
|
|
562 |
# if empty, return |
|
|
563 |
if(nrow(selected_corners) == 0){ |
|
|
564 |
return(selected_corners) |
|
|
565 |
} |
|
|
566 |
selected_corners <- as.matrix(selected_corners) |
|
|
567 |
|
|
|
568 |
# circle settings |
|
|
569 |
circle <- FALSE |
|
|
570 |
if(ncol(selected_corners) > 2) |
|
|
571 |
circle <- TRUE |
|
|
572 |
|
|
|
573 |
# get circle radius |
|
|
574 |
if(circle){ |
|
|
575 |
selected_radius <- selected_corners[,3:4, drop = FALSE] |
|
|
576 |
selected_corners <- selected_corners[,seq_len(2), drop = FALSE] |
|
|
577 |
} |
|
|
578 |
|
|
|
579 |
# get image info |
|
|
580 |
imginfo <- getImageInfo(image) |
|
|
581 |
|
|
|
582 |
# correct for offset effect |
|
|
583 |
limits <- cbind(ranges$x, ranges$y) |
|
|
584 |
selected_corners <- selected_corners - matrix(rep(limits[1,], nrow(selected_corners)), nrow = nrow(selected_corners), byrow = T) |
|
|
585 |
|
|
|
586 |
# correct for zoom effect |
|
|
587 |
width <- limits[2,1]-limits[1,1] |
|
|
588 |
height <- limits[2,2]-limits[1,2] |
|
|
589 |
if(max(height,width) > max.pixel.size){ |
|
|
590 |
if(inherits(image, "Image_Array")){ |
|
|
591 |
n.series <- ImageArray::len(image) |
|
|
592 |
cur_width <- width |
|
|
593 |
cur_height <- height |
|
|
594 |
for(ii in 2:n.series){ |
|
|
595 |
cur_width <- width/(2^(ii-1)) |
|
|
596 |
cur_height <- height/(2^(ii-1)) |
|
|
597 |
if(max(cur_height, cur_width) <= max.pixel.size){ |
|
|
598 |
break |
|
|
599 |
} |
|
|
600 |
} |
|
|
601 |
selected_corners <- selected_corners*ceiling(cur_width)/width |
|
|
602 |
if(circle){ |
|
|
603 |
selected_radius <- selected_radius*ceiling(cur_width)/width |
|
|
604 |
} |
|
|
605 |
} else { |
|
|
606 |
selected_corners <- selected_corners*max.pixel.size/width |
|
|
607 |
if(circle){ |
|
|
608 |
selected_radius <- selected_radius*max.pixel.size/width |
|
|
609 |
} |
|
|
610 |
} |
|
|
611 |
} |
|
|
612 |
|
|
|
613 |
# make data frame |
|
|
614 |
if(circle){ |
|
|
615 |
selected_corners <- as.data.frame(cbind(selected_corners,selected_radius)) |
|
|
616 |
colnames(selected_corners) <- c("x", "y", "rx", "ry") |
|
|
617 |
} else { |
|
|
618 |
selected_corners <- as.data.frame(selected_corners) |
|
|
619 |
colnames(selected_corners) <- c("x", "y") |
|
|
620 |
} |
|
|
621 |
|
|
|
622 |
# return |
|
|
623 |
selected_corners |
|
|
624 |
} |
|
|
625 |
|
|
|
626 |
#' @importFrom stats na.omit |
|
|
627 |
transformSpatialLayer <- function(g_spatial, image, ranges, max.pixel.size){ |
|
|
628 |
|
|
|
629 |
# correct for zoom |
|
|
630 |
ind <- (g_spatial$data$x > ranges$x[1] & g_spatial$data$x < ranges$x[2]) & (g_spatial$data$y > ranges$y[1] & g_spatial$data$y < ranges$y[2]) |
|
|
631 |
g_spatial$data <- g_spatial$data[ind,] |
|
|
632 |
|
|
|
633 |
# is.na |
|
|
634 |
g_spatial$data <- stats::na.omit(g_spatial$data) |
|
|
635 |
|
|
|
636 |
# correct for zoom effect |
|
|
637 |
limits <- data.frame(x = ranges$x, y = ranges$y) |
|
|
638 |
width <- limits[2,1]-limits[1,1] |
|
|
639 |
height <- limits[2,2]-limits[1,2] |
|
|
640 |
if(max(height,width) > max.pixel.size){ |
|
|
641 |
if(inherits(image, "Image_Array")){ |
|
|
642 |
n.series <- ImageArray::len(image) |
|
|
643 |
cur_width <- width |
|
|
644 |
cur_height <- height |
|
|
645 |
for(ii in 2:n.series){ |
|
|
646 |
cur_width <- width/(2^(ii-1)) |
|
|
647 |
cur_height <- height/(2^(ii-1)) |
|
|
648 |
if(max(cur_height, cur_width) <= max.pixel.size){ |
|
|
649 |
break |
|
|
650 |
} |
|
|
651 |
} |
|
|
652 |
g_spatial$data$x <- g_spatial$data$x*ceiling(cur_width)/width |
|
|
653 |
g_spatial$data$y <- g_spatial$data$y*ceiling(cur_width)/width |
|
|
654 |
} else { |
|
|
655 |
g_spatial$data$x <- g_spatial$data$x*max.pixel.size/width |
|
|
656 |
g_spatial$data$y <- g_spatial$data$y*max.pixel.size/width |
|
|
657 |
} |
|
|
658 |
} |
|
|
659 |
|
|
|
660 |
# correct for offset effect |
|
|
661 |
# g_spatial$data$x <- g_spatial$data$x - min(g_spatial$data$x) |
|
|
662 |
# g_spatial$data$y <- g_spatial$data$y - min(g_spatial$data$y) |
|
|
663 |
g_spatial$data$x <- g_spatial$data$x - limits[1,1] |
|
|
664 |
g_spatial$data$y <- g_spatial$data$y - limits[1,2] |
|
|
665 |
|
|
|
666 |
# return |
|
|
667 |
g_spatial |
|
|
668 |
} |
|
|
669 |
|
|
|
670 |
#### |
|
|
671 |
# Shiny Utilities #### |
|
|
672 |
#### |
|
|
673 |
|
|
|
674 |
#' internal Text input with button |
|
|
675 |
#' |
|
|
676 |
#' Reproduced since it is not exported in the Shiny namespace. |
|
|
677 |
#' |
|
|
678 |
#' @importFrom shiny tags validateCssUnit |
|
|
679 |
#' |
|
|
680 |
#' @keywords internal |
|
|
681 |
textInputwithButton <- function (textinputId, label, buttoninputId, value = "", width = NULL, placeholder = NULL, ...) |
|
|
682 |
{ |
|
|
683 |
textvalue <- restoreInput(id = textinputId, default = value) |
|
|
684 |
buttonvalue <- restoreInput(id = buttoninputId, default = NULL) |
|
|
685 |
div(class = "form-group shiny-input-container", |
|
|
686 |
style = css(width = shiny::validateCssUnit(width), display = "inline-block"), |
|
|
687 |
shinyInputLabel(textinputId, label), |
|
|
688 |
shiny::tags$input(id = textinputId, |
|
|
689 |
style = css(width = "80%", float = "left"), |
|
|
690 |
type = "text", class = "shiny-input-text form-control", |
|
|
691 |
value = textvalue, placeholder = placeholder), |
|
|
692 |
shiny::tags$button(id = buttoninputId, |
|
|
693 |
style = css(width = "20%", float = "left"), |
|
|
694 |
type = "button", class = "btn btn-default action-button", |
|
|
695 |
`data-val` = buttonvalue, disabled = NULL, list(shiny::icon("trash")), ...) |
|
|
696 |
) |
|
|
697 |
} |
|
|
698 |
|
|
|
699 |
#' Shiny's internal \code{shinyInputLabel} function |
|
|
700 |
#' |
|
|
701 |
#' Reproduced since it is not exported in the Shiny namespace. |
|
|
702 |
#' |
|
|
703 |
#' @importFrom shiny tags |
|
|
704 |
#' |
|
|
705 |
#' @keywords internal |
|
|
706 |
shinyInputLabel <- function(inputId, label=NULL) { |
|
|
707 |
shiny::tags$label(label, |
|
|
708 |
class = "control-label", |
|
|
709 |
class = if (is.null(label)) "shiny-label-null", |
|
|
710 |
`for` = inputId |
|
|
711 |
) |
|
|
712 |
} |
|
|
713 |
|
|
|
714 |
#### |
|
|
715 |
# Auxiliary #### |
|
|
716 |
#### |
|
|
717 |
|
|
|
718 |
#' makeCircleData |
|
|
719 |
#' |
|
|
720 |
#' creates circle ROI from selected corners |
|
|
721 |
#' |
|
|
722 |
#' @keywords internal |
|
|
723 |
makeCircleData <- function(selected_corners) { |
|
|
724 |
radius <- sqrt((selected_corners[1,1] - selected_corners[2,1])^2 + (selected_corners[1,2] - selected_corners[2,2])^2) |
|
|
725 |
data.frame(x = selected_corners[1,1], y = selected_corners[1,2], rx = radius, ry = radius) |
|
|
726 |
} |
|
|
727 |
|
|
|
728 |
#' point.in.circle |
|
|
729 |
#' |
|
|
730 |
#' checks if points are in the defined circle ROI with a (x,y) center and radius |
|
|
731 |
#' |
|
|
732 |
#' @keywords internal |
|
|
733 |
point.in.circle <- function(x,y,circle.x, circle.y, radius) { |
|
|
734 |
x <- x - circle.x |
|
|
735 |
y <- y - circle.y |
|
|
736 |
distall <- sqrt((x - circle.x)^2 + (y - circle.y)^2) |
|
|
737 |
return(distall < radius) |
|
|
738 |
} |