Feature("Bitmask tool").tag("@regress"); const assert = require("assert"); const IMAGE = "https://data.heartex.net/open-images/train_0/mini/0030019819f25b28.jpg"; const config = ` `; // Add cleanup hook Before(async ({ I }) => { I.amOnPage("/"); }); Scenario("Basic Bitmask drawing", async ({ I, LabelStudio, AtImageView, AtLabels }) => { const params = { config, data: { image: IMAGE }, annotations: [{ id: 1, result: [] }], }; I.amOnPage("/"); LabelStudio.init(params); LabelStudio.waitForObjectsReady(); await AtImageView.lookForStage(); I.say("Select Bitmask tool"); I.pressKey("B"); AtLabels.clickLabel("Test"); I.say("Draw a simple mask"); AtImageView.drawThroughPoints([ [20, 20], [20, 40], [40, 40], [40, 20], [20, 20], ]); I.say("Check if mask was created"); const result = await LabelStudio.serialize(); assert.strictEqual(result.length, 1); assert.ok(result[0].value.imageDataURL); }); Scenario("Bitmask eraser functionality", async ({ I, LabelStudio, AtImageView, AtLabels }) => { const params = { config, data: { image: IMAGE }, annotations: [{ id: 1, result: [] }], }; I.amOnPage("/"); LabelStudio.init(params); LabelStudio.waitForObjectsReady(); await AtImageView.lookForStage(); I.say("Select Bitmask tool and draw initial mask"); I.pressKey("B"); AtLabels.clickLabel("Test"); AtImageView.drawThroughPoints([ [20, 20], [20, 40], [40, 40], [40, 20], [20, 20], ]); I.say("Switch to eraser and erase part of the mask"); I.pressKey("E"); AtImageView.drawThroughPoints([ [25, 25], [35, 35], ]); I.say("Check if mask was modified"); const result = await LabelStudio.serialize(); assert.strictEqual(result.length, 1); assert.ok(result[0].value.imageDataURL); }); Scenario("Bitmask size controls", async ({ I, LabelStudio, AtImageView, AtLabels }) => { const params = { config, data: { image: IMAGE }, annotations: [{ id: 1, result: [] }], }; I.amOnPage("/"); LabelStudio.init(params); LabelStudio.waitForObjectsReady(); await AtImageView.lookForStage(); I.say("Select Bitmask tool"); I.pressKey("B"); AtLabels.clickLabel("Test"); I.say("Test size increase shortcut"); I.pressKey("]"); AtImageView.drawThroughPoints([[30, 30]]); I.say("Test size decrease shortcut"); I.pressKey("["); AtImageView.drawThroughPoints([[50, 50]]); I.say("Check if masks were created with different sizes"); const result = await LabelStudio.serialize(); assert.strictEqual(result.length, 1); assert.ok(result[0].value.imageDataURL); }); Scenario("Bitmask hover and selection", async ({ I, LabelStudio, AtImageView, AtLabels, AtOutliner }) => { const params = { config, data: { image: IMAGE }, annotations: [{ id: 1, result: [] }], }; I.amOnPage("/"); LabelStudio.init(params); LabelStudio.waitForObjectsReady(); await AtImageView.lookForStage(); I.say("Create initial mask"); I.pressKey("B"); AtLabels.clickLabel("Test"); AtImageView.drawThroughPoints([ [20, 20], [20, 40], [40, 40], [40, 20], [20, 20], ]); I.say("Verify selection behavior"); AtOutliner.seeRegions(1); I.say("Click on the region"); AtImageView.clickAt(30, 30); AtOutliner.seeSelectedRegion(); }); Scenario("Verify Bitmask drawing content", async ({ I, LabelStudio, AtImageView, AtLabels }) => { const params = { config, data: { image: IMAGE }, annotations: [{ id: 1, result: [] }], }; I.amOnPage("/"); LabelStudio.init(params); LabelStudio.waitForObjectsReady(); await AtImageView.lookForStage(); I.say("Select Bitmask tool"); I.pressKey("B"); AtLabels.clickLabel("Test"); I.say("Draw a rectangle mask"); AtImageView.drawThroughPoints([ [20, 20], [20, 40], [40, 40], [40, 20], [20, 20], ]); I.say("Verify mask content"); const result = await LabelStudio.serialize(); assert.strictEqual(result.length, 1); assert.ok(result[0].value.imageDataURL); // Verify that the imageDataURL contains actual pixel data const imageData = result[0].value.imageDataURL; assert.ok(imageData.startsWith("data:image/png;base64,")); // Decode base64 and verify it's not empty const base64Data = imageData.replace("data:image/png;base64,", ""); const decodedData = Buffer.from(base64Data, "base64"); assert.ok(decodedData.length > 0, "Decoded image data should not be empty"); }); Scenario("Verify Bitmask pixel content", async ({ I, LabelStudio, AtImageView, AtLabels }) => { const params = { config, data: { image: IMAGE }, annotations: [{ id: 1, result: [] }], }; I.amOnPage("/"); LabelStudio.init(params); LabelStudio.waitForObjectsReady(); await AtImageView.lookForStage(); I.say("Select Bitmask tool"); I.pressKey("B"); AtLabels.clickLabel("Test"); I.say("Draw a rectangle mask"); AtImageView.drawThroughPoints([ [20, 20], [20, 40], [40, 40], [40, 20], [20, 20], ]); I.say("Verify mask content and dimensions"); const result = await LabelStudio.serialize(); assert.strictEqual(result.length, 1); assert.ok(result[0].value.imageDataURL); // Get all data we need before making assertions // Retry mechanism to wait for bbox coordinates to be calculated let bbox = null; let attempts = 0; const maxAttempts = 10; while (!bbox && attempts < maxAttempts) { try { bbox = await I.executeScript(() => { const region = Htx.annotationStore.selected.regions[0]; if (!region) return null; if (!region.bboxCoordsCanvas) return null; return region.bboxCoordsCanvas; }); if (!bbox) { attempts++; await I.wait(100); // Wait 100ms before retrying } } catch (error) { attempts++; await I.wait(100); // Wait 100ms before retrying } } if (!bbox) { throw new Error("Bbox coordinates not available after multiple attempts"); } // Define thresholds for assertions const THRESHOLD = 5; const EXPECTED_SIZE = 40; // Verify that the bbox exists assert.ok(bbox, "Bounding box should exist"); // Calculate actual dimensions const width = bbox.right - bbox.left; const height = bbox.bottom - bbox.top; // Verify that the bbox has the expected size assert.ok( Math.abs(width - EXPECTED_SIZE) <= THRESHOLD, `Width should be close to ${EXPECTED_SIZE} pixels (got ${width})`, ); assert.ok( Math.abs(height - EXPECTED_SIZE) <= THRESHOLD, `Height should be close to ${EXPECTED_SIZE} pixels (got ${height})`, ); // Verify that the bbox is roughly square assert.ok( Math.abs(width - height) <= THRESHOLD, `Width and height should be similar (got width=${width}, height=${height})`, ); }); Scenario("Verify Bitmask canvas fit", async ({ I, LabelStudio, AtImageView, AtLabels }) => { const params = { config, data: { image: IMAGE }, annotations: [{ id: 1, result: [] }], }; I.amOnPage("/"); LabelStudio.init(params); LabelStudio.waitForObjectsReady(); await AtImageView.lookForStage(); I.say("Select Bitmask tool"); I.pressKey("B"); AtLabels.clickLabel("Test"); I.say("Draw a mask that covers the entire canvas height"); AtImageView.drawThroughPoints([ [10, 0], // Start from top [10, 100], // Draw to bottom [30, 100], // Complete rectangle [30, 0], // Back to top [10, 0], // Close the path ]); I.say("Verify mask content and image scaling"); const result = await LabelStudio.serialize(); assert.strictEqual(result.length, 1); assert.ok(result[0].value.imageDataURL); // Ensure the region is selected I.say("Ensure region is selected"); AtImageView.clickAt(20, 50); // Click in the middle of our drawn region // Get canvas dimensions and verify the underlying image scaling const canvasInfo = await I.executeScript(() => { // Get the canvas element const canvas = document.querySelector("canvas"); if (!canvas) throw new Error("Canvas not found"); // Get canvas dimensions const canvasWidth = canvas.width; const canvasHeight = canvas.height; // Get the original image element const imageElement = document.querySelector("img"); if (!imageElement) throw new Error("Image element not found"); // Get the original image dimensions const imageWidth = imageElement.naturalWidth; const imageHeight = imageElement.naturalHeight; // Get the region and its imageDataURL for verification const region = Htx.annotationStore.selected.regions[0]; if (!region) throw new Error("Region not found"); // For bitmask regions, imageDataURL is stored directly on the region const imageDataURL = region.imageDataURL; if (!imageDataURL) throw new Error("Region imageDataURL not available"); return { canvasWidth, canvasHeight, imageWidth, imageHeight, imageDataURL, }; }); // Verify canvas dimensions exist assert.ok(canvasInfo.canvasWidth > 0, "Canvas width should be positive"); assert.ok(canvasInfo.canvasHeight > 0, "Canvas height should be positive"); assert.ok(canvasInfo.imageWidth > 0, "Image width should be positive"); assert.ok(canvasInfo.imageHeight > 0, "Image height should be positive"); assert.ok(canvasInfo.imageDataURL, "Image data URL should exist"); // Verify that the image is properly scaled to fit the canvas // The image should be scaled to match the canvas dimensions const widthRatio = canvasInfo.canvasWidth / canvasInfo.imageWidth; const heightRatio = canvasInfo.canvasHeight / canvasInfo.imageHeight; // Check that the scaling ratios are reasonable (image should be scaled to fit canvas) assert.ok(widthRatio > 0, "Width scaling ratio should be positive"); assert.ok(heightRatio > 0, "Height scaling ratio should be positive"); // The image should be scaled to fit the canvas, but we don't require uniform scaling // as the image might be displayed with different aspect ratios or zoom levels // Instead, verify that both dimensions are scaled appropriately assert.ok(widthRatio >= 1 || heightRatio >= 1, "At least one dimension should be scaled up to fit canvas"); // Log the scaling information for debugging I.say(`Image scaling - Width: ${widthRatio.toFixed(3)}x, Height: ${heightRatio.toFixed(3)}x`); I.say( `Canvas: ${canvasInfo.canvasWidth}x${canvasInfo.canvasHeight}, Image: ${canvasInfo.imageWidth}x${canvasInfo.imageHeight}`, ); // Get the proper canvas and image frame sizes using the helper functions const { width: canvasSizeWidth, height: canvasSizeHeight } = await AtImageView.getCanvasSize(); const { width: imageFrameWidth, height: imageFrameHeight } = await AtImageView.getImageFrameSize(); // Verify that the stage/canvas dimensions match the image frame dimensions // The stage should be sized to fit the image properly assert.ok( Math.abs(canvasSizeWidth - imageFrameWidth) <= 1, `Stage width (${canvasSizeWidth}) should match image frame width (${imageFrameWidth}) within 1 pixel`, ); assert.ok( Math.abs(canvasSizeHeight - imageFrameHeight) <= 1, `Stage height (${canvasSizeHeight}) should match image frame height (${imageFrameHeight}) within 1 pixel`, ); });