NaturalIntelligence / imglab

To speedup and simplify image labeling/ annotation process with multiple supported formats.
https://solothought.com/imglab/
MIT License
989 stars 611 forks source link

svg.js : Add actions to zoom in / out svg panel #52

Closed amitguptagwl closed 6 years ago

amitguptagwl commented 6 years ago

Starting point: https://naturalintelligence.github.io/imglab/v2.html

It is quite difficult to work on large images. Hence a feature is required to zoom in / out image with SVG.

What to do

  1. There is a tool button (2nd last on toolbox) "Zoom In". It needs to be renamed to "Zoom" (config.js)

  2. Add an actions tag.html file in tags\actions folder with necessary functionality

    • When user clicks on "Zoom" tool button, related actions should be loaded in action bar: Zoom in , Zoom out
    • When user clicks on particular action then image with SVG overlay (workarea.tag.html) should be zoom in/out
  3. Include new tag file in v2.html

Check technical documentation to know how to add an action bar.

andrewogle commented 6 years ago

hello I would like to try and tackle this issue if that's okay.

amitguptagwl commented 6 years ago

Yes please.

Bookmark this project for easy discovery. You can also join team to support opensource community

amitguptagwl commented 6 years ago

@dragonhangnail Have you get the chance to look into?

andrewogle commented 6 years ago

yes, I am sorry this is taking so long this is my first time working on something like this.

amitguptagwl commented 6 years ago

Not an issue. I just wanted to check if you need some help.

amitguptagwl commented 6 years ago

@dragonhangnail I've hided the zoom button from toolbar until implemented. So that users will not try it.

rclim-11 commented 6 years ago

Apart from the UI, is the zoom capability supposed to work? I tried using the shortcut Alt-M but the image scale remains unchanged.

amitguptagwl commented 6 years ago

@rclim-11 zoom functionality is not implemented yet. the purpose of Alt+M is to select move tool or rather deselect any drawing tool.

alvinvoo commented 6 years ago

Hi @amitguptagwl , please refer below screenshot. Is this how you want it to look like?

imglab_ 52

<action-zoom>
  <style>
    .zoom-button{
      display: inline-block;
      padding: .3em;
      cursor: pointer;
    }
    .zoom-icon{

    }
    .zoom-selected{
        background: coral;
    }
    .zoom-button:not(.zoom-selected):hover{
        background: grey;
    }
  </style>
  <div>
    <div class="zoom-button" onclick={ zoomme }>
      <img src="img/icons/zoomin.svg" class="zoom-icon" >
    </div>
    <div class="zoom-button" onclick={ zoomme }>
      <img src="img/icons/zoomout.svg" class="zoom-icon" >
    </div>
  </div>
  <script>
    var tag = this;
    tag.zoomme = function (e){
      //TODO: should it unseect all the tools or tools in current toolbox
      $(".zoom-selected").removeClass("zoom-selected");
    }
  </script>
</action-zoom>

I wrote up the zoom-action.tag.html that you commented out in index.html. Of course, there's no logic to process the image yet, that will take me a while longer. But please let me know if I am going the right direction. Thank you.

amitguptagwl commented 6 years ago

Yes. your understanding is correct. And I love the way you confirm.

I may not be available for any discussion, fix, or new changes for next 1 week. Though I'll try my best.

alvinvoo commented 6 years ago

Hi @amitguptagwl I've finished the zoom function. Screencast below may take a while to load... imglab_ 52_1

Each image actually stores it's own instance of scale and scaled sizes.

I've changed 4 files in total: images-slider.tag.html, workarea.tag.html, trackinglines.tag.html & zoom-action.tag.html (newly added)

First, I realized that it is better that each 'imgSelected' actually stores it's own scaled sizes, so I added 3 new variables to size object in images-slider.tag.html

        function updateDimentions(imgFileSrc, imageDataObject) {
            var img = new Image();
            img.onload = function() {
                imageDataObject.size = {
                    width : this.width,
                    height : this.height,
                    scaledWidth: this.width, //here
                    scaledHeight: this.height, //here
                    imageScale: 1 //here
                }
                addImgToStore(imageDataObject.name, imageDataObject.size);
            }
            img.src = imgFileSrc;
        }

Zoom action in zoom-action.tag.html is straight forward. Users can zoom in as much as they want but they cannot zoom out less than 10%. Every time after zoom is pressed, the workarea is re-mounted.

    var tag = this;
    tag.zoomin = function (e){
      imgSelected.size.imageScale += 0.1;

      rescaleImage();
    }
    tag.zoomout = function (e){
      if(imgSelected.size.imageScale<0.2)return;
      imgSelected.size.imageScale -= 0.1;

      rescaleImage();
    }

    function rescaleImage(){
      imgSelected.size.scaledWidth = Math.floor(imgSelected.size.width * imgSelected.size.imageScale);
      imgSelected.size.scaledHeight = Math.floor(imgSelected.size.height * imgSelected.size.imageScale);

      riot.mount("workarea",{ img : imgSelected });
    }

In workarea.tag.html. The img and work-canvas will be updated according to the image's scaled size instead. Updating trackinglines is the tricky part.

...
        this.on('mount', function () {

            $("#canvas-container img").css("opacity", appConfig.imageOpacity || 1);

            $("#canvas-container img").css({"width": opts.img.size.scaledWidth, "height": opts.img.size.scaledHeight});
            $("#work-canvas").attr({"width": opts.img.size.scaledWidth, "height": opts.img.size.scaledHeight});

            myCanvas = new SVG('work-canvas').size(opts.img.size.scaledWidth, opts.img.size.scaledHeight);
...

To update the h_line and v_line of trackinglines, I amended the trackinglines.tag.html, such that instead of taking opts.width/opts.height everytime this tag is mounted, I take the scaled sizes from imgSelected instead. opts values cannot be changed once tag is mounted

            $(tag.refs["h_line"]).width(imgSelected.size.scaledWidth > canvasContainer.width() ? imgSelected.size.scaledWidth : canvasContainer.width() );
            $(tag.refs["v_line"]).height(imgSelected.size.scaledHeight > canvasContainer.height() ? imgSelected.size.scaledHeight : canvasContainer.height() );

In workarea.tag.html, I removed the opts arguments for trackinglines.

    <div id="canvas-container">
        <img id="img" src={ opts.img.src } width="{ opts.img.size.width }" height="{ opts.img.size.height }" />
        <div id="work-canvas" width="{ opts.img.size.width }" height="{ opts.img.size.height }"></div>
        <trackinglines></trackinglines>
    </div>

Let me know if this is acceptable. If yes, I will create the PR. Thanks. =)

*PS: Some ideas I thought about when developing this:

  1. Shall we have a 'reset' zoom button? Such that user can click and image will reset to default size? Based on the code I have above, the original width and height of imgSelected is stored separately, so this should be easily doable.
  2. The 'zoomed scale' for each image is stored separately BUT the opacity is using appConfig.imageOpacity, which is global, that means if you changed the opacity on one image, the rest of the images are all affected. Would it be nicer to store this value in imgSelected instead? With each image having it's own opacity adjustable?
amitguptagwl commented 6 years ago

Sorry for the late response, in travel.

  1. Shall we have a 'reset' zoom button?

Yes! it was in plan. We can combine it with this PR. I also wanted to use a slider or text box in between of zoom in/out buttons. So users know the current zoom level.

  1. ... appConfig.imageOpacity ...

We kept it globally because of usability. User needs to reduce opacity so that label shapes are easily visible. If I'll be labeling images, I would like to set it only once. So we can skip this.

One question (before I go in detail), have you tried to label images while testing zoom option? How it is working?

alvinvoo commented 6 years ago

Hi @amitguptagwl I didn't take into account the feature points for zooming & I also didn't take into account the saving of the image scale. So, the feature points won't 'zoom together' as of now. And also the zoom scale is not saved (i.e. when you close and open the application and select the same image again, the image starts off as original size)

imglab_ 14_1

amitguptagwl commented 6 years ago

I didn't take into account the feature points for zooming

The whole purpose of providing zoom feature to enable users to label small parts of an image. So the label shapes should also zoom with the same level. However feature points should not be zoomed.

As we're using svj.js for labeling, https://github.com/svgdotjs/svg.panzoom.js can be used for this implementation.

I also didn't take into account the saving of the image scale. So, the feature points won't 'zoom together' as of now. And also the zoom scale is not saved (i.e. when you close and open the application and select the same image again, the image starts off as original size)

It's fine. It is expected.

alvinvoo commented 6 years ago

Hi @amitguptagwl

I have been playing around with svg.panzoom.js but this library doesn't seems to work well with the existing libraries. What I did was just included in the library and added in the panZoom call.

            myCanvas = new SVG('work-canvas').size(opts.img.size.scaledWidth, opts.img.size.scaledHeight).panZoom({zoomMin: 0.1, zoomMax: 20});

imglab_ 52_zoom_problem

I will take some more time to look at this library in detail.

I also wanted to use a slider or text box in between of zoom in/out buttons. So users know the current zoom level.

I have addressed this, by adding in the input box and reset button.

imglab_ 52_3

*P.S I grabbed the reset button from some website and its probably licensed. Maybe you can make a new one ;)

amitguptagwl commented 6 years ago

We're using common-fonts in our application. So we can use icon-cw probably to reset zoom level.

I've never tried panZoom library before. So probably there is some compatibility issue. We may try some other libraries:

alvinvoo commented 6 years ago

Hi @amitguptagwl ! I swear I burned some brain cells for this.. Please have a look imglab_ 52_final I have used https://github.com/ariutta/svg-pan-zoom library. My code structure (as compared to my post 10 days ago) is still pretty much the same for the image zoom in/out part, but for the shapes I have added two new keys in labellingData object:

store.js:
function attachShapeToImg(id, type, bbox, points){
    labellingData[ imgSelected.name ].shapes.push( {
        "id" : id,
        "label" : "unlabelled",
         ....
        "zoomScale" : 1,
        "defaultZoomScale": 1/imgSelected.size.imageScale//this is in relation with the image
    } );

EVERY shape that is either added before or after a zoom in/out needs to have their own zoomScale, like the imgSelected has imageScale, with an additional defaultZoomScale, because each shape is IN RELATION to the parent image, hence the imageScale default of 1 doesnt mean that the shape's default zoom is 1, i.e. the shape might be added when the image is already zoomed in to scale 2 etc.. if the shape is added when image is at zoom 2, then its defaultZoomScale is 0.5 when the user click the reset zoom button.

At the workarea.tag.html, I added an additional function. This makes use of the library, and this function is called at the end of drawOnCanvas() like so setZoomLevel(shape.zoomScale, currentShape);

        function setZoomLevel(zoomScale, currentShape){
          var panZoomCanvas = svgPanZoom("#"+currentShape.parent().id(),{
            panEnabled: false,
            zoomEnabled: true,
            zoomScaleSensitivity: 0.1,
            minZoom: 0.1,
            maxZoom: 10,
            fit:false,
            center: false
          });
          panZoomCanvas.zoomAtPoint(zoomScale,{x:0, y:0});
        }

A bit of refactoring of the tag.zoom function in zoom-action.tag.html:

tag.zoom = function (e){

      if(!imgSelected) return;
      var mulScale = 0;
      switch (e.target.dataset.zoomType) {
        case 'in':
          if(imgSelected.size.imageScale>=10)return;
          mulScale = 1;
          break;
        case 'out':
          if(imgSelected.size.imageScale<=0.1)return;
          mulScale = -1;
          break;
        case 'reset':
          mulScale = 0;
          break;
      }

      const preImgSelectedScale = imgSelected.size.imageScale;
      imgSelected.size.imageScale = (mulScale===0) ? 1 : preImgSelectedScale+mulScale*appConfig.zoomStepSize;

      const img = labellingData[imgSelected.name];
      if(img){//if there are labels
        img.shapes.forEach((shape)=>{
          shape.zoomScale = (mulScale===0) ? shape.defaultZoomScale : shape.zoomScale*imgSelected.size.imageScale/preImgSelectedScale;
        })
      }

      rescaleImage();
    }

This line is the magic line, each shape should scale IN RELATION to the image.

shape.zoomScale = (mulScale===0) ? shape.defaultZoomScale : shape.zoomScale*imgSelected.size.imageScale/preImgSelectedScale;

I added the zoomStepSize as a config item in appConfig.

var appConfig = {
    autosave : {
        syncingInterval : 10 * 1000,  //10 seconds
        enable : true,
        deleteIfExported: true, //Mark the data as saved when exported as nimn format, and delte the copy from browser cache.
    },
    featurePointSize: 3,
    zoomStepSize: 0.1
};

Let me know if this is ok. There's quite a lot of code change. Thanks.

amitguptagwl commented 6 years ago

You have strong brain cells. :smile:

It seems perfectly fine to me.

amitguptagwl commented 6 years ago

@Alvin-Voo In case if you want to join this project as collaborator, please visit this

alvinvoo commented 6 years ago

@amitguptagwl Alright, I have accepted the invitation. I have also created the PR, please review. Thank you!

amitguptagwl commented 6 years ago

@Alvin-Voo PR has been merged but the zom option can't seen.

alvinvoo commented 6 years ago

@amitguptagwl let me have a look

alvinvoo commented 6 years ago

Sorry @amitguptagwl I missed out the config.js. Let me create another PR.

alvinvoo commented 6 years ago

@amitguptagwl Alright.. I see it ;)

amitguptagwl commented 6 years ago

Grt!!

I observed that zoom out button doesn't work when we directly click on that but works fin when we click on image or zoom out in first. (P3)

Moreover, is it possible to not to zoom feature point? If not I believe we can reset their radius. However this approach doesn't seem feasible as there can be multiple points and we'll have to reset radius of every point. Any other suggestion? (P2)

alvinvoo commented 6 years ago

I observed that zoom out button doesn't work when we directly click on that but works fin when we click on image or zoom out in first.

Heh? You mean when we first load the image and click zoom out? It works fine.

Moreover, is it possible to not to zoom feature point? If not I believe we can reset their radius. However this approach doesn't seem feasible as there can be multiple points and we'll have to reset radius of every point. Any other suggestion?

Because now the matrix transforms everything within the svg, you can check this out in the dev console, the library basically applies a viewport and a transform for e.g. transform="matrix(0.699999988079071,0,0,0.699999988079071,0,0) to all the components including 'circle' which are the feature points, so this is how it works. And these 'circles'/feature points have to be scaled (transform) together as well, if not their position will be wrong. Their radius has always remained at 3 by default (or whatever the user input..) TL:DR the 'zoom' doesn't really change the actual size of the shapes, it merely transforms them.

amitguptagwl commented 6 years ago

Heh? You mean when we first load the image and click zoom out? It works fine.

Not sure why I was facing the problem in first attempt. However it seems working as expected now. So please ignore it.

Because now the matrix transforms everything within the svg ...

Agree. Please ignore it for the time being. I'll check later if some alternatives come in mind.