Last active
August 2, 2024 11:39
-
-
Save m-albert/f2abd79d258d1e1f311b220dee4e25ac to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<docs lang="markdown"> | |
[TODO: write documentation for this plugin.] | |
</docs> | |
<config lang="json"> | |
{ | |
"name": "Untitled Plugin", | |
"type": "window", | |
"tags": [], | |
"ui": "", | |
"version": "0.1.0", | |
"cover": "", | |
"description": "[TODO: describe this plugin with one sentence.]", | |
"icon": "extension", | |
"inputs": null, | |
"outputs": null, | |
"api_version": "0.1.8", | |
"env": "", | |
"permissions": [], | |
"requirements": ["https://cdn.jsdelivr.net/npm/imjoy-rpc@0.5.6/dist/hypha-rpc-websocket.min.js"], | |
"dependencies": [], | |
"defaults": {"w": 20, "h": 10} | |
} | |
</config> | |
<script lang="javascript"> | |
class BioImageIOColabAnnotator { | |
async setup() { | |
api.log('initialized') | |
} | |
/* | |
Shape Layer Interface: | |
name: String, the name of the layer | |
id: String, the id of the layer | |
update_config: Function, update the config layer config, it takes one argument: | |
config: the new config, it can contain one or more options described in Arguments. For example, it can be used to update the markup tool setting. | |
clear_features: Function, a function that can be called for clear all the features in the layer, it takes no arguments | |
update_feature: Function, a function for updating the feature, it takes two arguments: | |
id: String, the id of an existing feature to be updated | |
new_feature: Object, the new feature object with geometry and properties | |
set_features: Function, replace the features in the layer with an array of new features, it takes one argument: | |
features: Array, an array of new features | |
select_feature: Function, select a feature, it takes one argument: | |
id: String, the id of an existing feature to be selected | |
select_features: Function, select an array of features, it takes one argument: | |
ids: Array, an array of features ids | |
add_feature: Function, add a new feature, it takes one argument: | |
new_feature: Object, the new feature object | |
add_features: Function, add an array of new features, it takes one argument: | |
new_features: Array, an array of features | |
remove_feature: Function, remove a feature, it takes one argument: | |
id: String, the id of an existing feature to be removed | |
remove_features: Function, remove an array of features, it takes one argument: | |
ids: Array, an array of features ids | |
get_features: Function, get all the features of the layer, it takes no argument | |
*/ | |
constructor() { | |
this.image = null; // Current image | |
this.mask = null; // Current mask | |
this.filename = null; // Filename of the current image | |
this.imageLayer = null; // Layer displaying the image | |
this.annotationLayer_pos = null; // Layer displaying the annotations | |
this.annotationLayer_neg = null; // Layer displaying the annotations | |
this.category = null; // Category of the current image | |
this.pos_features = null; // Features for the positive class | |
this.neg_features = null; // Features for the negative class | |
this.image_basename = null; // Base name of the current image | |
this.edgeColor_pos = "green"; // Default edge color for annotations | |
this.edgeColor_neg = "red"; // Default edge color for annotations | |
this.edge_width = 10; // Default edge width for annotations | |
this.selected_feature = null; // Selected feature | |
this.selected_feature_class = null; // Class of the selected feature | |
this.selected_feature_id = null; // ID of the selected feature | |
} | |
async run(ctx) { | |
// Extract configuration settings | |
const config = ctx.config || {}; | |
const serverUrl = config.server_url || "https://ai.imjoy.io"; | |
const annotationServiceId = config.annotation_service_id || "correction-tool"; // default for testing plugin | |
await api.showMessage(`Connecting to server ${annotationServiceId}....`); | |
// Create and display the viewer window | |
const viewer = await api.showDialog({src: "https://kaibu.org/#/app", fullscreen: true}); | |
await viewer.set_mode("lite"); | |
//await api.showMessage(`Connecting to server ${serverUrl}....`); | |
// Login before connecting and then use userid instead of new client_id | |
// TODO: Add login functionality | |
// // Connect to the Hypha server | |
// const server = await hyphaWebsocketClient.connectToServer({ | |
// server_url: serverUrl, | |
// token:"_token_", | |
// workspace:"_workspace_", | |
// }); | |
// Connect to the Hypha server | |
const server = await hyphaWebsocketClient.connectToServer({ | |
server_url: serverUrl, | |
token: config.token, | |
workspace: config.workspace, | |
}); | |
// Get the bioimageio-colab service from the server | |
let biocolab; | |
try { | |
biocolab = await server.getService(annotationServiceId); | |
} catch (e) { | |
await api.alert(`Failed to get the bioimageio-colab annotation service (id=${annotationServiceId}). (Error: ${e})`); | |
return; | |
} | |
await viewer.set_sliders([ | |
{ | |
_rintf: true, | |
name: "Classification", | |
min: 0, | |
max: 1, | |
step: 1, | |
value: 1, | |
change_callback: (value) => { | |
// return if no image is loaded | |
if (!this.image) return; | |
this.category = value === 1 ? "Good" : "Bad"; | |
console.log("z slider changed., category: ", this.category); | |
api.showMessage("New classification: " + this.category); | |
} | |
}, | |
]); | |
// Function to get a new image and set up the viewer | |
// takes a basename as input, by default it will get a random image | |
const getData = async (basename=null) => { | |
if (this.image !== null) { | |
// Remove existing layers if there is any image loaded | |
await viewer.remove_layer({id: this.imageLayer.id}); | |
await viewer.remove_layer({id: this.annotationLayer_pos.id}); | |
// remove classificaiton widget | |
// await viewer.remove_widget({name: "Classification"}); | |
// await viewer.remove_layer({id: this.annotationLayer_neg.id}); | |
} | |
// [this.image, this.filename, this.newname] = await biocolab.get_random_image(); | |
[this.image, this.pos_features, this.neg_features, this.image_basename, this.loaded_saved, this.category] = await biocolab.get_data_by_basename(basename); | |
this.imageLayer = await viewer.view_image(this.image, {name: "Micrograph"}); | |
// Add the segmented features as polygons to the annotation layer | |
this.annotationLayer_pos = await viewer.add_shapes(this.pos_features, { | |
shape_type: "path", | |
edge_color: this.edgeColor_pos, | |
draw_edge_color: this.edgeColor_pos, | |
edge_width: this.edge_width, | |
draw_edge_width: this.edge_width, | |
name: "Keep", | |
_rintf: true, | |
select_enabled: true, | |
draw_shape_type: "LineString", | |
draw_freehand: false, | |
select_feature_callback: (feature) => { | |
this.selected_feature = feature; | |
}, | |
}); | |
this.annotationLayer_neg = await viewer.add_shapes(this.neg_features, { | |
shape_type: "path", | |
edge_color: this.edgeColor_neg, | |
draw_edge_color: this.edgeColor_neg, | |
edge_width: this.edge_width, | |
draw_edge_width: this.edge_width, | |
name: "Discard", | |
_rintf: true, | |
select_enabled: true, | |
draw_shape_type: "LineString", | |
draw_freehand: false, | |
select_feature_callback: (feature) => { | |
this.selected_feature = feature; | |
}, | |
}); | |
await viewer.update_slider("Classification", this.category === "Good" ? 1 : 0) | |
}; | |
// Function to save the annotation | |
const saveCorrection = async () => { | |
if (!this.annotationLayer_pos) return; | |
const annotation_pos = await this.annotationLayer_pos.get_features(); | |
const annotation_neg = await this.annotationLayer_neg.get_features(); | |
// save annotation even if it is empty | |
// alert about current category | |
console.log("saving, category: ", this.category); | |
// api.alert("Saving correction for " + this.image_basename + "!" + " Category: " + this.category); | |
await biocolab.save_correction(annotation_pos, annotation_neg, this.image_basename, [this.image._rshape[0], this.image._rshape[1]], this.category); | |
await api.showMessage("Saved correction for " + this.image_basename + "!" + " Category: " + this.category); | |
}; | |
const node_dbclick_callback = async (node) => { | |
// await api.alert("selected node:" + JSON.stringify(node)) | |
// if not is not a leaf node (isLead) property, alert user to expand the folder and select a file | |
if (!node.isLeaf) { | |
await api.alert("Please expand the folder and select a file"); | |
return; | |
} | |
// save the current annotation and load the selected image | |
// await api.alert("selected node:" + node.title) | |
await saveCorrection(); | |
await viewer.clear_layers(); | |
await getData(node.data.image_basename); | |
} | |
const tree = await viewer.add_widget( | |
{ | |
"_rintf": true, | |
"type": "tree", | |
"name": "Sample selection", | |
"node_dbclick_callback": node_dbclick_callback, | |
"nodes": await biocolab.get_widget_node_list_of_basenames(), | |
} | |
) | |
const swap_category = async () => { | |
if (!this.selected_feature) { | |
await api.alert("No feature selected"); | |
return; | |
} | |
const features_pos = await this.annotationLayer_pos.get_features(); | |
// check if feature id is in the positive class | |
const pos_is_source = features_pos.features.find(f => f.id === this.selected_feature.id); | |
if (pos_is_source) { | |
await this.annotationLayer_pos.remove_feature(this.selected_feature.id); | |
this.selected_feature.properties.edge_color = this.edgeColor_neg; | |
await this.annotationLayer_neg.add_feature(this.selected_feature); | |
} else { | |
await this.annotationLayer_neg.remove_feature(this.selected_feature.id); | |
this.selected_feature.properties.edge_color = this.edgeColor_pos; | |
await this.annotationLayer_pos.add_feature(this.selected_feature); | |
} | |
this.selected_feature = null; | |
}; | |
// Add a control widget with a button to load the next image | |
await viewer.add_widget({ | |
_rintf: true, | |
name: "Correcting", | |
type: "control", | |
elements: [ | |
{ | |
type: "button", | |
label: "Swap category", | |
callback: swap_category, | |
}, | |
], | |
}); | |
// // Add a control widget with a button to load the next image | |
// await viewer.add_widget({ | |
// _rintf: true, | |
// name: "Debugging", | |
// type: "control", | |
// elements: [ | |
// { | |
// type: "button", | |
// label: "show category", | |
// callback: async () => { | |
// console.log("category: ", this.category); | |
// }, | |
// }, | |
// ], | |
// }); | |
const remove_feature = async () => { | |
if (!this.selected_feature) { | |
await api.alert("No feature selected"); | |
return; | |
} | |
const features_pos = await this.annotationLayer_pos.get_features(); | |
const features_neg = await this.annotationLayer_neg.get_features(); | |
// check if feature id is in the positive class | |
const pos_is_source = features_pos.features.find(f => f.id === this.selected_feature.id); | |
const neg_is_source = features_neg.features.find(f => f.id === this.selected_feature.id); | |
if (pos_is_source) { | |
await this.annotationLayer_pos.remove_feature(this.selected_feature.id); | |
} else if (neg_is_source) { | |
await this.annotationLayer_neg.remove_feature(this.selected_feature.id); | |
} | |
this.selected_feature = null; | |
}; | |
// Add a control widget for removing lines (the kaibu interface seems to | |
// have a bug with the remove button that shows sometimes) | |
await viewer.add_widget({ | |
_rintf: true, | |
name: "Removing", | |
type: "control", | |
elements: [ | |
// { | |
// type: "button", | |
// label: "Remove all", | |
// callback: async () => { | |
// await this.annotationLayer_pos.clear_features(); | |
// await this.annotationLayer_neg.clear_features(); | |
// }, | |
// }, | |
{ | |
type: "button", | |
label: "Remove line", | |
callback: remove_feature, | |
}, | |
], | |
}); | |
// Load the initial image | |
// await getData(); | |
await api.showMessage("Ready to annotate!"); | |
} | |
} | |
api.export(new BioImageIOColabAnnotator()) | |
</script> | |
<window lang="html"> | |
</window> | |
<style lang="css"> | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment