├── VERSION ├── man ├── schooner-ndvi.1.ronn ├── schooner-ndvi.1 ├── schooner-multibalance.1.ronn ├── schooner-multibalance.1 ├── schooner-blend.1.ronn ├── schooner-blend.1 ├── schooner-stitch.1.ronn ├── schooner-stitch.1 ├── schooner-contrast.1.ronn ├── schooner-contrast.1 ├── schooner-cloud.1.ronn ├── schooner-cloud.1 ├── schooner-ndvi.1.html ├── schooner-multibalance.1.html ├── schooner-blend.1.html ├── schooner-stitch.1.html ├── schooner-contrast.1.html └── schooner-cloud.1.html ├── .gitignore ├── src ├── schooner-ndvi.cc ├── schooner-multibalance.cc ├── schooner-contrast.cc ├── schooner-cloud.cc ├── utils.h ├── schooner-stitch.cc └── schooner-blend.cc ├── README ├── Makefile ├── LICENSE ├── index.html └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 -------------------------------------------------------------------------------- /man/schooner-ndvi.1.ronn: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | src/schooner-blend 3 | src/schooner-cloud 4 | src/schooner-contrast 5 | src/schooner-multibalance 6 | src/schooner-ndvi 7 | src/schooner-stitch 8 | -------------------------------------------------------------------------------- /man/schooner-ndvi.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "SCHOONER\-NDVI" "1" "March 2015" "propublica" "schooner-tk" 5 | . 6 | .SH "NAME" 7 | \fBschooner\-ndvi\fR 8 | -------------------------------------------------------------------------------- /man/schooner-multibalance.1.ronn: -------------------------------------------------------------------------------- 1 | schooner-multibalance(1) -- color correct a batch of images 2 | =========================================================== 3 | 4 | ## SYNOPSIS 5 | 6 | `schooner-multibalance` [...] 7 | 8 | ## DESCRIPTION 9 | 10 | **schooner-multibalance** automatically corrects each band of many 11 | datasets so that each dataset has a similar color profile. This is 12 | useful as a preprocessing step to **schooner-blend**. 13 | 14 | ## SEE ALSO 15 | 16 | schooner-blend(1), schooner-cloud(1), schooner-contrast(1), 17 | schooner-stitch(1) -------------------------------------------------------------------------------- /src/schooner-ndvi.cc: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | int main(int argc, char **argv) { 4 | cv::Mat ir, vis, res; 5 | GDALDatasetH src, out; 6 | check(argc == 3, 7 | "usage: schooner-ndvi .tif .tif .tif"); 8 | 9 | ir = cv::imread(argv[1]); 10 | vis = cv::imread(argv[2]); 11 | 12 | res = (ir - vis) / (ir + vis + 0.00000001); 13 | 14 | cv::imwrite(argv[3], res); 15 | src = GDALOpen(argv[1], GA_ReadOnly); 16 | out = GDALOpen(argv[3], GA_Update); 17 | 18 | assign_projection(src, out); 19 | 20 | GDALClose(src); 21 | GDALClose(out); 22 | return 0; 23 | error: 24 | return -1; 25 | } -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | |=|-o-|=| 2 | 3 | _~ 4 | _~ )_)_~ 5 | )_))_))_) 6 | _!__!__!_ 7 | \______t/ 8 | ~~~~~~~~~~~~~ 9 | schooner-tk 10 | 11 | 12 | schooner-tk is a collection of utilities for averaging, color correcting, 13 | and removing artifacts from satellite images. It complements the GDAL 14 | utilities and the wonderful landsat util. 15 | 16 | Docs: https://propublica.github.io/schooner-tk 17 | Issues: https://github.com/propublica/schooner-tk/issues 18 | -------------------------------------------------------------------------------- /man/schooner-multibalance.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "SCHOONER\-MULTIBALANCE" "1" "March 2015" "propublica" "schooner-tk" 5 | . 6 | .SH "NAME" 7 | \fBschooner\-multibalance\fR \- color correct a batch of images 8 | . 9 | .SH "SYNOPSIS" 10 | \fBschooner\-multibalance\fR [\fIdatasets\fR\.\.\.] 11 | . 12 | .SH "DESCRIPTION" 13 | \fBschooner\-multibalance\fR automatically corrects each band of many datasets so that each dataset has a similar color profile\. This is useful as a preprocessing step to \fBschooner\-blend\fR\. 14 | . 15 | .SH "SEE ALSO" 16 | schooner\-blend(1), schooner\-cloud(1), schooner\-contrast(1), schooner\-stitch(1) 17 | -------------------------------------------------------------------------------- /man/schooner-blend.1.ronn: -------------------------------------------------------------------------------- 1 | schooner-blend(1) -- blend multiple raster datasets together 2 | ============================================================ 3 | 4 | ## SYNOPSIS 5 | 6 | `schooner-blend` [...] out.tif 7 | 8 | ## DESCRIPTION 9 | 10 | The **schooner-blend** tool averages multiple together 11 | on a per pixel basis in order to remove temporary artifacts such as 12 | small clouds, airplane contrails, and sensor malfunctions. 13 | 14 | ## CAVEATS 15 | 16 | Each input dataset needs to be the same size and in the same 17 | projection. **schooner-blend** will exit with a return code of -1 if 18 | a dataset is the incorrect size. You might want to preprocess the 19 | input images with **schooner-contrast** and **schooner-cloud** 20 | before running **schooner-blend**. 21 | 22 | ## SEE ALSO 23 | 24 | schooner-contrast(1), schooner-cloud(1), schooner-multibalance(1), 25 | schooner-stitch(1) -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OS := $(shell uname -s) 2 | ifeq ($(OS),Linux) 3 | CC = g++ 4 | CXX = g++ 5 | FLAGS = -std=c++11 6 | endif 7 | 8 | ifeq ($(OS),Darwin) 9 | CC = clang++ 10 | CXX = clang++ 11 | FLAGS = -std=c++11 -stdlib=libc++ 12 | endif 13 | 14 | RONN = $(wildcard man/*.ronn) 15 | HTML = $(RONN:.ronn=.html) 16 | 17 | SRCS = $(wildcard src/*.cc) 18 | BINS = $(basename $(SRCS)) 19 | 20 | all: doc binaries $(HTML) 21 | doc: $(HTML) 22 | binaries: $(BINS) 23 | 24 | man/%.html: man/%.ronn 25 | ronn --manual=schooner-tk --organization=propublica $< 26 | 27 | CXXFLAGS = $(shell gdal-config --cflags) -g $(FLAGS) $(shell pkg-config --cflags opencv) -I./src/ 28 | LDLIBS = $(shell gdal-config --libs) $(filter-out -lippicv, $(shell pkg-config --libs opencv)) $(FLAGS) 29 | 30 | clean: 31 | rm man/*.html man/*.1 $(BINS) 32 | 33 | install: all 34 | install $(BINS) /usr/local/bin 35 | install man/*.1 /usr/local/share/man/man1/ 36 | 37 | .PHONY: all clean doc binaries install 38 | -------------------------------------------------------------------------------- /src/schooner-multibalance.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "utils.h" 6 | 7 | int main(int argc, char** argv) { 8 | std::vector dst; 9 | std::vector images; 10 | check(argc > 1, "usage: schooner-multibalance ..."); 11 | 12 | for (int i = 1; i < argc; i++) { 13 | cv::Mat rgb = get_image(argv[i]); 14 | 15 | images.push_back(rgb); 16 | } 17 | 18 | balance(images, dst); 19 | 20 | GDALAllRegister(); 21 | 22 | for (int i = 1; i < argc; i++) { 23 | std::string out(argv[i]); 24 | out.append(".balanced.tif"); 25 | std::cout << "writing " << out << std::endl; 26 | cv::imwrite(out, dst[i - 1]); 27 | 28 | GDALDatasetH gsrc = GDALOpen(argv[i], GA_ReadOnly); 29 | GDALDatasetH gdst = GDALOpen(out.c_str(), GA_Update); 30 | 31 | assign_projection(gsrc, gdst); 32 | 33 | GDALClose(gsrc); 34 | GDALClose(gdst); 35 | } 36 | 37 | return 0; 38 | error: 39 | return -1; 40 | } -------------------------------------------------------------------------------- /man/schooner-blend.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "SCHOONER\-BLEND" "1" "March 2015" "propublica" "schooner-tk" 5 | . 6 | .SH "NAME" 7 | \fBschooner\-blend\fR \- blend multiple raster datasets together 8 | . 9 | .SH "SYNOPSIS" 10 | \fBschooner\-blend\fR [\fIdatasets\fR\.\.\.] out\.tif 11 | . 12 | .SH "DESCRIPTION" 13 | The \fBschooner\-blend\fR tool averages multiple \fIdatasets\fR together on a per pixel basis in order to remove temporary artifacts such as small clouds, airplane contrails, and sensor malfunctions\. 14 | . 15 | .SH "CAVEATS" 16 | Each input dataset needs to be the same size and in the same projection\. \fBschooner\-blend\fR will exit with a return code of \-1 if a dataset is the incorrect size\. You might want to preprocess the input images with \fBschooner\-contrast\fR and \fBschooner\-cloud\fR before running \fBschooner\-blend\fR\. 17 | . 18 | .SH "SEE ALSO" 19 | schooner\-contrast(1), schooner\-cloud(1), schooner\-multibalance(1), schooner\-stitch(1) 20 | -------------------------------------------------------------------------------- /man/schooner-stitch.1.ronn: -------------------------------------------------------------------------------- 1 | schooner-stitch(1) -- seamlessly stitch multiple images together 2 | ================================================================ 3 | 4 | ## SYNOPSIS 5 | 6 | `schooner-stitch` [...] out.tif 7 | 8 | ## DESCRIPTION 9 | 10 | **schooner-stitch** is the headlining act of the schooner toolkit. 11 | The tool 12 | 13 | ## CAVEATS 14 | 15 | This tool relies on OpenCV 3.0 which is still in development, on OS X 16 | you can use homebrew to install it: 17 | 18 | brew tap homebrew/science 19 | brew install opencv3 20 | 21 | on linux, you'll probably need to install OpenCV from source. 22 | 23 | ## BUGS 24 | 25 | Due to a slew of bugs in OS X's video card drivers you want to 26 | install OpenCV without OpenCL. Otherwise when you attempt to stitch 27 | together many large images together your computer will crash, HARD. 28 | In addition, there the tool is RAM limited, so you _will_ run out 29 | memory if you are blending multiple large images. 30 | 31 | ## SEE ALSO 32 | 33 | schooner-blend(1), schooner-cloud(1), schooner-contrast(1), 34 | schooner-multibalance(1) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ProPublica 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 | 23 | -------------------------------------------------------------------------------- /man/schooner-stitch.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "SCHOONER\-STITCH" "1" "March 2016" "propublica" "schooner-tk" 5 | . 6 | .SH "NAME" 7 | \fBschooner\-stitch\fR \- seamlessly stitch multiple images together 8 | . 9 | .SH "SYNOPSIS" 10 | \fBschooner\-stitch\fR [\fIdatasets\fR\.\.\.] out\.tif 11 | . 12 | .SH "DESCRIPTION" 13 | \fBschooner\-stitch\fR is the headlining act of the schooner toolkit\. The tool 14 | . 15 | .SH "CAVEATS" 16 | This tool relies on OpenCV 3\.0 which is still in development, on OS X you can use homebrew to install it: 17 | . 18 | .P 19 | brew tap homebrew/science brew install opencv3 20 | . 21 | .P 22 | on linux, you\'ll probably need to install OpenCV from source\. 23 | . 24 | .SH "BUGS" 25 | Due to a slew of bugs in OS X\'s video card drivers you want to install OpenCV without OpenCL\. Otherwise when you attempt to stitch together many large images together your computer will crash, HARD\. In addition, there the tool is RAM limited, so you \fIwill\fR run out memory if you are blending multiple large images\. 26 | . 27 | .SH "SEE ALSO" 28 | schooner\-blend(1), schooner\-cloud(1), schooner\-contrast(1), schooner\-multibalance(1) 29 | -------------------------------------------------------------------------------- /man/schooner-contrast.1.ronn: -------------------------------------------------------------------------------- 1 | schooner-contrast(1) -- color correct a single raster image. 2 | =============================================================== 3 | 4 | ## SYNOPSIS 5 | 6 | `schooner-contrast` .tif .tif 7 | 8 | ## DESCRIPTION 9 | 10 | The **schooner-contrast** tool attempts to automatically color 11 | correct a landsat image. It uses two algorithms, the first is CLAHE 12 | (Contrast Limited Adaptive Histogram Equalization) which attempts 13 | to color correct an image while improving the local contrast of the 14 | image. 15 | 16 | The other is simple histogram stretching and clipping. The tool 17 | calculates a histogram by band, and stretches the raster values to 18 | the range 0 to 255. It also discards the top and bottom 0.05% of 19 | values. 20 | 21 | ## CAVEATS 22 | 23 | Unlike **landsat-util**, **schooner-contrast** does not perform 24 | gamma correction. If you are working with landsat 8 images it is 25 | probably better to use **landsat-util**'s builtin color correction. 26 | 27 | If you are working with older landsat data, or data from sensors like 28 | MODIS, **schooner-contrast** works acceptably well. 29 | 30 | Due to the way OpenCV works, this utility currently converts everything 31 | to 8 bit images. 32 | 33 | ## SEE ALSO 34 | 35 | schooner-blend(1), schooner-cloud(1), schooner-multibalance(1), 36 | schooner-stitch(1) -------------------------------------------------------------------------------- /man/schooner-contrast.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "SCHOONER\-CONTRAST" "1" "March 2015" "propublica" "schooner-tk" 5 | . 6 | .SH "NAME" 7 | \fBschooner\-contrast\fR \- color correct a single raster image\. 8 | . 9 | .SH "SYNOPSIS" 10 | \fBschooner\-contrast\fR \fIin\fR\.tif \fIout\fR\.tif 11 | . 12 | .SH "DESCRIPTION" 13 | The \fBschooner\-contrast\fR tool attempts to automatically color correct a landsat image\. It uses two algorithms, the first is CLAHE (Contrast Limited Adaptive Histogram Equalization) which attempts to color correct an image while improving the local contrast of the image\. 14 | . 15 | .P 16 | The other is simple histogram stretching and clipping\. The tool calculates a histogram by band, and stretches the raster values to the range 0 to 255\. It also discards the top and bottom 0\.05% of values\. 17 | . 18 | .SH "CAVEATS" 19 | Unlike \fBlandsat\-util\fR, \fBschooner\-contrast\fR does not perform gamma correction\. If you are working with landsat 8 images it is probably better to use \fBlandsat\-util\fR\'s builtin color correction\. 20 | . 21 | .P 22 | If you are working with older landsat data, or data from sensors like MODIS, \fBschooner\-contrast\fR works acceptably well\. 23 | . 24 | .P 25 | Due to the way OpenCV works, this utility currently converts everything to 8 bit images\. 26 | . 27 | .SH "SEE ALSO" 28 | schooner\-blend(1), schooner\-cloud(1), schooner\-multibalance(1), schooner\-stitch(1) 29 | -------------------------------------------------------------------------------- /src/schooner-contrast.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "utils.h" 11 | 12 | int main(int argc, char** argv) { 13 | if (argc != 3) { 14 | std::cout << "usage: schooner-contrast src dst" << std::endl; 15 | exit(1); 16 | } 17 | 18 | cv::Mat rgb = get_image(argv[1]); 19 | 20 | std::vector images; 21 | images.push_back(rgb); 22 | std::vector out; 23 | 24 | balance(images, out); 25 | 26 | cv::Mat rgbc = out.at(0); 27 | rgbc /= 256.0; 28 | rgbc.convertTo(rgbc, CV_8U); 29 | cv::Mat lab; 30 | 31 | cv::cvtColor(rgbc, lab, cv::COLOR_RGB2Lab); 32 | 33 | std::vector lab_planes(3); 34 | cv::split(lab, lab_planes); 35 | 36 | cv::Ptr clahe = cv::createCLAHE(); 37 | clahe->setClipLimit(1); 38 | cv::Mat dst; 39 | clahe->apply(lab_planes[0], dst); 40 | 41 | dst.copyTo(lab_planes[0]); 42 | cv::merge(lab_planes, lab); 43 | cv::Mat clahe_img; 44 | 45 | cv::cvtColor(lab, clahe_img, cv::COLOR_Lab2RGB); 46 | cv::imwrite(argv[2], clahe_img); 47 | 48 | GDALAllRegister(); 49 | GDALDatasetH gsrc = GDALOpen(argv[1], GA_ReadOnly); 50 | GDALDatasetH gdst = GDALOpen(argv[2], GA_Update); 51 | 52 | assign_projection(gsrc, gdst); 53 | 54 | GDALClose(gsrc); 55 | GDALClose(gdst); 56 | return 0; 57 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | schooner-tk!!! 5 | 55 | 56 | 57 | 93 | 94 | -------------------------------------------------------------------------------- /man/schooner-cloud.1.ronn: -------------------------------------------------------------------------------- 1 | schooner-cloud(1) -- create a cloud mask from a landsat 8 QA band 2 | ================================================================= 3 | 4 | ## SYNOPSIS 5 | 6 | `schooner-cloud` .tif .tif 7 | 8 | ## DESCRIPTION 9 | 10 | The **schooner-blend** tool creates a cloud and snow mask from a 11 | landsat 8 quality assessment band. These bands are distributed 12 | within Landsat 8 tarballs and are named like: 13 | 14 | _BQA.TIF 15 | 16 | This kind of mask is useful when averaging multiple datasets 17 | together with **schooner-blend**. Once you've run this command to 18 | create a mask you need to use something like: 19 | 20 | gdal_calc.py -A .tif -B .tif --calc="A*B" --allBands=B --overwrite --type=Byte 21 | 22 | to apply it to your final image. 23 | 24 | ## CAVEATS 25 | 26 | This tool only works with Landsat 8 quality bands and removes that 27 | band's classified snow and cloud flags. It uses the masks defined 28 | in the Landsat 8 documentation at: 29 | 30 | http://landsat.usgs.gov/L8QualityAssessmentBand.php 31 | 32 | **schooner-cloud** treats all "maybe" bits as definitely classified 33 | as cloudy. 34 | 35 | As of this writing the Landsat program is reprocessing old images 36 | to include a cloud flag using the cfmask algorithm, and this tool 37 | is expected to work when the QA bands are updated. 38 | 39 | ## OTHER TOOLS 40 | 41 | If you want to try to create a better quality assessment band for 42 | Landsat 8 images you might want to try the fmask algorithm here: 43 | 44 | https://code.google.com/p/cfmask/ 45 | 46 | ## SEE ALSO 47 | 48 | schooner-contrast(1), schooner-blend(1), schooner-multibalance(1), 49 | schooner-stitch(1) -------------------------------------------------------------------------------- /man/schooner-cloud.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "SCHOONER\-CLOUD" "1" "March 2015" "propublica" "schooner-tk" 5 | . 6 | .SH "NAME" 7 | \fBschooner\-cloud\fR \- create a cloud mask from a landsat 8 QA band 8 | . 9 | .SH "SYNOPSIS" 10 | \fBschooner\-cloud\fR \fIquality\-assessment\-band\fR\.tif \fIout\fR\.tif 11 | . 12 | .SH "DESCRIPTION" 13 | The \fBschooner\-blend\fR tool creates a cloud and snow mask from a landsat 8 quality assessment band\. These bands are distributed within Landsat 8 tarballs and are named like: 14 | . 15 | .P 16 | \fIlandsat\-id\fR_BQA\.TIF 17 | . 18 | .P 19 | This kind of mask is useful when averaging multiple datasets together with \fBschooner\-blend\fR\. Once you\'ve run this command to create a mask you need to use something like: 20 | . 21 | .P 22 | gdal_calc\.py \-A \fImask\fR\.tif \-B \fIlandsat\fR\.tif \-\-calc="A*B" \-\-allBands=B \-\-overwrite \-\-type=Byte 23 | . 24 | .P 25 | to apply it to your final image\. 26 | . 27 | .SH "CAVEATS" 28 | This tool only works with Landsat 8 quality bands and removes that band\'s classified snow and cloud flags\. It uses the masks defined in the Landsat 8 documentation at: 29 | . 30 | .IP "" 4 31 | . 32 | .nf 33 | 34 | http://landsat\.usgs\.gov/L8QualityAssessmentBand\.php 35 | . 36 | .fi 37 | . 38 | .IP "" 0 39 | . 40 | .P 41 | \fBschooner\-cloud\fR treats all "maybe" bits as definitely classified as cloudy\. 42 | . 43 | .P 44 | As of this writing the Landsat program is reprocessing old images to include a cloud flag using the cfmask algorithm, and this tool is expected to work when the QA bands are updated\. 45 | . 46 | .SH "OTHER TOOLS" 47 | If you want to try to create a better quality assessment band for Landsat 8 images you might want to try the fmask algorithm here: 48 | . 49 | .P 50 | https://code\.google\.com/p/cfmask/ 51 | . 52 | .SH "SEE ALSO" 53 | schooner\-contrast(1), schooner\-blend(1), schooner\-multibalance(1), schooner\-stitch(1) 54 | -------------------------------------------------------------------------------- /src/schooner-cloud.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "utils.h" 5 | 6 | int main(int argc, char *argv[]) { 7 | GDALDatasetH mask = NULL; 8 | GDALDatasetH out = NULL; 9 | uint16_t *scanline = NULL; 10 | uint8_t *tiny_scanline = NULL; 11 | int width, height; 12 | check(argc == 3, 13 | "usage: quality-layer.tif mask.tif\n" 14 | "run this after to add the mask out the identified cloudy and snowy " 15 | "pixels\n" 16 | " gdal_calc.py -A mask.tif -B ~/landsat.tif --calc=\"A*B\" " 17 | "--allBands=B --overwrite --type=Byte"); 18 | 19 | GDALAllRegister(); 20 | mask = GDALOpen(argv[1], GA_ReadOnly); 21 | width = GDALGetRasterXSize(mask), height = GDALGetRasterYSize(mask); 22 | check(mask, "couldn't open %s", argv[1]); 23 | out = GDALCreate(GDALGetDriverByName("GTiff"), argv[2], width, height, 1, 24 | GDT_UInt16, NULL); 25 | check(out, "couldn't create %s", argv[2]); 26 | scanline = (uint16_t *)calloc(GDALGetRasterXSize(mask), sizeof(uint16_t)); 27 | tiny_scanline = (uint8_t *)calloc(GDALGetRasterXSize(mask), sizeof(uint16_t)); 28 | 29 | for (int y = 0; y < height; y++) { 30 | printf("row: %i\r", y + 1); 31 | fflush(stdout); 32 | GDALRasterIO(GDALGetRasterBand(mask, 1), GF_Read, 0, y, width, 1, scanline, 33 | width, 1, GDT_UInt16, 0, 0); 34 | for (int x = 0; x < width; x++) { 35 | if ((scanline[x] & (1 << 15)) || // maybe cloud 36 | (scanline[x] & (1 << 13)) || // maybe cirrus 37 | (scanline[x] & (1 << 13)) || // maybe snow 38 | (scanline[x] & (1 << 7)) || // future cloud shadow 39 | scanline[x] & 1 // fill pixel 40 | ) { 41 | tiny_scanline[x] = 0; 42 | } else { 43 | tiny_scanline[x] = 1; 44 | } 45 | } 46 | GDALRasterIO(GDALGetRasterBand(out, 1), GF_Write, 0, y, width, 1, 47 | tiny_scanline, width, 1, GDT_Byte, 0, 0); 48 | } 49 | puts(""); 50 | 51 | assign_projection(mask, out); 52 | 53 | GDALClose(out); 54 | GDALClose(mask); 55 | free(scanline); 56 | free(tiny_scanline); 57 | return -1; 58 | error: 59 | if (mask) GDALClose(mask); 60 | return -1; 61 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Schooner-tk 2 | =========== 3 | 4 | A collection of utilities for averaging, color correcting, and removing 5 | artifacts from satellite images. It complements [GDAL](http://www.gdal.org/) and 6 | [landsat util](https://github.com/developmentseed/landsat-util). 7 | 8 | Installation 9 | ------------ 10 | 11 | Schooner-tk requires OpenCV 3. To install OpenCV 3 on **OSX**, use Homebrew: 12 | 13 | brew install opencv3 14 | 15 | unless you want to fight with `CFLAGS`, `LDLIBS`, and `PKG_CONFIG_PATH` go ahead and link opencv3 16 | 17 | brew link opencv3 --force 18 | 19 | Be forewarned this will mess with anything that relies on opencv 2. 20 | 21 | For **linux distributions**, use `apt-get`, `pacman`, `yum`, or whichever package manager is preferred. 22 | 23 | Once OpenCV is installed, **clone the repository**. 24 | 25 | git clone git@github.com:propublica/schooner-tk.git 26 | cd schooner-tk 27 | 28 | Use **make** to compile and install schooner. 29 | 30 | make && make install 31 | 32 | Once installed, each command can be called from your terminal with: 33 | 34 | schooner- 35 | 36 | Utilities 37 | --------- 38 | 39 | #### Schooner-blend 40 | 41 | `schooner-blend` averages multiple datasets together on a per pixel basis 42 | in order to remove temporary artifacts such as small clouds, airplane 43 | contrails, and sensor malfunctions. 44 | 45 | Each input dataset must be the same size and in the same projection. 46 | 47 | #### Schooner-cloud 48 | 49 | `schooner-cloud` creates a cloud and snow mask from a Landsat 8 Quality 50 | Assessment band. 51 | 52 | #### Schooner-contrast 53 | 54 | `schooner-contrast` attempts to automatically color correct a landsat image. 55 | It uses two algorithms, the first is CLAHE (Contrast Limited Adaptive Histogram 56 | Equalization) which attempts to color correct an image while improving the 57 | local contrast of the image. 58 | 59 | The other is simple histogram stretching and clipping. The tool calculates a 60 | histogram by band, and stretches the raster values to the range 0 to 255. It 61 | also discards the top and bottom 0.05% of values. 62 | 63 | #### Schooner-multibalance 64 | 65 | `schooner-multibalance` automatically corrects each band of many datasets so 66 | that each dataset has a similar color profile. This is useful as a preprocessing 67 | step to schooner-blend. 68 | 69 | #### Schooner-stich 70 | 71 | `schooner-stitch` seamlessly stitches multiple images together. 72 | 73 | Learn more 74 | ---------- 75 | 76 | For more detail, please see the [official documentation](https://propublica.github.io/schooner-tk/). 77 | For an example of a schooner-powered workflow, see [jqtr.de/schooner/](http://jqtr.de/technical/2015/05/27/schooner.html). 78 | -------------------------------------------------------------------------------- /man/schooner-ndvi.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | schooner-ndvi(1) 7 | 44 | 45 | 52 | 53 |
54 | 55 | 58 | 59 |
    60 |
  1. schooner-ndvi(1)
  2. 61 |
  3. schooner-tk
  4. 62 |
  5. schooner-ndvi(1)
  6. 63 |
64 | 65 |

NAME

66 |

67 | schooner-ndvi 68 |

69 | 70 | 71 | 72 |
    73 |
  1. propublica
  2. 74 |
  3. March 2015
  4. 75 |
  5. schooner-ndvi(1)
  6. 76 |
77 | 78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef SCHOONER_UTILS_H 2 | #define SCHOONER_UTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define check(err, mess, args...) \ 10 | if (!(err)) { \ 11 | fprintf(stderr, mess, ##args); \ 12 | goto error; \ 13 | } 14 | 15 | // stretches histograms across multiple images 16 | void balance(std::vector &images, std::vector &dst) { 17 | std::vector > minmax; 18 | 19 | for (cv::Mat image : images) { 20 | std::vector chans; 21 | cv::split(image, chans); 22 | int i = 0; 23 | for (cv::Mat chan : chans) { 24 | cv::Mat sorted(chan.reshape(0, 1).rows, chan.reshape(0, 1).cols, 25 | chan.type()); 26 | chan.reshape(0, 1).copyTo(sorted); 27 | cv::sort(sorted, sorted, cv::SORT_EVERY_ROW + cv::SORT_ASCENDING); 28 | 29 | if (minmax.size() < i + 1) minmax.push_back(std::make_pair(DBL_MAX, 0)); 30 | 31 | int black_index = 0; 32 | while (sorted.at(0, black_index) == 0 && 33 | black_index < sorted.cols) 34 | black_index++; 35 | 36 | int white_index = sorted.cols; 37 | while (sorted.at(0, white_index) == 65535 && 38 | white_index > 0) 39 | white_index--; 40 | 41 | std::pair &d = minmax.at(i); 42 | d.first = fmin( 43 | d.first, 44 | sorted.at( 45 | 0, (int)(white_index - black_index) * 0.01 / 100 + black_index)); 46 | d.second = fmax( 47 | d.second, 48 | sorted.at( 49 | 0, (int)(white_index - black_index) * 99.9 / 100 + black_index)); 50 | i++; 51 | } 52 | } 53 | 54 | for (cv::Mat image : images) { 55 | std::vector chans; 56 | cv::split(image, chans); 57 | 58 | for (int i = 0; i < chans.size(); i++) { 59 | std::pair &d = minmax.at(i); 60 | chans[i] = (chans[i] - d.first) / (d.second - d.first) * 65535; 61 | } 62 | 63 | cv::Mat out; 64 | cv::merge(chans, out); 65 | dst.push_back(out); 66 | } 67 | } 68 | 69 | cv::Mat get_image(char *arg) { 70 | cv::Mat rgb = cv::imread(arg, cv::IMREAD_ANYDEPTH | cv::IMREAD_ANYCOLOR); 71 | 72 | if (rgb.data == NULL) { 73 | std::cout << "couldn't read image: " << arg << std::endl; 74 | exit(1); 75 | } 76 | 77 | if (!(rgb.type() == CV_8U || rgb.type() == CV_8S || rgb.type() == CV_8SC1 || 78 | rgb.type() == CV_8SC2 || rgb.type() == CV_8SC3 || 79 | rgb.type() == CV_8SC4 || rgb.type() == CV_8UC1 || 80 | rgb.type() == CV_8UC2 || rgb.type() == CV_8UC3 || 81 | rgb.type() == CV_8UC4 || rgb.type() == CV_16U || rgb.type() == CV_16S || 82 | rgb.type() == CV_16SC1 || rgb.type() == CV_16SC2 || 83 | rgb.type() == CV_16SC3 || rgb.type() == CV_16SC4 || 84 | rgb.type() == CV_16UC1 || rgb.type() == CV_16UC2 || 85 | rgb.type() == CV_16UC3 || rgb.type() == CV_16UC4)) { 86 | std::cout << "schooner-contrast requires 8bit or 16bit images." 87 | << std::endl; 88 | exit(1); 89 | } 90 | 91 | if (rgb.type() == CV_8U || rgb.type() == CV_8S || rgb.type() == CV_8SC1 || 92 | rgb.type() == CV_8SC2 || rgb.type() == CV_8SC3 || rgb.type() == CV_8SC4 || 93 | rgb.type() == CV_8UC1 || rgb.type() == CV_8UC2 || rgb.type() == CV_8UC3 || 94 | rgb.type() == CV_8UC4) { 95 | rgb.convertTo(rgb, CV_16U); 96 | rgb *= 256.0; 97 | } 98 | return rgb; 99 | } 100 | 101 | void assign_projection(GDALDatasetH src, GDALDatasetH dst) { 102 | double transform[6]; 103 | if (GDALGetGeoTransform(src, transform) == CE_None) 104 | GDALSetGeoTransform(dst, transform); 105 | 106 | if (GDALGetProjectionRef(src) != NULL) 107 | GDALSetProjection(dst, GDALGetProjectionRef(src)); 108 | } 109 | 110 | #endif -------------------------------------------------------------------------------- /man/schooner-multibalance.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | schooner-multibalance(1) - color correct a batch of images 7 | 44 | 45 | 52 | 53 |
54 | 55 | 61 | 62 |
    63 |
  1. schooner-multibalance(1)
  2. 64 |
  3. schooner-tk
  4. 65 |
  5. schooner-multibalance(1)
  6. 66 |
67 | 68 |

NAME

69 |

70 | schooner-multibalance - color correct a batch of images 71 |

72 | 73 |

SYNOPSIS

74 | 75 |

schooner-multibalance [datasets...]

76 | 77 |

DESCRIPTION

78 | 79 |

schooner-multibalance automatically corrects each band of many 80 | datasets so that each dataset has a similar color profile. This is 81 | useful as a preprocessing step to schooner-blend.

82 | 83 |

SEE ALSO

84 | 85 |

schooner-blend(1), schooner-cloud(1), schooner-contrast(1), 86 | schooner-stitch(1)

87 | 88 | 89 |
    90 |
  1. propublica
  2. 91 |
  3. March 2015
  4. 92 |
  5. schooner-multibalance(1)
  6. 93 |
94 | 95 |
96 | 97 | 98 | -------------------------------------------------------------------------------- /man/schooner-blend.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | schooner-blend(1) - blend multiple raster datasets together 7 | 44 | 45 | 52 | 53 |
54 | 55 | 62 | 63 |
    64 |
  1. schooner-blend(1)
  2. 65 |
  3. schooner-tk
  4. 66 |
  5. schooner-blend(1)
  6. 67 |
68 | 69 |

NAME

70 |

71 | schooner-blend - blend multiple raster datasets together 72 |

73 | 74 |

SYNOPSIS

75 | 76 |

schooner-blend [datasets...] out.tif

77 | 78 |

DESCRIPTION

79 | 80 |

The schooner-blend tool averages multiple datasets together 81 | on a per pixel basis in order to remove temporary artifacts such as 82 | small clouds, airplane contrails, and sensor malfunctions.

83 | 84 |

CAVEATS

85 | 86 |

Each input dataset needs to be the same size and in the same 87 | projection. schooner-blend will exit with a return code of -1 if 88 | a dataset is the incorrect size. You might want to preprocess the 89 | input images with schooner-contrast and schooner-cloud 90 | before running schooner-blend.

91 | 92 |

SEE ALSO

93 | 94 |

schooner-contrast(1), schooner-cloud(1), schooner-multibalance(1), 95 | schooner-stitch(1)

96 | 97 | 98 |
    99 |
  1. propublica
  2. 100 |
  3. March 2015
  4. 101 |
  5. schooner-blend(1)
  6. 102 |
103 | 104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /man/schooner-stitch.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | schooner-stitch(1) - seamlessly stitch multiple images together 7 | 44 | 45 | 52 | 53 |
54 | 55 | 63 | 64 |
    65 |
  1. schooner-stitch(1)
  2. 66 |
  3. schooner-tk
  4. 67 |
  5. schooner-stitch(1)
  6. 68 |
69 | 70 |

NAME

71 |

72 | schooner-stitch - seamlessly stitch multiple images together 73 |

74 | 75 |

SYNOPSIS

76 | 77 |

schooner-stitch [datasets...] out.tif

78 | 79 |

DESCRIPTION

80 | 81 |

schooner-stitch is the headlining act of the schooner toolkit. 82 | The tool

83 | 84 |

CAVEATS

85 | 86 |

This tool relies on OpenCV 3.0 which is still in development, on OS X 87 | you can use homebrew to install it:

88 | 89 |

brew tap homebrew/science 90 | brew install opencv3

91 | 92 |

on linux, you'll probably need to install OpenCV from source.

93 | 94 |

BUGS

95 | 96 |

Due to a slew of bugs in OS X's video card drivers you want to 97 | install OpenCV without OpenCL. Otherwise when you attempt to stitch 98 | together many large images together your computer will crash, HARD. 99 | In addition, there the tool is RAM limited, so you will run out 100 | memory if you are blending multiple large images.

101 | 102 |

SEE ALSO

103 | 104 |

schooner-blend(1), schooner-cloud(1), schooner-contrast(1), 105 | schooner-multibalance(1)

106 | 107 | 108 |
    109 |
  1. propublica
  2. 110 |
  3. March 2016
  4. 111 |
  5. schooner-stitch(1)
  6. 112 |
113 | 114 |
115 | 116 | 117 | -------------------------------------------------------------------------------- /man/schooner-contrast.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | schooner-contrast(1) - color correct a single raster image. 7 | 44 | 45 | 52 | 53 |
54 | 55 | 62 | 63 |
    64 |
  1. schooner-contrast(1)
  2. 65 |
  3. schooner-tk
  4. 66 |
  5. schooner-contrast(1)
  6. 67 |
68 | 69 |

NAME

70 |

71 | schooner-contrast - color correct a single raster image. 72 |

73 | 74 |

SYNOPSIS

75 | 76 |

schooner-contrast in.tif out.tif

77 | 78 |

DESCRIPTION

79 | 80 |

The schooner-contrast tool attempts to automatically color 81 | correct a landsat image. It uses two algorithms, the first is CLAHE 82 | (Contrast Limited Adaptive Histogram Equalization) which attempts 83 | to color correct an image while improving the local contrast of the 84 | image.

85 | 86 |

The other is simple histogram stretching and clipping. The tool 87 | calculates a histogram by band, and stretches the raster values to 88 | the range 0 to 255. It also discards the top and bottom 0.05% of 89 | values.

90 | 91 |

CAVEATS

92 | 93 |

Unlike landsat-util, schooner-contrast does not perform 94 | gamma correction. If you are working with landsat 8 images it is 95 | probably better to use landsat-util's builtin color correction.

96 | 97 |

If you are working with older landsat data, or data from sensors like 98 | MODIS, schooner-contrast works acceptably well.

99 | 100 |

Due to the way OpenCV works, this utility currently converts everything 101 | to 8 bit images.

102 | 103 |

SEE ALSO

104 | 105 |

schooner-blend(1), schooner-cloud(1), schooner-multibalance(1), 106 | schooner-stitch(1)

107 | 108 | 109 |
    110 |
  1. propublica
  2. 111 |
  3. March 2015
  4. 112 |
  5. schooner-contrast(1)
  6. 113 |
114 | 115 |
116 | 117 | 118 | -------------------------------------------------------------------------------- /man/schooner-cloud.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | schooner-cloud(1) - create a cloud mask from a landsat 8 QA band 7 | 44 | 45 | 52 | 53 |
54 | 55 | 63 | 64 |
    65 |
  1. schooner-cloud(1)
  2. 66 |
  3. schooner-tk
  4. 67 |
  5. schooner-cloud(1)
  6. 68 |
69 | 70 |

NAME

71 |

72 | schooner-cloud - create a cloud mask from a landsat 8 QA band 73 |

74 | 75 |

SYNOPSIS

76 | 77 |

schooner-cloud quality-assessment-band.tif out.tif

78 | 79 |

DESCRIPTION

80 | 81 |

The schooner-blend tool creates a cloud and snow mask from a 82 | landsat 8 quality assessment band. These bands are distributed 83 | within Landsat 8 tarballs and are named like:

84 | 85 |

landsat-id_BQA.TIF

86 | 87 |

This kind of mask is useful when averaging multiple datasets 88 | together with schooner-blend. Once you've run this command to 89 | create a mask you need to use something like:

90 | 91 |

gdal_calc.py -A mask.tif -B landsat.tif --calc="A*B" --allBands=B --overwrite --type=Byte

92 | 93 |

to apply it to your final image.

94 | 95 |

CAVEATS

96 | 97 |

This tool only works with Landsat 8 quality bands and removes that 98 | band's classified snow and cloud flags. It uses the masks defined 99 | in the Landsat 8 documentation at:

100 | 101 |
http://landsat.usgs.gov/L8QualityAssessmentBand.php
102 | 
103 | 104 |

schooner-cloud treats all "maybe" bits as definitely classified 105 | as cloudy.

106 | 107 |

As of this writing the Landsat program is reprocessing old images 108 | to include a cloud flag using the cfmask algorithm, and this tool 109 | is expected to work when the QA bands are updated.

110 | 111 |

OTHER TOOLS

112 | 113 |

If you want to try to create a better quality assessment band for 114 | Landsat 8 images you might want to try the fmask algorithm here:

115 | 116 |

https://code.google.com/p/cfmask/

117 | 118 |

SEE ALSO

119 | 120 |

schooner-contrast(1), schooner-blend(1), schooner-multibalance(1), 121 | schooner-stitch(1)

122 | 123 | 124 |
    125 |
  1. propublica
  2. 126 |
  3. March 2015
  4. 127 |
  5. schooner-cloud(1)
  6. 128 |
129 | 130 |
131 | 132 | 133 | -------------------------------------------------------------------------------- /src/schooner-stitch.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "utils.h" 8 | 9 | class Bounds { 10 | public: 11 | Bounds() {} 12 | 13 | int FromDataset(GDALDataset *ds) { 14 | double geotransform[6]; 15 | CPLErr err = ds->GetGeoTransform(geotransform); 16 | check(err == CE_None, NULL); 17 | 18 | ul.first = geotransform[0]; 19 | ul.second = geotransform[3]; 20 | lr.first = ul.first + ds->GetRasterXSize() * geotransform[1]; 21 | lr.second = ul.second + ds->GetRasterYSize() * geotransform[5]; 22 | 23 | return false; 24 | error: 25 | return true; 26 | } 27 | 28 | void extend(Bounds obounds) { 29 | ul.first = std::min(obounds.ul.first, ul.first); 30 | ul.second = std::max(obounds.ul.second, ul.second); 31 | lr.first = std::max(obounds.lr.first, lr.first); 32 | lr.second = std::min(obounds.lr.second, lr.second); 33 | } 34 | 35 | std::pair ul; 36 | std::pair lr; 37 | }; 38 | 39 | int main(int argc, char **argv) { 40 | if (argc < 3) { 41 | std::cout << "usage: stitch out.tif" << std::endl; 42 | return -1; 43 | }; 44 | 45 | // declarations 46 | std::vector datasets; 47 | Bounds bounds; 48 | bool err; 49 | int j = 1; 50 | double geotransform[6]; 51 | int xsize, ysize; 52 | cv::Ptr blender = 53 | cv::detail::Blender::createDefault(cv::detail::Blender::FEATHER, false); 54 | std::vector sizes; 55 | std::vector corners; 56 | cv::Mat result, result_mask; 57 | GDALDataset *outds = NULL; 58 | cv::Size dst_sz; 59 | float blend_width; 60 | 61 | // init gdal 62 | GDALAllRegister(); 63 | 64 | // open datasets 65 | for (int i = 1; i < argc - 1; i++) { 66 | GDALDataset *dataset = (GDALDataset *)GDALOpen(argv[i], GA_ReadOnly); 67 | check(dataset != NULL, "Could not open %s\n", argv[i]); 68 | datasets.push_back(dataset); 69 | } 70 | 71 | err = bounds.FromDataset(datasets.at(0)); 72 | check(err == false, "%s doesn't have geo information", argv[1]); 73 | 74 | // fill out bookkeeping structures 75 | j = 1; 76 | for (GDALDataset *ds : datasets) { 77 | Bounds obounds; 78 | obounds.FromDataset(ds); 79 | check(err == false, "%s doesn't have geo information", argv[j]); 80 | bounds.extend(obounds); 81 | j++; 82 | } 83 | 84 | // no need to check err, we did that when we built the bounds up 85 | datasets[0]->GetGeoTransform(geotransform); 86 | xsize = (bounds.lr.first - bounds.ul.first) / geotransform[1]; 87 | ysize = (bounds.lr.second - bounds.ul.second) / geotransform[5]; 88 | printf("Creating dataset %s with size %d x %d \n", argv[argc - 1], xsize, 89 | ysize); 90 | printf("and bounds UL: %f %f LR: %f %f\n", bounds.ul.first, bounds.ul.second, 91 | bounds.lr.first, bounds.lr.second); 92 | 93 | geotransform[0] = bounds.ul.first; 94 | geotransform[2] = 0; 95 | geotransform[3] = bounds.ul.second; 96 | geotransform[4] = 0; 97 | 98 | // build sizes and point vectors 99 | j = 1; 100 | for (GDALDataset *ds : datasets) { 101 | Bounds obounds; 102 | obounds.FromDataset(ds); 103 | std::cout << ds->GetRasterXSize() << ", " << ds->GetRasterYSize() 104 | << std::endl; 105 | cv::Point pt( 106 | round((obounds.ul.first - geotransform[0]) / geotransform[1]), 107 | round((obounds.ul.second - geotransform[3]) / geotransform[5])); 108 | corners.push_back(pt); 109 | std::cout << "placing " << argv[j] << " at " << pt.x << ", " << pt.y 110 | << std::endl; 111 | 112 | cv::Size s( 113 | ((obounds.lr.first - geotransform[0]) / geotransform[1]) - pt.x, 114 | ((obounds.lr.second - geotransform[3]) / geotransform[5]) - pt.y); 115 | sizes.push_back(s); 116 | std::cout << "sizing " << argv[j] << " at " << s.width << ", " << s.height 117 | << std::endl; 118 | 119 | j++; 120 | } 121 | 122 | // from 123 | // https://github.com/Itseez/opencv/blob/0726c4d4ea80e73c96ccee7bd3ef5f71f46ac82b/samples/cpp/stitching_detailed.cpp#L799 124 | dst_sz = cv::detail::resultRoi(corners, sizes).size(); 125 | blend_width = sqrt(static_cast(dst_sz.area())) * 1 / 100.f; 126 | std::cout << "Blending sharpness set to " << 1.f / blend_width << std::endl; 127 | dynamic_cast(blender.get()) 128 | ->setSharpness(1.f / blend_width); 129 | 130 | blender->prepare(corners, sizes); 131 | 132 | for (int i = 1; i < argc - 1; i++) { 133 | cv::Mat im = cv::imread(argv[i]); 134 | resize(im, im, sizes.at(i - 1), cv::INTER_LANCZOS4); 135 | std::vector channels; 136 | cv::split(im, channels); 137 | im.convertTo(im, CV_16SC3); 138 | std::cout << "merging " << argv[i] << std::endl; 139 | blender->feed(im, 0xFFFFFF & channels.at(0), corners.at(i - 1)); 140 | } 141 | 142 | blender->blend(result, result_mask); 143 | std::cout << "writing " << argv[argc - 1] << std::endl; 144 | cv::imwrite(argv[argc - 1], result); 145 | std::cout << "done" << std::endl; 146 | 147 | outds = (GDALDataset *)GDALOpen(argv[argc - 1], GA_Update); 148 | check(outds != NULL, "Could not open %s\n", argv[argc - 1]); 149 | 150 | outds->SetGeoTransform(geotransform); 151 | outds->SetProjection(datasets.at(0)->GetProjectionRef()); 152 | 153 | GDALClose(outds); 154 | for (GDALDataset *ds : datasets) GDALClose(ds); 155 | return 0; 156 | error: 157 | if (outds) GDALClose(outds); 158 | for (GDALDataset *ds : datasets) GDALClose(ds); 159 | return -1; 160 | } -------------------------------------------------------------------------------- /src/schooner-blend.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "utils.h" 10 | 11 | const char *usage = "usage: schooner-blend out.tif"; 12 | 13 | void close_datasets(GDALDatasetH *datasets, int num) { 14 | for (int i = 0; i < num; i++) 15 | if (datasets[i] != NULL) { 16 | GDALClose(datasets[i]); 17 | datasets[i] = NULL; 18 | } 19 | } 20 | 21 | bool open_datasets(GDALDatasetH *datasets, int num, char **files) { 22 | for (int i = 0; i < num; i++) { 23 | int (*checks[])(GDALDatasetH) = {GDALGetRasterCount, GDALGetRasterXSize, 24 | GDALGetRasterYSize}; 25 | int j; 26 | datasets[i] = GDALOpen(files[i], GA_ReadOnly); 27 | check(datasets[i] != NULL, "Couldn't open %s\n%s", files[i], 28 | CPLGetLastErrorMsg()); 29 | 30 | for (j = 0; j < 3; j++) 31 | check(checks[j](datasets[0]) == checks[j](datasets[i]), 32 | "%s not the same size as %s (%i vs %i)\n", files[0], files[i], 33 | checks[j](datasets[0]), checks[j](datasets[i])); 34 | } 35 | 36 | return true; 37 | error: 38 | return false; 39 | } 40 | 41 | int sort_int(const void *a, const void *b) { 42 | return (*(uint8_t *)a > *(uint8_t *)b) ? 1 : -1; 43 | } 44 | 45 | int median(std::vector els) { 46 | if (els.size() == 0) return 0; 47 | if (els.size() == 1) return els.at(0); 48 | std::sort(els.begin(), els.end()); 49 | int median; 50 | int size = els.size(); 51 | if (size % 2 == 0) { 52 | median = els.at(size / 2); 53 | } else { 54 | median = (els.at(size / 2) + els.at((size + 2) / 2)) / 2; 55 | } 56 | return median; 57 | } 58 | 59 | void process_datasets(GDALDatasetH out, GDALDatasetH *datasets, int num) { 60 | int bands = GDALGetRasterCount(datasets[0]); 61 | int width = GDALGetRasterXSize(datasets[0]); 62 | int height = GDALGetRasterYSize(datasets[0]); 63 | uint16_t *outscan = (uint16_t *)calloc(width, sizeof(uint16_t)); 64 | uint16_t *scanline[num]; 65 | for (int i = 0; i < num; i++) 66 | scanline[i] = (uint16_t *)calloc(width, sizeof(uint16_t)); 67 | 68 | // for each band 69 | for (int b = 1; b <= bands; b++) { 70 | // and each row 71 | for (int y = 0; y < height; y++) { 72 | // grab the scanlines 73 | for (int d = 0; d < num; d++) { 74 | GDALRasterBandH band = GDALGetRasterBand(datasets[d], b); 75 | GDALRasterIO(band, GF_Read, 0, y, width, 1, scanline[d], width, 1, 76 | GDT_UInt16, 0, 0); 77 | } 78 | 79 | // populate the out scanline with the average value across datasets 80 | for (int x = 0; x < width; x++) { 81 | uint16_t pixels[num]; 82 | for (int d = 0; d < num; d++) pixels[d] = 0; 83 | int size = 0; 84 | for (int d = 0; d < num; d++) { 85 | if (scanline[d][x] > 0) { 86 | pixels[size] = scanline[d][x]; 87 | size++; 88 | } 89 | } 90 | 91 | // average the pixels 92 | std::vector sorted; 93 | for (int i = 0; i < size; i++) sorted.push_back(pixels[i]); 94 | int med = median(sorted); 95 | 96 | std::vector deviations; 97 | for (int i = 0; i < size; i++) 98 | deviations.push_back(std::abs(sorted.at(i) - med)); 99 | int mad = median(deviations); 100 | 101 | // Boris Iglewicz and David Hoaglin (1993), "Volume 16: How to Detect 102 | // and Handle Outliers" 103 | int tot = 0; 104 | int real_size = size; 105 | for (int i = 0; i < size; i++) { 106 | float m = mad != 0 107 | ? std::abs(0.6745 * ((float)pixels[i] - (float)med) / 108 | (float)mad) 109 | : 0; 110 | if (m < 1.0) { 111 | tot += pixels[i]; 112 | } else { 113 | real_size--; 114 | } 115 | } 116 | outscan[x] = real_size != 0 ? tot / real_size : pixels[0]; 117 | } 118 | GDALRasterBandH band = GDALGetRasterBand(out, b); 119 | GDALRasterIO(band, GF_Write, 0, y, width, 1, outscan, width, 1, 120 | GDT_UInt16, 0, 0); 121 | printf("Band %i: %.2f%%\r", b, (float)y / (float)height * 100); 122 | fflush(stdout); 123 | } 124 | printf("Band %i: done \n", b); 125 | } 126 | 127 | for (int i = 0; i < num; i++) free(scanline[i]); 128 | } 129 | 130 | int main(int argc, char *argv[]) { 131 | if (argc < 3) { 132 | printf("usage: schooner-blend out.tif"); 133 | return -1; 134 | }; 135 | GDALAllRegister(); 136 | int num = argc - 2; 137 | char *out; 138 | char **options = NULL; 139 | GDALDatasetH datasets[num], outds = NULL; 140 | memset(datasets, 0, sizeof(datasets)); 141 | 142 | bool err = open_datasets(datasets, num, argv + 1); 143 | check(err, "Could not open all datasets.\n"); 144 | 145 | out = argv[argc - 1]; 146 | 147 | options = CSLSetNameValue(options, "PHOTOMETRIC", "RGB"); 148 | 149 | outds = GDALCreate(GDALGetDriverByName("GTiff"), out, 150 | GDALGetRasterXSize(datasets[0]), 151 | GDALGetRasterYSize(datasets[0]), 152 | GDALGetRasterCount(datasets[0]), GDT_UInt16, options); 153 | check(outds, "Couldn't create the output dataset %s.\n", out); 154 | 155 | process_datasets(outds, datasets, num); 156 | assign_projection(datasets[0], outds); 157 | close_datasets(datasets, num); 158 | return 0; 159 | error: 160 | close_datasets(datasets, num); 161 | if (outds) GDALClose(outds); 162 | return -1; 163 | } 164 | --------------------------------------------------------------------------------