├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.cfg ├── setup.py ├── stitching ├── __init__.py ├── __main__.py └── application.py └── tests └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .cache 3 | .coverage 4 | .coverage.* 5 | .eggs/ 6 | .env 7 | .hypothesis/ 8 | .idea/ 9 | .installed.cfg 10 | .ipynb_checkpoints 11 | .Python 12 | .python-version 13 | .ropeproject 14 | .scrapy 15 | .spyderproject 16 | .tox/ 17 | .webassets-cache 18 | *,cover 19 | *.egg 20 | *.egg-info/ 21 | *.log 22 | *.manifest 23 | *.mo 24 | *.pot 25 | *.py[cod] 26 | *.so 27 | *.spec 28 | *$py.class 29 | build/ 30 | celerybeat-schedule 31 | coverage.xml 32 | develop-eggs/ 33 | dist/ 34 | docs/_build/ 35 | downloads/ 36 | eggs/ 37 | env/ 38 | ENV/ 39 | htmlcov/ 40 | instance/ 41 | lib/ 42 | lib64/ 43 | local_settings.py 44 | nosetests.xml 45 | parts/ 46 | pip-delete-this-directory.txt 47 | pip-log.txt 48 | sdist/ 49 | target/ 50 | var/ 51 | venv/ 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD 3-Clause License 2 | 3 | Copyright © 2016 Broad Institute, Inc. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the Broad Institute, Inc. nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED “AS IS.” BROAD MAKES NO EXPRESS OR IMPLIED 20 | REPRESENTATIONS OR WARRANTIES OF ANY KIND REGARDING THE SOFTWARE AND 21 | COPYRIGHT, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE, CONFORMITY WITH ANY DOCUMENTATION, 23 | NON-INFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, WHETHER OR NOT 24 | DISCOVERABLE. IN NO EVENT SHALL BROAD, THE COPYRIGHT HOLDERS, OR CONTRIBUTORS 25 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO PROCUREMENT OF SUBSTITUTE 27 | GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 28 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 30 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF, HAVE REASON TO KNOW, OR IN 31 | FACT SHALL KNOW OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | If, by operation of law or otherwise, any of the aforementioned warranty 34 | disclaimers are determined inapplicable, your sole remedy, regardless of the 35 | form of action, including, but not limited to, negligence and strict 36 | liability, shall be replacement of the software with an updated version if one 37 | exists. 38 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CellProfiler/stitching/c7897351599e5500dd8029075e43e5f0dbb8193e/MANIFEST.in -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Stitching 2 | ========= 3 | 4 | Stitch images to form a montage. 5 | 6 | Requirements 7 | ============ 8 | 9 | Java development kit 10 | 11 | Python 2.7.12 12 | 13 | pip 14 | 15 | numpy 16 | $ pip install numpy 17 | 18 | scipy 19 | $ pip install scipy 20 | 21 | click 22 | $ pip install click 23 | 24 | skimage 25 | $ pip install skimage 26 | 27 | python-bioformats 28 | $ pip install python-bioformats 29 | 30 | matplotlib 31 | $ pip install matplotlib 32 | 33 | Additional in Windows OS: Visual C++ 9.0 34 | 35 | Installation 36 | ============ 37 | 38 | $ git clone https://github.com/CellProfiler/stitching.git 39 | 40 | $ cd /path/to/stitching 41 | 42 | $ pip install -e . 43 | 44 | Info 45 | ==== 46 | Users may notice that when opening and zooming the images in IDEAS (and also in ImageJ, IrfanView) the images appear to be higher resolution, but this is because the software has an adjustable DPI setting, which uses interpolation to scale the images up (adding pixels) for display and publication. 47 | 48 | Use 49 | === 50 | 51 | $ python stitching -o path/to/OUTPUT_DIRECTORY path/to/IMAGE 52 | 53 | Generates per-channel tiled images from IMAGE saved to OUTPUT_DIRECTORY. Each image in IMAGE has shape 55px by 55px and 54 | is padded with random noise. Files are named "ch1.tif", "ch2.tif", ..., one for each channel. 55 | 56 | Optional: 57 | 58 | --image-size 59 | Set the window size of the tile images (default: --image-size 55). 60 | If user sets a size bigger than the original images, each of the tile images will be padded with its own background. 61 | If user sets a size smaller than the original images, each of the tile images will be cropped toward its center. 62 | --montage-size 63 | Set the size of the final montage (default: --montage-size 30). 64 | If the total number of original images could not fill up the desired montage size, the montage will be padded with black background. 65 | For example: 66 | 67 | Number of total images: 1000 68 | 69 | User setting: --image-size 20 --montage-size 30 70 | 71 | The result would be 2 montages at the size of 600x600; one montage is filled up with 900 (30x30) tile images, one montage contains 100 tile images and remaining space is filled up by black background. 72 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CellProfiler/stitching/c7897351599e5500dd8029075e43e5f0dbb8193e/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | data_files=[ 5 | ( 6 | 'stitching_data', 7 | [ 8 | "data/example.cif" 9 | ] 10 | ) 11 | ], 12 | entry_points={ 13 | "console_scripts": [ 14 | "stitching=stitching.__main__:__main__", 15 | ] 16 | }, 17 | install_requires=[ 18 | # "python-bioformats", 19 | "click", 20 | "javabridge", 21 | "numpy", 22 | "scikit-image" 23 | ], 24 | # dependency_links=[ 25 | # "https://github.com/CellProfiler/python-bioformats.git@b8bea8f03d782aad083aa6f085083b059c7caac3#egg=python_bioformats" 26 | # ], 27 | license="BSD", 28 | name="stitching", 29 | packages=setuptools.find_packages( 30 | exclude=[ 31 | "docs", 32 | "tests" 33 | ] 34 | ), 35 | url="https://github.com/CellProfiler/stitching", 36 | version="1.0.0" 37 | ) 38 | -------------------------------------------------------------------------------- /stitching/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CellProfiler/stitching/c7897351599e5500dd8029075e43e5f0dbb8193e/stitching/__init__.py -------------------------------------------------------------------------------- /stitching/__main__.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import os.path 4 | 5 | import bioformats 6 | import bioformats.formatreader 7 | import click 8 | import javabridge 9 | import javabridge.jutil 10 | import numpy 11 | import numpy.random 12 | import skimage.io 13 | from skimage.util import montage 14 | 15 | 16 | @click.command() 17 | @click.argument("image", type=click.Path(exists=True)) 18 | @click.option("-o", "--output", type=click.Path(exists=False)) 19 | @click.option("--image-size", default=55) 20 | @click.option("--montage-size", default=30) 21 | 22 | 23 | def __main__(image, output, image_size, montage_size): 24 | try: 25 | javabridge.start_vm(class_path=bioformats.JARS, max_heap_size='8G') 26 | 27 | os.mkdir(output) 28 | 29 | __stitch(image, output, image_size, montage_size) 30 | finally: 31 | javabridge.kill_vm() 32 | 33 | 34 | def __stitch(filename, output, image_size, montage_size): 35 | reader = bioformats.formatreader.get_image_reader("tmp", path=filename) 36 | 37 | image_count = javabridge.call(reader.metadata, "getImageCount", "()I") 38 | 39 | channel_count = javabridge.call(reader.metadata, "getChannelCount", "(I)I", 0) 40 | 41 | n_chunks = __compute_chunks(image_count/2,montage_size) 42 | chunk_size = montage_size**2 43 | 44 | for channel in range(channel_count): 45 | for chunk in range(n_chunks): 46 | try: 47 | images = [ 48 | reader.read(c=channel, series=image) for image in range(image_count)[::2][chunk*chunk_size:(chunk+1)*chunk_size] 49 | ] 50 | except javabridge.jutil.JavaException: 51 | break 52 | 53 | images = [__pad_or_crop(image, image_size) for image in images] 54 | 55 | montage = skimage.util.montage(numpy.asarray(images), 0) 56 | 57 | ## Optional: if you want to stretch the intensity of the image to uint8 range 58 | #montage = _rescale(montage) 59 | 60 | if chunk == (n_chunks-1): 61 | montage = __pad_to_same_chunk_size(montage, image_size, montage_size) 62 | 63 | skimage.io.imsave(os.path.join(output, "Ch{:d}_{:d}.tif".format(channel + 1, chunk + 1)), montage) 64 | 65 | 66 | def __pad_or_crop(image, image_size): 67 | bigger = max(image.shape[0], image.shape[1], image_size) 68 | 69 | pad_x = float(bigger - image.shape[0]) 70 | pad_y = float(bigger - image.shape[1]) 71 | 72 | pad_width_x = (int(math.floor(pad_x / 2)), int(math.ceil(pad_x / 2))) 73 | pad_width_y = (int(math.floor(pad_y / 2)), int(math.ceil(pad_y / 2))) 74 | 75 | mean, std = _sample(image) 76 | 77 | def normal(vector, pad_width, iaxis, kwargs): 78 | vector[:pad_width[0]] = numpy.random.normal(mean, std, vector[:pad_width[0]].shape) 79 | vector[-pad_width[1]:] = numpy.random.normal(mean, std, vector[-pad_width[1]:].shape) 80 | return vector 81 | 82 | if (image_size > image.shape[0]) & (image_size > image.shape[1]): 83 | return numpy.pad(image, (pad_width_x, pad_width_y), normal) 84 | else: 85 | if bigger > image.shape[1]: 86 | temp_image = numpy.pad(image, (pad_width_y), normal) 87 | else: 88 | if bigger > image.shape[0]: 89 | temp_image = numpy.pad(image, (pad_width_x), normal) 90 | else: 91 | temp_image = image 92 | return temp_image[int((temp_image.shape[0] - image_size)/2):int((temp_image.shape[0] + image_size)/2),int((temp_image.shape[1] - image_size)/2):int((temp_image.shape[1] + image_size)/2)] 93 | 94 | 95 | def __pad_to_same_chunk_size(small_montage, image_size, montage_size): 96 | pad_x = float(montage_size*image_size - small_montage.shape[0]) 97 | 98 | pad_y = float(montage_size*image_size - small_montage.shape[1]) 99 | 100 | npad = ((0,int(pad_y)), (0,int(pad_x))) 101 | 102 | return numpy.pad(small_montage, pad_width=npad, mode='constant', constant_values=0) 103 | 104 | 105 | def __compute_chunks(n_images, montage_size): 106 | 107 | def remainder(images, groups): 108 | return (images - groups * (montage_size ** 2)) 109 | 110 | n_groups = 1 111 | 112 | while remainder(n_images, n_groups) > 0: 113 | n_groups += 1 114 | 115 | return n_groups 116 | 117 | 118 | def _sample(x): 119 | corners = numpy.asarray(( 120 | x[:10, :10].flatten(), 121 | x[:10, -10:].flatten(), 122 | x[-10:, :10].flatten(), 123 | x[-10:, -10:].flatten() 124 | )) 125 | 126 | means = numpy.mean(corners, axis=1) 127 | stds = numpy.std(corners, axis=1) 128 | 129 | # Choose the corner with the lowest standard deviation. 130 | # This is most likely to be background, in the majority 131 | # of observed cases. 132 | std = numpy.min(stds) 133 | 134 | idx = numpy.where(stds == std)[0][0] 135 | mean = means[idx] 136 | 137 | return mean, std 138 | 139 | 140 | # (Optional) Stretch the intensity scale to visible 8-bit images. 141 | def _rescale(image): 142 | vmin, vmax = scipy.stats.scoreatpercentile(image, (0.01, 99.95)) 143 | 144 | return skimage.exposure.rescale_intensity(image, in_range=(vmin, vmax), out_range=numpy.uint8).astype(numpy.uint8) 145 | 146 | 147 | if __name__ == "__main__": 148 | __main__() 149 | -------------------------------------------------------------------------------- /stitching/application.py: -------------------------------------------------------------------------------- 1 | # import matplotlib 2 | # import skimage.io 3 | # import tkinter 4 | # import tkinter.filedialog 5 | # 6 | # matplotlib.use("TkAgg") 7 | # 8 | # 9 | # class Application: 10 | # def __init__(self, master): 11 | # self.master = master 12 | # 13 | # master.title("Stitching") 14 | # 15 | # menu = tkinter.Menu(root) 16 | # 17 | # file_menu = tkinter.Menu(menu, tearoff=0) 18 | # 19 | # file_menu.add_command(label="Open…", command=self.open) 20 | # 21 | # file_menu.add_separator() 22 | # 23 | # file_menu.add_command(label="Save As…", command=self.save_as) 24 | # 25 | # file_menu.add_command(label="Export As…", command=self.export_as) 26 | # 27 | # menu.add_cascade(label="File", menu=file_menu) 28 | # 29 | # master.config(menu=menu) 30 | # 31 | # self.image = None 32 | # 33 | # self.photo = None 34 | # 35 | # def open(self): 36 | # filename = tkinter.filedialog.askopenfilename() 37 | # 38 | # self.image = skimage.io.imread(filename) 39 | # 40 | # self.photo = self.image 41 | # 42 | # def save_as(self): 43 | # filename = tkinter.filedialog.asksaveasfilename() 44 | # 45 | # skimage.io.imsave(filename, self.image) 46 | # 47 | # def export_as(self): 48 | # filename = tkinter.filedialog.asksaveasfilename() 49 | # 50 | # skimage.io.imsave(filename, self.image) 51 | # 52 | # root = tkinter.Tk() 53 | # 54 | # application = Application(root) 55 | # 56 | # root.mainloop() 57 | 58 | import matplotlib 59 | matplotlib.use('TkAgg') 60 | 61 | from numpy import arange, sin, pi 62 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg 63 | from matplotlib.backend_bases import key_press_handler 64 | import skimage.data 65 | 66 | 67 | from matplotlib.figure import Figure 68 | 69 | import sys 70 | 71 | if sys.version_info[0] < 3: 72 | import Tkinter as Tk 73 | else: 74 | import tkinter as Tk 75 | 76 | root = Tk.Tk() 77 | 78 | root.wm_title("Embedding in TK") 79 | 80 | image = skimage.data.camera() 81 | 82 | figure, axis = matplotlib.pyplot.subplots( 83 | ncols=1 84 | ) 85 | 86 | axis.imshow(image) 87 | 88 | axis.set_axis_off() 89 | 90 | canvas = FigureCanvasTkAgg(figure, master=root) 91 | 92 | canvas.get_tk_widget().pack() 93 | 94 | Tk.mainloop() 95 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CellProfiler/stitching/c7897351599e5500dd8029075e43e5f0dbb8193e/tests/__init__.py --------------------------------------------------------------------------------