6
0
Prechádzať zdrojové kódy

Merge 'master' into '150-flask-sqlalchemy'

Dimitri Korsch 3 rokov pred
rodič
commit
f166a1da1d

+ 1 - 0
pycs/frontend/WebServer.py

@@ -14,6 +14,7 @@ from pycs.frontend.endpoints.ListLabelProviders import ListLabelProviders
 from pycs.frontend.endpoints.ListModels import ListModels
 from pycs.frontend.endpoints.ListProjects import ListProjects
 from pycs.frontend.endpoints.additional.FolderInformation import FolderInformation
+from pycs.frontend.endpoints.data.GetCroppedFile import GetCroppedFile
 from pycs.frontend.endpoints.data.GetFile import GetFile
 from pycs.frontend.endpoints.data.GetPreviousAndNextFile import GetPreviousAndNextFile
 from pycs.frontend.endpoints.data.GetResizedFile import GetResizedFile

+ 52 - 0
pycs/frontend/endpoints/data/GetCroppedFile.py

@@ -0,0 +1,52 @@
+import os
+import re
+
+from eventlet import tpool
+from flask import abort
+from flask import send_from_directory
+from flask.views import View
+
+from pycs.database.Database import Database
+from pycs.util.FileOperations import crop_file
+
+
+class GetCroppedFile(View):
+    """
+    return the image crop defined by the result.
+    """
+    # pylint: disable=arguments-differ
+    methods = ['GET']
+
+    def __init__(self, db: Database):
+        # pylint: disable=invalid-name
+        self.db = db
+
+    def dispatch_request(self, file_id: int, resolution: str, crop_box: str):
+        # get file from database
+        file = self.db.file(file_id)
+        if file is None:
+            return abort(404)
+
+        project = file.project()
+
+        if not os.path.exists(file.absolute_path):
+            abort(404, "File not found!")
+
+        # extract desired crop
+        resolution = re.split(r'[^0-9]', resolution)
+        max_width = int(resolution[0])
+        max_height = int(resolution[1]) if len(resolution) > 1 else 2 ** 24
+
+        crop_box = re.split(r'[^0-9.]', crop_box)
+        crop_x = float(crop_box[0])
+        crop_y = float(crop_box[1]) if len(crop_box) > 1 else 0
+        crop_w = float(crop_box[2]) if len(crop_box) > 2 else 1 - crop_x
+        crop_h = float(crop_box[3]) if len(crop_box) > 3 else 1 - crop_y
+
+        # crop file
+        file_directory, file_name = tpool.execute(crop_file, file, project.root_folder,
+                                                  crop_x, crop_y, crop_w, crop_h,
+                                                  max_width, max_height)
+
+        # send to client
+        return send_from_directory(file_directory, file_name)

+ 9 - 4
pycs/frontend/endpoints/data/GetResizedFile.py

@@ -7,7 +7,7 @@ from flask import send_from_directory
 from flask.views import View
 
 from pycs.database.File import File
-from pycs.util import file_ops
+from pycs.util.FileOperations import resize_file
 
 
 class GetResizedFile(View):
@@ -22,6 +22,9 @@ class GetResizedFile(View):
         # get file from database
         file = File.get_or_404(file_id)
 
+        if not os.path.exists(file.absolute_path):
+            abort(404, "File not found!")
+
         if not os.path.exists(file.absolute_path):
             abort(404, "File not found!")
 
@@ -30,8 +33,10 @@ class GetResizedFile(View):
         max_width = int(resolution[0])
         max_height = int(resolution[1]) if len(resolution) > 1 else 2 ** 24
 
-        # send data
-        file_directory, file_name = tpool.execute(file_ops.resize_file,
-                                                  file, file.project.root_folder,
+        # resize file
+        file_directory, file_name = tpool.execute(resize_file, file, 
+                                                  file.project.root_folder,
                                                   max_width, max_height)
+
+        # send to client
         return send_from_directory(file_directory, file_name)

+ 1 - 1
pycs/frontend/endpoints/data/UploadFile.py

@@ -8,7 +8,7 @@ from werkzeug import formparser
 
 from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationManager import NotificationManager
-from pycs.util.FileParser import file_info
+from pycs.util.FileOperations import file_info
 
 
 class UploadFile(View):

+ 1 - 1
pycs/frontend/endpoints/projects/ExecuteExternalStorage.py

@@ -11,7 +11,7 @@ from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
 from pycs.jobs.JobRunner import JobRunner
-from pycs.util.FileParser import file_info
+from pycs.util.FileOperations import file_info
 
 
 class ExecuteExternalStorage(View):

+ 233 - 0
pycs/util/FileOperations.py

@@ -0,0 +1,233 @@
+import os
+from typing import Tuple
+
+import cv2
+from PIL import Image
+
+from pycs.database.File import File
+
+DEFAULT_JPEG_QUALITY = 80
+
+
+def file_info(data_folder: str, file_name: str, file_ext: str):
+    """
+    Receive file type, frame count and frames per second.
+    The last two are always None for images.
+
+    :param data_folder: path to data folder
+    :param file_name: file name
+    :param file_ext: file extension
+    :return: file type, frame count, frames per second
+    """
+    # determine file type
+    if file_ext.lower() in ['.jpg', '.png']:
+        file_type = 'image'
+
+    elif file_ext.lower() in ['.mp4']:
+        file_type = 'video'
+
+    else:
+        raise ValueError(f"Unsupported file extension: {file_ext}!")
+
+    # determine frames and fps for video files
+    if file_type == 'image':
+        frames = None
+        fps = None
+    else:
+        file_path = os.path.join(data_folder, file_name + file_ext)
+        video = cv2.VideoCapture(file_path)
+
+        frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
+        fps = video.get(cv2.CAP_PROP_FPS)
+
+        video.release()
+
+    # return values
+    return file_type, frames, fps
+
+
+def resize_file(file: File, project_root: str, max_width: int, max_height: int) -> Tuple[str, str]:
+    """
+    If file type equals video this function extracts a thumbnail first. It calls resize_image
+    to resize and returns the resized files directory and name.
+
+    :param file: file object
+    :param project_root: project root folder path
+    :param max_width: maximum image or thumbnail width
+    :param max_height: maximum image or thumbnail height
+    :return: resized file directory, resized file name
+    """
+    abs_file_path = file.absolute_path
+
+    # extract video thumbnail
+    if file.type == 'video':
+        abs_target_path = os.path.join(os.getcwd(), project_root, 'temp', f'{file.uuid}.jpg')
+        create_thumbnail(abs_file_path, abs_target_path)
+
+        abs_file_path = abs_target_path
+
+    # resize image file
+    abs_target_path = os.path.join(os.getcwd(),
+                                   project_root,
+                                   'temp',
+                                   f'{file.uuid}_{max_width}_{max_height}.jpg')
+    result = resize_image(abs_file_path, abs_target_path, max_width, max_height)
+
+    # return path
+    if result is not None:
+        return os.path.split(abs_target_path)
+
+    return os.path.split(abs_file_path)
+
+
+def crop_file(file: File, project_root: str,
+              x: float, y: float, w: float, h: float,
+              max_width: int, max_height: int) -> Tuple[str, str]:
+    """
+    gets a file for the given file_id, crops the according image to the
+    bounding box and saves the crops in the temp folder of the project.
+
+    :param file: file object
+    :param project_root: project root folder path
+    :param max_width: maximum image or thumbnail width
+    :param max_height: maximum image or thumbnail height
+    :param x: relative x-coordinate of the top left corner
+    :param y: relative y-coordinate of the top left corner
+    :param w: relative width of the bounding box
+    :param h: relative height of the bounding box
+
+    :return: directory and file name of the cropped patch
+    """
+    abs_file_path = file.absolute_path
+
+    # extract video thumbnail
+    if file.type == 'video':
+        abs_target_path = os.path.join(os.getcwd(), project_root, 'temp', f'{file.uuid}.jpg')
+        create_thumbnail(abs_file_path, abs_target_path)
+
+        abs_file_path = abs_target_path
+
+    # crop image file
+    abs_target_path = os.path.join(os.getcwd(),
+                                   project_root,
+                                   'temp',
+                                   f'{file.uuid}_{x}_{y}_{w}_{h}.jpg')
+    result = crop_image(abs_file_path, abs_target_path, x, y, w, h)
+
+    if result:
+        abs_file_path = abs_target_path
+
+    # resize image
+    abs_target_path = os.path.join(os.getcwd(),
+                                   project_root,
+                                   'temp',
+                                   f'{file.uuid}_{max_width}_{max_height}_{x}_{y}_{w}_{h}.jpg')
+    result = resize_image(abs_file_path, abs_target_path, max_width, max_height)
+
+    if result:
+        abs_file_path = abs_target_path
+
+    # return image
+    return os.path.split(abs_file_path)
+
+
+def create_thumbnail(file_path: str, target_path: str) -> None:
+    """
+    extract a thumbnail from a video
+
+    :param file_path: path to source file
+    :param target_path: path to target file
+    :return:
+    """
+    # return if file exists
+    if os.path.exists(target_path):
+        return
+
+    # load video
+    video = cv2.VideoCapture(file_path)
+
+    # create thumbnail
+    _, image = video.read()
+    cv2.imwrite(target_path, image)
+
+    # close video file
+    video.release()
+
+
+def resize_image(file_path: str, target_path: str, max_width: int, max_height: int) -> bool:
+    """
+    Resize an image so width < `max_width` and height < `max_height` applies.
+    If the image is already smaller than the given dimensions no new file is stored.
+
+    :param file_path: path to source file
+    :param target_path: path to target file
+    :param max_width: maximum image width
+    :param max_height: maximum image height
+    :return: `True` if a resize operation was performed or the target file already exists
+    """
+    # return if file exists
+    if os.path.exists(target_path):
+        return True
+
+    # load full size image
+    image = Image.open(file_path)
+    img_width, img_height = image.size
+
+    # abort if file is smaller than desired
+    if img_width < max_width and img_height < max_height:
+        return False
+
+    # calculate target size
+    target_width = int(max_width)
+    target_height = int(max_width * img_height / img_width)
+
+    if target_height > max_height:
+        target_height = int(max_height)
+        target_width = int(max_height * img_width / img_height)
+
+    # resize image
+    resized_image = image.resize((target_width, target_height))
+
+    # save to file
+    resized_image.save(target_path, quality=DEFAULT_JPEG_QUALITY)
+    return True
+
+
+def crop_image(file_path: str, target_path: str, x: float, y: float, w: float, h: float) -> bool:
+    """
+    Crop an image with the given coordinates, width and height.
+    If however no crop is applied no new file is stored.
+
+    :param file_path: path to source file
+    :param target_path: path to target file
+    :param x: crop x position (normalized)
+    :param y: crop y position (normalized)
+    :param w: crop width (normalized)
+    :param h: crop height (normalized)
+    :return: `True` if a crop operation was performed or the target file already exists
+    """
+    # return if file exists
+    if os.path.exists(target_path):
+        return True
+
+    # abort if no crop would be applied
+    if x <= 0 and y <= 0 and w >= 1 and h >= 1:
+        return False
+
+    # load full size image
+    image = Image.open(file_path)
+    img_width, img_height = image.size
+
+    # calculate absolute crop position
+    crop_x1 = int(img_width * x)
+    crop_y1 = int(img_height * y)
+    crop_x2 = min(int(img_width * w) + crop_x1, img_width)
+    crop_y2 = min(int(img_height * h) + crop_y1, img_height)
+
+    # crop image
+    print(crop_x1, crop_y1, crop_x2, crop_y2)
+    cropped_image = image.crop((crop_x1, crop_y1, crop_x2, crop_y2))
+
+    # save to file
+    cropped_image.save(target_path, quality=DEFAULT_JPEG_QUALITY)
+    return True

+ 0 - 40
pycs/util/FileParser.py

@@ -1,40 +0,0 @@
-from os import path
-
-import cv2
-
-
-def file_info(data_folder: str, file_name: str, file_ext: str):
-    """
-    Receive file type, frame count and frames per second.
-    The last two are always None for images.
-
-    :param data_folder: path to data folder
-    :param file_name: file name
-    :param file_ext: file extension
-    :return: file type, frame count, frames per second
-    """
-    # determine file type
-    if file_ext.lower() in ['.jpg', '.png']:
-        ftype = 'image'
-
-    elif file_ext.lower() in ['.mp4']:
-        ftype = 'video'
-
-    else:
-        raise ValueError(f"Unsupported file extension: {file_ext}!")
-
-    # determine frames and fps for video files
-    if ftype == 'image':
-        frames = None
-        fps = None
-    else:
-        file_path = path.join(data_folder, file_name + file_ext)
-        video = cv2.VideoCapture(file_path)
-
-        frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
-        fps = video.get(cv2.CAP_PROP_FPS)
-
-        video.release()
-
-    # return values
-    return ftype, frames, fps

+ 31 - 49
webui/src/components/media/annotated-image.vue

@@ -9,8 +9,9 @@
                    :label="label"
                    @label="label = $event"
                    :labels="labels"
-                   @infoBox="toggleInfoBox()"
                    :zoomBox="zoomBox"
+                   :infoBox="infoBox !== false"
+                   @infoBox="infoBox = $event; interaction=false"
                    @unzoom="zoomBox=false; interaction=false"
                    @prevzoom="$refs.overlay.prevZoom()"
                    @nextzoom="$refs.overlay.nextZoom()"/>
@@ -48,17 +49,17 @@
                             :video="video"
                             :results="results"
                             :labels="labels"
-                            @crop="selectCrop($event)"
+                            :crop="infoBox"
+                            @crop="infoBox = $event"
                             :zoom="zoomBox"
                             @zoom="zoomBox = $event"/>
       </div>
 
-      <cropped-image ref="info"
-                     :width="300"
-                     :margin="10"
+      <cropped-image v-if="infoBox !== false"
                      :labels="labels"
-                     @closed="infoBoxClosed"
-                     />
+                     :file="current"
+                     :box="infoBox"
+                     @close="infoBox=false"/>
     </template>
   </div>
 </template>
@@ -93,13 +94,13 @@ export default {
 
     // get model
     this.$root.socket.get(`/projects/${this.$root.project.identifier}/model`)
-        .then(response => response.json())
-        .then(model => {
-          this.supported.labeledImages = model.supports.includes('labeled-images');
-          this.supported.labeledBoundingBoxes = model.supports.includes('labeled-bounding-boxes');
-          this.supported.boundingBoxes = this.supported.labeledBoundingBoxes
-              || model.supports.includes('bounding-boxes');
-        });
+      .then(response => response.json())
+      .then(model => {
+        this.supported.labeledImages = model.supports.includes('labeled-images');
+        this.supported.labeledBoundingBoxes = model.supports.includes('labeled-bounding-boxes');
+        this.supported.boundingBoxes = this.supported.labeledBoundingBoxes
+          || model.supports.includes('bounding-boxes');
+      });
   },
   destroyed: function () {
     window.removeEventListener('resize', this.resize);
@@ -133,7 +134,7 @@ export default {
         frame: 0,
         play: false
       },
-      selectedCrop: null,
+      infoBox: false,
       zoomBox: false,
       supported: {
         labeledImages: false,
@@ -184,27 +185,6 @@ export default {
     }
   },
   methods: {
-
-    toggleInfoBox: function() {
-      let infoBox = this.$refs.info;
-      infoBox.toggle();
-      if (infoBox.visible)
-        this.interaction = "info-box";
-      else {
-        this.interaction = false;
-        this.$refs.overlay.deselectAllBoxes()
-      }
-    },
-
-    selectCrop: function(event) {
-        this.$refs.info.show(event);
-    },
-
-    infoBoxClosed: function() {
-      this.interaction = false;
-      this.$refs.overlay.deselectAllBoxes()
-    },
-
     resize: function () {
       const rect = this.$refs.root.getBoundingClientRect();
 
@@ -272,6 +252,9 @@ export default {
       }
     },
     editResult: function (result) {
+      if (this.infoBox && result.identifier === this.infoBox.identifier)
+        this.infoBox = result;
+
       for (let i = 0; i < this.results.length; i++) {
         if (this.results[i].identifier === result.identifier) {
           this.$set(this.results, i, result);
@@ -281,11 +264,11 @@ export default {
     },
     getLabels: function () {
       this.$root.socket.get(`/projects/${this.$root.project.identifier}/labels`)
-          .then(response => response.json())
-          .then(labels => {
-            this.labels = [];
-            labels.forEach(this.addLabelToList);
-          });
+        .then(response => response.json())
+        .then(labels => {
+          this.labels = [];
+          labels.forEach(this.addLabelToList);
+        });
     },
     addLabelToList: function (label) {
       if (label['project_id'] !== this.$root.project.identifier)
@@ -324,15 +307,14 @@ export default {
         this.zoomBox = false;
 
         this.$root.socket.get(`/data/${newVal.identifier}/results`)
-            .then(response => response.json())
-            .then(results => {
-              this.results = results;
-            })
-        let infoBox = this.$refs.info;
-        if (infoBox == undefined)
-          return;
-        infoBox.box = null;
+          .then(response => response.json())
+          .then(results => {
+            this.results = results;
+          });
       }
+    },
+    infoBox: function () {
+      setTimeout(this.resize, 1);
     }
   }
 }

+ 4 - 22
webui/src/components/media/annotation-box.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="annotation-box btn"
        :style="style"
-       :class="{draggable: draggable, shine: shine}"
+       :class="{draggable: draggable}"
        @mousedown.prevent="click" @touchstart.prevent="click">
     <template v-if="draggable">
       <div class="nw" @mousedown.prevent.stop="resize('nw')" @touchstart.prevent.stop="resize('nw')"/>
@@ -35,11 +35,6 @@ export default {
     'labels',
     'shine'
   ],
-  data: function() {
-    return {
-      selected: false
-    }
-  },
   computed: {
     labelName: function () {
       if (!this.box || !this.box.label)
@@ -66,16 +61,9 @@ export default {
         }
       }
 
-      let bgAlpha = 0.6;
-      let borderAlpha = 0.8;
-      if (!this.selected) {
-        bgAlpha *= 0.5;
-        borderAlpha *= 0.5;
-      }
-
       return {
-        backgroundColor: `rgba(${color}, ${bgAlpha})`,
-        borderColor: `rgba(${color}, ${borderAlpha})`,
+        backgroundColor: `rgba(${color}, ${this.shine ? 0.6 : 0.3})`,
+        borderColor: `rgba(${color}, ${this.shine ? 0.8 : 0.4})`,
         top: this.position.y * 100 + '%',
         left: this.position.x * 100 + '%',
         width: this.position.w * 100 + '%',
@@ -114,8 +102,7 @@ export default {
         event.stopPropagation();
       }
       if (this.croppable) {
-        this.$emit('crop', this);
-        this.selected = true;
+        this.$emit('crop', this.box);
         event.stopPropagation();
       }
     },
@@ -137,11 +124,6 @@ export default {
   border: 1px solid transparent;
 }
 
-.annotation-box.shine {
-  background: none !important;
-  border-width: 5px;
-}
-
 .label {
   position: absolute;
   bottom: 0.25rem;

+ 3 - 16
webui/src/components/media/annotation-overlay.vue

@@ -16,10 +16,10 @@
                     :zoomable="interaction === 'zoom-box'"
                     :croppable="interaction === 'info-box'"
                     :labels="labels"
-                    :shine="zoom"
+                    :shine="crop && box.identifier === crop.identifier"
                     @move="move"
                     @resize="resize"
-                    @crop="crop($event)"
+                    @crop="$emit('crop', $event)"
                     @zoom="zoomBox(index, $event)"/>
 
     <annotation-box v-if="current"
@@ -52,6 +52,7 @@ export default {
     'video',
     'results',
     'labels',
+    'crop',
     'zoom'
   ],
   data: function () {
@@ -269,20 +270,6 @@ export default {
           break;
       }
     },
-    crop: function (box) {
-      this.deselectAllBoxes();
-      if (box == undefined)
-        return;
-
-      this.$emit('crop', box);
-    },
-
-    deselectAllBoxes: function(){
-      this.$refs.box.forEach( child => {
-        child.selected = false;
-      })
-    },
-
     zoomBox: function (index) {
       if (this.boundingBoxes.length === 0) {
         this.zoomed = false;

+ 60 - 104
webui/src/components/media/cropped-image.vue

@@ -1,125 +1,81 @@
 <template>
-    <div v-if="visible" class="cropped-image">
-      <div id="close_btn" class="btn"
-        @mousedown.prevent="close"
-        @touchstart.prevent="close">
-        <img alt="close button" src="@/assets/icons/cross.svg">
-      </div>
-      <div class="image-container" :style="style">
-        <div v-if="box">
-          <h3>{{labelName}}</h3>
-          <img :src="src" ref="crop" alt="crop" />
-        </div>
-        <div v-else>Select a crop to show</div>
-      </div>
+  <div class="cropped-image">
+    <div class="close-button"
+         @mousedown.prevent="$emit('close', true)"
+         @touchstart.prevent="$emit('close', true)">
+      <img alt="close button" src="@/assets/icons/cross.svg">
     </div>
 
+    <div v-if="src" class="image-container">
+      <h3>{{ label }}</h3>
 
+      <img alt="crop" :src="src"/>
+    </div>
+    <div v-else>
+      click a bounding box
+    </div>
+  </div>
 </template>
 
 <script>
-    export default {
-        name: "cropped-image",
-        props: ["width", "margin", "labels"],
-
-
-        // mounted: function() {}
-        // destroyed: function() {}
-
-        data: function() {
-            return {
-                visible: false,
-                box: null,
-            }
-        },
-
-        computed: {
-          src: function () {
-            if (!this.box)
-              return ''
-            var width = this.width - 2 * this.margin /*padding*/;
-            return this.$root.socket.url(`/results/${this.box.identifier}/crop/${width}x${width}`)
-          },
-
-          style: function() {
-
-            return {
-              minWidth: `${this.width}px`,
-              margin: `${this.margin}px`,
-            }
-
-          },
-          labelName: function() {
-            if (!this.box)
-              return ''
-
-            if (this.box.label == null)
-              return 'Unknown'
-
-            let name = 'Not Found';
-            for (let label of this.labels){
-              if (label.identifier != this.box.label)
-                continue;
-
-              name = label.name;
-              break;
-            }
-            return name
-          },
-
-        },
-
-        methods: {
-            toggle: function () {
-              this.set_visible(!this.visible);
-            },
-
-            set_visible: function (visible) {
-              this.visible = visible;
-
-              if (!visible)
-                this.box = null;
-
-              this.$emit("visibility-change", visible);
-            },
-
-            show: function (box_element) {
-              this.box = box_element.box;
-            },
-
-            close: function() {
-              this.set_visible(false);
-              this.$emit("closed");
-            }
-        },
+export default {
+  name: "cropped-image",
+  props: ['labels', 'file', 'box'],
+  computed: {
+    src: function () {
+      if (!this.box)
+        return false;
+
+      const file = this.file.identifier;
+      const uuid = this.file.uuid;
+      const x = this.box.data.x;
+      const y = this.box.data.y;
+      const w = this.box.data.w;
+      const h = this.box.data.h;
+
+      return this.$root.socket.url(`/data/${file}/1024/${x}x${y}x${w}x${h}?uuid=${uuid}`)
+    },
+    label: function () {
+      if (!this.box)
+        return false;
+      if (this.box.label == null)
+        return 'Unknown'
+
+      for (let label of this.labels)
+        if (label.identifier === this.box.label)
+          return label.name;
+
+      return 'Not found';
     }
+  }
+}
 </script>
 
 <style scoped>
-
 .cropped-image {
+  width: 40%;
   height: 100%;
-
-  background-color: rgba(133, 133, 133, 0.7);
-}
-
-.image-container {
-  /*align-items: center;*/
-  justify-content: center;
+  background-color: #858585;
+  position: relative;
+  padding: 2rem 1rem;
   text-align: center;
-  display: flex;
 }
 
-.image-container img {
-  border-width: 2px;
-  border-style: solid;
+.close-button {
+  position: absolute;
+  top: 0;
+  right: 0;
+  padding: 0.5rem 0.67rem;
+  cursor: pointer;
+  filter: invert(1);
 }
 
-#close_btn {
-  justify-content: right;
-  text-align: right;
-
-  margin:  5px;
+.close-button img {
+  width: 1.5rem;
 }
 
+.image-container img {
+  border: 2px solid;
+  max-width: 100%;
+}
 </style>

+ 11 - 5
webui/src/components/media/options-bar.vue

@@ -65,10 +65,11 @@
          class="image"
          title="Show info of a bounding box (I)"
          :class="{active: interaction === 'info-box'}"
-         @click="$emit('infoBox')">
+         @click="$emit('interaction', 'info-box')">
       <img alt="zoom bounding box" src="@/assets/icons/info.svg">
     </div>
 
+    <!--
     <div ref="zoom_annotation"
          class="image"
          title="zoom bounding box (X)"
@@ -90,6 +91,7 @@
          @click="$emit('nextzoom', true)">
       <img alt="zoom next bounding box" src="@/assets/icons/chevron-right.svg">
     </div>
+    -->
 
     <div class="spacer"/>
 
@@ -127,14 +129,14 @@ import LabelSelector from "@/components/media/label-selector";
 export default {
   name: "options-bar",
   components: {LabelSelector},
-  props: ['file', 'interaction', 'filter', 'label', 'labels', 'zoomBox'],
+  props: ['file', 'interaction', 'filter', 'label', 'labels', 'infoBox', 'zoomBox'],
   created: function () {
     // get data
     this.getJobs();
 
     this.$root.socket.get(`/projects/${this.$root.project.identifier}/model`)
-        .then(response => response.json())
-        .then(model => this.model = model);
+      .then(response => response.json())
+      .then(model => this.model = model);
 
     // subscribe to changes
     this.$root.socket.on('connect', this.getJobs);
@@ -223,6 +225,9 @@ export default {
         case 'v':
           this.$refs.zoom_next_annotation.click();
           break;
+        case 'i':
+          this.$refs.crop_info.click();
+          break;
       }
     },
     getJobs: function () {
@@ -305,7 +310,8 @@ img {
 
   width: 30rem;
   max-width: 90vw;
-  max-height: 90vh;
+  max-height: 100%;
+  padding-bottom: 0;
 
   animation: label-selector-animation 0.5s ease-out forwards;
 }