package com.elphel.imagej.tileprocessor;
/**
 **
 ** CLTPass3d - A single processing "pass" over the image set. May be both actual
 ** FPGA operation or result of merging data from multiple passes
 **
 ** Copyright (C) 2017 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  CLTPass3d.java is free software: you can redistribute it and/or modify
 **  it under the terms of the GNU General Public License as published by
 **  the Free Software Foundation, either version 3 of the License, or
 **  (at your option) any later version.
 **
 **  This program is distributed in the hope that it will be useful,
 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 **  GNU General Public License for more details.
 **
 **  You should have received a copy of the GNU General Public License
 **  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ** -----------------------------------------------------------------------------**
 **
 */

import java.awt.Rectangle;
import java.util.concurrent.atomic.AtomicInteger;

public class CLTPass3d{
//	static double  max_overexposed = 0.8; // TODO: make parameter
		public   double [][]    disparity; // per-tile disparity set for the pass[tileY][tileX]
		public   int    [][]    tile_op;   // what was done in the current pass
		private  double [][]    disparity_sav; // saved disparity
		private  int    [][]    tile_op_sav;   // saved tile_op
		public   double [][]    disparity_map =  null; // add 4 layers - worst difference for the port
		public   double [][]    lazy_eye_data = null;
		public   int            lma_cluster_size = -1;
		public   boolean []     lazy_eye_force_disparity = null;

		double []               calc_disparity = null; // composite disparity, calculated from "disparity", and "disparity_map" fields

		                                       // using horizontal features and corr_magic_scale
		// used directly in TileProcessor.compositeScan()
		double []               calc_disparity_hor =   null; // composite disparity, calculated from "disparity", and "disparity_map" fields
		double []               calc_disparity_vert =  null; // composite disparity, calculated from "disparity", and "disparity_map" fields
		double []               calc_disparity_combo = null; // composite disparity, calculated from "disparity", and "disparity_map" fields
		double []               strength =             null; // composite strength, initially uses a copy of raw 4-sensor correlation strength
		double []               strength_hor =         null; // updated hor strength, initially uses a copy of raw measured
		double []               strength_vert =        null; // updated hor strength, initially uses a copy of raw measured
		// Bg disparity & strength is calculated from the supertiles and used instead of the tile disparity if it is too weak. Assuming, that
		// foreground features should have good correlation details, and if the tile does not nhave them it likely belongs to the background.
		// calculate disparity and strength from the (lapped) supertiles, using lowest allowed (>= minBgDisparity) disparity histogram maximums
		// of the supertiles this tile belongs to
		private double          minBgDisparity =        0.0;
		private double          minBgFract =            0.0; // Use the lowest maximum if the strength strength (of all maximus >= minBgDisparity)
		                                                     // exceeds minBgFract, otherwise proceed to the next one (and accumulate strength)
		private double []       bgTileDisparity =      null;
		private double []       bgTileStrength =       null;
		public  boolean []      border_tiles =         null; // these are border tiles, zero out alpha
		public  boolean []      selected =             null; // which tiles are selected for this layer
		public  double [][][][] texture_tiles;
		// texture_selection is only used for the GPU and if not null means it is for the GPU
		public boolean []       texture_selection =    null; // use by the GPU to set texture to generate
				
		public  double [][]      max_tried_disparity =  null; //[ty][tx] used for combined passes, shows maximal disparity for this tile, regardless of results
		public  boolean         is_combo =             false;
		public  boolean         is_measured =          false;
		public  String          texture = null; // relative (to x3d) path
		public  Rectangle       texture_bounds; // in tiles, not pixels !
		public  int             dbg_index;
		public  int             disparity_index = ImageDtt.DISPARITY_INDEX_CM; // may also be ImageDtt.DISPARITY_INDEX_POLY
		public double [][]      tiles_RBGA =           null;

		SuperTiles              superTiles = null;
		TileProcessor           tileProcessor;
		public CLTPass3d (TileProcessor tileProcessor)
		{
			this.tileProcessor = tileProcessor;
		}

		public CLTPass3d (TileProcessor tileProcessor, int mode)
		{
			this.tileProcessor = tileProcessor;
			switch (mode){
			case 0:
				tile_op =   new int [tileProcessor.getTilesY()][tileProcessor.getTilesX()];
				disparity = new double [tileProcessor.getTilesY()][tileProcessor.getTilesX()];
				break;

			}
		}

		public TileProcessor getTileProcessor()
		{
			return this.tileProcessor;
		}

		public double [][][][] getTextureTiles()
		{
			return 	texture_tiles;
		}
		// texture_selection is only used for the GPU and if not null means it is for the GPU
		public boolean [] getTextureSelection()
		{
			return texture_selection;
		}
		
		public void setTextureSelection(boolean [] selection) {
			texture_selection = selection;
		}

		public void setTextureSelection(int indx, boolean sel) {
			texture_selection[indx] = sel;
		}

		public double [][] getTilesRBGA()
		{
			return tiles_RBGA;
		}

		public void setTilesRBGA(double [][] rgba)
		{
			tiles_RBGA = rgba;
		}
		
		/*
		public float [][] getTextureImages()
		{
			return 	texture_img;
		}

		public Rectangle getTextureWoi()
		{
			return 	texture_woi;
		}
		*/
		
		public double [][] getMaxTriedDisparity()
		{
			return max_tried_disparity;
		}
		
		
		public double [][] getTileRBGA(
				int num_layers) // 4 or 12
		{
//			if (texture_img != null) {
//				System.out.println("FIXME: implement replacement for the GPU-generated textures (using macro mode?)");
//			}
			double [][] tones = getTilesRBGA();
			if (tones != null) {
				int nl = tones.length;
				if (nl > num_layers) {
					double [][] tones1 = new double [num_layers][tones[0].length];
					for (int n = 0; n < num_layers; n++) {
						tones1[n] = tones[n];
					}
					return tones1;
				} else {
					return tones;
				}
			}
			System.out.println("FIXME: should not get here, tones should be calculated earlier");
			
			if (texture_tiles == null) return null;
			int tilesY = texture_tiles.length;
			int tilesX = 0;
			int nl = 0;
			for (int ty = 0; ty < tilesY; ty++){
				if (texture_tiles[ty] != null){
					tilesX = texture_tiles[ty].length;
					for (int tx = 0; tx < tilesX; tx++){
						if (texture_tiles[ty][tx] != null){
							nl = texture_tiles[ty][tx].length;
							break;
						}
					}
					if (nl > 0) break;
				}
				if (nl > 0) break;
			}
			if (num_layers > nl) num_layers = nl;
			int numTiles = tilesX * tilesY;
			double [] scales = new double [num_layers];
			for (int n = 0; n < num_layers; n++){
				if       (n < 3)  scales[n] = 1.0/255.0; // R,B,G
				else if  (n == 3) scales[n] = 1.0; //alpha
				else if  (n < 8)  scales[n] = 1.0; // ports 0..3
				else              scales[n] = 1.0/255.0; // RBG rms, in 1/255 units, but small
			}
			double [][] tileTones = new double [num_layers][numTiles];
			for (int ty = 0; ty < tilesY; ty++ ) if (texture_tiles[ty] != null){
				for (int tx = 0; tx < tilesX; tx++ ) if (texture_tiles[ty][tx] != null) {
					int indx = ty * tilesX + tx;
					for (int n = 0; n < num_layers; n++) if (texture_tiles[ty][tx][n] != null){
						double s = 0.0;
						for (int i = 0; i < texture_tiles[ty][tx][n].length; i++){
							s += texture_tiles[ty][tx][n][i];
						}
						s /= (texture_tiles[ty][tx][n].length/4); // overlapping tiles
						s *= scales[n];
						tileTones[n][indx] = s;
					}
				}
			}
			return tileTones;
		}

		public String getTextureName()
		{
			if (texture != null) {
				return texture;
			} else {
				return "null-texture-name";
			}
		}


		// Will not work if texture is disabled
		public  void            updateSelection(){ // add updating border tiles? -- only for CPU!
			int tilesX = tileProcessor.getTilesX();
			int tilesY = tileProcessor.getTilesY();
			selected = new boolean[tilesY*tilesX];
			int minX = tilesX, minY = tilesY, maxX = -1, maxY = -1;
			/*
			if (texture_img != null) { // using GPU output
//tileProcessor.getTileSize()
				if (texture_woi != null) {
					int tile_size = tileProcessor.getTileSize();
					texture_bounds = new Rectangle(
							texture_woi.x/tile_size, texture_woi.y/tile_size, texture_woi.width/tile_size, texture_woi.height/tile_size);
					// setting full rectangle as selected, not just textures? Use some other method?
					for (int ty = texture_bounds.y; ty < (texture_bounds.y + texture_bounds.height); ty++) {
						for (int tx = texture_bounds.x; tx < (texture_bounds.x + texture_bounds.width); tx++) {
							selected[ty*tilesX+tx] = true;
						}
					}
					return;
				}
			}
			*/
			if (texture_tiles != null) {
				for (int ty = 0; ty < tilesY; ty++) for (int tx = 0; tx < tilesX; tx++){
					if (texture_tiles[ty][tx] != null) {
						selected[ty * tilesX + tx] = true;
						if (maxX < tx) maxX  = tx;
						if (minX > tx) minX  = tx;
						if (maxY < ty) maxY  = ty;
						if (minY > ty) minY  = ty;
					} else {
						selected[ty * tilesX + tx] = false; // may be omitted
					}
				}
			}
			if (maxX < 0) {
				texture_bounds = null;
			} else {
				texture_bounds = new Rectangle(minX, minY, maxX - minX +1, maxY - minY +1 );
			}
		}
		public  Rectangle  getTextureBounds(){
			return texture_bounds;
		}

		public boolean isProcessed(){
			return calc_disparity != null;
		}

		public boolean isMeasured(){
			return is_measured;
//			return (disparity_map != null) && (disparity != null); // 	disparity == null for composite scans
		}

		public boolean isCombo(){
			return is_combo;
		}

		/**
		 * Called after each measurement
		 */
		public void resetProcessed(){

			if (disparity_map != null) fixNaNDisparity();
			calc_disparity =       null; // composite disparity, calculated from "disparity", and "disparity_map" fields
			calc_disparity_hor =   null; // composite disparity, calculated from "disparity", and "disparity_map" fields
			calc_disparity_vert =  null; // composite disparity, calculated from "disparity", and "disparity_map" fields
			calc_disparity_combo = null; // composite disparity, calculated from "disparity", and "disparity_map" fields
			strength =             null; // composite strength, initially uses a copy of raw 4-sensor correleation strength
			strength_hor =         null; // updated hor strength, initially uses a copy of raw measured
			strength_vert =        null; // updated hor strength, initially uses a copy of raw measured
			bgTileDisparity =      null;
			bgTileStrength =       null;
//			border_tiles =         null; // these are border tiles, zero out alpha
//			selected =             null; // which tiles are selected for this layer
			superTiles =           null;
			setTilesRBGA(null);

		}
		/**
		 * Get FPGA-calculated per-tile maximal differences between the particular image and the average one.
		 * @return per-camera sesnor array of line-scan differences
		 */

		public double [][] getDiffs (){
			if (disparity_map == null) return null;
			double  [][] these_diffs =    new double[ImageDtt.QUAD][];
			for (int i = 0; i< ImageDtt.QUAD; i++) these_diffs[i] = disparity_map[ImageDtt.IMG_DIFF0_INDEX + i];
			return these_diffs;
		}

		public void resetCalc(){ // only needed if the same task was reused
			calc_disparity = null;
			strength =       null;
			strength_hor =   null;
			strength_vert =  null;
			superTiles =     null;

		}

		public boolean [] getLazyEyeForceDisparity() {
			return lazy_eye_force_disparity;
		}

		public void setLazyEyeForceDisparity(boolean [] lazy_eye_force_disparity) {
			this.lazy_eye_force_disparity = lazy_eye_force_disparity;
		}

		public double [][] getLazyEyeData() {
			return lazy_eye_data;
		}

		public void setLazyEyeData(double [][] lazy_eye_data) {
			this.lazy_eye_data = lazy_eye_data;
		}

		public int getLazyEyeClusterSize() {
			return lma_cluster_size;
		}
		public void setLazyEyeClusterSize(int lma_cluster_size) {
			this.lma_cluster_size = lma_cluster_size;
		}



		public boolean [] getSelected(){
			return selected;
		}

		public boolean [] getBorderTiles(){
			return this.border_tiles;
		}

		public void setSelected (boolean [] selected) {
			this.selected = selected;
		}
		public void setBorderTiles (boolean [] border_tiles) {
			this.border_tiles = border_tiles;
		}


		public void fixNaNDisparity()
		{
			fixNaNDisparity(
					null,
					disparity_map[disparity_index],
					disparity_map[ImageDtt.DISPARITY_STRENGTH_INDEX]);
			fixNaNDisparity(
					null,
					disparity_map[ImageDtt.DISPARITY_INDEX_HOR],
					disparity_map[ImageDtt.DISPARITY_INDEX_HOR_STRENGTH]);
			fixNaNDisparity(
					null,
					disparity_map[ImageDtt.DISPARITY_INDEX_VERT],
					disparity_map[ImageDtt.DISPARITY_INDEX_VERT_STRENGTH]);
		}


		public void fixNaNDisparity(
				boolean [] select,   // which tiles to correct (null - all)
				double [] disparity,
				double [] strength)
		{
			// depends on direction, but that is OK - just converge faster when smoothing
			int tilesX = tileProcessor.getTilesX();
			int tilesY = tileProcessor.getTilesY();
			int [] dirs8 = {-tilesX,  -tilesX + 1, 1, tilesX +1, tilesX, tilesX - 1, -1, -tilesX - 1};
			for (int ty = 1; ty < (tilesY -1); ty ++) for (int tx = 1; tx < (tilesX -1); tx++){
				int nt = ty * tilesX + tx;
				if (Double.isNaN(disparity[nt]) && ((select == null) || select[nt])) {
					if (strength != null) strength[nt] = 0.0;
					double sd = 0.0, sw = 0.0;
					for (int dir=0; dir < dirs8.length; dir++){
						int nt1 = nt + dirs8[dir];
//						if (!Double.isNaN(disparity[nt1]) && ((select == null) || !select[nt1])) {
						if (!Double.isNaN(disparity[nt1])) { // for wide borders - use neighbors already defined too
							double w = (strength == null) ? 1.0 : strength[nt1];
							sd += w * disparity[nt1];
							sw += w;
						}
					}
					if (sw > 0.0) sd /= sw;
					disparity[nt] = sd;
				}
			}
			// on top/bottom/right/left rows replace NaN disparity with 0.0;
			for (int ty = 0; ty < tilesY; ty ++) {
				int nt = ty * tilesX + 0;
				if (Double.isNaN(disparity[nt]) && ((select == null) || select[nt])) {
					if (strength != null) strength[nt] = 0.0;
					disparity[nt] = 0.0;
				}
				nt = ty * tilesX + tilesX -1;
				if (Double.isNaN(disparity[nt]) && ((select == null) || select[nt])) {
					if (strength != null) strength[nt] = 0.0;
					disparity[nt] = 0.0;
				}
			}
			for (int tx = 0; tx < tilesX; tx ++) {
				int nt = 0 * tilesX + tx;
				if (Double.isNaN(disparity[nt]) && ((select == null) || select[nt])) {
					if (strength != null) strength[nt] = 0.0;
					disparity[nt] = 0.0;
				}
				nt = (tilesY -1) * tilesX + tx;
				if (Double.isNaN(disparity[nt]) && ((select == null) || select[nt])) {
					if (strength != null) strength[nt] = 0.0;
					disparity[nt] = 0.0;
				}
			}
		}

		public double [] combineHorVertStrength(
				boolean combineHor,
				boolean combineVert)
		{
			getStrength();     // clone if not done yet
			if (combineHor){
				double [] hstrength = getHorStrength();
				for (int i = 0; i < strength.length; i++) {
					if (strength[i] < hstrength[i]) strength[i] = hstrength[i];
				}
			}
			if (combineVert){
				double [] vstrength = getVertStrength();
				for (int i = 0; i < strength.length; i++) {
					if (strength[i] < vstrength[i]) strength[i] = vstrength[i];
				}
			}
			return strength;
		}

		public double [] combineSuper(
				boolean updateStrength, // use ST strength if true, keep original (update disparity only) if false
				double  stStrengthScale,
				double useSuper){
			if (bgTileDisparity == null) { // no supertile disparity is available
				return null;
			}

			double [] strength = getStrength();
			double [] disparity = getDisparity(0);

			for (int i = 0; i < disparity.length; i++){
				if (strength[i] < useSuper)  {
					disparity[i] = bgTileDisparity[i];
					if (updateStrength) strength[i] =  stStrengthScale*bgTileStrength[i];
				}
			}
			return disparity;
		}

		public double [] getOverexposedFraction(){
			return (disparity_map != null)? disparity_map[ImageDtt.OVEREXPOSED] : null;
		}


		/**
		 * Returns per-tile correlation "strength". Initially - copy of the FPGA-generated data, but later may be replaced by a combination
		 * of the combined data from 4-sensor (4-pair) correlation and horizontal/vertical pairs only to improve detection of vertical/
		 * horizontal features
		 * @return line-scan array of per-tile correlation strength by reference (not a copy), so it can be modified
		 */
		public double [] getStrength(){
			double trustedCorrelation = tileProcessor.getTrustedCorrelation();
			double max_overexposure = tileProcessor.getMaxOverexposure();
			if (strength == null){
				strength =  disparity_map[ImageDtt.DISPARITY_STRENGTH_INDEX].clone();
				if (trustedCorrelation > 0.0){
					for (int i = 0; i < strength.length; i++){
						if (Math.abs(disparity_map[disparity_index][i]) > trustedCorrelation) strength[i] = 0.0; // too far
					}
				}
				double [] overexposed = disparity_map[ImageDtt.OVEREXPOSED];
				if ((max_overexposure > 0.0) && (overexposed != null)){
					for (int i = 0; i < strength.length; i++){
						if (overexposed[i] > max_overexposure) strength[i] = 0.0; // too overexposed
					}
				}
			}
			return strength;
		}
		/**
		 * Get four pairs (original) correlation strength. Not a copy
		 * @return line-scan array of per-tile horizontal pairs correlation strength by reference (not a copy)
		 */
		public double [] getOriginalStrength(){
			if (disparity_map != null) return disparity_map[ImageDtt.DISPARITY_STRENGTH_INDEX];
			else return getStrength(); // after replacing with rig data
		}
		/**
		 * Get horizontal pairs correlation strength for vertical features. Not a copy
		 * @return line-scan array of per-tile horizontal pairs correlation strength by reference (not a copy)
		 */
		public double [] getHorStrength(){
			double trustedCorrelation = tileProcessor.getTrustedCorrelation();
			double max_overexposure = tileProcessor.getMaxOverexposure();
			if (strength_hor == null) {
				strength_hor = disparity_map[ImageDtt.DISPARITY_INDEX_HOR_STRENGTH].clone();
				if (trustedCorrelation > 0.0){
					for (int i = 0; i < strength_hor.length; i++){
						if (Math.abs(disparity_map[ImageDtt.DISPARITY_INDEX_HOR][i]) > trustedCorrelation) strength_hor[i] = 0.0; // too far
					}
				}
				double [] overexposed = disparity_map[ImageDtt.OVEREXPOSED];
				if ((max_overexposure > 0.0) && (overexposed != null)){
					for (int i = 0; i < strength_hor.length; i++){
						if (overexposed[i] > max_overexposure) strength_hor[i] = 0.0; // too overexposed
					}
				}

			}
			return strength_hor;
		}
		/**
		 * Get vertical pairs correlation strength for horizontal features. Not a copy
		 * @return line-scan array of per-tile horizontal pairs correlation strength by reference (not a copy)
		 */
		public double [] getVertStrength(){
			double trustedCorrelation = tileProcessor.getTrustedCorrelation();
			double max_overexposure = tileProcessor.getMaxOverexposure();
			if (strength_vert == null) {
				strength_vert = disparity_map[ImageDtt.DISPARITY_INDEX_VERT_STRENGTH].clone();
				if (trustedCorrelation > 0.0){
					for (int i = 0; i < strength_vert.length; i++){
						if (Math.abs(disparity_map[ImageDtt.DISPARITY_INDEX_VERT][i]) > trustedCorrelation) strength_vert[i] = 0.0; // too far
					}
				}
				double [] overexposed = disparity_map[ImageDtt.OVEREXPOSED];
				if ((max_overexposure > 0.0) && (overexposed != null)){
					for (int i = 0; i < strength_hor.length; i++){
						if (overexposed[i] > max_overexposure) strength_vert[i] = 0.0; // too overexposed
					}
				}

			}
			return strength_vert;
		}

		/**
		 * Combine per-tile disparity values from correlation combined with pre-programmed initial disparity shift.
		 * @return line-scan array of per-tile disparity by reference (not a copy), so it can be modified
		 */

		public double [] getDisparity() // get calculated combo disparity
		{
			return getDisparity(0);
		}
		/**
		 * Get one of the line-scan per-tile correlation data.
		 * @param mode 0 - final data (initially copy FPGA generated 4-pair correlation)
		 *             1 - original FPGA generated 4-sensor correlation
		 *             2 - 2 - horizontal pairs correlation, detecting vertical features
		 *             3 - 2 - vertical pairs correlation, detecting horizontal features
		 * @return line-scan array of per-tile disparity by reference (not a copy), so it can be modified
		 */

		public double [] getDisparity(int mode) // mode = 0 - normal disparity, 1 - hor, 2 - vert
		{
			if (calc_disparity == null) conditionDisparity();
			switch (mode) {
			case 1: return calc_disparity;
			case 2: return calc_disparity_hor;
			case 3: return calc_disparity_vert;
			default: if (calc_disparity_combo == null) calc_disparity_combo = calc_disparity.clone();
				return calc_disparity_combo;
			}
		}

		// methods to "condition" measured disparity values
		public void conditionDisparity()
		{
			conditionDisparity(disparity_index);
		}

		public void conditionDisparity(int disparity_index)
		{
			int tilesX = tileProcessor.getTilesX();
			int tilesY = tileProcessor.getTilesY();
			double corr_magic_scale = tileProcessor.getMagicScale();


			this.disparity_index = disparity_index;
			calc_disparity =      new double[tilesY*tilesX];
			calc_disparity_hor =  new double[tilesY*tilesX];
			calc_disparity_vert = new double[tilesY*tilesX];
			for (int i = 0; i < tilesY; i++){
				for (int j = 0; j < tilesX; j++){
					int indx = i * tilesX + j;
					calc_disparity[indx] =      disparity_map[disparity_index][indx]/corr_magic_scale +               this.disparity[i][j];
					calc_disparity_hor[indx] =  disparity_map[ImageDtt.DISPARITY_INDEX_HOR][indx]/corr_magic_scale +  this.disparity[i][j];
					calc_disparity_vert[indx] = disparity_map[ImageDtt.DISPARITY_INDEX_VERT][indx]/corr_magic_scale + this.disparity[i][j];
				}
			}
			calc_disparity_combo = calc_disparity.clone(); // for now - just clone, can be modified separately and combined with hor/vert
		}
		
		// bypassing calculations
		public void setCalcDisparityStrength(
				double [] disparity,
				double [] strength) {
			this.calc_disparity = disparity;
			this.strength =       strength;
			calc_disparity_combo = calc_disparity.clone();
		}

		/**
		 * Replaces current combo disparity for tiles that are weak and do not have any neighbor within disparity range from this one
		 * @param selection optional boolean mask of tiles to use/update
		 * @param weakStrength maximal strength of the tile to be considered weak one
		 * @param maxDiff maximal difference from the most similar neighbor to be considered an outlier
		 * @param disparityFar minimal acceptable disparity for weak tiles
		 * @param disparityNear maximal acceptable disparity for weak tiles
		 * @return mask of weak (replaced) tiles
		 *
		 * Replace weak by a weighted average of non-weak. If there are none - use weak ones, including this one too.
		 */
		public boolean[] replaceWeakOutliers(
				final boolean [] selection,
				final double weakStrength,    // strength to be considered weak, subject to this replacement
				final double maxDiff,
				final double maxDiffPos,      // Replace weak outlier tiles that have higher disparity than weighted average
				final double maxDiffNeg,      // Replace weak outlier tiles that have lower disparity than weighted average
				final double disparityFar,
				final double disparityNear,
				final int debugLevel)
		{
			final int tilesX = tileProcessor.getTilesX();
			final int tilesY = tileProcessor.getTilesY();

			final int nTiles = tilesX*tilesY;
			final boolean [] weakOutliers = new boolean [nTiles];
			int [] dirs8 = {-tilesX,  -tilesX + 1, 1, tilesX +1, tilesX, tilesX - 1, -1, -tilesX - 1};
			final int [] dirs = dirs8;
			final double [] disparity = getDisparity(0);
			final double [] strength =  getStrength();
			final double absMinDisparity = 0.5 * disparityFar; // adjust? below this is definitely wrong (weak)
			final double absMaxDisparity = 1.5 * disparityNear; // change?
			final int dbg_nTile = (debugLevel > 0) ? 43493: -1; // x=77,y=134; // 42228; // x = 108, y = 130 46462; // 41545;
			final Thread[] threads = ImageDtt.newThreadArray(tileProcessor.threadsMax);
			// first pass = find outliers
			final AtomicInteger ai = new AtomicInteger(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					@Override
					public void run() {
						for (int nTile = ai.getAndIncrement(); nTile < nTiles; nTile = ai.getAndIncrement()) {
							if (((strength[nTile] < weakStrength) ||
									(disparity[nTile] < absMinDisparity) ||
									(disparity[nTile] > absMaxDisparity))&& ((selection == null) || selection[nTile])) {
								if (nTile == dbg_nTile){
									System.out.println("replaceWeakOutliers():1 nTile="+nTile);
								}
								double [] dbg_disparity = disparity;
								double dbg_disparity_nTile = disparity[nTile];
								double dbg_disparityFar = disparityFar;
								double dbg_disparityNear = disparityNear;
								boolean [] dbg_weakOutliers = weakOutliers;
								int tileY = nTile / tilesX;
								int tileX = nTile % tilesX;
								if ((tileY > 0) && (tileY < (tilesY -1)) &&(tileX > 0) && (tileX < (tilesX -1))){ // disregard outer row/cols
									weakOutliers[nTile] = true;
									boolean hasNeighbors = false;
									double sd = 0.0, sw = 0.0;
									for (int dir = 0; dir< dirs.length; dir++){
										int nTile1 = nTile + dirs[dir];
										double dbg_disparity_nTile1 = disparity[nTile1];
										if (((selection == null) || selection[nTile1]) &&
												 (disparity[nTile1] >= disparityFar) && // don't count on too near/too far for averaging
												 (disparity[nTile1] <= disparityNear)){
											double w = strength[nTile1];
											sw += w;
											sd += w * disparity[nTile1];
											hasNeighbors = true;
											if (Math.abs(disparity[nTile]-disparity[nTile1]) <= maxDiff){ // any outlier - will be false
												weakOutliers[nTile] = false;
//												break;
											}
										}
									}
									if (sw >= 0.0) {
										sd /= sw;
										if      (disparity[nTile] < (sd - maxDiffNeg)) weakOutliers[nTile] = true;
										else if (disparity[nTile] > (sd + maxDiffPos)) weakOutliers[nTile] = true;
									}
									if (disparity[nTile] < disparityFar)  weakOutliers[nTile] = true;
									if (disparity[nTile] > disparityNear) weakOutliers[nTile] = true;
									if (!hasNeighbors) {
										weakOutliers[nTile] = false; // lone tile or NaN among NaNs
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);

			// second pass - replace outliers
			final double [] src_disparity = disparity.clone();
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					@Override
					public void run() {
						for (int nTile = ai.getAndIncrement(); nTile < nTiles; nTile = ai.getAndIncrement()) {
							if (nTile == dbg_nTile){
								System.out.println("replaceWeakOutliers():2 nTile="+nTile);
							}
							if (weakOutliers[nTile]) {
								double sw = 0.0, sd = 0.0;
								for (int dir = 0; dir< dirs.length; dir++){
									int nTile1 = nTile + dirs[dir];
									if (!weakOutliers[nTile1] && ((selection == null) || selection[nTile1 ]) ) {
										double w = strength[nTile1];
										sw += w;
										sd += w * src_disparity[nTile1];
									}
								}
								if (sw == 0) { // Nothing strong around - repeat with weak and this one too.
									double w = strength[nTile];
									if (!Double.isNaN( src_disparity[nTile])) {
										sw += w;
										sd += w * src_disparity[nTile];
									}
									for (int dir = 0; dir< dirs.length; dir++){
										int nTile1 = nTile + dirs[dir];
										if ((selection == null) || selection[nTile1 ]) {
											w = strength[nTile1];
											if (!Double.isNaN( src_disparity[nTile1])) {
												sw += w;
												sd += w * src_disparity[nTile1];
											}
										}
									}
								}
								if (sw > 0) { // should be, do nothing if not
									disparity[nTile] = sd/sw;
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			return weakOutliers;
		}

		public boolean [] getUntestedBackgroundBorder (
				final boolean    [] known,
				final double     [] disparity,
				final double        grow_disp_step,
				final int     debugLevel)
		{
			final int tilesX = tileProcessor.getTilesX();
			final int tilesY = tileProcessor.getTilesY();
			final int num_tiles = tilesX * tilesY;
			final TileNeibs tnImage = new TileNeibs(tilesX, tilesY); // num_tiles/tilesX);
			final boolean [] untested_bgnd = new boolean [num_tiles];
			final Thread[] threads = ImageDtt.newThreadArray(tileProcessor.threadsMax);
			final AtomicInteger ai = new AtomicInteger(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					@Override
					public void run() {
						for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) {
							if (known[nTile]){
								int tX = nTile % tilesX;
								int tY = nTile / tilesX;
								double max_disp = max_tried_disparity[tY][tX] + grow_disp_step;
								for (int dir = 0; dir < 8; dir++){
									int nTile1 = tnImage.getNeibIndex(nTile, dir);
									if ((nTile1 >=0) && known[nTile1] && (disparity[nTile1] > max_disp)) {
										untested_bgnd[nTile] = true;
										break;
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			return untested_bgnd;
		}

		public boolean [] measuredTiles ()
		{
			final int tilesX = tileProcessor.getTilesX();
			final int tilesY = tileProcessor.getTilesY();
			final int num_tiles = tilesX * tilesY;
			final boolean [] measured = new boolean [num_tiles];
			final Thread[] threads = ImageDtt.newThreadArray(tileProcessor.threadsMax);
			final AtomicInteger ai = new AtomicInteger(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					@Override
					public void run() {
						for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) {
								int tX = nTile % tilesX;
								int tY = nTile / tilesX;
								measured[nTile] = tile_op[tY][tX] != 0;
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			return measured;
		}

		public void saveTileOpDisparity()
		{
			disparity_sav =disparity.clone();
			for (int i = 0; i < disparity.length; i++ ) if (disparity[i]!= null) disparity_sav[i] = disparity[i].clone();
			tile_op_sav =tile_op.clone();
			for (int i = 0; i < tile_op.length; i++ ) if (tile_op[i]!= null) tile_op_sav[i] = tile_op[i].clone();
		}
		public void restoreTileOpDisparity()
		{
			disparity = disparity_sav;
			tile_op = tile_op_sav;
		}
		public void restoreKeepTileOpDisparity()
		{
			restoreTileOpDisparity();
			saveTileOpDisparity();
		}

		public int setTileOpDisparity(
				boolean [] selection,
				double []  disparity)
		{
			int op = ImageDtt.setImgMask(0, 0xf);
			op =     ImageDtt.setPairMask(op,0xf);
			op =     ImageDtt.setForcedDisparity(op,true);
			return setTileOpDisparity(
					op,         // int        tile_op,
					selection,  // boolean [] selection,
					disparity); // double []  disparity)
		}

		public int setTileOpDisparity(
				int        tile_op,
				boolean [] selection,
				double []  disparity)
		{
			final int tilesX = tileProcessor.getTilesX();
			final int tilesY = tileProcessor.getTilesY();
			this.disparity =   new double [tilesY][tilesX];
			if (selection != null) {
				this.tile_op =     new int [tilesY][tilesX];
			}
			int num_op_tiles = 0;

			for (int ty = 0; ty < tilesY; ty++) for (int tx = 0; tx <tilesX; tx++){
				int indx =  tilesX * ty + tx;
				if (selection == null) {
					this.disparity[ty][tx] = (disparity == null)? 0.0: disparity[indx];
					if (this.tile_op[ty][tx] != 0){
						num_op_tiles ++;
					}
				} else {
					if (selection[indx]) {
						this.disparity[ty][tx] = (disparity == null)? 0.0: disparity[indx];
						this.tile_op[ty][tx] = tile_op;
						num_op_tiles ++;
					} else {
						this.disparity[ty][tx] = 0.0;
						this.tile_op[ty][tx] = 0;
					}
				}
			}
			return num_op_tiles;
		}

		/**
		 * Set next measurement disparity from last calculated
		 */
		public void updateDisparity()
		{
			setTileOpDisparity(null, getDisparity(0));
		}


		public double [] getSecondMaxDiff (
				final boolean averaged)
		{
			final double [][] diffs = getDiffs();
			if (diffs == null) return null;

			final int tilesX = tileProcessor.getTilesX();
			final int tilesY = tileProcessor.getTilesY();
			final int num_tiles = tilesX * tilesY;
			final double [] second_max = new double [num_tiles];
			final boolean [] measured =  measuredTiles ();
			final Thread[] threads = ImageDtt.newThreadArray(tileProcessor.threadsMax);
			final AtomicInteger ai = new AtomicInteger(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					@Override
					public void run() {
						for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) {
							int imax1 = 0;
							for (int ip = 1; ip < diffs.length; ip++){
								if (diffs[ip][nTile] > diffs[imax1][nTile]) imax1 = ip;
							}
							int imax2 = (imax1 == 0)? 1 : 0;
							for (int ip = 0; ip < diffs.length; ip++) if (ip != imax1) {
								if (diffs[ip][nTile] > diffs[imax2][nTile]) imax2 = ip;
							}
							second_max[nTile] = diffs[imax2][nTile];
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			if (!averaged) return second_max;
			final TileNeibs tnImage = new TileNeibs(tilesX, tilesY); // num_tiles/tilesX);
			final double [] second_max_averaged = new double [num_tiles];
			final double [] dir_weights = {1.0/16, 1.0/8, 1.0/16, 1.0/8, 1.0/16, 1.0/8, 1.0/16, 1.0/8, 1.0/4};
			ai.set(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					@Override
					public void run() {
						for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) if (measured[nTile]) {
							double sw = 0.0;
							double swd = 0.0;
							for (int dir = 0; dir < 9; dir++){ // including 8 - center
								int nTile1 = tnImage.getNeibIndex(nTile, dir);
								if ((nTile1 >=0) && measured[nTile1]) {
									sw +=  dir_weights[dir];
									swd += dir_weights[dir] * second_max[nTile1] ;
								}
							}
							second_max_averaged[nTile] = swd/sw;
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			return second_max_averaged;
		}



		// same, but 2 steps around
		public boolean [] getUntestedBackgroundBorder2 (
				final boolean    [] known,
				final double     [] disparity,
				final double        grow_disp_step,
				final int     debugLevel)
		{
			final int tilesX = tileProcessor.getTilesX();
			final int tilesY = tileProcessor.getTilesY();
			final int num_tiles = tilesX * tilesY;
			final TileNeibs tnImage = new TileNeibs(tilesX, tilesY); // num_tiles/tilesX);
			final boolean [] untested_bgnd = new boolean [num_tiles];
			final Thread[] threads = ImageDtt.newThreadArray(tileProcessor.threadsMax);
			final AtomicInteger ai = new AtomicInteger(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					@Override
					public void run() {
						for (int nTile = ai.getAndIncrement(); nTile < num_tiles; nTile = ai.getAndIncrement()) {
							if (known[nTile]){
								int tX = nTile % tilesX;
								int tY = nTile / tilesX;
								double max_disp = max_tried_disparity[tY][tX] + grow_disp_step;
								for (int dir = 0; dir < 24; dir++){
									int nTile1 = tnImage.getNeibIndex2(nTile, dir);
									if ((nTile1 >=0) && known[nTile1] && (disparity[nTile1] > max_disp)) {
										untested_bgnd[nTile] = true;
										break;
									}
								}
							}
						}
					}
				};
			}
			ImageDtt.startAndJoin(threads);
			return untested_bgnd;
		}






		public SuperTiles getSuperTiles()
		{
			return this.superTiles;
		}

		public SuperTiles setSuperTiles(
				double     step_near,
				double     step_far,
				double     step_threshold,
				double     min_disparity,
				double     max_disparity,
				double     stBlurSigma,
				boolean    smplMode, //        = true;   // Use sample mode (false - regular tile mode)
				MeasuredLayersFilterParameters mlfp,
				int        measSel)
		{
			this.superTiles = new SuperTiles(
					this,
					step_near,
					step_far,
					step_threshold,
					min_disparity,
					max_disparity,
					stBlurSigma,
					smplMode, //        = true;   // Use sample mode (false - regular tile mode)
					mlfp,
					measSel);
			return this.superTiles;
		}
		public double [] showDisparityHistogram(
				double [][][][] disparity_strength, // pre-calculated disparity/strength [per super-tile][per-measurement layer][2][tiles] or null
				boolean [][]    tile_sel, // null  or per-measurement layer, per-tile selection. For each layer null - do not use, {} - use all

				boolean    smplMode, //        = true;   // Use sample mode (false - regular tile mode)
				MeasuredLayersFilterParameters mlfp,
				int        measSel)
		{
			if (this.superTiles == null){
				return null;
			}
			return this.superTiles.showDisparityHistogram(
					disparity_strength, // pre-calculated disparity/strength [per super-tile][per-measurement layer][2][tiles] or null
					tile_sel, // null  or per-measurement layer, per-tile selection. For each layer null - do not use, {} - use all

					smplMode, //        = true;   // Use sample mode (false - regular tile mode)
					mlfp,
					measSel);
		}

		public double[] getSuperTileStrength()
		{
			if (this.superTiles == null){
				return null;
			}
			return superTiles.stStrength;
		}

		public double [][] getBgDispStrength()
		{
			if ((bgTileDisparity == null) || (bgTileStrength == null)){
				double [][] rslt = {bgTileDisparity,bgTileStrength};
				return rslt;
			}
			return getBgDispStrength(
					this.minBgDisparity,
					this.minBgFract);
		}


		public double [][] getBgDispStrength(
				final double minBgDisparity,
				final double minBgFract)
		{
			if (superTiles == null){
				return null;
			}
			if ((minBgDisparity != this.minBgDisparity) || (minBgFract != this.minBgFract)){
				this.minBgDisparity = minBgDisparity;
				this.minBgFract = minBgFract;
				superTiles.bgDisparity = null; // per super-tile
				superTiles.bgStrength = null; // per super-tile
				bgTileDisparity = null; // per tile
				bgTileStrength = null; // per tile
			}
			if ((superTiles.bgDisparity == null) || (superTiles.bgStrength == null)){
				if (superTiles.getBgDispStrength(
						minBgDisparity,
						minBgFract) == null) {
					superTiles.bgDisparity = null; // per super-tile
					superTiles.bgStrength = null; // per super-tile
					bgTileDisparity = null; // per tile
					bgTileStrength = null; // per tile
					return null; // failed
				}
				// now lap-combine supertiles, get this.* from superTiles.*

				double [][] bgTileDispStrength = superTiles.getBgTileDispStrength();
				bgTileDisparity = bgTileDispStrength[0];
				bgTileStrength =  bgTileDispStrength[1];
			}
			double [][] rslt = {bgTileDisparity,bgTileStrength};
			return rslt;
		}

		public double [] getBgDisparity(){
			return bgTileDisparity;
		}
		public double [] getBgStrength(){
			return bgTileStrength;
		}

	} // end of class CLTPass3d