// Script to compute a streak-free FFT from the front-most image. The streaks originate mainly
// due to the edges of the image. By applying a circular Butterworth Filter to damp the
// edges, those streaks can be removed (largely). Scan instabilities (skips and jogs) will
// still cause some vertical and horizontal streaking. Note if a filtered FFT is inverted, 
// then the mask will be apparent in the computed image - so use filtered FFTs for crystallographic purposes only.

// D. R. G. Mitchell, adminnospam@dmscripting.com (remove the nospam to make this address work)
// version:20191021, v1.0, October 2019, www.dmscripting.com

// The FFT function will work on a region of interest (if present). The FFT is computed for the
// largest (centred) square region within the image or region of interest. To ensure representative information
// create regions of interest which are square (hold down SHIFT while stretching a region of interest).

// The dialog presents options to compute either the full or reduced FFT.
// It also provides the option to compute the unfiltered (no Butterworth Filter) or
// filtered image.

// The bottom half of the dialog allows the Butterworth Filter parameters to be tailored. 
// The zero radius is a radial position where the filter intensity drops to half. Smaller
// values (eg 0.3) result in smaller filter diameters  - more of the image gets blocked. Typically
// use the largest radius which ensures the image edges are damped to zero - or close thereto.
// Butterworth Order affects the slope of the edge of the filter. Small values (eg 5) result in a gradually
// falling filter edge. Avoid excessively high values (rapidly falling edge), as this is akin to a sharp edge.

// Checking the Show Butterworth Filter box will display the Butterworth Filter being used by the script.



// The default butterworth zero radius (0.35 - 0.8) - larger values push the filter cut off closer to the edge

number defaultbutterworthzeroradius=0.4
if(!getpersistentnumbernote("Streak-free FFT:Settings:Butterworth Zero Radius (0.35-0.8)", defaultbutterworthzeroradius))
	{
		setpersistentnumbernote("Streak-free FFT:Settings:Butterworth Zero Radius (0.35-0.8)", defaultbutterworthzeroradius)
	}


// The default butterworth order (5-15) - controls the slope of the gradation of the image filter. Higher values
// create a steeper (sharper) transiton

number defaultbutterworthorder=12
if(!getpersistentnumbernote("Streak-free FFT:Settings:Butterworth Order (5-15)", defaultbutterworthorder))
	{
		setpersistentnumbernote("Streak-free FFT:Settings:Butterworth Order (5-15)", defaultbutterworthorder)
	}


// The radio setting for full or reduced FFT

number defaultfullorreducedradio=0 // 0=full, 1=reduced
if(!getpersistentnumbernote("Streak-free FFT:Settings:Full or Reduced FFT Radio (0-1)", defaultfullorreducedradio))
	{
		setpersistentnumbernote("Streak-free FFT:Settings:Full or Reduced FFT Radio (0-1)", defaultfullorreducedradio)
	}


// The radio setting for filtered or unfiltered radio

number defaultfilterradio=0 // 0=filtered, 1=unfiltered
if(!getpersistentnumbernote("Streak-free FFT:Settings:Filtered or Unfiltered Radio (0-1)", defaultfilterradio))
	{
		setpersistentnumbernote("Streak-free FFT:Settings:Filtered or Unfiltered Radio (0-1)", defaultfilterradio)
	}


// The check box to show the Butterworth Filter

number defaultshowfiltercheckbox=0 // 0=do not show, 1=show
if(!getpersistentnumbernote("Streak-free FFT:Settings:Show Filter Check Box (0-1)", defaultshowfiltercheckbox))
	{
		setpersistentnumbernote("Streak-free FFT:Settings:Show Filter Check Box (0-1)", defaultshowfiltercheckbox)
	}


// The display width for images (in pixels

number defaultdisplaywidth=400 
if(!getpersistentnumbernote("Streak-free FFT:Settings:Display Width (pxls)", defaultdisplaywidth))
	{
		setpersistentnumbernote("Streak-free FFT:Settings:Display Width (pxls)", defaultdisplaywidth)
	}


class StreakFreeFFTDialog : uiframe
{
// Zooms an image to the specified width

void zoomtosize(object self, image img, number newxsize)
	{
		// If the passed in size is <128 do nothing. 

		if(newxsize<128) return


		// Get some info on the image and show it

		number xsize, ysize
		getsize(img, xsize, ysize)
		number newysize=ysize*(newxsize/xsize)
		showimage(img)


		// Source the displays, documents and windows and set the new 
		// content area of the image to the desired value

		imagedisplay imgdisp=img.imagegetimagedisplay(0)
		imagedocument imgdoc=getfrontimagedocument()
		documentwindow docwin=getdocumentwindow(0)
		docwin.windowsetcontentsize(newxsize, newysize)


		// Compute the transform from image to display and maximise the image to fit the new window

		number i2voffx, i2voffy, i2vscalex, i2vscaley, vtop, vleft, vbottom, vright
		imgdisp.ComponentGetChildToviewTransform(i2voffx, i2voffy, i2vscalex, i2vscaley)
		objecttransformtransformrect(i2voffx, i2voffy, i2vscalex, i2vscaley, 0, 0, ysize, xsize,vtop, vleft, vbottom, vright)
		ImageDocumentMaximizeRectInView( imgDoc, vtop, vleft, vbottom, vright ) 
	}


// Returns a Butterworth  filter

image butterworthfilter(object self, number imgsize, number bworthorder, number zeroradius)
	{
		// See John Russ's Image Processing Handbook, 2nd Edn, p 316
		image butterworthimg=realimage("",4,imgsize, imgsize)
		butterworthimg=0


		// note the halfpointconst value sets the value of the filter at the halfway point
		// ie where the radius = zeroradius. A value of 0.414 sets this value to 0.5
		// a value of 1 sets this point to root(2)

		number halfpointconst=0.414
		butterworthimg=1/(1+halfpointconst*(iradius/zeroradius)**(2*bworthorder))
		return butterworthimg
	}

	//this is the response when the FFT button is pressed

	void fftresponse(object self)
		{
			// Source the front-most image 

			number nodocs=countdocumentwindowsoftype(5)
			if(nodocs<1)
				{
					showalert("Ensure a high resolution image is displayed.",2)
					return
				}

			if(imageisdatatypecomplex(getfrontimage()))
				{
					showalert("This does not work on FFTs. Ensure a high resolution image is shown front-most.",2)
					return
				}
			

			image front:=getfrontimage()[]
			number xsize, ysize
			getsize(front, xsize, ysize)
			
			if(ysize<2)
				{
					showalert("This does not work on 1D spectra or profiles.",2)
					return
				}
			
			string imgname=getname(front)


			// Excise the largest square region from the image

			number mindim=min(xsize, ysize)
			number centrex=xsize/2
			number centrey=ysize/2

			number top=centrey-mindim/2
			number left=centrex-mindim/2
			number bottom=centrey+mindim/2
			number right=centrex+mindim/2

			image excisedimage=front[top, left, bottom, right]
			number excisedx, excisedy
			getsize(excisedimage, excisedx, excisedy)


			// Now apply the Butterworth filter

			number butterworthorder, zeroradius
			dlggetvalue(self.lookupelement("butterworthorderfield"), butterworthorder)
			dlggetvalue(self.lookupelement("zeroradiusfield"), zeroradius)
			number filterradioval
			dlggetvalue(self.lookupelement("filterradio"), filterradioval)

			number filterzeroradius=excisedx*zeroradius
			image butterworthimage=self.butterworthfilter(excisedx, butterworthorder,  filterzeroradius)
			image filterimage
			if(filterradioval==1) filterimage=excisedimage*butterworthimage
			else filterimage=excisedimage
			
			
			// Do the FFT and label and calibrate the result

			compleximage fftimage=realfft(filterimage)
			converttopackedcomplex(fftimage)

			
			// Source the dialog settings
			
			number fftmode
			dlggetvalue(self.lookupelement("fullorreducedradio"), fftmode)			
			number showfilter
			dlggetvalue(self.lookupelement("showfiltercheckbox"), showfilter)
			number displaywidth
			getpersistentnumbernote("Streak-free FFT:Settings:Display Width (pxls)", displaywidth)
			if(displaywidth<128) displaywidth=128


			// Show the filter image if the check box is set
			
			number wintop, winleft, winbottom, winright
			if(showfilter) 
				{
					showimage(butterworthimage)
					self.zoomtosize(butterworthimage, displaywidth)
					documentwindow bworthwin=getdocumentwindow(0)
					bworthwin.windowsetframeposition(0,0)
					bworthwin.windowgetframebounds(wintop, winleft, winbottom, winright)
					setname(butterworthimage, "Butterworth Filter")
					setstringnote(butterworthimage, "Streak-Free FFT:Image ID", "Butterworth Filter")
				}

		
		// Display the full or reduced FFT
		
		compleximage finalfft
		
		if(fftmode==0) 
			{	
				finalfft:=fftimage
				showimage(finalfft)
				self.zoomtosize(finalfft, displaywidth)
				documentwindow fftwin=getdocumentwindow(0)
				fftwin.windowsetframeposition(winright, wintop)
				setname(finalfft, "FFT of "+imgname)

			}
		else // reduced FFT
			{
				finalfft=reducedfft(filterimage)
				converttopackedcomplex(finalfft)
				showimage(finalfft)
				self.zoomtosize(finalfft, displaywidth)
				documentwindow fftwin=getdocumentwindow(0)
				fftwin.windowsetframeposition(winright, wintop)
				setname(finalfft, "Reduced FFT of "+imgname)
			}


			// copy the calibration and tags to the final FFT

			number originx, originy, scalex, scaley
			string unitx, unity
			front.imagegetdimensioncalibration(0, originx, scalex, unitx, 0)
			front.imagegetdimensioncalibration(1, originy, scaley, unity, 0)

			number fftxsize, fftysize
			getsize(finalfft, fftxsize, fftysize)
			number fftoriginx=(fftxsize+1)/2
			number fftoriginy=(fftysize+1)/2

			number fftscalex=1/(xsize*scalex)
			number fftscaley=1/(ysize*scaley)
			string fftunitx="1/"+unitx
			string fftunity="1/"+unity

			taggroup fronttags=front.imagegettaggroup()
			taggroup ffttags=finalfft.imagegettaggroup()
			taggroupcopytagsfrom(ffttags, fronttags)
			finalfft.imagesetdimensioncalibration(0, fftoriginx, fftscalex, fftunitx, 1)
			finalfft.imagesetdimensioncalibration(1, fftoriginy, fftscaley, fftunity, 1)
		}


// Responds when the zero radius field is changed

void zeroradiusfieldchanged(object self, taggroup tg)
	{
		number zeroradius=tg.dlggetvalue()
		if(zeroradius<0.2) zeroradius=0.2
		if(zeroradius>0.8) zeroradius=0.8
		tg.dlgvalue(zeroradius)
		setpersistentnumbernote("Streak-free FFT:Settings:Butterworth Zero Radius (0.35-0.8)", zeroradius)
	}


// Responds when the Butterworth Order field is changed

void bworthorderfieldchanged(object self, taggroup tg)
	{
		number bworthorder=tg.dlggetvalue()
		if(bworthorder<5) bworthorder=5
		if(bworthorder>15) bworthorder=15
		tg.dlgvalue(bworthorder)
		setpersistentnumbernote("Streak-free FFT:Settings:Butterworth Order (5-15)", bworthorder)
	}


// Responds when the FFT mode radio is changed

void Fullorreducedradiochanged(object self, taggroup tg)
	{
		number radioval=tg.dlggetvalue()
		setpersistentnumbernote("Streak-free FFT:Settings:Full or Reduced FFT Radio (0-1)", radioval)
	}


// Responds when the Filter radio is changed

void filterradiochanged(object self, taggroup tg)
	{
		number radioval=tg.dlggetvalue()
		setpersistentnumbernote("Streak-free FFT:Settings:Filtered or Unfiltered Radio (0-1)", radioval)
	}


// Responds when the Show Butterworth Filter check box is changed

void showfiltercheckboxchanged(object self, taggroup tg)
	{
		number checkval=tg.dlggetvalue()
		setpersistentnumbernote("Streak-free FFT:Settings:Show Filter Check Box (0-1)", checkval)
	}


// Responds when the dialog is closed.

void abouttoclosedocument(object self)
	{
		documentwindow dialogwin=self.getframewindow()
		number top, left
		dialogwin.windowgetframeposition(left, top)
		setpersistentnumbernote("Streak-free FFT:Settings:Dialog Top", top)
		setpersistentnumbernote("Streak-free FFT:Settings:Dialog Left", left)
	}
	
	
// this function creates a taggroup containing the dialog elements:
// a box within which a simple button is present.

taggroup MakeDialog(object self)
	{
		TagGroup dialog_items;	
		TagGroup dialog = DLGCreateDialog("S-F FFT", dialog_items)

		// Creates a box in the dialog which surrounds the button

		taggroup fftbox_items
		taggroup fftbox=dlgcreatebox("  FFT  ", fftbox_items)
		fftbox.dlgexternalpadding(3,5)
		fftbox.dlginternalpadding(10,15)


		// Creates the button

		TagGroup fftButton = DLGCreatePushButton(" FFT", "fftresponse").dlginternalpadding(10,0)
		fftButton.dlgexternalpadding(0,0)
		
		
		// Creates the Butterworth zero radius field
		
		number zeroradius
		getpersistentnumbernote("Streak-free FFT:Settings:Butterworth Zero Radius (0.35-0.8)", zeroradius)
		taggroup label=dlgcreatelabel("Zero Radius")
		taggroup bworthradiusfield=dlgcreaterealfield(zeroradius, 6, 4).dlgchangedmethod("zeroradiusfieldchanged").dlgidentifier("zeroradiusfield")
		taggroup bworthradiusgroup=dlggroupitems(label, bworthradiusfield).dlgtablelayout(1,2,0)


		// Creates the Butterworh Order field
		
		number bworthorder
		getpersistentnumbernote("Streak-free FFT:Settings:Butterworth Order (5-15)", bworthorder)
		label=dlgcreatelabel("B'Worth Order")
		taggroup bworthorderfield=dlgcreateintegerfield(bworthorder, 6).dlgchangedmethod("bworthorderfieldchanged").dlgidentifier("butterworthorderfield")
		taggroup bworthordergroup=dlggroupitems(label, bworthorderfield).dlgtablelayout(1,2,0)
		
		taggroup fieldgroup=dlggroupitems(bworthradiusgroup, bworthordergroup).dlgtablelayout(2,1,0)


		// Creates the full or reduced FFT radio
		
		number fullorreducedradioval
		getpersistentnumbernote("Streak-free FFT:Settings:Full or Reduced FFT Radio (0-1)", fullorreducedradioval)
		taggroup fullorreducedradio_items
		label=dlgcreatelabel("FFT Mode")
		taggroup fullorreducedradio=dlgcreateradiolist(fullorreducedradio_items, fullorreducedradioval, "fullorreducedradiochanged").dlgidentifier("fullorreducedradio")
		fullorreducedradio_items.dlgaddelement(dlgcreateradioitem("Full   ",0))
		fullorreducedradio_items.dlgaddelement(dlgcreateradioitem("Reduced",1))
		taggroup fullorreducedradiogroup=dlggroupitems(label, fullorreducedradio).dlgtablelayout(1,2,0)


		// Create the unfiltered or filtered radio
		
		number filterradioval
		getpersistentnumbernote("Streak-free FFT:Settings:Filtered or Unfiltered Radio (0-1)", filterradioval)
		taggroup filterradio_items
		label=dlgcreatelabel("Filtering")
		taggroup filterradio=dlgcreateradiolist(filterradio_items, filterradioval, "filterradiochanged").dlgidentifier("filterradio")
		filterradio_items.dlgaddelement(dlgcreateradioitem("Unfiltered   ",0))
		filterradio_items.dlgaddelement(dlgcreateradioitem("Filtered",1))
		taggroup filterradiogroup=dlggroupitems(label, filterradio).dlgtablelayout(1,2,0)

		taggroup radiogroup=dlggroupitems(fullorreducedradiogroup, filterradiogroup).dlgtablelayout(2,1,0)


		// Create a checkbox to show the filter
		
		number showfilterval
		getpersistentnumbernote("Streak-free FFT:Settings:Show Filter Check Box (0-1)", showfilterval)
		label=dlgcreatelabel("Show Butterworth Filter")
		taggroup showfiltercheckbox=dlgcreatecheckbox("",showfilterval, "showfiltercheckboxchanged").dlgidentifier("showfiltercheckbox").dlgexternalpadding(0,5)
		taggroup showfiltercheckgroup=dlggroupitems(label, showfiltercheckbox).dlgtablelayout(2,1,0)


		// Adds the items to the FFT box
		
		fftbox_items.dlgaddelement(fftButton)
		fftbox_items.dlgaddelement(radiogroup)
		
		
		// Create a filter box and add items to that
		
		taggroup filterbox_items
		taggroup filterbox=dlgcreatebox("  Filter  ", filterbox_items).dlgexternalpadding(3,0).dlginternalpadding(10,8)
		
		filterbox_items.dlgaddelement(fieldgroup)
		filterbox_items.dlgaddelement(showfiltercheckgroup)


		// Add the two boxes to the dialog
		
		dialog_items.dlgaddelement(fftbox)
		dialog_items.dlgaddelement(filterbox)
		taggroup footer=dlgcreatelabel("D. R. G. Mitchell, v1.0, Oct. 2019")
		dialog_items.dlgaddelement(footer)
		return dialog
	}

// The constructor this calls the makedialog() function and displays it

StreakFreeFFTDialog(object self)
	{

		// Locate the previous dialog position

		number top, left, screenwidth, screenheight
		getscreensize(screenwidth, screenheight)
		getpersistentnumbernote("Streak-free FFT:Settings:Dialog Top", top)
		getpersistentnumbernote("Streak-free FFT:Settings:Dialog Left", left)
		
		// Ensure it is on the screen
		
		if(top<0) top=0
		if(top>(screenheight*0.85)) top=screenheight*0.85
		if(left<0) left=0
		if(left>(screenwidth*0.85)) left=screenwidth*0.85
		
		
		// Construct and display the dialog
		
		self.init( self.makeDialog())
		self.display("S-F FFT").WindowSetFramePosition(left,top)
	}


// The destructor is called when the object (the dialog) is closed

~StreakFreeFFTDialog(object self)
	{
	}
}

// allocates the above function which puts it all together

alloc(StreakFreeFFTDialog)