001/******************************************************************************* 002 * This software is provided as a supplement to the authors' textbooks on digital 003 * image processing published by Springer-Verlag in various languages and editions. 004 * Permission to use and distribute this software is granted under the BSD 2-Clause 005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 006 * Copyright (c) 2006-2023 Wilhelm Burger, Mark J. Burge. All rights reserved. 007 * Visit https://imagingbook.com for additional details. 008 ******************************************************************************/ 009 010package imagingbook.common.sift.scalespace; 011 012import ij.process.FloatProcessor; 013import imagingbook.common.filter.linear.GaussianFilterSeparable; 014 015/** 016 * <p> 017 * Represents a single scale level in a generic hierarchical scale space. See Secs. 25.1.4 for more details. Pixel data 018 * are represented as one-dimensional {@code float} arrays. This class defines no public constructor. 019 * </p> 020 * <p> 021 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing – An Algorithmic Introduction</em>, 3rd ed, Springer 022 * (2022). 023 * </p> 024 * 025 * @author WB 026 * @version 2022/11/20 removed FloatProcessor as superclass 027 */ 028public class ScaleLevel { 029 030 private final int width, height; 031 private final float[] data; 032 033 private double absoluteScale; 034 035 // ------------------------------ 036 037 /** 038 * Constructor (non-public). 039 */ 040 ScaleLevel(int width, int height, float[] data, double absoluteScale) { 041 this.width = width; 042 this.height = height; 043 this.data = (data != null) ? data : new float[width * height]; 044 this.absoluteScale = absoluteScale; 045 } 046 047 /** 048 * Constructor (non-public). 049 */ 050 ScaleLevel(ScaleLevel level, double absoluteScale) { 051 this(level.width, level.height, level.data.clone(), absoluteScale); 052 } 053 054 // ------------------------------ 055 056 /** 057 * Returns the width of this scale level. 058 * @return the width of this scale level 059 */ 060 public int getWidth() { 061 return this.width; 062 } 063 064 /** 065 * Returns the height of this scale level. 066 * @return the height of this scale level 067 */ 068 public int getHeight() { 069 return this.height; 070 } 071 072 /** 073 * Returns a reference to the internal (one-dimensional) data array of this scale level. 074 * 075 * @return to the internal data array 076 */ 077 public float[] getData() { 078 return this.data; 079 } 080 081 /** 082 * Returns the absolute scale assigned to this scale level. 083 * 084 * @return the absolute scale 085 */ 086 public double getAbsoluteScale() { 087 return this.absoluteScale; 088 } 089 090 // ------------------------------ 091 092 /** 093 * Returns a new ImageJ {@link FloatProcessor} with the same size and pixel data as this scale level. Note that the 094 * pixel data are not duplicated but shared, i.e., subsequent modifications to the new {@link FloatProcessor} are 095 * transparent and directly affect the contents of this scale level. Thus the resulting {@link FloatProcessor} only 096 * serves as a wrapper for the data in this scale level. 097 * 098 * @return a new {@link FloatProcessor} instance 099 */ 100 FloatProcessor toFloatProcessor() { 101 return new FloatProcessor(this.width, this.height, this.data); 102 } 103 104 /** 105 * Decimates this scale level by factor 2 in both directions and returns a new scale level. 106 * 107 * @return a new, decimated scale level 108 */ 109 ScaleLevel decimate() { // returns a 2:1 subsampled copy of this ScaleLevel 110 final int w1 = this.getWidth(); 111 final int h1 = this.getHeight(); 112 final int w2 = w1 / 2; 113 final int h2 = h1 / 2; 114 115 // new (decimated) level has the same absolute scale: 116 ScaleLevel level2 = new ScaleLevel(w2, h2, null, this.absoluteScale); 117 // resample data: 118 for (int v2 = 0 ; v2 < h2; v2++) { 119 int v1 = 2 * v2; 120 for (int u2 = 0 ; u2 < w2; u2++) { 121 int u1 = 2 * u2; 122 level2.setValue(u2, v2, this.getValue(u1, v1)); 123 } 124 } 125 return level2; //new ScaleLevel(w2, h2, pixels2, absoluteScale); 126 } 127 128 // sometimes we need to set the scale after instantiation: 129 void setAbsoluteScale(double sigma) { 130 this.absoluteScale = sigma; 131 } 132 133 /** 134 * Returns the element value at the specified position of this scale level. An exception is thrown if the position 135 * is outside the scale level's boundaries. 136 * 137 * @param u horizontal position 138 * @param v vertical position 139 * @return the element value 140 */ 141 public float getValue(int u, int v) { 142 return this.data[v * this.width + u]; 143 } 144 145 /** 146 * Sets the element value at the specified position of this scale level. An exception is thrown if the position is 147 * outside the scale level's boundaries. 148 * 149 * @param u horizontal position 150 * @param v vertical position 151 * @param val the new element value 152 */ 153 private void setValue(int u, int v, float val) { 154 this.data[v * this.width + u] = val; 155 } 156 157 /** 158 * Collects and returns the 3x3 neighborhood values at this scale level at center position (u,v). The result is 159 * stored in the supplied 3x3 array. 160 * 161 * @param u horizontal position 162 * @param v vertical position 163 * @param nh the 3x3 array where to insert the neighborhood values 164 */ 165 void get3x3Neighborhood(final int u, final int v, final float[][] nh) { 166 for (int i = 0, x = u - 1; i < 3; i++, x++) { 167 for (int j = 0, y = v - 1; j < 3; j++, y++) { 168 nh[i][j] = this.getValue(x, y); 169 } 170 } 171 } 172 173 /** 174 * Calculates the gradient at the specified scale level position in polar form. The results (gradient magnitude and 175 * direction) are placed in the supplied 2-element array. 176 * 177 * @param u horizontal position 178 * @param v vertical position 179 * @param grad a 2-element array for gradient magnitude and direction 180 */ 181 public void getGradientPolar(int u, int v, final double[] grad) { 182 final double grad_x = this.getValue(u+1, v) - this.getValue(u-1, v); // x-component of local gradient 183 final double grad_y = this.getValue(u, v+1) - this.getValue(u, v-1); // y-component of local gradient 184 grad[0] = Math.hypot(grad_x, grad_y); // local gradient magnitude (E) 185 grad[1] = Math.atan2(grad_y, grad_x); // local gradient direction (phi) 186 } 187 188 189 /** 190 * Applies a Gaussian filter to this scale level, which is modified. 191 * 192 * @param sigma the width of the Gaussian 193 */ 194 void filterGaussian(double sigma) { 195 FloatProcessor fp = this.toFloatProcessor(); 196 new GaussianFilterSeparable(sigma).applyTo(fp); 197 } 198 199 // --------------------------------------- 200 201 @Override 202 public String toString() { 203 return String.format("%s[w=%d h=%d absScale=%.4f]", getClass().getSimpleName(), width, height, absoluteScale); 204 } 205}