├── preview.png ├── babel.config.js ├── src ├── shims-vue.d.ts ├── plugins │ └── vuetify.ts ├── main.ts ├── shims-tsx.d.ts ├── utils.js └── App.vue ├── public ├── fonts │ ├── roboto-v19-latin-100.woff │ ├── roboto-v19-latin-100.woff2 │ ├── roboto-v19-latin-regular.woff │ └── roboto-v19-latin-regular.woff2 ├── icons │ ├── success.svg │ ├── enqueued.svg │ ├── error.svg │ └── processing.svg └── index.html ├── .gitignore ├── Dockerfile ├── tsconfig.json ├── LICENSE ├── README.md ├── package.json └── backend └── server.py /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cemfi/measure-detector/HEAD/preview.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | 4 | export default Vue; 5 | } 6 | -------------------------------------------------------------------------------- /public/fonts/roboto-v19-latin-100.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cemfi/measure-detector/HEAD/public/fonts/roboto-v19-latin-100.woff -------------------------------------------------------------------------------- /public/fonts/roboto-v19-latin-100.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cemfi/measure-detector/HEAD/public/fonts/roboto-v19-latin-100.woff2 -------------------------------------------------------------------------------- /public/fonts/roboto-v19-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cemfi/measure-detector/HEAD/public/fonts/roboto-v19-latin-regular.woff -------------------------------------------------------------------------------- /public/fonts/roboto-v19-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cemfi/measure-detector/HEAD/public/fonts/roboto-v19-latin-regular.woff2 -------------------------------------------------------------------------------- /src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib'; 3 | import 'vuetify/src/stylus/app.styl'; 4 | 5 | Vue.use(Vuetify, {}); 6 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import './plugins/vuetify'; 3 | import App from './App.vue'; 4 | 5 | Vue.config.productionTip = false; 6 | 7 | new Vue({ 8 | render: h => h(App), 9 | }).$mount('#app'); 10 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | __pycache__ 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | # Project specific files 24 | backend/model.pb 25 | dist -------------------------------------------------------------------------------- /public/icons/success.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/icons/enqueued.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/icons/error.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM node:lts-alpine as build-stage 3 | WORKDIR /usr/src/app 4 | COPY . . 5 | RUN npm install 6 | RUN npm run build 7 | 8 | 9 | # production stage 10 | FROM tensorflow/tensorflow:1.13.1-py3 as production-stage 11 | RUN apt-get update && apt-get install -y curl 12 | RUN pip3 install pillow hug gunicorn 13 | RUN mkdir -p /usr/src/app 14 | 15 | WORKDIR /usr/src/app 16 | 17 | RUN curl -L https://github.com/OMR-Research/MeasureDetector/releases/download/v1.0/2019-05-16_faster-rcnn-inception-resnet-v2.pb --output model.pb 18 | 19 | COPY backend ./ 20 | COPY --from=build-stage /usr/src/app/dist ./ 21 | 22 | EXPOSE 8000 23 | 24 | # ENTRYPOINT ["/bin/bash", "startup.sh"] 25 | CMD ["gunicorn", "--bind=0.0.0.0:8000", "--timeout=180", "--workers=2", "server:__hug_wsgi__"] 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "resolveJsonModule": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env", 17 | "vuetify" 18 | ], 19 | "typeRoots": [ 20 | "./@types", 21 | "./node_modules/@types" 22 | ], 23 | "paths": { 24 | "@/*": [ 25 | "src/*" 26 | ] 27 | }, 28 | "lib": [ 29 | "esnext", 30 | "dom", 31 | "dom.iterable", 32 | "scripthost" 33 | ] 34 | }, 35 | "include": [ 36 | "src/**/*.ts", 37 | "src/**/*.tsx", 38 | "src/**/*.vue", 39 | "tests/**/*.ts", 40 | "tests/**/*.tsx", 41 | "@types/**/*.ts" 42 | ], 43 | "exclude": [ 44 | "node_modules" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 cemfi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Deep Optical Measure Detector 4 | 5 | This is a self contained package of the *Deep Optical Measure Detector*. It can be utilized to generate measure annotations in the MEI format (compatible to v3 & v4) for handwritten and typeset score images. 6 | 7 | ## Disclaimer 8 | This code was meant as a quick functional demonstration. It is **not** production ready or even documented! Handle with care. 9 | 10 | ## How to Run 11 | 1. Make sure to have [Docker](https://www.docker.com/) installed and running properly with at least 4 GB of RAM assigned to Docker. 12 | 13 | 2. Run the container in a terminal: 14 | ```bash 15 | docker run -p 8000:8000 -i --rm cemfi/measure-detector 16 | ``` 17 | 18 | 3. Go to [http://localhost:8000](http://localhost:8000) and drop some images. Be patient, the detection is computationally pretty heavy. 19 | 20 | ## Acknowledgements 21 | The DNN model was trained by [Alexander Pacha](https://github.com/apacha/), see [this project](https://github.com/OMR-Research/MeasureDetector/). 22 | Thanks also to [Alexander Leemhuis](https://github.com/AlexL164) for meticulously annotating hundreds of score images for the dataset. 23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Measure Detector 10 | 35 | 36 | 37 | 38 | 42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { ulid } from 'ulid'; 4 | 5 | function dynamicSort(property) { 6 | let sortOrder = 1; 7 | if (property[0] === '-') { 8 | sortOrder = -1; 9 | property = property.substr(1); 10 | } 11 | return function (a, b) { 12 | const result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0; 13 | return result * sortOrder; 14 | }; 15 | } 16 | 17 | function getTemplate(date, version) { 18 | return ` 19 | 20 | 21 | 22 | </titleStmt> 23 | <pubStmt/> 24 | </fileDesc> 25 | <encodingDesc> 26 | <appInfo> 27 | <application isodate="${date}" version="${version}"> 28 | <name>Deep Optical Measure Detector</name> 29 | <p>Measures detected with Deep Optical Measure Detector</p> 30 | </application> 31 | </appInfo> 32 | </encodingDesc> 33 | </meiHead> 34 | <music> 35 | <facsimile> 36 | </facsimile> 37 | <body> 38 | <mdiv xml:id="mdiv_${ulid()}" n="1" label=""> 39 | <score> 40 | <scoreDef/> 41 | <section> 42 | <pb n="1"/> 43 | </section> 44 | </score> 45 | </mdiv> 46 | </body> 47 | </music> 48 | </mei>` 49 | } 50 | 51 | export default { dynamicSort, getTemplate }; 52 | -------------------------------------------------------------------------------- /public/icons/processing.svg: -------------------------------------------------------------------------------- 1 | <svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="cog" class="svg-inline--fa fa-cog fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#f96d00" d="M452.515 237l31.843-18.382c9.426-5.441 13.996-16.542 11.177-27.054-11.404-42.531-33.842-80.547-64.058-110.797-7.68-7.688-19.575-9.246-28.985-3.811l-31.785 18.358a196.276 196.276 0 0 0-32.899-19.02V39.541a24.016 24.016 0 0 0-17.842-23.206c-41.761-11.107-86.117-11.121-127.93-.001-10.519 2.798-17.844 12.321-17.844 23.206v36.753a196.276 196.276 0 0 0-32.899 19.02l-31.785-18.358c-9.41-5.435-21.305-3.877-28.985 3.811-30.216 30.25-52.654 68.265-64.058 110.797-2.819 10.512 1.751 21.613 11.177 27.054L59.485 237a197.715 197.715 0 0 0 0 37.999l-31.843 18.382c-9.426 5.441-13.996 16.542-11.177 27.054 11.404 42.531 33.842 80.547 64.058 110.797 7.68 7.688 19.575 9.246 28.985 3.811l31.785-18.358a196.202 196.202 0 0 0 32.899 19.019v36.753a24.016 24.016 0 0 0 17.842 23.206c41.761 11.107 86.117 11.122 127.93.001 10.519-2.798 17.844-12.321 17.844-23.206v-36.753a196.34 196.34 0 0 0 32.899-19.019l31.785 18.358c9.41 5.435 21.305 3.877 28.985-3.811 30.216-30.25 52.654-68.266 64.058-110.797 2.819-10.512-1.751-21.613-11.177-27.054L452.515 275c1.22-12.65 1.22-25.35 0-38zm-52.679 63.019l43.819 25.289a200.138 200.138 0 0 1-33.849 58.528l-43.829-25.309c-31.984 27.397-36.659 30.077-76.168 44.029v50.599a200.917 200.917 0 0 1-67.618 0v-50.599c-39.504-13.95-44.196-16.642-76.168-44.029l-43.829 25.309a200.15 200.15 0 0 1-33.849-58.528l43.819-25.289c-7.63-41.299-7.634-46.719 0-88.038l-43.819-25.289c7.85-21.229 19.31-41.049 33.849-58.529l43.829 25.309c31.984-27.397 36.66-30.078 76.168-44.029V58.845a200.917 200.917 0 0 1 67.618 0v50.599c39.504 13.95 44.196 16.642 76.168 44.029l43.829-25.309a200.143 200.143 0 0 1 33.849 58.529l-43.819 25.289c7.631 41.3 7.634 46.718 0 88.037zM256 160c-52.935 0-96 43.065-96 96s43.065 96 96 96 96-43.065 96-96-43.065-96-96-96zm0 144c-26.468 0-48-21.532-48-48 0-26.467 21.532-48 48-48s48 21.533 48 48c0 26.468-21.532 48-48 48z"></path></svg> 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "measure-detector", 3 | "version": "1.0.3", 4 | "author": { 5 | "name": "Simon Waloschek", 6 | "email": "s.waloschek@cemfi.de" 7 | }, 8 | "contributors": [ 9 | { 10 | "name": "Alexander Pacha", 11 | "email": "alexander.pacha@tuwien.ac.at" 12 | }, 13 | { 14 | "name": "Daniel Röwenstrunk", 15 | "email": "roewenstrunk@uni-paderborn.de" 16 | } 17 | ], 18 | "private": true, 19 | "scripts": { 20 | "serve": "vue-cli-service serve", 21 | "build": "vue-cli-service build", 22 | "lint": "vue-cli-service lint" 23 | }, 24 | "dependencies": { 25 | "axios": "^0.18.1", 26 | "core-js": "^2.6.5", 27 | "file-saver": "^2.0.2", 28 | "jquery": "^3.4.1", 29 | "ulid": "^2.3.0", 30 | "vkbeautify": "^0.99.3", 31 | "vue": "^2.6.10", 32 | "vue-top-progress": "^0.7.0", 33 | "vuetify": "^1.5.5" 34 | }, 35 | "devDependencies": { 36 | "@vue/cli-plugin-babel": "^3.7.0", 37 | "@vue/cli-plugin-eslint": "^3.7.0", 38 | "@vue/cli-plugin-typescript": "^3.7.0", 39 | "@vue/cli-service": "^3.7.0", 40 | "@vue/eslint-config-airbnb": "^4.0.0", 41 | "@vue/eslint-config-typescript": "^4.0.0", 42 | "babel-eslint": "^10.0.1", 43 | "eslint": "^5.16.0", 44 | "eslint-plugin-vue": "^5.0.0", 45 | "stylus": "^0.54.5", 46 | "stylus-loader": "^3.0.1", 47 | "typescript": "^3.4.3", 48 | "vue-cli-plugin-vuetify": "^0.5.0", 49 | "vue-template-compiler": "^2.5.21", 50 | "vuetify-loader": "^1.0.5" 51 | }, 52 | "eslintConfig": { 53 | "root": true, 54 | "env": { 55 | "node": true 56 | }, 57 | "extends": [ 58 | "plugin:vue/essential", 59 | "@vue/airbnb", 60 | "@vue/typescript" 61 | ], 62 | "rules": { 63 | "linebreak-style": 0, 64 | "no-console": 0 65 | }, 66 | "parserOptions": { 67 | "parser": "@typescript-eslint/parser" 68 | } 69 | }, 70 | "postcss": { 71 | "plugins": { 72 | "autoprefixer": {} 73 | } 74 | }, 75 | "browserslist": [ 76 | "> 1%", 77 | "last 2 versions" 78 | ], 79 | "vue": { 80 | "publicPath": "./", 81 | "productionSourceMap": false 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /backend/server.py: -------------------------------------------------------------------------------- 1 | from functools import cmp_to_key 2 | import io 3 | 4 | import hug 5 | import numpy as np 6 | import tensorflow as tf 7 | from PIL import Image 8 | 9 | 10 | # Initialize graph 11 | detection_graph = tf.Graph() 12 | detection_graph.as_default() 13 | od_graph_def = tf.GraphDef() 14 | with tf.gfile.GFile('model.pb', 'rb') as fid: 15 | serialized_graph = fid.read() 16 | od_graph_def.ParseFromString(serialized_graph) 17 | tf.import_graph_def(od_graph_def, name='') 18 | sess = tf.Session() 19 | 20 | 21 | @hug.response_middleware() 22 | def process_data(request, response, resource): 23 | response.set_header('Access-Control-Allow-Origin', '*') 24 | 25 | 26 | def compare_measure_bounding_boxes(self, other): 27 | """Compares bounding boxes of two measures and returns which one should come first""" 28 | if self['ulx'] >= other['ulx'] and self['uly'] >= other['uly']: 29 | return +1 # self after other 30 | elif self['ulx'] < other['ulx'] and self['uly'] < other['uly']: 31 | return -1 # other after self 32 | else: 33 | overlap_y = min(self['lry'] - other['uly'], other['lry'] - self['uly']) \ 34 | / min(self['lry'] - self['uly'], other['lry'] - other['uly']) 35 | if overlap_y >= 0.5: 36 | if self['ulx'] < other['ulx']: 37 | return -1 38 | else: 39 | return 1 40 | else: 41 | if self['ulx'] < other['ulx']: 42 | return 1 43 | else: 44 | return -1 45 | 46 | 47 | def infer(image: np.ndarray): 48 | ops = tf.get_default_graph().get_operations() 49 | all_tensor_names = {output.name for op in ops for output in op.outputs} 50 | tensor_dict = {} 51 | for key in [ 52 | 'num_detections', 53 | 'detection_boxes', 54 | 'detection_scores', 55 | 'detection_classes' 56 | ]: 57 | tensor_name = key + ':0' 58 | 59 | if tensor_name in all_tensor_names: 60 | tensor_dict[key] = tf.get_default_graph().get_tensor_by_name(tensor_name) 61 | 62 | image_tensor = tf.get_default_graph().get_tensor_by_name('image_tensor:0') 63 | 64 | # Run inference 65 | output_dict = sess.run(tensor_dict, feed_dict={image_tensor: np.expand_dims(image, 0)}) 66 | 67 | # All outputs are float32 numpy arrays, so convert types as appropriate 68 | output_dict['num_detections'] = int(output_dict['num_detections'][0]) 69 | output_dict['detection_classes'] = output_dict['detection_classes'][0].astype(np.uint8) 70 | output_dict['detection_boxes'] = output_dict['detection_boxes'][0] 71 | output_dict['detection_scores'] = output_dict['detection_scores'][0] 72 | 73 | return output_dict 74 | 75 | 76 | @hug.static('/') 77 | def user_interface(): 78 | return('/usr/src/app',) 79 | 80 | 81 | @hug.post('/upload') 82 | def detect_measures(body, cors: hug.directives.cors="*"): 83 | """Takes an image file and returns measure bounding boxes as JSON""" 84 | 85 | image = Image.open(io.BytesIO(body['image'])).convert("RGB") 86 | (image_width, image_height) = image.size 87 | image_np = np.array(image) 88 | 89 | output_dict = infer(image_np) 90 | measures = [] 91 | 92 | for idx in range(output_dict['num_detections']): 93 | if output_dict['detection_classes'][idx] == 1 and output_dict['detection_scores'][idx] > 0.5: 94 | y1, x1, y2, x2 = output_dict['detection_boxes'][idx] 95 | 96 | y1 = y1 * image_height 97 | y2 = y2 * image_height 98 | x1 = x1 * image_width 99 | x2 = x2 * image_width 100 | 101 | measures.append({ 102 | 'ulx': x1, 103 | 'uly': y1, 104 | 'lrx': x2, 105 | 'lry': y2 106 | }) 107 | else: 108 | break 109 | 110 | measures.sort(key=cmp_to_key(compare_measure_bounding_boxes)) 111 | 112 | return {'measures': measures} 113 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <v-app style="height: 100%"> 3 | <vue-topprogress ref="topProgress" color="#f96d00" :trickle="false"/> 4 | <v-toolbar app dark color="#222831"> 5 | <v-toolbar-title class="headline"> 6 | <a href="http://www.cemfi.de/" target="_blank" style="text-decoration: none;"> 7 | <span style="color:#f96d00">cemfi.</span> 8 | </a> 9 | <span class="font-weight-thin">Deep Optical Measure Detector</span> 10 | </v-toolbar-title> 11 | <v-spacer></v-spacer> 12 | v{{version}} 13 | </v-toolbar> 14 | 15 | <v-content> 16 | <v-container style="height:100%;" @drop.prevent="onFileDrop" @dragover.prevent> 17 | <v-card style="height:100%;"> 18 | <div v-if="images.length == 0" class="dropZone"> 19 | <span class="display-1">Drop image files here</span> 20 | <p 21 | style="margin-top:20px" 22 | >(All files will be sorted alphanumerically before processing.)</p> 23 | </div> 24 | <v-list v-else dense> 25 | <v-list-tile 26 | avatar 27 | v-for="(image, index) in images" 28 | :key="index" 29 | @click.stop="showImage(index)" 30 | > 31 | <v-list-tile-avatar> 32 | <img 33 | :src="`${publicPath}icons/${image.status}.svg`" 34 | style="width:32px;height:32px" 35 | v-bind:class="{ rotate: image.status === 'processing' }" 36 | > 37 | </v-list-tile-avatar> 38 | <div class="ellipsize-left">{{image.file.name}}</div> 39 | </v-list-tile> 40 | </v-list> 41 | </v-card> 42 | </v-container> 43 | <v-dialog v-model="showViewer" max-width="1000"> 44 | <v-card dark color="#222831" class="pa-3"> 45 | <canvas ref="canvas" style="max-width:100%" @click="showViewer = false"/> 46 | <span class="ellipsize-left" style="font-size:13px">{{ viewerFilename }}</span> 47 | </v-card> 48 | </v-dialog> 49 | </v-content> 50 | </v-app> 51 | </template> 52 | 53 | <script> 54 | import { vueTopprogress } from 'vue-top-progress'; 55 | import axios from 'axios'; 56 | import saveAs from 'file-saver'; 57 | import $ from 'jquery'; 58 | import { ulid } from 'ulid'; 59 | import vkbeautify from 'vkbeautify'; 60 | 61 | import { version } from '../package.json'; 62 | import utils from './utils'; 63 | 64 | axios.defaults.timeout = 180000; // 3 min timeout 65 | 66 | export default { 67 | name: 'App', 68 | components: { 69 | vueTopprogress, 70 | }, 71 | data() { 72 | return { 73 | publicPath: process.env.BASE_URL, 74 | version, 75 | images: [], 76 | countFinished: 0, 77 | showViewer: false, 78 | viewerFilename: null, 79 | }; 80 | }, 81 | methods: { 82 | onFileDrop(event) { 83 | this.images = []; 84 | this.countFinished = 0; 85 | 86 | const files = []; 87 | Array.from(event.dataTransfer.files).forEach((file) => { 88 | files.push(file); 89 | }); 90 | files.sort(utils.dynamicSort('name')); 91 | files.forEach((file) => { 92 | this.images.push({ file, status: 'enqueued' }); 93 | }); 94 | this.$refs.topProgress.start(); 95 | this.processNext(); 96 | }, 97 | showImage(index) { 98 | const image = this.images[index]; 99 | const reader = new FileReader(); 100 | 101 | this.viewerFilename = image.file.name; 102 | this.$refs.canvas.width = 0; 103 | this.$refs.canvas.height = 0; 104 | 105 | reader.onload = (event) => { 106 | const imageData = new Image(); 107 | imageData.src = reader.result; 108 | imageData.onload = () => { 109 | this.$refs.canvas.width = imageData.width; 110 | this.$refs.canvas.height = imageData.height; 111 | const ctx = this.$refs.canvas.getContext('2d'); 112 | ctx.drawImage(imageData, 0, 0); 113 | if (image.measures !== undefined) { 114 | ctx.lineWidth = '3'; 115 | ctx.strokeStyle = '#f96d00'; 116 | ctx.fillStyle = '#22283144'; 117 | image.measures.forEach((measure) => { 118 | ctx.beginPath(); 119 | ctx.rect( 120 | measure.ulx, 121 | measure.uly, 122 | measure.lrx - measure.ulx, 123 | measure.lry - measure.uly, 124 | ); 125 | ctx.fill(); 126 | }); 127 | image.measures.forEach((measure) => { 128 | ctx.beginPath(); 129 | ctx.rect( 130 | measure.ulx, 131 | measure.uly, 132 | measure.lrx - measure.ulx, 133 | measure.lry - measure.uly, 134 | ); 135 | ctx.stroke(); 136 | }); 137 | } 138 | }; 139 | this.showViewer = true; 140 | }; 141 | reader.readAsDataURL(image.file); 142 | }, 143 | processNext() { 144 | const nextElement = this.images.find( 145 | element => element.status === 'enqueued', 146 | ); 147 | 148 | // All elements processed 149 | if (nextElement === undefined) { 150 | this.generateMei(); 151 | return; 152 | } 153 | 154 | nextElement.status = 'processing'; 155 | const reader = new FileReader(); 156 | reader.onload = (event) => { 157 | const imageData = new Image(); 158 | imageData.src = reader.result; 159 | imageData.onload = () => { 160 | nextElement.width = imageData.width; 161 | nextElement.height = imageData.height; 162 | }; 163 | }; 164 | reader.readAsDataURL(nextElement.file); 165 | 166 | const formData = new FormData(); 167 | formData.append('image', nextElement.file); 168 | axios 169 | .post('/upload', formData, { 170 | headers: { 171 | // 'Access-Control-Allow-Origin': '*', 172 | 'Content-Type': 'multipart/form-data', 173 | }, 174 | }) 175 | .then((response) => { 176 | nextElement.measures = response.data.measures; 177 | nextElement.status = 'success'; 178 | this.countFinished += 1; 179 | this.$refs.topProgress.progress = 100 * (this.countFinished / this.images.length); 180 | this.processNext(); 181 | }) 182 | .catch((error) => { 183 | nextElement.status = 'error'; 184 | this.countFinished += 1; 185 | this.$refs.topProgress.progress = 100 * (this.countFinished / this.images.length); 186 | console.log(error); 187 | this.processNext(); 188 | }); 189 | }, 190 | generateMei() { 191 | const template = utils.getTemplate( 192 | new Date().toISOString(), 193 | this.version, 194 | ); 195 | const meiXml = $($.parseXML(template)); 196 | 197 | const meiFacsimile = meiXml.find('facsimile').first(); 198 | const meiSection = meiXml.find('section').first(); 199 | 200 | this.images.forEach((page, p) => { 201 | const meiSurfaceId = `surface_${ulid()}`; 202 | 203 | if (page.status === 'success') { 204 | meiFacsimile.append( 205 | `<surface xml:id="${meiSurfaceId}" 206 | n="${p + 1}" 207 | ulx="0" 208 | uly="0" 209 | lrx="${page.width - 1}" 210 | lry="${page.height - 1}" 211 | />`, 212 | ); 213 | const meiSurface = meiFacsimile.find('surface').last(); 214 | 215 | meiSurface.append( 216 | `<graphic xml:id="graphic_${ulid()}" 217 | target="${page.file.name}" 218 | width="${page.width}px" 219 | height="${page.height}px" 220 | />`, 221 | ); 222 | 223 | page.measures.forEach((measure, m) => { 224 | const meiZoneId = `zone_${ulid()}`; 225 | meiSurface.append( 226 | `<zone xml:id="${meiZoneId}" 227 | type="measure" 228 | ulx="${Math.floor(measure.ulx)}" 229 | uly="${Math.floor(measure.uly)}" 230 | lrx="${Math.floor(measure.lrx)}" 231 | lry="${Math.floor(measure.lry)}" 232 | />`, 233 | ); 234 | 235 | meiSection.append( 236 | `<measure xml:id="measure_${ulid()}" 237 | n="${m + 1}" 238 | label="${m + 1}" 239 | facs="#${meiZoneId}" 240 | />`, 241 | ); 242 | 243 | if ( 244 | page.measures.length > m + 1 245 | && page.measures[m + 1].ulx < measure.ulx 246 | ) { 247 | meiSection.append('<sb />'); 248 | } 249 | }); 250 | 251 | meiSection.append(`<pb xml:id="pb_${ulid()}" n="${p + 2}" facs="#${meiSurfaceId}"/>`); 252 | } 253 | }); 254 | 255 | let meiString = new XMLSerializer().serializeToString(meiXml.get(0)); 256 | meiString = vkbeautify.xml(meiString.replace(/ xmlns=""/g, '')); 257 | saveAs( 258 | new Blob([meiString], { type: 'text/xml;charset=utf-8' }), 259 | 'measure_annotations.xml', 260 | ); 261 | }, 262 | }, 263 | }; 264 | </script> 265 | 266 | <style> 267 | .ellipsize-left { 268 | /* Standard CSS ellipsis */ 269 | white-space: nowrap; 270 | overflow: hidden; 271 | text-overflow: ellipsis; 272 | width: 100%; 273 | 274 | /* Beginning of string */ 275 | direction: rtl; 276 | text-align: left; 277 | } 278 | 279 | .dropZone { 280 | position: absolute; 281 | top: 50%; 282 | left: 50%; 283 | transform: translate(-50%, -50%); 284 | color: #222831; 285 | text-align: center; 286 | } 287 | 288 | .rotate { 289 | animation: rotation 2s infinite linear; 290 | } 291 | 292 | @keyframes rotation { 293 | from { 294 | transform: rotate(0deg); 295 | } 296 | to { 297 | transform: rotate(359deg); 298 | } 299 | } 300 | </style> 301 | --------------------------------------------------------------------------------