Let’s look an illustrative real-world manifestation of this conceptual framework. SAR images are subject to speckle, a type of image distortion or noise which affects pixels in a stochastic, multiplicative way. Why does this occur?
Consider that the returned wave associated with a given pixel likely isn’t the result of a single interation with a single pointwise object. If multiple reflectors (or similarly, a continuous reflector) lie within the pixel, then the returned wave is the sum of multiple individual waves.
Let’s imagine we have a pixel with two identical point reflectors lying within it. How does the returned wave change as a function of where those reflectors are located? Move the blue and red targets in the pixel below:
Code
chart = {const svg = d3.create("svg").attr("viewBox", [0,0, width, height])// define a bounding rectangle svg.append("rect").attr("x", width/7).attr("y", stroke_width).attr("width",5*width/7).attr("height", height -2*stroke_width).attr("stroke","black").attr("stroke-width", stroke_width).attr("fill-opacity",0.087124976) //This is a hard-coded value based on the prescribed physical parameters. It's hardcoded because it's easier than dealing with the resulting reactive dependencies since this cell should never be run again// define data used for circlesconst circles = d3.range(2).map(i => ({x: (i+2) * width /5,y: (i+1) * height /3,index: i, }));// define circles as graphic objects svg.selectAll("circle").data(circles).join("circle").attr("cx", d => d.x).attr("cy", d => d.y).attr("r", radius).attr("fill", d => d3.schemeCategory10[d.index*3]).attr("id",function(d,i) {return i}).call(drag)return svg.node();}chart2 = {const svg = d3.create("svg").attr("viewBox", [0,0, width, height])//define a function to take the canvas position of the leftside chart to the// corresponding region in the second chart// it's messy because I solved the system by hand and didn't simplify, RIP// to future Jake if I ever have to un-hardcode the object position valuesfunctionlineartransform(xposition){return (2*height/5- radius)/(5/7*width -2*radius-2)*xposition + width/7+2/5*height + radius/2- (2*height/5- radius)*(width/7+radius+1)/(5/7*width-2*radius-2) }// define a earth curvature line svg.append("rect").attr("x", width/7).attr("y", height -2*stroke_width).attr("width",5*width/7).attr("height", stroke_width/2).attr("stroke","black").attr("stroke-width", stroke_width).attr("fill-opacity",0)// define lines going from the target to the satellites (first, so they appear under them) svg.append("line").attr("x1", width/7+3*height/20).attr("x2",lineartransform(x1)).attr("y1",3*height/20).attr("y2", height -2*stroke_width).style("stroke-width", stroke_width).style("stroke","blue") svg.append("line").attr("x1", width/7+3*height/20).attr("x2",lineartransform(x2)).attr("y1",3*height/20).attr("y2", height -2*stroke_width).style("stroke-width", stroke_width).style("stroke","red")// define a rectangle representing the satellite svg.append("rect").attr("x", width/7+ height/10).attr("y", height/10).attr("width", height/10).attr("height", height/10).attr("stroke","black").attr("stroke-width", stroke_width).attr("fill-opacity",.50)// define notches indicating the bounds of the pixel svg.append("line").attr("x1", width/7+2*height/5).attr("x2", width/7+2*height/5).attr("y1", height -2*stroke_width).attr("y2", height -2*stroke_width - height/20).style("stroke-width", stroke_width).style("stroke","black") svg.append("line").attr("x1", width/7+4*height/5).attr("x2", width/7+4*height/5).attr("y1", height -2*stroke_width).attr("y2", height -2*stroke_width - height/20).style("stroke-width", stroke_width).style("stroke","black")// define data used for circlesconst circles = d3.range(2).map(i => ({x: (i+2) * width /5,y: (i+1) * height /3,index: i, }));// define circles as graphic objects svg.selectAll("circle").data(circles).join("circle").attr("cx",function(d,i) {if (i ==0) {returnlineartransform(x1)}else {returnlineartransform(x2)} }).attr("cy", height -2*stroke_width).attr("r", radius/2).attr("fill", d => d3.schemeCategory10[d.index*3]).attr("id",function(d,i) {return i})// add text svg.append("text").attr("x", width/7).attr("y",.94*height).style("font-size","28px").text("side view") svg.append("text").attr("x", width/2).attr("y",.15*height).style("font-size","24px").style("font-style","italic").text("*wildly not to scale")return svg.node();}
Here we’ve considered a SAR satellite 600km above Earth, emitting 50cm wavelength radiation with a pixel resolution of 3m.
The distance from each target to the satellite affects where in their cycle each wave is re-intercepted, and thus the difference determines whether those returned waves interfere constructively or deconstructively. Thus a random distribution of targets creates a random difference in distance to the satellite, creating a random level of interference in each pixel. This means the intensity of each pixel is randomly (though not necessarily uniformly) multiplied by a value from 0 (perfect deconstructive interference) to 1 (perfect constructive interference). This is the origin of speckle, and reason why it manifests as multiplicative noise (as opposed to additive).
Let’s now consider two pixels: one with more, stronger scatterers and one with fewer, weaker scatterers – perhaps pixel one belongs to some dense shrubland, while the other is a freshly plowed field. The distribution of scatterers is likely quite random – what happens to the brightness of these pixels under different configurations?
Code
targetnum_1 =10;targetnum_2 =10;max_radius_p1 =35;min_radius_p1 =25;max_radius_p2 =20;min_radius_p2 =15;radii_p1 = {var data = [];for (var i =1; i <= targetnum_1; i++) { data.push(Math.random()*(max_radius_p1-min_radius_p1) + min_radius_p1); }return data;}radii_p2 = {var data = [];for (var i =1; i <= targetnum_2; i++) { data.push(Math.random()*(max_radius_p2-min_radius_p2) + min_radius_p2); }return data;}
amplitude_p1 = {var dummy = vtoggle1;var amplitude_naught = radii_p1[0];var dt_naught =2*Math.sqrt( sat_z**2+ (pixel1_positions[0][0]/width*pixel_width - sat_x)**2+ (pixel1_positions[0][1]/width*pixel_width - sat_y)**2 );var phase_naught = dt_naught / wavelength *2*Math.PI;//successively add the waves, determining the new phases and amplitudesfor (var i =1; i <= targetnum_1-1; i++) {//compute the phase of the next subwavevar dt_next =2*Math.sqrt( sat_z**2+ (pixel1_positions[i][0]/width*pixel_width - sat_x)**2+ (pixel1_positions[i][1]/width*pixel_width - sat_y)**2 );var phase_next = dt_next / wavelength *2*Math.PI;//compute the amplitude of the new sumvar new_amplitude =Math.sqrt( amplitude_naught**2+ radii_p1[i]**2+2*amplitude_naught*radii_p1[i]*Math.cos(phase_next-phase_naught) );//compute the phase of the new sumvar new_phase =Math.atan( (amplitude_naught*Math.sin(phase_naught) + radii_p1[i]*Math.sin(phase_next))/(amplitude_naught*Math.cos(phase_naught) + radii_p1[i]*Math.cos(phase_next)) ) amplitude_naught = new_amplitude; phase_naught = new_phase; }return amplitude_naught}amplitude_p2 = {var dummy = vtoggle1;var amplitude_naught = radii_p2[0];var dt_naught =2*Math.sqrt( sat_z**2+ (pixel2_positions[0][0]/width*pixel_width - sat_x)**2+ (pixel2_positions[0][1]/width*pixel_width - sat_y)**2 );var phase_naught = dt_naught / wavelength *2*Math.PI;//successively add the waves, determining the new phases and amplitudesfor (var i =1; i <= targetnum_1-1; i++) {//compute the phase of the next subwavevar dt_next =2*Math.sqrt( sat_z**2+ (pixel2_positions[i][0]/width*pixel_width - sat_x)**2+ (pixel2_positions[i][1]/width*pixel_width - sat_y)**2 );var phase_next = dt_next / wavelength *2*Math.PI;//compute the amplitude of the new sumvar new_amplitude =Math.sqrt( amplitude_naught**2+ radii_p2[i]**2+2*amplitude_naught*radii_p2[i]*Math.cos(phase_next-phase_naught) );//compute the phase of the new sumvar new_phase =Math.atan( (amplitude_naught*Math.sin(phase_naught) + radii_p2[i]*Math.sin(phase_next))/(amplitude_naught*Math.cos(phase_naught) + radii_p2[i]*Math.cos(phase_next)) ) amplitude_naught = new_amplitude; phase_naught = new_phase; }return amplitude_naught}{mutable brightnesses.push({x: amplitude_p1,p:"Pixel 1"});mutable brightnesses.push({x: amplitude_p2,p:"Pixel 2"});mutable brightnesses = mutable brightnesses;}
Code
//histogram to be populated with the brightness values of the two pixelsmutable brightnesses = [];
Selecting the Auto Redistribute option to generate a few hundred random configurations of each pixel type, we can see in the developing histogram of their brightnesses that while the pixel with more strongly reflecting targets is on average brighter, there are many times when the pixel with weaker targets actually gives a higher return. This is due to the random level of interference we’ve called speckle. We can exploit the fact that the average value remains higher to identify likely high-return areas in speckle filtering. As an aside, notice that the distributions of brightness appear relatively Gaussian – this is a lovely invocation of the Central Limit Theorem.