diff --git a/Deblending/scarlet_tutorial.ipynb b/Deblending/scarlet_tutorial.ipynb index 298ecaa6..e4ae262d 100644 --- a/Deblending/scarlet_tutorial.ipynb +++ b/Deblending/scarlet_tutorial.ipynb @@ -6,8 +6,8 @@ "source": [ "# Deblending with *Scarlet*\n", "
Owner(s): **Fred Moolekamp** ([@fred3m](https://github.com/LSSTScienceCollaborations/StackClub/issues/new?body=@fred3m))\n", - "
Last Verified to Run: **2019-08-16**\n", - "
Verified Stack Release: **w_2019-31**\n", + "
Last Verified to Run: **2020-07-10**\n", + "
Verified Stack Release: **v20.0.0**\n", "\n", "The purpose of this tutorial is to familiarize you with the basics of using *scarlet* to model blended scenes, and how tweaking various objects and parameters affects the resulting model. A tutorial that is more specific to using scarlet in the context of the LSST DM Science Pipelines is also available.\n", "\n", @@ -74,124 +74,6 @@ "metadata": {}, "outputs": [], "source": [ - "# Display the sources\n", - "# Display the sources\n", - "def display_sources(sources, observation, norm=None, subset=None, combine=False, show_sed=True):\n", - " \"\"\"Display the data and model for all sources in a blend\n", - "\n", - " This convenience function is used to display all (or a subset) of\n", - " the sources and (optionally) their SED's.\n", - " \"\"\"\n", - " if subset is None:\n", - " # Show all sources in the blend\n", - " subset = range(len(sources))\n", - " for m in subset:\n", - " # Load the model for the source\n", - " src = sources[m]\n", - " if hasattr(src, \"components\"):\n", - " components = len(src.components)\n", - " else:\n", - " components = 1\n", - " # Convolve the model with the psfs in the observation\n", - " model = observation.render(src.get_model())\n", - " # Extract the bounding box that contains the non-zero\n", - " # pixels in the model\n", - " bbox = scarlet.bbox.trim(np.sum(model, axis=0), min_value=1e-2)\n", - " bb = (slice(None), *bbox.slices)\n", - " # Adjust the stretch based on the maximum flux in the model for the current source\n", - " if model.max() > 10 * bg_rms.max():\n", - " norm = AsinhMapping(minimum=model.min(), stretch=model.max()*.05, Q=10)\n", - " else:\n", - " norm = LinearMapping(minimum=model.min(), maximum=model.max())\n", - "\n", - " # Select the image patch the overlaps with the source and convert it to an RGB image\n", - " img_rgb = scarlet.display.img_to_rgb(images[bb], norm=norm)\n", - "\n", - " # Build a model for each component in the model\n", - " if hasattr(src, \"components\"):\n", - " rgb = []\n", - " for component in src.components:\n", - " # Convert the model to an RGB image\n", - " _model = observation.get_model(component.get_model())\n", - " _rgb = scarlet.display.img_to_rgb(_model[bb], norm=norm)\n", - " rgb.append(_rgb)\n", - " else:\n", - " # There is only a single component\n", - " rgb = [scarlet.display.img_to_rgb(model[bb], norm=norm)]\n", - "\n", - " # Display the image and model\n", - " figsize = [6,3]\n", - " columns = 2\n", - " # Calculate the number of columns needed and shape of the figure\n", - " if show_sed:\n", - " figsize[0] += 3\n", - " columns += 1\n", - " if not combine:\n", - " figsize[0] += 3*(components-1)\n", - " columns += components-1\n", - " # Build the figure\n", - " fig = plt.figure(figsize=figsize)\n", - " ax = [fig.add_subplot(1,columns,n+1) for n in range(columns)]\n", - " ax[0].imshow(img_rgb)\n", - " ax[0].set_title(\"Data: Source {0}\".format(m))\n", - " for n, _rgb in enumerate(rgb):\n", - " ax[n+1].imshow(_rgb)\n", - " if combine:\n", - " ax[n+1].set_title(\"Initial Model\")\n", - " else:\n", - " ax[n+1].set_title(\"Component {0}\".format(n))\n", - " if show_sed:\n", - " if components > 1:\n", - " for comp in src:\n", - " ax[-1].plot(comp.sed)\n", - " else:\n", - " ax[-1].plot(src.sed)\n", - " ax[-1].set_title(\"SED\")\n", - " ax[-1].set_xlabel(\"Band\")\n", - " ax[-1].set_ylabel(\"Intensity\")\n", - " # Mark the current source in the image\n", - " if components > 1:\n", - " y,x = src.components[0].pixel_center\n", - " else:\n", - " y,x = src.pixel_center\n", - " ax[0].plot(x-bb[2].start, y-bb[1].start, 'rx', mew=2)\n", - " plt.tight_layout()\n", - " plt.show()\n", - "\n", - "def display_model_residual(images, blend, peaks, norm):\n", - " \"\"\"Display the data, model, and residual for a given result\n", - " \"\"\"\n", - " model = blend.get_model()\n", - " residual = images-model\n", - " print(\"Data range: {0:.3f} to {1:.3f}\\nresidual range: {2:.3f} to {3:.3f}\\nrms: {4:.3f}\".format(\n", - " np.min(images),\n", - " np.max(images),\n", - " np.min(residual),\n", - " np.max(residual),\n", - " np.sqrt(np.std(residual)**2+np.mean(residual)**2)\n", - " ))\n", - " # Create RGB images\n", - " img_rgb = scarlet.display.img_to_rgb(images, norm=norm)\n", - " model_rgb = scarlet.display.img_to_rgb(model, norm=norm)\n", - " residual_rgb = scarlet.display.img_to_rgb(residual)\n", - "\n", - " # Show the data, model, and residual\n", - " fig = plt.figure(figsize=(15,5))\n", - " ax = [fig.add_subplot(1,3,n+1) for n in range(3)]\n", - " ax[0].imshow(img_rgb)\n", - " ax[0].set_title(\"Data\")\n", - " ax[1].imshow(model_rgb)\n", - " ax[1].set_title(\"Model\")\n", - " ax[2].imshow(residual_rgb)\n", - " ax[2].set_title(\"Residual\")\n", - " for k,component in enumerate(blend.components):\n", - " y,x = component.pixel_center\n", - " #px, py = peaks[k]\n", - " ax[0].plot(x, y, \"gx\")\n", - " #ax[0].plot(px, py, \"rx\")\n", - " ax[1].text(x, y, k, color=\"r\")\n", - " plt.show()\n", - "\n", "def show_psfs(psfs, filters, norm=None):\n", " rows = int(np.ceil(len(psfs)/3))\n", " columns = min(len(psfs), 3)\n", @@ -256,6 +138,7 @@ "peaks = data[\"peaks\"]\n", "psfs = data[\"psfs\"]\n", "filters = [\"G\", \"R\", \"I\", \"Z\", \"Y\"]\n", + "\n", "# Only a rough estimate of the background is needed\n", "# to initialize and resize the sources\n", "bg_rms = np.std(images, axis=(1,2))\n", @@ -263,6 +146,7 @@ "\n", "# Use Asinh scaling for the images\n", "norm = AsinhMapping(minimum=images.min(), stretch=images.max()/20, Q=10)\n", + "\n", "# Convert the image to an RGB image\n", "img_rgb = scarlet.display.img_to_rgb(images, norm=norm)\n", "plt.imshow(img_rgb)\n", @@ -272,6 +156,29 @@ "plt.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Psf display " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also takea look at the `psfs`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show_psfs(psfs, filters)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -285,6 +192,15 @@ "See https://fred3m.github.io/scarlet/user_docs.html#Frame-and-Observation for more on `Frame`s and `Observation`s." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from functools import partial" + ] + }, { "cell_type": "code", "execution_count": null, @@ -292,17 +208,20 @@ "outputs": [], "source": [ "# Create a PSF image of a narrow gaussian to use as our image PSF\n", - "model_psf = scarlet.generate_psf_image(scarlet.gaussian, shape=(41, 41), amplitude=1, sigma=.9)\n", + "amplitude=1.\n", + "model_psf = scarlet.PSF(partial(scarlet.psf.gaussian, sigma=0.9), shape=(None,41, 41)).image * amplitude\n", "model_psf /= model_psf.sum()\n", "# Make sure that the observation PSF is normalized (otherwise the scaling in PSF matching might be off)\n", "psfs = psfs / psfs.sum(axis=(1, 2))[:, None, None]\n", + "\n", "# Create the initial frame (metadata for the model).\n", "# Note that we initialized a PSF with shape (Ny, Nx) but a frame\n", "# expects a PSf with shape (bands, Ny, Nx), so we have to\n", "# broadcast the model_psf into an extra dimension\n", - "frame = scarlet.Frame(images.shape, psfs=model_psf[None])\n", + "frame = scarlet.Frame(images.shape, psfs=model_psf, channels=filters)\n", + "\n", "# Create our observation\n", - "observation = scarlet.Observation(images, psfs=psfs).match(frame)" + "observation = scarlet.Observation(images, psfs=psfs, channels=filters, weights=weights).match(frame)" ] }, { @@ -315,22 +234,32 @@ "\n", "The different classes that inherit from `Source` mainly differ in how they are initialized, and otherwise behave similarly during the optimization routine. This section illustrates the differences between different source initialization classes.\n", "\n", - "The simplest source is a single component intialized with only a single pixel (at the center of the object) turned on.\n", - "\n", "### *WARNING* \n", "Scarlet accepts source positions using the numpy/C++ convention of (y,x), which is different than the astropy and LSST stack convention of (x,y)." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Below we demonstrate the usage of `ExtendedSource`, which initializes each object as a single component with maximum flux at the peak that falls off monotonically and has 180 degree symmetry." + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "sources = [scarlet.PointSource(frame, (peak[1], peak[0]), observation) for peak in peaks]\n", + "sources = [scarlet.ExtendedSource(frame, (peak[1], peak[0]), observation) for peak in peaks]\n", "\n", "# Display the initial guess for each source\n", - "display_sources(sources, observation, norm=norm)" + "scarlet.display.show_sources(sources,\n", + " norm=norm,\n", + " observation=observation,\n", + " show_rendered=True,\n", + " show_observed=True)\n", + "plt.show()" ] }, { @@ -339,7 +268,7 @@ "source": [ "## Exercise:\n", "\n", - "* Experiment with the above code by using `ExtendedSource`, which initializes each object as a single component with maximum flux at the peak that falls off monotonically and has 180 degree symmetry; and using `MultiComponentSource`, which models a source as two components (a bulge and a disk) that are each symmetric and montonically decreasing from the peak.\n", + "* Experiment with the above code by using ; and using `MultiComponentSource`, which models a source as two components (a bulge and a disk) that are each symmetric and montonically decreasing from the peak.\n", "\n", "# Deblending a scene\n", "\n", @@ -369,29 +298,20 @@ "outputs": [], "source": [ "# Fit the data until the relative error is <= 1e-3,\n", - "# for a maximum of 100 iterations\n", - "blend.fit(100, e_rel=1e-3)\n", - "print(\"Deblending completed in {0} iterations\".format(blend.it))\n", - "display_model_residual(images, blend, peaks, norm)\n", - "display_sources(sources, observation, norm)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Exercises\n", - "\n", - "* Experiment by running the above code using different source models (for example `ExtendedSource`) to see how initializtion affects the belnding results." + "# for a maximum of 200 iterations\n", + "blend = scarlet.Blend(sources, observation)\n", + "%time blend.fit(200, e_rel = 1e-3)\n", + "print(\"scarlet ran for {0} iterations to logL = {1}\".format(len(blend.loss), -blend.loss[-1]))\n", + "plt.plot(-np.array(blend.loss))\n", + "plt.xlabel('Iteration')\n", + "plt.ylabel('log-Likelihood')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Update Functions\n", - "\n", - "The above models used the default constraints: perfect symmetry and a weighted monotonicity that decreases from the peak. So each source is created with the update function, equivalent to the following:" + "There are two options for displaying the scene, using `scarlet.display.show_scene` function. This shows the model along with the observation information and the residuals defined as: `observation.images - model`. " ] }, { @@ -400,54 +320,20 @@ "metadata": {}, "outputs": [], "source": [ - "from scarlet import measurement, update\n", - "\n", - "class MySource(scarlet.ExtendedSource):\n", - " def update(self):\n", - " \"\"\"Default update parameters for a PointSource\n", - " This method can be overwritten if a different set of constraints\n", - " or update functions is desired.\n", - " \"\"\"\n", - " # Keep track of the iteration so we can skip certain updates\n", - " # based on the iteration number\n", - " if self._parent is None:\n", - " it = 0\n", - " else:\n", - " it = self._parent.it\n", - "\n", - " # Update the central pixel location (pixel_center)\n", - " self.pixel_center = measurement.max_pixel(self.morph, self.pixel_center)\n", - "\n", - " if self.symmetric:\n", - " # Update the centroid position every 5th iteration\n", - " if it % 5 == 0:\n", - " self.pixel_center, self.shift = measurement.psf_weighted_centroid(self.morph,\n", - " self._centroid_weight,\n", - " self.pixel_center)\n", - "\n", - " # make the morphology perfectly symmetric\n", - " update.symmetric(self, algorithm=\"kspace\")\n", - "\n", - " if self.monotonic:\n", - " # make the morphology monotonically decreasing\n", - " update.monotonic(self, self.pixel_center)\n", - "\n", - " update.positive(self) # Make the SED and morph non-negative\n", - " update.normalized(self) # Use MORPH_MAX normalization\n", - " return self" + "scarlet.display.show_scene(sources, norm=norm,linear=True, \n", + " observation=observation, show_observed=True, \n", + " label_sources=True, \n", + " show_rendered=True, \n", + " show_residual=True\n", + " )\n", + "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Exercises\n", - "\n", - "In the code above modify the `MySource.update` method to use different constraints and run the code block following this cell for the following:\n", - "\n", - "* Add the keyword argument `use_nearest=True` to `update.monotonic` and see how that affects the results.\n", - "\n", - "* Comment out the lines that set `self.pixel_center` so that positions are not updated and see how that changes the results." + "You can also do it by hand. " ] }, { @@ -456,22 +342,42 @@ "metadata": {}, "outputs": [], "source": [ - "offset = np.random.rand(len(peaks), 2)/2\n", - "sources = [MySource(frame, (peak[1]+offset[k][0], peak[0]+offset[k][1]), observation, bg_rms) for k, peak in enumerate(peaks)]\n", - "blend = scarlet.Blend(sources, observation)\n", - "blend.fit(100, e_rel=1e-3)\n", - "print(\"Deblending completed in {0} iterations\".format(blend.it))\n", - "display_model_residual(images, blend, peaks, norm)\n", - "display_sources(sources, observation, norm)" + "# Load the model and calculate the residual\n", + "model = blend.get_model()\n", + "model_ = observation.render(model) # adapt model to observations. \n", + "residual = images-model_\n", + "\n", + "# Create RGB images\n", + "model_rgb = scarlet.display.img_to_rgb(model_, norm=norm)\n", + "residual_rgb = scarlet.display.img_to_rgb(residual)\n", + "\n", + "# Show the data, model, and residual\n", + "fig = plt.figure(figsize=(15,5))\n", + "ax = [fig.add_subplot(1,3,n+1) for n in range(3)]\n", + "ax[0].imshow(img_rgb)\n", + "ax[0].set_title(\"Data\")\n", + "ax[1].imshow(model_rgb)\n", + "ax[1].set_title(\"Model\")\n", + "ax[2].imshow(residual_rgb)\n", + "ax[2].set_title(\"Residual\")\n", + "\n", + "for k,component in enumerate(blend):\n", + " y,x = component.center\n", + " ax[0].text(x, y, k, color=\"w\")\n", + " ax[1].text(x, y, k, color=\"w\")\n", + " ax[2].text(x, y, k, color=\"w\")\n", + "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Extra Credit\n", + "## Exercises\n", + "\n", + "* Experiment by running the above code using different source models (for example `MultiComponentSource` or `PointSource`) to see how initializtion affects the belnding results.\n", "\n", - "* Modify `MySource` to create a source that has multiple components. You might want to take a look at [MultiComponentSource](https://github.com/fred3m/scarlet/blob/master/scarlet/source.py#L478) to get an understanding about how to do this, but try your own initialization function." + "* Change the value of `e_rel` in the above fit and try to understand how it affects the results. " ] } ], @@ -491,9 +397,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.2" + "version": "3.7.6" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 }