浏览代码

Resolve "upload images for prediction and labeling"

Eric Tröbs 4 年之前
父节点
当前提交
5fbf1fd013

+ 1 - 1
README.md

@@ -25,7 +25,7 @@ install python dependencies
 
 ```bash
 pip install numpy Pillow scipy tensorflow
-pip install eventlet python-socketio
+pip install eventlet flask python-socketio
 ```
 
 start server

+ 2 - 1
pycs/ApplicationStatus.py

@@ -19,7 +19,8 @@ class ApplicationStatus(ObservableDict):
 
         # initialize data structure
         super().__init__({
+            'settings': settings,
             'models': {},
             'projects': [],
-            'settings': settings
+            'jobs': []
         })

+ 122 - 0
pycs/frontend/FileProvider.py

@@ -0,0 +1,122 @@
+from os import path, mkdir, getcwd
+from time import time
+from uuid import uuid1
+
+from eventlet import tpool
+from flask import Flask, request, make_response, send_from_directory
+
+from pycs.ApplicationStatus import ApplicationStatus
+
+
+class FileProvider(Flask):
+    def __init__(self, app_status: ApplicationStatus, cors=False):
+        super().__init__(__name__)
+
+        # add download handler
+        @self.route('/media/<identifier>', methods=['GET'])
+        def media(identifier):
+            # get current project
+            opened_projects = list(filter(lambda x: x['status'] == 'open', app_status['projects']))
+            if len(opened_projects) == 0:
+                return make_response('no open project available', 500)
+
+            current_project = opened_projects[0]
+            file_directory = path.join(getcwd(), 'projects', current_project['id'], 'data')
+
+            # get object
+            data_objects = list(filter(lambda x: x['id'] == identifier, current_project['data']))
+            if len(data_objects) == 0:
+                return make_response('data object not avilable', 500)
+
+            target_object = data_objects[0]
+
+            # return data
+            file_name = target_object['id'] + target_object['extension']
+            return send_from_directory(file_directory, file_name)
+
+        # add upload handler
+        @self.route('/upload', methods=['POST'])
+        def upload():
+            file_uuid = str(uuid1())
+
+            # get current project path
+            opened_projects = list(filter(lambda x: x['status'] == 'open', app_status['projects']))
+            if len(opened_projects) == 0:
+                return make_response('no open project available', 500)
+
+            current_project = opened_projects[0]
+            upload_path = path.join('projects', current_project['id'], 'data')
+
+            # get file object
+            # TODO read stream and parse data
+            # TODO update progress correctly
+            files = request.files
+
+            if 'file' not in files.keys():
+                return make_response('no file uploaded', 500)
+
+            file = files['file']
+            file_name = file.filename
+
+            # add job to app status
+            job = app_status['jobs'].append({
+                'id': file_uuid,
+                'type': 'upload',
+                'progress': 0,
+                'filename': file_name,
+                'created': int(time()),
+                'finished': None
+            })
+
+            if not path.exists(upload_path):
+                mkdir(upload_path)
+
+            # open file handler
+            file_name, file_extension = path.splitext(file_name)
+            file_path = path.join(upload_path, f'{file_uuid}{file_extension}')
+
+            with open(file_path, 'wb') as file_handler:
+                # prepare some properties
+                file_size = int(request.form['size']) if 'size' in request.form.keys() else None
+                file_stream = file.stream
+
+                # define handler function
+                def file_write():
+                    data = file_stream.read(262144)  # 256 kiB
+                    file_handler.write(data)
+                    return len(data)
+
+                # transfer blocks to storage and update progress
+                progress = 0
+                while True:
+                    read_bytes = tpool.execute(file_write)
+                    progress += read_bytes
+
+                    if file_size is not None:
+                        job['progress'] = progress / file_size
+
+                    if read_bytes == 0:
+                        break
+
+            # set progress to 1 after upload is done
+            job['progress'] = 1
+            job['finished'] = int(time())
+
+            # add to project files
+            if 'data' not in current_project:
+                current_project['data'] = []
+
+            current_project['data'].append({
+                'id': file_uuid,
+                'name': file_name,
+                'extension': file_extension,
+                'size': progress,
+                'created': job['created']
+            })
+
+            # create response and allow cors if needed
+            response = make_response()
+            if cors:
+                response.headers['Access-Control-Allow-Origin'] = '*'
+
+            return response

+ 8 - 3
pycs/frontend/WebServer.py

@@ -4,23 +4,28 @@ import eventlet
 import socketio
 
 from pycs.ApplicationStatus import ApplicationStatus
+from pycs.frontend.FileProvider import FileProvider
 
 
 class WebServer:
     def __init__(self, app_status: ApplicationStatus):
         # initialize web server
+        # TODO svg files need the correct mime type to be displayed correctly
         if exists('webui/index.html'):
             print('production build')
+            files = FileProvider(app_status)
             self.__sio = socketio.Server()
-            self.__app = socketio.WSGIApp(self.__sio, static_files={'/': 'webui/'})
+            self.__app = socketio.WSGIApp(self.__sio, files, static_files={'/': 'webui/'})
         elif exists('webui/dist/index.html'):
             print('production build')
+            files = FileProvider(app_status)
             self.__sio = socketio.Server()
-            self.__app = socketio.WSGIApp(self.__sio, static_files={'/': 'webui/dist/'})
+            self.__app = socketio.WSGIApp(self.__sio, files, static_files={'/': 'webui/dist/'})
         else:
             print('development build')
+            files = FileProvider(app_status, cors=True)
             self.__sio = socketio.Server(cors_allowed_origins='*')
-            self.__app = socketio.WSGIApp(self.__sio)
+            self.__app = socketio.WSGIApp(self.__sio, files)
 
         # save every change in application status and send it to the client
         app_status.subscribe(self.__update_application_status, immediate=True)

+ 5 - 1
pycs/observable/ObservableList.py

@@ -21,5 +21,9 @@ class ObservableList(list, Observable):
         Observable._notify(self)
 
     def append(self, value):
-        super().append(Observable.create(value, self))
+        obs = Observable.create(value, self)
+
+        super().append(obs)
         Observable._notify(self)
+
+        return obs

+ 4 - 2
test/test_application_status.py

@@ -7,9 +7,10 @@ class TestApplicationStatus(unittest.TestCase):
     def test_load_default(self):
         aso = ApplicationStatus()
         self.assertEqual({
+            'settings': {},
             'models': {},
             'projects': [],
-            'settings': {}
+            'jobs': []
         }, aso)
 
     def test_load_from_object(self):
@@ -20,9 +21,10 @@ class TestApplicationStatus(unittest.TestCase):
 
         aso = ApplicationStatus(settings=settings)
         self.assertEqual({
+            'settings': settings,
             'models': {},
             'projects': [],
-            'settings': settings
+            'jobs': []
         }, aso)
 
 

+ 7 - 0
webui/src/App.vue

@@ -21,12 +21,17 @@
                              :status="status"
                              :socket="socket"
                              v-on:open="show('settings')"/>
+
         <template v-else>
           <project-settings-window v-if="window.content === 'settings'"
                                    :current-project="currentProject"
                                    :current-project-path="currentProjectPath"
                                    :status="status"
                                    :socket="socket"/>
+
+          <project-data-window v-if="window.content === 'add_data'"
+                               :status="status"/>
+
           <about-window v-if="window.content === 'about'"/>
         </template>
       </div>
@@ -41,10 +46,12 @@ import TopNavigationBar from "@/components/window/top-navigation-bar";
 import SideNavigationBar from "@/components/window/side-navigation-bar";
 import ProjectSettingsWindow from "@/components/projects/project-settings-window";
 import AboutWindow from "@/components/other/about-window";
+import ProjectDataWindow from "@/components/projects/project-data-window";
 
 export default {
   name: 'App',
   components: {
+    ProjectDataWindow,
     AboutWindow,
     ProjectSettingsWindow,
     SideNavigationBar,

+ 4 - 0
webui/src/assets/icons/package-dependencies.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
+    <path fill-rule="evenodd"
+          d="M6.122.392a1.75 1.75 0 011.756 0l5.25 3.045c.54.313.872.89.872 1.514V7.25a.75.75 0 01-1.5 0V5.677L7.75 8.432v6.384a1 1 0 01-1.502.865L.872 12.563A1.75 1.75 0 010 11.049V4.951c0-.624.332-1.2.872-1.514L6.122.392zM7.125 1.69l4.63 2.685L7 7.133 2.245 4.375l4.63-2.685a.25.25 0 01.25 0zM1.5 11.049V5.677l4.75 2.755v5.516l-4.625-2.683a.25.25 0 01-.125-.216zm11.672-.282a.75.75 0 10-1.087-1.034l-2.378 2.5a.75.75 0 000 1.034l2.378 2.5a.75.75 0 101.087-1.034L11.999 13.5h3.251a.75.75 0 000-1.5h-3.251l1.173-1.233z"></path>
+</svg>

+ 124 - 0
webui/src/components/base/file-input.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="file-input"
+       :class="{ active: drag }"
+       @dragenter="dragenter"
+       @dragleave="dragleave"
+       @drop="drop">
+    <input type="file"
+           multiple
+           @change="change">
+
+    <img src="@/assets/icons/package-dependencies.svg">
+    <div class="text">
+      Click or drop a file here to upload it to the current project directory.
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "file-input",
+  props: ['url'],
+  data: function() {
+    return {
+      drag: false
+    }
+  },
+  methods: {
+    change: function(event) {
+      event.preventDefault();
+      this.drag = false;
+
+      const result = [];
+      for (let i = 0; i < event.target.files.length; i++) {
+        result.push(event.target.files[i]);
+      }
+
+      this.upload(result);
+    },
+    dragenter: function(event) {
+      event.preventDefault();
+      this.drag = true;
+    },
+    dragleave: function(event) {
+      event.preventDefault();
+      this.drag = false;
+    },
+    drop: function(event) {
+      event.preventDefault();
+      this.drag = false;
+
+      if (event.dataTransfer.items) {
+        const result = [];
+        for (let i = 0; i < event.dataTransfer.items.length; i++) {
+          if (event.dataTransfer.items[i].kind === 'file') {
+            const file = event.dataTransfer.items[i].getAsFile();
+            result.push(file);
+          }
+        }
+
+        this.upload(result);
+      }
+    },
+    upload: function(files) {
+      for (let file of files) {
+        const form = new FormData();
+        form.append('file', file);
+        form.append('size', file.size);
+
+        fetch(this.url, {
+          method: 'POST',
+          body: form
+        })
+            .then(() => console.log('done'))
+            .catch(e => console.log(e));
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.file-input {
+  position: relative;
+
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  padding: 2rem;
+
+  border: 1px dashed var(--primary);
+  border-radius: 1rem;
+
+  transition: background-color 2s, color 1s;
+}
+
+.file-input.active {
+  background-color: var(--primary);
+  color: white;
+}
+
+input {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+
+  z-index: 9;
+  opacity: 0;
+}
+
+img {
+  width: 3rem;
+  margin-bottom: 1rem;
+  opacity: 0.6;
+  transition: opacity 1s, filter 1s;
+}
+
+.file-input.active img {
+  opacity: 1;
+  filter: invert(1);
+}
+</style>

+ 33 - 0
webui/src/components/base/progress-bar.vue

@@ -0,0 +1,33 @@
+<template>
+  <div class="progress-bar">
+    <div class="bar" :style="{ width: width }"></div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "progress-bar",
+  props: ['progress'],
+  computed: {
+    width: function() {
+      return (this.progress * 100) + '%';
+    }
+  }
+}
+</script>
+
+<style scoped>
+.progress-bar {
+  border: 1px solid rgba(0, 0, 0, 0.4);
+  border-radius: 0.2rem;
+  position: relative;
+}
+
+.bar {
+  background-color: var(--primary);
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+</style>

+ 67 - 0
webui/src/components/projects/project-data-window.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="project-data-window">
+    <!-- TODO use valid url -->
+    <file-input :url="url"></file-input>
+
+    <template v-if="uploads.length > 0">
+      <h1>Uploads</h1>
+
+      <div class="uploads">
+        <template v-for="up in uploads">
+          <div :key="up.id + '-div'">{{ up.filename }}</div>
+          <progress-bar :key="up.id + '-progress'" :progress="up.progress"></progress-bar>
+        </template>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import FileInput from "@/components/base/file-input";
+import ProgressBar from "@/components/base/progress-bar";
+
+export default {
+  name: "project-data-window",
+  components: {ProgressBar, FileInput},
+  props: ['status'],
+  data: function() {
+    return {
+      'url': 'http://' + window.location.hostname + ':5000/upload'
+    }
+  },
+  computed: {
+    uploads: function() {
+      const filtered = this.status.jobs.filter(x => x.type === 'upload');
+      filtered.sort(this.sortUploads);
+      return filtered;
+    }
+  },
+  methods: {
+    sortUploads: function(u, v) {
+      if (u.created < v.created)
+        return 1;
+      else
+        return -1;
+    }
+  }
+}
+</script>
+
+<style scoped>
+.project-data-window {
+  padding: 1rem;
+}
+
+h1 {
+  margin-bottom: 1rem;
+}
+
+.uploads {
+  display: grid;
+  grid-template-columns: auto 1fr;
+}
+
+.progress-bar {
+  margin-left: 1rem;
+}
+</style>

+ 7 - 0
webui/src/components/window/side-navigation-bar.vue

@@ -15,6 +15,13 @@
         <span>Project Settings</span>
       </div>
 
+      <div class="item"
+           :class="{active: window.content === 'add_data', inactive: !projectPath}"
+           @click="ifProjectIsOpened(show, 'add_data')">
+        <img src="@/assets/icons/package-dependencies.svg">
+        <span>Add Data</span>
+      </div>
+
       <div class="item"
            :class="{inactive: !projectPath}"
            @click="close">