├── LICENSE.md ├── README.md ├── example.jpg ├── projection.c └── result.png /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Original Copyright (c) 2016 F.P. Sluiter 4 | Modified (equirectangular remap) Copyright (c) 2017 Philipp Rouast 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # equirectangular-remap 2 | 3 | Generate maps for conversions from spherical to equirectangular video in [ffmpeg](http://ffmpeg.org). 4 | 5 | Works for videos which represent a half sphere - tested with [360fly](http://www.360fly.com). 6 | 7 | Adapted from the example given for ffmpeg's [`RemapFilter`](https://trac.ffmpeg.org/wiki/RemapFilter). 8 | 9 | ## Guide 10 | 11 | ### Building 12 | 13 | 1. Install ffmpeg 14 | 2. Checkout the source of this repository 15 | 3. Build: `$ gcc projection.c -Wall -o project -lm` 16 | 17 | ### Running 18 | 19 | Create maps `example_x.pgm` and `example_y.pgm` for dimensions `400 x 400`: 20 | 21 | ``` 22 | $ ./project -x example_x.pgm -y example_y.pgm -h 400 -w 400 -r 400 -c 400 -m equirectangular --verbose 23 | ``` 24 | 25 | Apply the maps to the image `example.jpg`: 26 | 27 | ``` 28 | $ ffmpeg -i example.jpg -i example_x.pgm -i example_y.pgm -lavfi remap result.png 29 | ``` 30 | 31 | ### Result 32 | 33 | **Example spherical image** 34 | 35 | ![Example](https://github.com/prouast/equirectangular-remap/blob/master/example.jpg?style=centerme) 36 | 37 | **Equirectangular result** 38 | 39 | ![Result](https://github.com/prouast/equirectangular-remap/blob/master/result.png?style=centerme) -------------------------------------------------------------------------------- /example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prouast/equirectangular-remap/7534f99fe41348361801ec30139e13b9f66fab20/example.jpg -------------------------------------------------------------------------------- /projection.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | /* Compile with: gcc projection.c -Wall -o project -lm 9 | * math.h does not like to be linked directly... 10 | * Example call: ./project -x test_x.pgm -y test_y.pgm -h 400 -w 400 -r 400 -c 400 -m equirectangular --verbose 11 | * ./project -x fly360_x.pgm -y fly360_y.pgm -h 1504 -w 1504 -r 752 -c 1504 -m equirectangular --verbose 12 | * Example command: ffmpeg -i input.jpg -i test_x.pgm -i test_y.pgm -lavfi remap out.png 13 | * ffmpeg -i fly360.mp4 -i fly360_x.pgm -i fly360_y.pgm -lavfi remap out.mp4 14 | * 15 | * # Sources 16 | * - https://trac.ffmpeg.org/wiki/RemapFilter 17 | * - https://en.wikipedia.org/wiki/Spherical_coordinate_system 18 | * - https://en.wikipedia.org/wiki/Stereographic_projection 19 | * - https://en.wikipedia.org/wiki/Equirectangular_projection 20 | * - http://paulbourke.net/geometry/transformationprojection/ 21 | */ 22 | 23 | /* Flag set by ‘--verbose’. */ 24 | static int verbose_flag; 25 | 26 | typedef struct double2 { 27 | double x; 28 | double y; 29 | } double2; 30 | 31 | typedef struct double3 { 32 | double x; 33 | double y; 34 | double z; 35 | } double3; 36 | 37 | typedef struct polar2 { 38 | double r; 39 | double theta; 40 | } polar2; 41 | 42 | typedef struct polar3 { 43 | double r; 44 | double theta; 45 | double phi; 46 | } polar3; 47 | 48 | enum CameraMode { 49 | FRONT, 50 | EQUIRECTANGULAR 51 | }; 52 | 53 | typedef struct configuration { 54 | char* xmap_filename; 55 | char* ymap_filename; 56 | int xmap_set; 57 | int ymap_set; 58 | int rows; // target 59 | int cols; // target 60 | int height; // source 61 | int width; // source 62 | int rows_set; 63 | int cols_set; 64 | int height_set; 65 | int width_set; 66 | int crop; 67 | enum CameraMode mode; 68 | double thetaAdj; 69 | } configuration; 70 | 71 | /* Store command line options in configuration */ 72 | configuration parse_options(int argc, char **argv) { 73 | 74 | int c; 75 | configuration po; // to hold parsed options 76 | po.xmap_filename = NULL; 77 | po.ymap_filename = NULL; 78 | po.xmap_set = 0; 79 | po.ymap_set = 0; 80 | po.rows = 0; 81 | po.cols = 0; 82 | po.rows_set = 0; 83 | po.cols_set = 0; 84 | po.height_set = 0; 85 | po.width_set = 0; 86 | po.mode = FRONT; // default 87 | po.thetaAdj = 0; 88 | 89 | while (1) { 90 | 91 | static struct option long_options[] = { 92 | 93 | /* These options set a flag. */ 94 | {"verbose", no_argument, &verbose_flag, 1}, 95 | {"brief", no_argument, &verbose_flag, 0}, 96 | 97 | /* These options don’t set a flag. 98 | We distinguish them by their indices. */ 99 | {"help", no_argument, 0, 'q'}, 100 | 101 | /* options with arg*/ 102 | {"xmap", required_argument, 0, 'x'}, 103 | {"ymap", required_argument, 0, 'y'}, 104 | {"rows", required_argument, 0, 'r'}, // target 105 | {"cols", required_argument, 0, 'c'}, // target 106 | {"height", required_argument, 0, 'h'}, // source 107 | {"width", required_argument, 0, 'w'}, // source 108 | {"mode", required_argument, 0, 'm'}, 109 | {"crop", required_argument, 0, 'b'}, 110 | {"thetaAdj",required_argument, 0, 't'}, 111 | 112 | {0, 0, 0, 0} 113 | }; 114 | 115 | /* getopt_long stores the option index here. */ 116 | int option_index = 0; 117 | 118 | c = getopt_long (argc, argv, "qx:y:r:c:h:w:m:b:t:", 119 | long_options, &option_index); 120 | 121 | /* Detect the end of the options. */ 122 | if (c == -1) 123 | break; 124 | 125 | switch (c) { 126 | 127 | /* If this option set a flag, do nothing else now. */ 128 | case 0: 129 | if (long_options[option_index].flag != 0) break; 130 | printf("option %s", long_options[option_index].name); 131 | if (optarg) printf (" with arg %s", optarg); 132 | printf ("\n"); 133 | break; 134 | 135 | case 'x': 136 | po.xmap_filename = optarg; 137 | po.xmap_set++; 138 | break; 139 | 140 | case 'y': 141 | po.ymap_filename = optarg; 142 | po.ymap_set++; 143 | break; 144 | 145 | case 'h': 146 | po.height = atoi(optarg); 147 | po.height_set++; 148 | break; 149 | 150 | case 'w': 151 | po.width = atoi(optarg); 152 | po.width_set++; 153 | break; 154 | 155 | case 'c': 156 | po.cols = atoi(optarg); 157 | po.cols_set++; 158 | break; 159 | 160 | case 'r': 161 | po.rows = atoi(optarg); 162 | po.rows_set++; 163 | break; 164 | 165 | case 'b': 166 | po.crop = atoi(optarg); 167 | break; 168 | 169 | case 'm': 170 | if (strcmp(optarg, "front") == 0) { 171 | po.mode = FRONT; 172 | } else if (strcmp(optarg, "equirectangular") == 0) { 173 | po.mode = EQUIRECTANGULAR; 174 | } else /* default: */ { 175 | printf("Mode %s not implemented \n",optarg); exit(1); 176 | } 177 | break; 178 | 179 | case 't': 180 | po.thetaAdj = atof(optarg); 181 | break; 182 | 183 | /* getopt_long already printed an error message. */ 184 | case '?': 185 | 186 | case 'q': 187 | printf ("Usage: %s -x|--xmap FILE_x.pgm -y|--ymap FILE_y.pgm -h|--height 300 -w|--width 400 -r|--rows 600 -c|--cols 800 \n", argv[0]); 188 | printf ("h,w is source size, r,c is targetsize \n"); 189 | exit(1); 190 | break; 191 | 192 | default: 193 | abort(); 194 | } 195 | } 196 | 197 | /* Instead of reporting ‘--verbose’ 198 | and ‘--brief’ as they are encountered, 199 | we report the final status resulting from them. */ 200 | if (verbose_flag) { 201 | switch (po.mode) { 202 | case FRONT: printf("Mode: Front proj\n"); break; 203 | case EQUIRECTANGULAR: printf("Mode: Equirectangular proj\n"); break; 204 | default: printf("Mode not in verbose, exiting\n"); exit(1); 205 | } 206 | } 207 | 208 | /* Print any remaining command line arguments (not options). */ 209 | if (optind < argc) { 210 | printf ("ERROR: non-option ARGV-elements: "); 211 | while (optind < argc) 212 | printf ("%s ", argv[optind++]); 213 | putchar ('\n'); 214 | exit(1); 215 | } 216 | 217 | if (po.xmap_set != 1 || po.ymap_set != 1) {printf("ERROR: Xmap and ymap are mandatory arguments and have to appear only once!\ntry --help for help\n\n ");exit(-1);} 218 | if (po.rows_set != 1 || po.cols_set != 1) {printf("ERROR: Target Rows and Cols are mandatory arguments and have to appear only once!\ntry --help for help\n\n ");exit(-1);} 219 | if (po.height_set != 1 || po.width_set != 1) {printf("ERROR: Source Height and Width are mandatory arguments and have to appear only once!\ntry --help for help\n\n ");exit(-1);} 220 | 221 | return po; 222 | } 223 | 224 | #define MAXROWS 4500 225 | #define MAXCOLS 4500 226 | 227 | /* Write to file */ 228 | int pgmWrite_ASCII(char* filename, int rows, int cols, int **image, char* comment_string) { 229 | FILE* file; /* pointer to the file buffer */ 230 | // int maxval; /* maximum value in the image array */ 231 | long nwritten = 0; /* counter for the number of pixels written */ 232 | long x, y; /* for loop counters */ 233 | 234 | /* return 0 if the dimensions are larger than the image array. */ 235 | if (rows > MAXROWS || cols > MAXCOLS) { 236 | printf ("ERROR: row/col specifications larger than image array:\n"); 237 | return (0); 238 | } 239 | 240 | /* open the file; write header and comments specified by the user. */ 241 | if ((file = fopen(filename, "w")) == NULL) { 242 | printf("ERROR: file open failed\n"); 243 | return(0); 244 | } 245 | 246 | fprintf(file,"P2\n"); 247 | 248 | if (comment_string != NULL) fprintf(file,"# %s \n", comment_string); 249 | 250 | /* write the dimensions of the image */ 251 | fprintf(file,"%i %i \n", cols, rows); 252 | 253 | /* NOTE: MAXIMUM VALUE IS WHITE; COLOURS ARE SCALED FROM 0 - */ 254 | /* MAXVALUE IN A .PGM FILE. */ 255 | 256 | /* WRITE MAXIMUM VALUE TO FILE */ 257 | fprintf(file, "%d\n", (int)65535); 258 | 259 | /* Write data */ 260 | for (y = 0; y < rows; y++) { 261 | for (x = 0; x < cols; x++) { 262 | fprintf(file,"%i ", image[y][x]); 263 | nwritten++; 264 | } 265 | fprintf(file, "\n"); 266 | } 267 | fprintf(file, "\n"); 268 | 269 | printf ("\nNumber of pixels total (from rows * cols): %i\n", rows * cols); 270 | printf ("Number of pixels written in file %s: %ld\n\n", filename, nwritten); 271 | 272 | fclose(file); 273 | return(1); 274 | } 275 | 276 | /* So, to get the x’,y’ position for the circular image we will have to first pass the 277 | * coordinates x,y from the rectangular output image to spherical coordinates using the 278 | * first coordinate system, then those to the second shown spherical coordinate system, 279 | * then those to the polar projection and then pass the polar system to cardinal x’,y’. 280 | */ 281 | double2 evaluatePixel_Front(double2 outPos, double2 srcSize) { 282 | double theta, phi; 283 | double3 sphericCoords; 284 | double phi2_over_pi; 285 | double theta2; 286 | double2 inCentered; 287 | 288 | // Convert outcoords to radians (180 = pi, so half a sphere) 289 | theta = (1.0 - outPos.x) * M_PI; 290 | phi = outPos.y * M_PI; 291 | 292 | // Convert outcoords to spherical (x,y,z on unisphere) 293 | sphericCoords.x = cos(theta) * sin(phi); 294 | sphericCoords.y = sin(theta) * sin(phi); 295 | sphericCoords.z = cos(phi); 296 | 297 | // Convert spherical to input coordinates... 298 | theta2 = atan2(-sphericCoords.z, sphericCoords.x); 299 | phi2_over_pi = acos(sphericCoords.y) / M_PI; 300 | 301 | inCentered.x = (phi2_over_pi * cos(theta2) + 0.5) * srcSize.x; 302 | inCentered.y = (phi2_over_pi * sin(theta2) + 0.5) * srcSize.y; 303 | 304 | return inCentered; 305 | } 306 | 307 | /* 1. Define cartesian plane 308 | * 2. Reverse equirectangular projection from cartesian plane to polar coords in sphere 309 | * 3. Stereographic projection of polar coords from sphere to plane 310 | * 4. Convert polar coords to cartesian coords in plane 311 | * 5. Center and stretch according to source size 312 | */ 313 | double2 evaluatePixel_Equirectangular(double2 outPos, double2 srcSize, double thetaAdj) { 314 | 315 | double2 cartesianCoordsPlane; 316 | polar3 polarCoordsSphere; 317 | polar2 polarCoordsPlane; 318 | double2 result; 319 | 320 | // Define cartesianCoordsPlane 321 | cartesianCoordsPlane.x = 1.0 - outPos.x; 322 | cartesianCoordsPlane.y = 1.0 - outPos.y; 323 | 324 | // Reverse equirectangular projection 325 | // Convert cartesianCoordsPlane to polarCoordsSphere 326 | polarCoordsSphere.theta = cartesianCoordsPlane.x * 2.0 * M_PI + thetaAdj * M_PI / 180.0; 327 | polarCoordsSphere.phi = cartesianCoordsPlane.y * M_PI/2.0 + M_PI/2.0; 328 | 329 | // Stereographic projection 330 | // Convert polarCoordsSphere to polar coordinates on plane 331 | polarCoordsPlane.r = sin(polarCoordsSphere.phi)/(1.0-cos(polarCoordsSphere.phi)); 332 | polarCoordsPlane.theta = polarCoordsSphere.theta; 333 | 334 | // Convert polarCoordsPlane to cartesian coordinates; center and stretch 335 | result.x = (polarCoordsPlane.r * cos(polarCoordsPlane.theta) + 1.0)/2.0 * srcSize.x; 336 | result.y = (polarCoordsPlane.r * sin(polarCoordsPlane.theta) + 1.0)/2.0 * srcSize.y; 337 | 338 | // Coordinates of pixel in input which should be mapped onto given pixel in output 339 | return result; 340 | } 341 | 342 | /* Generate maps */ 343 | void gen_maps(configuration cfg, int** image_x, int** image_y) { 344 | int x, y; 345 | for (y = 0; y < cfg.rows; y++) { 346 | for (x = 0; x < cfg.cols; x++) { 347 | double2 outPos = {(double)x / (double)cfg.cols, 348 | (double)y / (double)cfg.rows}; 349 | double2 srcSize = {cfg.width, cfg.height}; 350 | double2 o; 351 | // Map output pixel (x, y) to corresponding input pixel 352 | switch (cfg.mode) { 353 | case FRONT: o = evaluatePixel_Front(outPos, srcSize); break; 354 | case EQUIRECTANGULAR: o = evaluatePixel_Equirectangular(outPos, srcSize, cfg.thetaAdj); break; 355 | default: printf("Mode not implemented\n"); exit(1); 356 | } 357 | image_x[y][x] = (int)round(o.x); 358 | image_y[y][x] = (int)round(o.y); 359 | } 360 | } 361 | } 362 | 363 | /* Main */ 364 | int main (int argc, char **argv) { 365 | int y; 366 | int** image_x; 367 | int** image_y; 368 | configuration cfg = parse_options(argc, argv); 369 | 370 | if (cfg.xmap_filename) printf("xmapfile: %s\n", cfg.xmap_filename); 371 | if (cfg.ymap_filename) printf("ymapfile: %s\n", cfg.ymap_filename); 372 | 373 | /* Allocate memory for maps */ 374 | image_x = malloc((cfg.rows) * sizeof(*image_x)); 375 | for (y = 0 ; y < (cfg.rows); y++) image_x[y] = malloc((cfg.cols) * sizeof(*(image_x[y]))); 376 | image_y = malloc((cfg.rows) * sizeof(*image_y)); 377 | for (y = 0; y < (cfg.rows); y++) image_y[y]= malloc((cfg.cols) * sizeof(*(image_y[y]))); 378 | 379 | /* Generate the maps */ 380 | printf("Generating maps\n"); 381 | gen_maps(cfg, image_x, image_y); 382 | 383 | /* Write files */ 384 | printf("Writing files\n"); 385 | pgmWrite_ASCII(cfg.ymap_filename, cfg.rows, cfg.cols,image_y, cfg.ymap_filename); 386 | pgmWrite_ASCII(cfg.xmap_filename, cfg.rows, cfg.cols,image_x, cfg.xmap_filename); 387 | 388 | /* Free memory */ 389 | if (image_y) { 390 | for (y = 0; y < cfg.rows; y++) free(image_y[y]); 391 | free(image_y); 392 | } 393 | if (image_x) { 394 | for (y = 0; y < cfg.rows; y++) free(image_x[y]); 395 | free(image_x); 396 | } 397 | 398 | /* Exit */ 399 | exit (0); 400 | } 401 | -------------------------------------------------------------------------------- /result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prouast/equirectangular-remap/7534f99fe41348361801ec30139e13b9f66fab20/result.png --------------------------------------------------------------------------------