Skip to content
This repository was archived by the owner on Apr 18, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
8e09f1b
Rename floodfill tools to magic wand in preparation of work.
Dec 12, 2022
981a978
Implements image segmentation Magic Wand.
Dec 12, 2022
c0cf520
Collapse multiple Magic Wand additions into the same class if appropr…
Dec 13, 2022
081ed47
Remove console statement.
Dec 13, 2022
78ec788
Stack classes from successive Magic Wand thresholds when we are able to.
Dec 13, 2022
e75c3b8
Be able to collapse wand regions even if panning has happened.
Dec 13, 2022
e33c243
Remove spurious comments.
Dec 14, 2022
4b5092f
Remove spurious console statement.
Dec 15, 2022
51d1eba
Fix doc issue.
Dec 15, 2022
a4de8eb
Fix code line that got hidden by comment somehow.
Dec 15, 2022
685c592
Double to single quotes to match new code style.
Dec 15, 2022
6616a26
Merge branch 'master' into magic-wand-clean-history
farioas Dec 20, 2022
910ccf1
Merge branch 'master' into magic-wand-clean-history
farioas Dec 20, 2022
572cd9a
Change protocol and storage bucket for Magic Wand example.
Jan 5, 2023
84eb812
Whitespace fix.
Jan 9, 2023
c16fe21
Capture undo/redoes and invalidate offscreen cache.
Jan 9, 2023
fab81de
Put Magic Wand behind a feature flag (fflag_feat_front_dev_4081_magic…
Jan 11, 2023
e154504
Fix path for feature flag file.
Jan 12, 2023
140ce34
Make the crossOrigin attribute more granular and flexible for differe…
Jan 12, 2023
5eee454
Remove listing the magic wand feature flag here.
Jan 12, 2023
f866237
Magic Wand will be on in front end by default, but the server config …
Jan 23, 2023
769dccb
Whitespace fix.
Jan 23, 2023
1ff7f52
e2e integration tests for the Magic Wand.
Jan 23, 2023
f904c85
Merge master.
Jan 24, 2023
680d4e1
Lint fix -- remove unused import.
Jan 24, 2023
c10becf
Doc fix.
Jan 26, 2023
70d745e
Merge branch 'master' into magic-wand-clean-history
Jan 26, 2023
e385e4c
Bring Brush tag in when Magicwand tag used, to deal with Eraser tool …
Jan 26, 2023
28e3753
Make sure the eraser shows up when just magicwand tag present.
Jan 27, 2023
7e31167
Make sure magicwand drafts are properly saved for autosave.
Jan 30, 2023
f64a24b
Enable eraser integration tests now that this bug is fixed.
Jan 30, 2023
862ae7b
Put duplicate tools panel logic behind magicwand feature flag as well.
Jan 30, 2023
a8d0e95
Merge master.
Jan 30, 2023
67cfc1d
Fix incorrect import path.
Jan 30, 2023
c5f48a2
Make Magic Wand feature flag more dynamic, and tie into it for e2e te…
Jan 30, 2023
5e75cff
Code review feedback.
Jan 30, 2023
00dfdce
Follow standard `node scripts/create-docs.js` flow.
Feb 2, 2023
1447b48
Fix docs so they are autogenerated correctly.
Feb 3, 2023
cdc8f24
Make code review feedback changes.
Feb 3, 2023
7e76ad7
Have the Brush be shown in the Magic Wand example as well.
Feb 3, 2023
7b66923
Lint and compilation fixes.
Feb 3, 2023
63296df
Merge master.
Feb 3, 2023
f243f2f
Make Magic Wand thresholding more robust with better UX.
Feb 7, 2023
e2696ff
Merge branch 'master' into magic-wand-clean-history
Feb 7, 2023
34511e0
Ensure currently selected region still works when changing between so…
Feb 8, 2023
b96f6e6
Removed unused imports.
Feb 9, 2023
52b5756
Change path to Heartex AWS buckets.
Feb 9, 2023
3f2c14c
Make changes based on code review feedback.
Feb 9, 2023
7d1383e
Add feature flags around unselect behavior.
Feb 9, 2023
126a478
Merge branch 'master' into magic-wand-clean-history
hlomzik Feb 10, 2023
8ecde03
nikitabelonogov Feb 10, 2023
eac3302
Fix region unselection for Selection, Erase & others
hlomzik Feb 10, 2023
3096976
Temporarily disable gallery in e2e because of CORS
hlomzik Feb 13, 2023
399a843
Merge branch 'master' into magic-wand-clean-history
hlomzik Feb 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/fragments/AtImageView.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ module.exports = {
},

/**
* Get pixel color at point
* Get pixel color at point
* @param {number} x
* @param {number} y
* @param {number[]} rgbArray
Expand Down
9 changes: 9 additions & 0 deletions e2e/tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,14 @@ const areEqualRGB = (a, b, tolerance) => {
return true;
};

const setKonvaLayersOpacity = ([opacity]) => {
const stage = window.Konva.stages[0];

for (const layer of stage.getLayers()) {
layer.canvas._canvas.style.opacity = opacity;
}
};

const getKonvaPixelColorFromPoint = ([x, y]) => {
const stage = window.Konva.stages[0];
const colors = [];
Expand Down Expand Up @@ -731,6 +739,7 @@ module.exports = {
getCanvasSize,
getImageSize,
getImageFrameSize,
setKonvaLayersOpacity,
setZoom,
whereIsPixel,
countKonvaShapes,
Expand Down
199 changes: 199 additions & 0 deletions e2e/tests/image.magic-wand.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/* global Feature, Scenario */

const {
initLabelStudio,
hasKonvaPixelColorAtPoint,
setKonvaLayersOpacity,
serialize,
waitForImage,
} = require('./helpers');
const assert = require('assert');

Feature('Test Image Magic Wand');

const CLOUDSHADOW = {
color: '#1EAE3B',
rgbArray: [30, 174, 59],
};
const HAZE = {
color: '#68eee5',
rgbArray: [104, 238, 229],
};
const CLOUD = {
color: '#1b32de',
rgbArray: [27, 50, 222],
};

const config = `
<View>
<Labels name="labels_1" toName="image_1">
<Label hotkey="1" value="Snow" background="#F61E33" />
<Label hotkey="2" value="Cloud Shadow" background="${CLOUDSHADOW.color}" />
<Label hotkey="3" value="Haze" background="${HAZE.color}" />
<Label hotkey="4" value="Cloud" background="${CLOUD.color}" />
<Label hotkey="5" value="Exclude" background="#2C2C2B" />
</Labels>
<MagicWand name="magicwand_1" toName="image_1" />
<View style="width: 100%; margin-bottom: 20%; margin-side: 10%;">
<Image name="image_1" value="$image" zoomControl="true" zoom="true" crossOrigin="anonymous" />
</View>
</View>`;

const annotationEmpty = {
id: '1000',
result: [],
};

// TODO: Change these URLs to heartex URLs, and ensure the heartex bucket allows CORS access
// for these to work.
const data = {
'image': [
'http://htx-pub.s3.amazonaws.com/samples/magicwand/magic_wand_scale_1_20200902_015806_26_2235_1B_AnalyticMS_00750_00750.jpg',
'http://htx-pub.s3.amazonaws.com/samples/magicwand/magic_wand_scale_2_20200902_015806_26_2235_1B_AnalyticMS_00750_00750.jpg',
'http://htx-pub.s3.amazonaws.com/samples/magicwand/magic_wand_scale_3_20200902_015806_26_2235_1B_AnalyticMS_00750_00750.jpg',
'http://htx-pub.s3.amazonaws.com/samples/magicwand/magic_wand_false_color_20200902_015806_26_2235_1B_AnalyticMS_00750_00750.jpg',
],
'thumb': 'http://htx-pub.s3.amazonaws.com/samples/magicwand/magic_wand_thumbnail_20200902_015806_26_2235_1B_AnalyticMS_00750_00750.jpg',
};

async function magicWand(I, { msg, fromX, fromY, toX, toY }) {
I.usePlaywrightTo(msg, async ({ page }) => {
await page.mouse.move(fromX, fromY);
await page.mouse.down();
await page.mouse.move(toX, toY);
await page.mouse.up();
});
I.wait(1); // Ensure that the magic wand brush region is fully finished being created.
}

async function assertMagicWandPixel(I, x, y, assertValue, rgbArray, msg) {
const hasPixel = await I.executeScript(hasKonvaPixelColorAtPoint, [x, y, rgbArray, 1]);

assert.equal(hasPixel, assertValue, msg);
}

Scenario('Make sure the magic wand works in a variety of scenarios', async function({ I, LabelStudio, AtImageView, AtSidebar }) {
const params = {
config,
data,
annotations: [annotationEmpty],
};

I.amOnPage('/');

LabelStudio.setFeatureFlags({
'fflag_feat_front_dev_4081_magic_wand_tool': true,
});

I.executeScript(initLabelStudio, params);

AtImageView.waitForImage();
await AtImageView.lookForStage();
I.executeScript(waitForImage);

I.say('Making sure magic wand button is present');
I.seeElement('.lsf-toolbar__group button[aria-label="magicwand"]');

I.say('Making sure Eraser button is present');
I.seeElement('.lsf-toolbar__group button[aria-label="eraser"]');

I.say('Select magic wand & cloud class');
I.pressKey('W');
I.pressKey('4');

AtSidebar.seeRegions(0);

I.say('Magic wanding clouds with cloud class in upper left of image');
await magicWand(I, { msg: 'Fill in clouds upper left', fromX: 258, fromY: 214, toX: 650, toY: 650 });
await magicWand(I, { msg: 'Fill in clouds lower left', fromX: 337, fromY: 777, toX: 650, toY: 650 });

I.say('Ensuring repeated magic wands back to back with same class collapsed into single region');
AtSidebar.seeRegions(1);
AtSidebar.see('Cloud');

// Force all the magic wand regions to be a consistent color with no opacity to make testing
// magic wand pixel colors more robust.
I.say('Ensuring cloud magic wand pixels are correctly filled color');
await I.executeScript(setKonvaLayersOpacity, [1.0]);
assertMagicWandPixel(I, 0, 0, false, CLOUD.rgbArray,
'Far upper left corner should not have magic wand cloud class');
assertMagicWandPixel(I, 260, 50, true, CLOUD.rgbArray,
'Upper left should have magic wand cloud class');
assertMagicWandPixel(I, 300, 620, true, CLOUD.rgbArray,
'Lower left should have magic wand cloud class');
assertMagicWandPixel(I, 675, 650, false, CLOUD.rgbArray,
'Far lower right corner should not have magic wand cloud class');

// Make sure the region made from this is correct.
I.say('Ensuring magic wand brushregion was created correctly');
const result = await I.executeScript(serialize);
const entry = result[1];

assert.equal(entry['from_name'], 'labels_1');
assert.equal(entry['type'], 'labels');
const labels = entry['value']['labels'];

assert.equal(labels.length, 1);
assert.equal(labels[0], 'Cloud');
assert.equal(entry['value']['rle'].length > 0, true);

// Undo the bottom left area we just added, make sure its gone but our region list is still
// 1, then redo it and ensure its back and our region list is still 1 again.
I.say('Undoing last cloud magic wand and ensuring it worked correctly');
I.click('button[aria-label="Undo"]');
assertMagicWandPixel(I, 300, 620, false, CLOUD.rgbArray,
'Undone lower left should not have magic wand cloud class anymore');
assertMagicWandPixel(I, 260, 50, true, CLOUD.rgbArray,
'Upper left should still have magic wand cloud class');
AtSidebar.seeRegions(1);

I.say('Redoing last cloud magic wand and ensuring it worked correctly');
I.click('button[aria-label="Redo"]');
assertMagicWandPixel(I, 300, 620, true, CLOUD.rgbArray,
'Redone lower left should have magic wand cloud class again');
assertMagicWandPixel(I, 260, 50, true, CLOUD.rgbArray,
'Upper left should still have magic wand cloud class');
AtSidebar.seeRegions(1);

I.say('Unselecting last magic wand region');
I.pressKey('Escape');

// @todo currently gallery doesn't work well with CORS, so this is not covered by test
// Change to the false color view, zoom in, and magic wand with a new class.
// I.say('Changing to false-color view');
// I.click('[class*="gallery--"] img[src*="false_color"]');

I.say('Zooming in');
I.click('button[aria-label="zoom-in"]');
I.click('button[aria-label="zoom-in"]');
I.click('button[aria-label="zoom-in"]');
I.click('button[aria-label="zoom-in"]');
I.click('button[aria-label="zoom-in"]');

I.say('Selecting cloud shadow class');
I.pressKey('2');

I.say('Magic wanding cloud shadows with cloud shadow class in center of zoomed image');
await magicWand(I, { msg: 'Cloud shadow in middle of image', fromX: 390, fromY: 500, toX: 500, toY: 500 });

I.say('Ensuring new cloud shadow magic wand region gets added to sidebar');
AtSidebar.seeRegions(2);
AtSidebar.see('Cloud Shadow');

I.say('Ensuring cloud shadow magic wand pixels are correctly filled color');
await I.executeScript(setKonvaLayersOpacity, [1.0]);
assertMagicWandPixel(I, 0, 0, false, CLOUDSHADOW.rgbArray,
'Zoomed upper left corner should not have cloud shadow');
assertMagicWandPixel(I, 350, 360, true, CLOUDSHADOW.rgbArray,
'Center area should have magic wand cloud shadow class');

// Make sure if you have a region selected then change the class the region class changes.
I.say('Changing class of existing selected region to Haze should change it to new class');
I.pressKey('3');
AtSidebar.seeRegions(2);
AtSidebar.dontSee('Cloud Shadow');
AtSidebar.see('Haze');
await I.executeScript(setKonvaLayersOpacity, [1.0]);
assertMagicWandPixel(I, 350, 360, true, HAZE.rgbArray,
'Center area should have magic wand haze class');
});
44 changes: 44 additions & 0 deletions examples/image_magic_wand/START.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

# Magic Wand for Image Segmentation

![Magic Wand](/images/screenshots/image_magic_wand.png "Magic Wand")

# Install

## Linux & Ubuntu guide

Install python and virtualenv

```bash
# install python and virtualenv
apt install python3.6
pip3 install virtualenv

# setup python virtual environment
virtualenv -p python3 env3
source env3/bin/activate

# install requirements
cd backend
pip install -r requirements.txt
```

## Cross Domain Image Access

Note that if you are storing images that you'd like to apply the Magic Wand to cross-domain, such as on Google Storage Buckets, you will have to [enable CORS headers for the storage buckets to enable cross-domain pixel access](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image) so that the Magic Wand can get the raw pixel data to threshold. By default browsers block JavaScript from accessing pixel-level image data unless the right CORS headers are set.

As an example, if you wanted to configure a Google Storage Bucket with the right headers, you might do the following:

```bash
gsutil cors set gcp_cors_config.json gs://BUCKET-NAME
```

Note that in the gcp_cors_config.json example given in this directory that we have set `origin` to `*`, which means all origins can access that data, as well as set `responseHeader` to `*`, which means all HTTP response headers can be accessed. In a real scenario you probably want to think through the security ramifications of this for your own particular Label Studio setup.

# Start

Magic Wand for image segmentation:

```bash
fflag_feat_front_dev_4081_magic_wand_tool=1 python server.py -c config.json -l ../examples/image_magic_wand/config.xml -i ../examples/image_magic_wand/tasks.json -o output
```
Loading