Skip to content

Instantly share code, notes, and snippets.

@m-albert
Last active August 2, 2024 11:39
Show Gist options
  • Save m-albert/f2abd79d258d1e1f311b220dee4e25ac to your computer and use it in GitHub Desktop.
Save m-albert/f2abd79d258d1e1f311b220dee4e25ac to your computer and use it in GitHub Desktop.
<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