jbilcke-hf HF staff commited on
Commit
b66d95a
1 Parent(s): 8a85f77
Files changed (3) hide show
  1. package-lock.json +72 -0
  2. package.json +1 -0
  3. src/index.mts +105 -83
package-lock.json CHANGED
@@ -14,6 +14,7 @@
14
  "@types/express": "^4.17.17",
15
  "@types/uuid": "^9.0.2",
16
  "archiver": "^6.0.1",
 
17
  "eventsource-parser": "^1.0.0",
18
  "express": "^4.18.2",
19
  "express-fileupload": "^1.4.0",
@@ -272,6 +273,21 @@
272
  "version": "3.2.4",
273
  "license": "MIT"
274
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  "node_modules/b4a": {
276
  "version": "1.6.4",
277
  "license": "ISC"
@@ -443,6 +459,17 @@
443
  "simple-swizzle": "^0.2.2"
444
  }
445
  },
 
 
 
 
 
 
 
 
 
 
 
446
  "node_modules/commander": {
447
  "version": "2.20.3",
448
  "license": "MIT"
@@ -585,6 +612,14 @@
585
  "node": ">=4.0.0"
586
  }
587
  },
 
 
 
 
 
 
 
 
588
  "node_modules/depd": {
589
  "version": "2.0.0",
590
  "license": "MIT",
@@ -835,6 +870,38 @@
835
  "node": ">=0.8.0"
836
  }
837
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
838
  "node_modules/formdata-polyfill": {
839
  "version": "4.0.10",
840
  "license": "MIT",
@@ -1434,6 +1501,11 @@
1434
  "node": ">= 0.10"
1435
  }
1436
  },
 
 
 
 
 
1437
  "node_modules/pump": {
1438
  "version": "3.0.0",
1439
  "license": "MIT",
 
14
  "@types/express": "^4.17.17",
15
  "@types/uuid": "^9.0.2",
16
  "archiver": "^6.0.1",
17
+ "axios": "^1.5.1",
18
  "eventsource-parser": "^1.0.0",
19
  "express": "^4.18.2",
20
  "express-fileupload": "^1.4.0",
 
273
  "version": "3.2.4",
274
  "license": "MIT"
275
  },
276
+ "node_modules/asynckit": {
277
+ "version": "0.4.0",
278
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
279
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
280
+ },
281
+ "node_modules/axios": {
282
+ "version": "1.5.1",
283
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
284
+ "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
285
+ "dependencies": {
286
+ "follow-redirects": "^1.15.0",
287
+ "form-data": "^4.0.0",
288
+ "proxy-from-env": "^1.1.0"
289
+ }
290
+ },
291
  "node_modules/b4a": {
292
  "version": "1.6.4",
293
  "license": "ISC"
 
459
  "simple-swizzle": "^0.2.2"
460
  }
461
  },
462
+ "node_modules/combined-stream": {
463
+ "version": "1.0.8",
464
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
465
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
466
+ "dependencies": {
467
+ "delayed-stream": "~1.0.0"
468
+ },
469
+ "engines": {
470
+ "node": ">= 0.8"
471
+ }
472
+ },
473
  "node_modules/commander": {
474
  "version": "2.20.3",
475
  "license": "MIT"
 
612
  "node": ">=4.0.0"
613
  }
614
  },
615
+ "node_modules/delayed-stream": {
616
+ "version": "1.0.0",
617
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
618
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
619
+ "engines": {
620
+ "node": ">=0.4.0"
621
+ }
622
+ },
623
  "node_modules/depd": {
624
  "version": "2.0.0",
625
  "license": "MIT",
 
870
  "node": ">=0.8.0"
871
  }
872
  },
873
+ "node_modules/follow-redirects": {
874
+ "version": "1.15.3",
875
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
876
+ "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
877
+ "funding": [
878
+ {
879
+ "type": "individual",
880
+ "url": "https://github.com/sponsors/RubenVerborgh"
881
+ }
882
+ ],
883
+ "engines": {
884
+ "node": ">=4.0"
885
+ },
886
+ "peerDependenciesMeta": {
887
+ "debug": {
888
+ "optional": true
889
+ }
890
+ }
891
+ },
892
+ "node_modules/form-data": {
893
+ "version": "4.0.0",
894
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
895
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
896
+ "dependencies": {
897
+ "asynckit": "^0.4.0",
898
+ "combined-stream": "^1.0.8",
899
+ "mime-types": "^2.1.12"
900
+ },
901
+ "engines": {
902
+ "node": ">= 6"
903
+ }
904
+ },
905
  "node_modules/formdata-polyfill": {
906
  "version": "4.0.10",
907
  "license": "MIT",
 
1501
  "node": ">= 0.10"
1502
  }
1503
  },
1504
+ "node_modules/proxy-from-env": {
1505
+ "version": "1.1.0",
1506
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1507
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
1508
+ },
1509
  "node_modules/pump": {
1510
  "version": "3.0.0",
1511
  "license": "MIT",
package.json CHANGED
@@ -17,6 +17,7 @@
17
  "@types/express": "^4.17.17",
18
  "@types/uuid": "^9.0.2",
19
  "archiver": "^6.0.1",
 
20
  "eventsource-parser": "^1.0.0",
21
  "express": "^4.18.2",
22
  "express-fileupload": "^1.4.0",
 
17
  "@types/express": "^4.17.17",
18
  "@types/uuid": "^9.0.2",
19
  "archiver": "^6.0.1",
20
+ "axios": "^1.5.1",
21
  "eventsource-parser": "^1.0.0",
22
  "express": "^4.18.2",
23
  "express-fileupload": "^1.4.0",
src/index.mts CHANGED
@@ -1,15 +1,12 @@
1
- import express from "express"
2
- import fileUpload from "express-fileupload"
3
- import path from "path"
4
- import os from "os"
5
- import fs from "fs"
6
- import archiver from "archiver"
7
- import ffmpeg from "fluent-ffmpeg"
8
-
9
- // import { initFolders } from "./initFolders.mts"
10
- import { runColmap, ColmapOptions } from "./colmap.mts"
11
-
12
- // initFolders()
13
 
14
  declare module 'express-serve-static-core' {
15
  interface Request {
@@ -17,87 +14,112 @@ declare module 'express-serve-static-core' {
17
  }
18
  }
19
 
20
- const app = express()
21
- const port = 7860
22
 
23
- const maxActiveRequests = 4
24
- let activeRequests = 0
25
 
26
- app.use(express.json({limit: '50mb'}))
27
- app.use(express.urlencoded({limit: '50mb', extended: true}))
28
- app.use(fileUpload())
29
 
30
- app.post("/", async (req, res) => {
31
- if (activeRequests >= maxActiveRequests) {
32
- res.status(503).json({message: "Service Unavailable: Max concurrent requests reached. Please try again later"}).end();
33
- return
34
  }
35
- activeRequests++
36
-
37
- if (!req.files || !req.files.data || req.files.data.mimetype !== 'video/mp4') {
38
- res.status(400).json({error: "Missing or invalid data file in request"}).end()
39
- return
40
- }
41
-
42
- let options: ColmapOptions = req.body
43
- let dataFile: fileUpload.UploadedFile = req.files.data
44
 
45
- let projectTempDir = path.join(os.tmpdir(), Math.random().toString().slice(2))
46
- let outputTempDir = path.join(os.tmpdir(), Math.random().toString().slice(2))
47
 
48
  try {
49
- fs.mkdirSync(projectTempDir)
50
- fs.mkdirSync(outputTempDir)
51
-
52
- await dataFile.mv(path.join(projectTempDir, dataFile.name))
53
-
54
- let imageFolder = path.join(projectTempDir, 'images');
55
- fs.mkdirSync(imageFolder)
56
-
57
- await new Promise((resolve, reject) => {
58
- ffmpeg(path.join(projectTempDir, dataFile.name))
59
- .outputOptions('-vf', 'fps=1') // Change this value depending on the number of frames you want from video.
60
- .output(path.join(imageFolder, 'image-%03d.png'))
61
- .on('end', resolve)
62
- .on('error', reject)
63
- .run()
64
- })
65
-
66
- options.projectPath = projectTempDir
67
- options.workspacePath = projectTempDir
68
- options.imagePath = imageFolder
69
-
70
- const result = await runColmap(options)
71
-
72
- let outputFilePath = path.join(outputTempDir, 'output.zip')
73
- let output = fs.createWriteStream(outputFilePath)
74
- let archive = archiver('zip')
75
-
76
- archive.directory(outputTempDir, false)
77
- archive.pipe(output)
78
- await archive.finalize()
79
-
80
- res.status(200)
81
- res.download(outputFilePath, 'output.zip', (error) => {
82
- if (!error) fs.rmSync(projectTempDir, {recursive: true, force: true})
83
- fs.rmSync(outputTempDir, {recursive: true, force: true})
84
- })
85
  } catch (error) {
86
- res.status(500).json({
87
- error: "Couldn't generate pose data",
88
- message: error
89
- }).end()
90
  } finally {
91
- activeRequests--
92
  }
93
  });
94
 
95
- app.get("/", async (req, res) => {
96
- res.status(200)
97
- res.write(`<html><head></head><body>
98
- Campose API is a micro-service used to generate came pose data from a set of images.
99
- </body></html>`)
100
- res.end()
101
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- app.listen(port, () => { console.log(`Open http://localhost:${port}`) })
 
 
 
 
 
 
 
 
 
 
 
1
+ import express, { Request, Response, NextFunction } from "express";
2
+ import path from "path";
3
+ import os from "os";
4
+ import fs from "fs";
5
+ import axios from "axios";
6
+ import fileUpload from "express-fileupload";
7
+ import archiver from "archiver";
8
+ import ffmpeg from "fluent-ffmpeg";
9
+ import { ColmapOptions, runColmap } from "./colmap.mts";
 
 
 
10
 
11
  declare module 'express-serve-static-core' {
12
  interface Request {
 
14
  }
15
  }
16
 
17
+ const app = express();
18
+ const port = 7860;
19
 
20
+ const maxActiveRequests = 4;
21
+ let activeRequests = 0;
22
 
23
+ app.use(express.json({limit: '50mb'}));
24
+ app.use(express.urlencoded({limit: '50mb', extended: true}));
25
+ app.use(fileUpload());
26
 
27
+ app.use("/", async (req: Request, res: Response, _next: NextFunction) => {
28
+ if (activeRequests++ >= maxActiveRequests) {
29
+ return res.status(503).send("Service Unavailable");
 
30
  }
 
 
 
 
 
 
 
 
 
31
 
32
+ const options: ColmapOptions = req.body;
33
+ let dataFile: fileUpload.UploadedFile | string = "";
34
 
35
  try {
36
+ if (req.is("json")) {
37
+ const { data } = await axios.get(req.body.assetUrl, {
38
+ responseType: "arraybuffer",
39
+ });
40
+ dataFile = Buffer.from(data, "binary");
41
+ }
42
+ // Request is not JSON type is file upload request
43
+ else {
44
+ if (!req.files || !req.files.data || req.files.data.mimetype !== 'video/mp4') {
45
+ return res.status(400).send("Missing or invalid data file in request");
46
+ }
47
+ dataFile = req.files.data;
48
+ }
49
+
50
+ const { projectTempDir, outputTempDir, imageFolder } = setupDirectories();
51
+ await handleFileStorage(dataFile, projectTempDir);
52
+
53
+ await generateImagesFromData(projectTempDir, imageFolder);
54
+
55
+ options.projectPath = projectTempDir;
56
+ options.workspacePath = projectTempDir;
57
+ options.imagePath = imageFolder;
58
+
59
+ // note: we don't need to read the result since this is a function having side effects on the file system
60
+ const result = await runColmap(options);
61
+ console.log("result:", result);
62
+
63
+ await createOutputArchive(outputTempDir);
64
+
65
+ res.download(path.join(outputTempDir, 'output.zip'), 'output.zip', (error) => {
66
+ if (!error) fs.rmSync(projectTempDir, {recursive: true, force: true});
67
+ fs.rmSync(outputTempDir, {recursive: true, force: true});
68
+ });
 
 
 
69
  } catch (error) {
70
+ res.status(500).send(`Couldn't generate pose data. Error: ${error}`);
 
 
 
71
  } finally {
72
+ activeRequests--;
73
  }
74
  });
75
 
76
+ app.get("/", async (req: Request, res: Response) => {
77
+ res.send("Campose API is a micro-service used to generate camera pose data from a set of images.");
78
+ });
79
+
80
+ app.listen(port, () => console.log(`Listening at http://localhost:${port}`));
81
+
82
+ function setupDirectories() {
83
+ const projectTempDir = path.join(os.tmpdir(), Math.random().toString().slice(2));
84
+ const outputTempDir = path.join(os.tmpdir(), Math.random().toString().slice(2));
85
+ const imageFolder = path.join(projectTempDir, 'images');
86
+
87
+ fs.mkdirSync(projectTempDir);
88
+ fs.mkdirSync(outputTempDir);
89
+ fs.mkdirSync(imageFolder);
90
+
91
+ return { projectTempDir, outputTempDir, imageFolder };
92
+ }
93
+
94
+ async function handleFileStorage(dataFile: fileUpload.UploadedFile | string, projectTempDir: string) {
95
+ if (typeof dataFile === "string") {
96
+ fs.writeFile(path.join(projectTempDir, "data.mp4"), dataFile, (err) => {
97
+ if (err) throw err;
98
+ });
99
+ } else {
100
+ await dataFile.mv(path.join(projectTempDir, dataFile.name));
101
+ }
102
+ }
103
+
104
+ function generateImagesFromData(projectTempDir: string, imageFolder: string) {
105
+ return new Promise<void>((resolve, reject) => {
106
+ ffmpeg(path.join(projectTempDir, 'data.mp4'))
107
+ .outputOptions('-vf', 'fps=1')
108
+ .output(path.join(imageFolder, 'image-%03d.png'))
109
+ .on('end', resolve)
110
+ .on('error', reject)
111
+ .run()
112
+ });
113
+ }
114
 
115
+ function createOutputArchive(outputTempDir: string) {
116
+ return new Promise<void>((resolve, reject) => {
117
+ const archive = archiver('zip', { zlib: { level: 9 } });
118
+ const output = fs.createWriteStream(path.join(outputTempDir, 'output.zip'));
119
+
120
+ archive.pipe(output);
121
+ archive.directory(path.join(outputTempDir, '/'), '');
122
+ archive.finalize();
123
+ resolve();
124
+ });
125
+ }