From 02e140ac9140f6bf638fac97730d848a52dcb7ea Mon Sep 17 00:00:00 2001 From: Gerardo Marx Date: Mon, 29 Nov 2021 12:45:23 -0600 Subject: [PATCH] first commit --- .classpath | 11 + .project | 17 + .settings/org.eclipse.jdt.core.prefs | 12 + IJ_Props.txt | 466 + bin/ij/CommandListener.class | Bin 0 -> 182 bytes bin/ij/CompositeImage.class | Bin 0 -> 17693 bytes bin/ij/Executer.class | Bin 0 -> 9063 bytes bin/ij/IJ$ExceptionHandler.class | Bin 0 -> 210 bytes bin/ij/IJ.class | Bin 0 -> 63092 bytes bin/ij/IJEventListener.class | Bin 0 -> 381 bytes bin/ij/ImageJ$ExceptionHandler.class | Bin 0 -> 1990 bytes bin/ij/ImageJ.class | Bin 0 -> 27375 bytes bin/ij/ImageJApplet.class | Bin 0 -> 1168 bytes bin/ij/ImageListener.class | Bin 0 -> 197 bytes bin/ij/ImagePlus$1.class | Bin 0 -> 1101 bytes bin/ij/ImagePlus.class | Bin 0 -> 76695 bytes bin/ij/ImageStack.class | Bin 0 -> 18090 bytes bin/ij/LookUpTable.class | Bin 0 -> 3996 bytes bin/ij/Macro.class | Bin 0 -> 4568 bytes bin/ij/Menus.class | Bin 0 -> 48849 bytes bin/ij/OtherInstance$ImageJInstance.class | Bin 0 -> 318 bytes bin/ij/OtherInstance$Implementation.class | Bin 0 -> 1947 bytes bin/ij/OtherInstance.class | Bin 0 -> 6868 bytes bin/ij/Prefs.class | Bin 0 -> 21106 bytes bin/ij/RecentOpener.class | Bin 0 -> 1381 bytes bin/ij/Undo.class | Bin 0 -> 5308 bytes bin/ij/VirtualStack.class | Bin 0 -> 9857 bytes bin/ij/WindowManager.class | Bin 0 -> 14811 bytes bin/ij/gui/Arrow.class | Bin 0 -> 11908 bytes bin/ij/gui/ColorChooser.class | Bin 0 -> 3651 bytes bin/ij/gui/ColorPanel.class | Bin 0 -> 1825 bytes bin/ij/gui/DialogListener.class | Bin 0 -> 191 bytes bin/ij/gui/EllipseRoi.class | Bin 0 -> 8454 bytes bin/ij/gui/FreehandRoi.class | Bin 0 -> 2990 bytes bin/ij/gui/GUI.class | Bin 0 -> 9560 bytes bin/ij/gui/GenericDialog$1.class | Bin 0 -> 936 bytes .../GenericDialog$BrowseButtonListener.class | Bin 0 -> 1758 bytes bin/ij/gui/GenericDialog$TextDropTarget.class | Bin 0 -> 1092 bytes bin/ij/gui/GenericDialog.class | Bin 0 -> 45118 bytes bin/ij/gui/HTMLDialog$1.class | Bin 0 -> 634 bytes bin/ij/gui/HTMLDialog$2.class | Bin 0 -> 760 bytes bin/ij/gui/HTMLDialog.class | Bin 0 -> 7404 bytes bin/ij/gui/HistogramPlot.class | Bin 0 -> 14643 bytes bin/ij/gui/HistogramWindow.class | Bin 0 -> 24891 bytes bin/ij/gui/ImageCanvas$1.class | Bin 0 -> 2119 bytes bin/ij/gui/ImageCanvas.class | Bin 0 -> 47610 bytes bin/ij/gui/ImageLayout.class | Bin 0 -> 3661 bytes bin/ij/gui/ImagePanel.class | Bin 0 -> 1126 bytes bin/ij/gui/ImageRoi.class | Bin 0 -> 4802 bytes bin/ij/gui/ImageWindow.class | Bin 0 -> 23752 bytes bin/ij/gui/Line$PointIterator.class | Bin 0 -> 2154 bytes bin/ij/gui/Line.class | Bin 0 -> 17254 bytes bin/ij/gui/MessageDialog.class | Bin 0 -> 3902 bytes bin/ij/gui/MultiLineLabel.class | Bin 0 -> 3576 bytes bin/ij/gui/NewImage.class | Bin 0 -> 14483 bytes bin/ij/gui/NonBlockingGenericDialog$1.class | Bin 0 -> 776 bytes bin/ij/gui/NonBlockingGenericDialog$2.class | Bin 0 -> 731 bytes bin/ij/gui/NonBlockingGenericDialog.class | Bin 0 -> 3297 bytes bin/ij/gui/OvalRoi.class | Bin 0 -> 11663 bytes bin/ij/gui/Overlay$1.class | Bin 0 -> 1177 bytes bin/ij/gui/Overlay.class | Bin 0 -> 14839 bytes bin/ij/gui/Plot.class | Bin 0 -> 96449 bytes bin/ij/gui/PlotCanvas.class | Bin 0 -> 6560 bytes bin/ij/gui/PlotContentsDialog.class | Bin 0 -> 22169 bytes bin/ij/gui/PlotDialog$1.class | Bin 0 -> 739 bytes bin/ij/gui/PlotDialog.class | Bin 0 -> 19632 bytes bin/ij/gui/PlotMaker.class | Bin 0 -> 186 bytes bin/ij/gui/PlotObject.class | Bin 0 -> 7364 bytes bin/ij/gui/PlotProperties.class | Bin 0 -> 1815 bytes bin/ij/gui/PlotVirtualStack.class | Bin 0 -> 2705 bytes bin/ij/gui/PlotWindow$1.class | Bin 0 -> 665 bytes bin/ij/gui/PlotWindow.class | Bin 0 -> 28603 bytes bin/ij/gui/PointRoi$1.class | Bin 0 -> 1347 bytes bin/ij/gui/PointRoi.class | Bin 0 -> 25935 bytes bin/ij/gui/PolygonRoi.class | Bin 0 -> 40172 bytes bin/ij/gui/ProfilePlot.class | Bin 0 -> 10525 bytes bin/ij/gui/ProgressBar.class | Bin 0 -> 3793 bytes bin/ij/gui/Roi$RoiPointsIteratorMask.class | Bin 0 -> 2231 bytes bin/ij/gui/Roi.class | Bin 0 -> 62479 bytes bin/ij/gui/RoiBrush.class | Bin 0 -> 3195 bytes bin/ij/gui/RoiDefaultsDialog.class | Bin 0 -> 3553 bytes bin/ij/gui/RoiListener.class | Bin 0 -> 361 bytes bin/ij/gui/RoiProperties.class | Bin 0 -> 15366 bytes bin/ij/gui/RotatedRectRoi.class | Bin 0 -> 7534 bytes bin/ij/gui/SaveChangesDialog.class | Bin 0 -> 3720 bytes bin/ij/gui/ScrollbarWithLabel$Icon.class | Bin 0 -> 4452 bytes bin/ij/gui/ScrollbarWithLabel.class | Bin 0 -> 4908 bytes bin/ij/gui/ShapeRoi.class | Bin 0 -> 26230 bytes bin/ij/gui/StackWindow$1.class | Bin 0 -> 1024 bytes bin/ij/gui/StackWindow$2.class | Bin 0 -> 825 bytes bin/ij/gui/StackWindow.class | Bin 0 -> 11461 bytes bin/ij/gui/TextRoi.class | Bin 0 -> 20750 bytes bin/ij/gui/Toolbar$1.class | Bin 0 -> 975 bytes bin/ij/gui/Toolbar.class | Bin 0 -> 55230 bytes bin/ij/gui/TrimmedButton.class | Bin 0 -> 1174 bytes bin/ij/gui/WaitForUserDialog.class | Bin 0 -> 4692 bytes bin/ij/gui/Wand.class | Bin 0 -> 6183 bytes bin/ij/gui/YesNoCancelDialog.class | Bin 0 -> 5098 bytes bin/ij/io/BitBuffer.class | Bin 0 -> 1251 bytes bin/ij/io/ByteVector.class | Bin 0 -> 1446 bytes bin/ij/io/DirectoryChooser$1.class | Bin 0 -> 2084 bytes bin/ij/io/DirectoryChooser.class | Bin 0 -> 4307 bytes bin/ij/io/DragAndDropHandler.class | Bin 0 -> 4429 bytes bin/ij/io/FileInfo.class | Bin 0 -> 6124 bytes bin/ij/io/FileOpener.class | Bin 0 -> 20595 bytes bin/ij/io/FileSaver.class | Bin 0 -> 23766 bytes bin/ij/io/ImageReader.class | Bin 0 -> 23737 bytes bin/ij/io/ImageWriter.class | Bin 0 -> 8697 bytes bin/ij/io/ImportDialog.class | Bin 0 -> 11673 bytes bin/ij/io/LogStream.class | Bin 0 -> 3831 bytes bin/ij/io/OpenDialog$1.class | Bin 0 -> 2214 bytes bin/ij/io/OpenDialog.class | Bin 0 -> 7082 bytes bin/ij/io/Opener$1.class | Bin 0 -> 2168 bytes bin/ij/io/Opener.class | Bin 0 -> 37268 bytes bin/ij/io/PluginClassLoader.class | Bin 0 -> 2783 bytes bin/ij/io/RandomAccessStream.class | Bin 0 -> 4415 bytes bin/ij/io/RoiDecoder.class | Bin 0 -> 17138 bytes bin/ij/io/RoiEncoder.class | Bin 0 -> 12573 bytes bin/ij/io/SaveDialog$1.class | Bin 0 -> 2892 bytes bin/ij/io/SaveDialog.class | Bin 0 -> 8504 bytes bin/ij/io/TextEncoder.class | Bin 0 -> 1961 bytes bin/ij/io/TiffDecoder.class | Bin 0 -> 22529 bytes bin/ij/io/TiffEncoder.class | Bin 0 -> 11684 bytes bin/ij/macro/Debugger.class | Bin 0 -> 376 bytes bin/ij/macro/ExtensionDescriptor.class | Bin 0 -> 7413 bytes bin/ij/macro/FunctionFinder.class | Bin 0 -> 8727 bytes bin/ij/macro/Functions.class | Bin 0 -> 193802 bytes bin/ij/macro/Interpreter.class | Bin 0 -> 51798 bytes bin/ij/macro/MacroConstants.class | Bin 0 -> 15951 bytes bin/ij/macro/MacroException.class | Bin 0 -> 431 bytes bin/ij/macro/MacroExtension.class | Bin 0 -> 463 bytes bin/ij/macro/MacroRunner.class | Bin 0 -> 5370 bytes bin/ij/macro/Program.class | Bin 0 -> 7616 bytes bin/ij/macro/ReturnException.class | Bin 0 -> 420 bytes bin/ij/macro/StartupRunner.class | Bin 0 -> 1386 bytes bin/ij/macro/Symbol.class | Bin 0 -> 1262 bytes bin/ij/macro/Tokenizer.class | Bin 0 -> 6610 bytes bin/ij/macro/Variable.class | Bin 0 -> 4264 bytes bin/ij/measure/Calibration.class | Bin 0 -> 10739 bytes bin/ij/measure/CurveFitter.class | Bin 0 -> 30784 bytes bin/ij/measure/Measurements.class | Bin 0 -> 1060 bytes bin/ij/measure/Minimizer$1.class | Bin 0 -> 751 bytes bin/ij/measure/Minimizer.class | Bin 0 -> 16208 bytes .../ResultsTable$ComparableEntry.class | Bin 0 -> 1345 bytes bin/ij/measure/ResultsTable.class | Bin 0 -> 39968 bytes bin/ij/measure/ResultsTableMacros$1.class | Bin 0 -> 1127 bytes bin/ij/measure/ResultsTableMacros.class | Bin 0 -> 8983 bytes bin/ij/measure/SplineFitter.class | Bin 0 -> 3768 bytes bin/ij/measure/UserFunction.class | Bin 0 -> 148 bytes bin/ij/plugin/AVI_Reader$raInputStream.class | Bin 0 -> 2399 bytes bin/ij/plugin/AVI_Reader.class | Bin 0 -> 39637 bytes bin/ij/plugin/AboutBox.class | Bin 0 -> 3900 bytes bin/ij/plugin/AnimatedGifEncoder2.class | Bin 0 -> 13914 bytes bin/ij/plugin/Animator.class | Bin 0 -> 9456 bytes bin/ij/plugin/AppearanceOptions.class | Bin 0 -> 6814 bytes bin/ij/plugin/ArrowToolOptions.class | Bin 0 -> 4426 bytes bin/ij/plugin/BMPDecoder.class | Bin 0 -> 5964 bytes bin/ij/plugin/BMP_Reader.class | Bin 0 -> 2887 bytes bin/ij/plugin/BMP_Writer.class | Bin 0 -> 6635 bytes bin/ij/plugin/BatchConverter.class | Bin 0 -> 7104 bytes bin/ij/plugin/BatchMeasure.class | Bin 0 -> 1709 bytes bin/ij/plugin/BatchProcessor.class | Bin 0 -> 17755 bytes bin/ij/plugin/Benchmark.class | Bin 0 -> 5821 bytes bin/ij/plugin/Binner.class | Bin 0 -> 10338 bytes bin/ij/plugin/BrowserLauncher.class | Bin 0 -> 3016 bytes bin/ij/plugin/CalibrationBar$LiveDialog.class | Bin 0 -> 3215 bytes bin/ij/plugin/CalibrationBar.class | Bin 0 -> 15385 bytes bin/ij/plugin/CanvasResizer.class | Bin 0 -> 4954 bytes bin/ij/plugin/ChannelArranger.class | Bin 0 -> 5932 bytes bin/ij/plugin/ChannelSplitter.class | Bin 0 -> 6108 bytes bin/ij/plugin/CircularRoiMaker.class | Bin 0 -> 3342 bytes bin/ij/plugin/ClassChecker.class | Bin 0 -> 3460 bytes bin/ij/plugin/Clipboard.class | Bin 0 -> 7020 bytes bin/ij/plugin/ColorPanel.class | Bin 0 -> 12780 bytes bin/ij/plugin/Colors.class | Bin 0 -> 8465 bytes bin/ij/plugin/CommandFinder$1.class | Bin 0 -> 1143 bytes bin/ij/plugin/CommandFinder$2.class | Bin 0 -> 1866 bytes .../plugin/CommandFinder$CommandAction.class | Bin 0 -> 1126 bytes ...CommandFinder$PromptDocumentListener.class | Bin 0 -> 1132 bytes bin/ij/plugin/CommandFinder$TableModel.class | Bin 0 -> 2130 bytes bin/ij/plugin/CommandFinder.class | Bin 0 -> 18916 bytes bin/ij/plugin/CommandLister.class | Bin 0 -> 3859 bytes bin/ij/plugin/Commands.class | Bin 0 -> 5240 bytes bin/ij/plugin/Compiler.class | Bin 0 -> 11135 bytes .../CompilerTool$JavaxCompilerTool.class | Bin 0 -> 2720 bytes .../CompilerTool$LegacyCompilerTool.class | Bin 0 -> 2445 bytes bin/ij/plugin/CompilerTool.class | Bin 0 -> 1026 bytes bin/ij/plugin/CompositeConverter.class | Bin 0 -> 4537 bytes bin/ij/plugin/Concatenator.class | Bin 0 -> 14749 bytes bin/ij/plugin/ContrastEnhancer.class | Bin 0 -> 10558 bytes bin/ij/plugin/ControlPanel.class | Bin 0 -> 15991 bytes bin/ij/plugin/Converter.class | Bin 0 -> 6522 bytes bin/ij/plugin/Coordinates.class | Bin 0 -> 7129 bytes bin/ij/plugin/DICOM.class | Bin 0 -> 6796 bytes bin/ij/plugin/DicomDecoder.class | Bin 0 -> 17974 bytes bin/ij/plugin/DicomDictionary.class | Bin 0 -> 37022 bytes bin/ij/plugin/Distribution.class | Bin 0 -> 6579 bytes bin/ij/plugin/DragAndDrop.class | Bin 0 -> 9430 bytes bin/ij/plugin/Duplicator.class | Bin 0 -> 22220 bytes bin/ij/plugin/EventListener.class | Bin 0 -> 3727 bytes bin/ij/plugin/FFT.class | Bin 0 -> 16815 bytes bin/ij/plugin/FFTMath.class | Bin 0 -> 5460 bytes bin/ij/plugin/FITS_Reader.class | Bin 0 -> 3088 bytes bin/ij/plugin/FITS_Writer.class | Bin 0 -> 9135 bytes bin/ij/plugin/FileInfoVirtualStack.class | Bin 0 -> 9294 bytes bin/ij/plugin/Filters3D$1.class | Bin 0 -> 1326 bytes bin/ij/plugin/Filters3D.class | Bin 0 -> 5327 bytes bin/ij/plugin/FitsDecoder.class | Bin 0 -> 4241 bytes bin/ij/plugin/FolderOpener.class | Bin 0 -> 23605 bytes bin/ij/plugin/GIF_Reader.class | Bin 0 -> 2427 bytes bin/ij/plugin/GaussianBlur3D.class | Bin 0 -> 4549 bytes bin/ij/plugin/GelAnalyzer.class | Bin 0 -> 13659 bytes bin/ij/plugin/GifDecoder.class | Bin 0 -> 10011 bytes bin/ij/plugin/GifFrame.class | Bin 0 -> 444 bytes bin/ij/plugin/GifWriter.class | Bin 0 -> 6351 bytes bin/ij/plugin/Grid.class | Bin 0 -> 10079 bytes bin/ij/plugin/GroupedZProjector.class | Bin 0 -> 3512 bytes bin/ij/plugin/Histogram.class | Bin 0 -> 7082 bytes bin/ij/plugin/Hotkeys.class | Bin 0 -> 6996 bytes bin/ij/plugin/HyperStackConverter.class | Bin 0 -> 9929 bytes bin/ij/plugin/HyperStackMaker.class | Bin 0 -> 7260 bytes bin/ij/plugin/HyperStackReducer.class | Bin 0 -> 8265 bytes bin/ij/plugin/ImageCalculator.class | Bin 0 -> 11098 bytes bin/ij/plugin/ImageInfo.class | Bin 0 -> 21599 bytes bin/ij/plugin/ImageJ_Updater.class | Bin 0 -> 8015 bytes bin/ij/plugin/ImagesToStack.class | Bin 0 -> 9995 bytes bin/ij/plugin/JavaProperties.class | Bin 0 -> 9658 bytes bin/ij/plugin/JavaScriptEvaluator.class | Bin 0 -> 3532 bytes bin/ij/plugin/JpegWriter.class | Bin 0 -> 7391 bytes bin/ij/plugin/LUT_Editor.class | Bin 0 -> 3678 bytes bin/ij/plugin/LZWEncoder.class | Bin 0 -> 4123 bytes bin/ij/plugin/LZWEncoder2.class | Bin 0 -> 4125 bytes bin/ij/plugin/ListVirtualStack.class | Bin 0 -> 7383 bytes bin/ij/plugin/LutLoader.class | Bin 0 -> 14049 bytes bin/ij/plugin/MacroInstaller.class | Bin 0 -> 16794 bytes bin/ij/plugin/Macro_Runner.class | Bin 0 -> 10586 bytes bin/ij/plugin/MeasurementsWriter.class | Bin 0 -> 2076 bytes bin/ij/plugin/Memory.class | Bin 0 -> 7008 bytes bin/ij/plugin/MontageMaker.class | Bin 0 -> 9902 bytes bin/ij/plugin/NeuQuant.class | Bin 0 -> 6812 bytes bin/ij/plugin/NewPlugin.class | Bin 0 -> 6274 bytes bin/ij/plugin/NextImageOpener.class | Bin 0 -> 4983 bytes bin/ij/plugin/Options.class | Bin 0 -> 9299 bytes bin/ij/plugin/Orthogonal_Views.class | Bin 0 -> 25900 bytes bin/ij/plugin/OverlayCommands.class | Bin 0 -> 14766 bytes bin/ij/plugin/OverlayLabels.class | Bin 0 -> 5023 bytes bin/ij/plugin/PGM_Reader.class | Bin 0 -> 8732 bytes bin/ij/plugin/PNG_Writer.class | Bin 0 -> 5963 bytes bin/ij/plugin/PNM_Writer.class | Bin 0 -> 4104 bytes bin/ij/plugin/Plots.class | Bin 0 -> 1571 bytes bin/ij/plugin/PlotsCanvas.class | Bin 0 -> 5191 bytes bin/ij/plugin/PlugIn.class | Bin 0 -> 141 bytes bin/ij/plugin/PlugInExecuter.class | Bin 0 -> 3346 bytes bin/ij/plugin/PlugInInterpreter.class | Bin 0 -> 622 bytes bin/ij/plugin/PluginInstaller.class | Bin 0 -> 8466 bytes bin/ij/plugin/PointToolOptions.class | Bin 0 -> 8440 bytes bin/ij/plugin/Profiler.class | Bin 0 -> 4374 bytes bin/ij/plugin/Projector.class | Bin 0 -> 25415 bytes bin/ij/plugin/ProxySettings.class | Bin 0 -> 3603 bytes bin/ij/plugin/RGBStackConverter.class | Bin 0 -> 10506 bytes bin/ij/plugin/RGBStackMerge.class | Bin 0 -> 14745 bytes bin/ij/plugin/RandomOvals.txt | 15 + bin/ij/plugin/Raw.class | Bin 0 -> 2773 bytes bin/ij/plugin/RectToolOptions.class | Bin 0 -> 3712 bytes bin/ij/plugin/Resizer.class | Bin 0 -> 14246 bytes bin/ij/plugin/RoiEnlarger.class | Bin 0 -> 9740 bytes bin/ij/plugin/RoiInterpolator.class | Bin 0 -> 4814 bytes bin/ij/plugin/RoiReader.class | Bin 0 -> 2812 bytes bin/ij/plugin/RoiRotator.class | Bin 0 -> 5176 bytes bin/ij/plugin/RoiScaler.class | Bin 0 -> 6666 bytes bin/ij/plugin/ScaleBar$BarDialog.class | Bin 0 -> 2258 bytes .../plugin/ScaleBar$BarDialogListener.class | Bin 0 -> 2551 bytes .../plugin/ScaleBar$MissingRoiException.class | Bin 0 -> 584 bytes .../ScaleBar$ScaleBarConfiguration.class | Bin 0 -> 1673 bytes bin/ij/plugin/ScaleBar.class | Bin 0 -> 17486 bytes bin/ij/plugin/Scaler.class | Bin 0 -> 14647 bytes bin/ij/plugin/ScreenGrabber.class | Bin 0 -> 3592 bytes bin/ij/plugin/Selection.class | Bin 0 -> 28742 bytes bin/ij/plugin/SimpleCommands$1.class | Bin 0 -> 705 bytes bin/ij/plugin/SimpleCommands.class | Bin 0 -> 10205 bytes bin/ij/plugin/Slicer.class | Bin 0 -> 23576 bytes bin/ij/plugin/SpecifyROI.class | Bin 0 -> 6388 bytes bin/ij/plugin/StackCombiner.class | Bin 0 -> 5329 bytes bin/ij/plugin/StackEditor.class | Bin 0 -> 10833 bytes bin/ij/plugin/StackInserter.class | Bin 0 -> 3044 bytes bin/ij/plugin/StackMaker.class | Bin 0 -> 3347 bytes bin/ij/plugin/StackPlotter.class | Bin 0 -> 2875 bytes bin/ij/plugin/StackReducer.class | Bin 0 -> 4135 bytes bin/ij/plugin/StackReverser.class | Bin 0 -> 2221 bytes bin/ij/plugin/StackWriter.class | Bin 0 -> 10511 bytes bin/ij/plugin/Stack_Statistics.class | Bin 0 -> 2725 bytes bin/ij/plugin/Startup.class | Bin 0 -> 4749 bytes bin/ij/plugin/Straightener.class | Bin 0 -> 8514 bytes bin/ij/plugin/SubHyperstackMaker.class | Bin 0 -> 7969 bytes bin/ij/plugin/SubstackMaker.class | Bin 0 -> 7517 bytes bin/ij/plugin/SurfacePlotter.class | Bin 0 -> 14680 bytes bin/ij/plugin/Text.class | Bin 0 -> 6265 bytes bin/ij/plugin/TextFileReader.class | Bin 0 -> 512 bytes bin/ij/plugin/TextReader.class | Bin 0 -> 5047 bytes bin/ij/plugin/TextWriter.class | Bin 0 -> 1265 bytes bin/ij/plugin/ThreadLister$1.class | Bin 0 -> 632 bytes bin/ij/plugin/ThreadLister.class | Bin 0 -> 3456 bytes bin/ij/plugin/Thresholder.class | Bin 0 -> 14805 bytes bin/ij/plugin/ThumbnailsCanvas.class | Bin 0 -> 7327 bytes bin/ij/plugin/TreePanel$1.class | Bin 0 -> 1373 bytes bin/ij/plugin/TreePanel$2.class | Bin 0 -> 1447 bytes bin/ij/plugin/TreePanel$3.class | Bin 0 -> 1308 bytes bin/ij/plugin/TreePanel.class | Bin 0 -> 16195 bytes bin/ij/plugin/URLOpener.class | Bin 0 -> 8560 bytes bin/ij/plugin/WandToolOptions.class | Bin 0 -> 3628 bytes bin/ij/plugin/WindowOrganizer.class | Bin 0 -> 5384 bytes bin/ij/plugin/XYCoordinates.class | Bin 0 -> 7855 bytes bin/ij/plugin/XY_Reader.class | Bin 0 -> 2397 bytes bin/ij/plugin/ZAxisProfiler.class | Bin 0 -> 9200 bytes .../plugin/ZProjector$AverageIntensity.class | Bin 0 -> 1471 bytes bin/ij/plugin/ZProjector$MaxIntensity.class | Bin 0 -> 1431 bytes bin/ij/plugin/ZProjector$MinIntensity.class | Bin 0 -> 1376 bytes bin/ij/plugin/ZProjector$RayFunction.class | Bin 0 -> 589 bytes .../plugin/ZProjector$StandardDeviation.class | Bin 0 -> 1901 bytes bin/ij/plugin/ZProjector.class | Bin 0 -> 18523 bytes bin/ij/plugin/Zoom.class | Bin 0 -> 8758 bytes .../filter/AVI_Writer$RaOutputStream.class | Bin 0 -> 1065 bytes bin/ij/plugin/filter/AVI_Writer.class | Bin 0 -> 14845 bytes bin/ij/plugin/filter/Analyzer.class | Bin 0 -> 32621 bytes .../plugin/filter/BackgroundSubtracter.class | Bin 0 -> 18803 bytes bin/ij/plugin/filter/Benchmark.class | Bin 0 -> 2408 bytes bin/ij/plugin/filter/Binary.class | Bin 0 -> 6991 bytes bin/ij/plugin/filter/Calibrator.class | Bin 0 -> 17475 bytes bin/ij/plugin/filter/Convolver.class | Bin 0 -> 16200 bytes bin/ij/plugin/filter/Duplicater.class | Bin 0 -> 1301 bytes bin/ij/plugin/filter/EDM.class | Bin 0 -> 10652 bytes .../plugin/filter/ExtendedPlugInFilter.class | Bin 0 -> 355 bytes bin/ij/plugin/filter/FFTCustomFilter.class | Bin 0 -> 6984 bytes bin/ij/plugin/filter/FFTFilter.class | Bin 0 -> 11913 bytes bin/ij/plugin/filter/Filler.class | Bin 0 -> 7990 bytes bin/ij/plugin/filter/Filters.class | Bin 0 -> 2527 bytes bin/ij/plugin/filter/FractalBoxCounter.class | Bin 0 -> 7143 bytes bin/ij/plugin/filter/GaussianBlur$1.class | Bin 0 -> 2874 bytes bin/ij/plugin/filter/GaussianBlur.class | Bin 0 -> 14589 bytes bin/ij/plugin/filter/ImageMath.class | Bin 0 -> 16598 bytes bin/ij/plugin/filter/ImageProperties.class | Bin 0 -> 13924 bytes bin/ij/plugin/filter/Info.class | Bin 0 -> 962 bytes bin/ij/plugin/filter/LineGraphAnalyzer.class | Bin 0 -> 3522 bytes bin/ij/plugin/filter/LutApplier.class | Bin 0 -> 4204 bytes bin/ij/plugin/filter/LutViewer.class | Bin 0 -> 3775 bytes bin/ij/plugin/filter/LutWindow.class | Bin 0 -> 2450 bytes bin/ij/plugin/filter/MaximumFinder.class | Bin 0 -> 31278 bytes bin/ij/plugin/filter/ParticleAnalyzer.class | Bin 0 -> 37002 bytes bin/ij/plugin/filter/PlugInFilter.class | Bin 0 -> 985 bytes bin/ij/plugin/filter/PlugInFilterRunner.class | Bin 0 -> 16751 bytes bin/ij/plugin/filter/Printer.class | Bin 0 -> 6161 bytes bin/ij/plugin/filter/RGBStackSplitter.class | Bin 0 -> 1308 bytes bin/ij/plugin/filter/RankFilters$1.class | Bin 0 -> 1785 bytes bin/ij/plugin/filter/RankFilters.class | Bin 0 -> 25548 bytes bin/ij/plugin/filter/RoiWriter.class | Bin 0 -> 2387 bytes bin/ij/plugin/filter/RollingBall.class | Bin 0 -> 1428 bytes bin/ij/plugin/filter/Rotator.class | Bin 0 -> 7827 bytes bin/ij/plugin/filter/SaltAndPepper.class | Bin 0 -> 1838 bytes bin/ij/plugin/filter/ScaleDialog.class | Bin 0 -> 5079 bytes bin/ij/plugin/filter/SetScaleDialog.class | Bin 0 -> 3311 bytes bin/ij/plugin/filter/Shadows.class | Bin 0 -> 3402 bytes bin/ij/plugin/filter/StackLabeler.class | Bin 0 -> 11386 bytes .../filter/ThresholdToSelection$Outline.class | Bin 0 -> 4395 bytes .../plugin/filter/ThresholdToSelection.class | Bin 0 -> 6004 bytes bin/ij/plugin/filter/Transformer.class | Bin 0 -> 3349 bytes bin/ij/plugin/filter/Translator.class | Bin 0 -> 3432 bytes bin/ij/plugin/filter/UnsharpMask.class | Bin 0 -> 3929 bytes bin/ij/plugin/filter/Writer.class | Bin 0 -> 1625 bytes bin/ij/plugin/filter/XYWriter.class | Bin 0 -> 3425 bytes bin/ij/plugin/frame/Channels.class | Bin 0 -> 8468 bytes bin/ij/plugin/frame/ColorCanvas.class | Bin 0 -> 7339 bytes bin/ij/plugin/frame/ColorGenerator.class | Bin 0 -> 4444 bytes bin/ij/plugin/frame/ColorPicker.class | Bin 0 -> 3521 bytes .../frame/ColorThresholder$BandPlot.class | Bin 0 -> 5607 bytes bin/ij/plugin/frame/ColorThresholder.class | Bin 0 -> 34837 bytes bin/ij/plugin/frame/Commands.class | Bin 0 -> 7118 bytes bin/ij/plugin/frame/ContrastAdjuster.class | Bin 0 -> 33889 bytes bin/ij/plugin/frame/ContrastPlot.class | Bin 0 -> 3947 bytes bin/ij/plugin/frame/DisplayChangeEvent.class | Bin 0 -> 995 bytes .../plugin/frame/DisplayChangeListener.class | Bin 0 -> 227 bytes bin/ij/plugin/frame/Editor.class | Bin 0 -> 49762 bytes bin/ij/plugin/frame/Fitter$1.class | Bin 0 -> 684 bytes bin/ij/plugin/frame/Fitter.class | Bin 0 -> 12737 bytes bin/ij/plugin/frame/IJEventMulticaster.class | Bin 0 -> 1177 bytes bin/ij/plugin/frame/LineWidthAdjuster.class | Bin 0 -> 7051 bytes .../frame/MemoryMonitor$PlotCanvas.class | Bin 0 -> 911 bytes bin/ij/plugin/frame/MemoryMonitor.class | Bin 0 -> 5324 bytes bin/ij/plugin/frame/PasteController.class | Bin 0 -> 2690 bytes bin/ij/plugin/frame/PlugInDialog.class | Bin 0 -> 2808 bytes bin/ij/plugin/frame/PlugInFrame.class | Bin 0 -> 2903 bytes bin/ij/plugin/frame/Recorder.class | Bin 0 -> 30608 bytes bin/ij/plugin/frame/RoiManager$1.class | Bin 0 -> 679 bytes bin/ij/plugin/frame/RoiManager$2.class | Bin 0 -> 934 bytes bin/ij/plugin/frame/RoiManager$3.class | Bin 0 -> 730 bytes bin/ij/plugin/frame/RoiManager$4.class | Bin 0 -> 1285 bytes .../frame/RoiManager$MultiMeasureRunner.class | Bin 0 -> 1781 bytes bin/ij/plugin/frame/RoiManager.class | Bin 0 -> 71709 bytes bin/ij/plugin/frame/SyncWindows.class | Bin 0 -> 21624 bytes bin/ij/plugin/frame/ThresholdAdjuster.class | Bin 0 -> 29034 bytes bin/ij/plugin/frame/ThresholdPlot.class | Bin 0 -> 7932 bytes bin/ij/plugin/frame/TrimmedLabel.class | Bin 0 -> 822 bytes bin/ij/plugin/tool/ArrowTool.class | Bin 0 -> 2419 bytes bin/ij/plugin/tool/BrushTool$Options.class | Bin 0 -> 3985 bytes bin/ij/plugin/tool/BrushTool.class | Bin 0 -> 10720 bytes bin/ij/plugin/tool/MacroToolRunner.class | Bin 0 -> 1175 bytes .../tool/OverlayBrushTool$Options.class | Bin 0 -> 5621 bytes bin/ij/plugin/tool/OverlayBrushTool.class | Bin 0 -> 6867 bytes bin/ij/plugin/tool/PixelInspectionTool.class | Bin 0 -> 2856 bytes bin/ij/plugin/tool/PixelInspector.class | Bin 0 -> 16289 bytes bin/ij/plugin/tool/PlugInTool.class | Bin 0 -> 2587 bytes bin/ij/plugin/tool/RoiRotationTool.class | Bin 0 -> 4943 bytes bin/ij/process/AutoThresholder$Method.class | Bin 0 -> 1961 bytes bin/ij/process/AutoThresholder.class | Bin 0 -> 20360 bytes bin/ij/process/BinaryInterpolator$IDT.class | Bin 0 -> 2099 bytes bin/ij/process/BinaryInterpolator.class | Bin 0 -> 4630 bytes bin/ij/process/BinaryProcessor.class | Bin 0 -> 6758 bytes bin/ij/process/Blitter.class | Bin 0 -> 703 bytes bin/ij/process/ByteBlitter.class | Bin 0 -> 5084 bytes bin/ij/process/ByteProcessor.class | Bin 0 -> 33859 bytes bin/ij/process/ByteStatistics.class | Bin 0 -> 5786 bytes bin/ij/process/ColorBlitter.class | Bin 0 -> 4443 bytes bin/ij/process/ColorProcessor.class | Bin 0 -> 35577 bytes bin/ij/process/ColorSpaceConverter.class | Bin 0 -> 9284 bytes bin/ij/process/ColorStatistics.class | Bin 0 -> 4661 bytes bin/ij/process/Cube.class | Bin 0 -> 1257 bytes bin/ij/process/DownsizeTable.class | Bin 0 -> 2562 bytes bin/ij/process/EllipseFitter.class | Bin 0 -> 5888 bytes bin/ij/process/FHT.class | Bin 0 -> 14834 bytes bin/ij/process/FloatBlitter.class | Bin 0 -> 4287 bytes bin/ij/process/FloatPolygon.class | Bin 0 -> 5752 bytes bin/ij/process/FloatProcessor.class | Bin 0 -> 30320 bytes bin/ij/process/FloatStatistics.class | Bin 0 -> 7568 bytes bin/ij/process/FloodFiller.class | Bin 0 -> 4888 bytes bin/ij/process/ImageConverter.class | Bin 0 -> 9291 bytes bin/ij/process/ImageProcessor.class | Bin 0 -> 57040 bytes bin/ij/process/ImageStatistics.class | Bin 0 -> 8685 bytes bin/ij/process/IntProcessor.class | Bin 0 -> 4759 bytes bin/ij/process/LUT.class | Bin 0 -> 3783 bytes bin/ij/process/MedianCut.class | Bin 0 -> 7588 bytes bin/ij/process/PolygonFiller.class | Bin 0 -> 6889 bytes bin/ij/process/ShortBlitter.class | Bin 0 -> 4240 bytes bin/ij/process/ShortProcessor.class | Bin 0 -> 33191 bytes bin/ij/process/ShortStatistics.class | Bin 0 -> 7039 bytes bin/ij/process/StackConverter.class | Bin 0 -> 9362 bytes bin/ij/process/StackProcessor.class | Bin 0 -> 9635 bytes bin/ij/process/StackStatistics.class | Bin 0 -> 9993 bytes bin/ij/process/TypeConverter.class | Bin 0 -> 6492 bytes bin/ij/text/TableListener.class | Bin 0 -> 182 bytes bin/ij/text/TextCanvas.class | Bin 0 -> 6266 bytes bin/ij/text/TextPanel.class | Bin 0 -> 30462 bytes bin/ij/text/TextWindow.class | Bin 0 -> 12324 bytes bin/ij/util/ArrayUtil.class | Bin 0 -> 2822 bytes bin/ij/util/DicomTools.class | Bin 0 -> 6353 bytes bin/ij/util/FloatArray.class | Bin 0 -> 2276 bytes bin/ij/util/FontUtil.class | Bin 0 -> 1801 bytes bin/ij/util/IJMath.class | Bin 0 -> 1193 bytes bin/ij/util/Java2.class | Bin 0 -> 2372 bytes bin/ij/util/StringSorter.class | Bin 0 -> 2977 bytes bin/ij/util/ThreadUtil$1.class | Bin 0 -> 1099 bytes bin/ij/util/ThreadUtil.class | Bin 0 -> 3725 bytes bin/ij/util/Tools$1.class | Bin 0 -> 965 bytes bin/ij/util/Tools$2.class | Bin 0 -> 992 bytes bin/ij/util/Tools.class | Bin 0 -> 12212 bytes bin/ij/util/WildcardMatch.class | Bin 0 -> 3549 bytes images/about.jpg | Bin 0 -> 1940 bytes images/microscope.gif | Bin 0 -> 148 bytes macros/AddParticles.txt | 12 + macros/Circle_Tool.txt | 19 + macros/CommandFinderTool.txt | 4 + macros/ConvertStackToBinary.txt | 19 + macros/DeveloperMenuTool.txt | 14 + macros/Filter_Plugin.src | 19 + macros/FloodFillTool.txt | 18 + macros/LUTMenuTool.txt | 19 + macros/Label_Tool.txt | 79 + macros/MagicMontageTools.txt | 448 + macros/MeasureStack.txt | 121 + macros/MoveSelection.txt | 40 + macros/My_Plugin.src | 17 + macros/Overlay Editing Tools.txt | 281 + macros/Plugin_Frame.src | 18 + macros/Prototype_Tool.src | 24 + macros/RoiMenuTool.txt | 88 + macros/Search.txt | 139 + macros/ShowAllLuts.txt | 37 + macros/SmoothWandTool.txt | 80 + macros/SprayCanTool.txt | 41 + macros/StacksMenuTool.txt | 15 + macros/StartupMacros.txt | 7 + macros/TimeStamp.ijm | 13 + plugins/Decarburization_Measurements.jar | Bin 0 -> 1518 bytes plugins/MacAdapter.class | Bin 0 -> 2005 bytes plugins/MacAdapter.source | 54 + plugins/MacAdapter9.class | Bin 0 -> 2272 bytes plugins/MacAdapter9.source | 58 + plugins/TESTPlugin_.jar | Bin 0 -> 759 bytes src/ij/CommandListener.java | 16 + src/ij/CompositeImage.java | 666 ++ src/ij/Executer.java | 277 + src/ij/IJ.java | 2573 ++++++ src/ij/IJEventListener.java | 17 + src/ij/ImageJ.java | 923 ++ src/ij/ImageJApplet.java | 44 + src/ij/ImageListener.java | 17 + src/ij/ImagePlus.java | 3438 +++++++ src/ij/ImageStack.java | 722 ++ src/ij/LookUpTable.java | 136 + src/ij/Macro.java | 178 + src/ij/Menus.java | 1720 ++++ src/ij/OtherInstance.java | 241 + src/ij/Prefs.java | 776 ++ src/ij/RecentOpener.java | 37 + src/ij/Undo.java | 215 + src/ij/VirtualStack.java | 335 + src/ij/WindowManager.java | 643 ++ src/ij/gui/Arrow.java | 393 + src/ij/gui/ColorChooser.java | 121 + src/ij/gui/DialogListener.java | 35 + src/ij/gui/EllipseRoi.java | 269 + src/ij/gui/FreehandRoi.java | 98 + src/ij/gui/GUI.java | 288 + src/ij/gui/GenericDialog.java | 1918 ++++ src/ij/gui/HTMLDialog.java | 149 + src/ij/gui/HistogramPlot.java | 382 + src/ij/gui/HistogramWindow.java | 719 ++ src/ij/gui/ImageCanvas.java | 1816 ++++ src/ij/gui/ImageLayout.java | 113 + src/ij/gui/ImagePanel.java | 28 + src/ij/gui/ImageRoi.java | 150 + src/ij/gui/ImageWindow.java | 763 ++ src/ij/gui/Line.java | 738 ++ src/ij/gui/MessageDialog.java | 85 + src/ij/gui/MultiLineLabel.java | 108 + src/ij/gui/NewImage.java | 466 + src/ij/gui/NonBlockingGenericDialog.java | 102 + src/ij/gui/OvalRoi.java | 430 + src/ij/gui/Overlay.java | 582 ++ src/ij/gui/Plot.java | 4379 +++++++++ src/ij/gui/PlotCanvas.java | 253 + src/ij/gui/PlotContentsDialog.java | 671 ++ src/ij/gui/PlotDialog.java | 551 ++ src/ij/gui/PlotMaker.java | 15 + src/ij/gui/PlotVirtualStack.java | 85 + src/ij/gui/PlotWindow.java | 917 ++ src/ij/gui/PointRoi.java | 987 ++ src/ij/gui/PolygonRoi.java | 1701 ++++ src/ij/gui/ProfilePlot.java | 341 + src/ij/gui/ProgressBar.java | 165 + src/ij/gui/Roi.java | 2957 ++++++ src/ij/gui/RoiBrush.java | 88 + src/ij/gui/RoiDefaultsDialog.java | 81 + src/ij/gui/RoiListener.java | 18 + src/ij/gui/RoiProperties.java | 428 + src/ij/gui/RotatedRectRoi.java | 229 + src/ij/gui/SaveChangesDialog.java | 98 + src/ij/gui/ScrollbarWithLabel.java | 233 + src/ij/gui/ShapeRoi.java | 1207 +++ src/ij/gui/StackWindow.java | 374 + src/ij/gui/TextRoi.java | 743 ++ src/ij/gui/Toolbar.java | 2134 +++++ src/ij/gui/TrimmedButton.java | 28 + src/ij/gui/WaitForUserDialog.java | 126 + src/ij/gui/Wand.java | 353 + src/ij/gui/YesNoCancelDialog.java | 138 + src/ij/io/BitBuffer.java | 65 + src/ij/io/DirectoryChooser.java | 137 + src/ij/io/DragAndDropHandler.java | 95 + src/ij/io/FileInfo.java | 270 + src/ij/io/FileOpener.java | 676 ++ src/ij/io/FileSaver.java | 837 ++ src/ij/io/ImageReader.java | 1069 +++ src/ij/io/ImageWriter.java | 323 + src/ij/io/ImportDialog.java | 362 + src/ij/io/LogStream.java | 203 + src/ij/io/OpenDialog.java | 266 + src/ij/io/Opener.java | 1388 +++ src/ij/io/PluginClassLoader.java | 105 + src/ij/io/RandomAccessStream.java | 192 + src/ij/io/RoiDecoder.java | 602 ++ src/ij/io/RoiEncoder.java | 469 + src/ij/io/SaveDialog.java | 265 + src/ij/io/TextEncoder.java | 54 + src/ij/io/TiffDecoder.java | 876 ++ src/ij/io/TiffEncoder.java | 555 ++ src/ij/macro/Debugger.java | 10 + src/ij/macro/ExtensionDescriptor.java | 298 + src/ij/macro/FunctionFinder.java | 232 + src/ij/macro/Functions.java | 8233 +++++++++++++++++ src/ij/macro/Interpreter.java | 2553 +++++ src/ij/macro/MacroConstants.java | 143 + src/ij/macro/MacroException.java | 15 + src/ij/macro/MacroExtension.java | 14 + src/ij/macro/MacroRunner.java | 174 + src/ij/macro/Program.java | 286 + src/ij/macro/ReturnException.java | 12 + src/ij/macro/StartupRunner.java | 32 + src/ij/macro/Symbol.java | 37 + src/ij/macro/Tokenizer.java | 293 + src/ij/macro/Variable.java | 147 + src/ij/measure/Calibration.java | 530 ++ src/ij/measure/CurveFitter.java | 1462 +++ src/ij/measure/Measurements.java | 21 + src/ij/measure/Minimizer.java | 849 ++ src/ij/measure/ResultsTable.java | 1637 ++++ src/ij/measure/ResultsTableMacros.java | 214 + src/ij/measure/SplineFitter.java | 159 + src/ij/measure/UserFunction.java | 23 + src/ij/plugin/AVI_Reader.java | 1612 ++++ src/ij/plugin/AboutBox.java | 76 + src/ij/plugin/Animator.java | 398 + src/ij/plugin/AppearanceOptions.java | 189 + src/ij/plugin/ArrowToolOptions.java | 84 + src/ij/plugin/BMP_Reader.java | 328 + src/ij/plugin/BMP_Writer.java | 247 + src/ij/plugin/BatchConverter.java | 154 + src/ij/plugin/BatchMeasure.java | 32 + src/ij/plugin/BatchProcessor.java | 480 + src/ij/plugin/Benchmark.java | 127 + src/ij/plugin/Binner.java | 286 + src/ij/plugin/BrowserLauncher.java | 128 + src/ij/plugin/CalibrationBar.java | 511 + src/ij/plugin/CanvasResizer.java | 131 + src/ij/plugin/ChannelArranger.java | 352 + src/ij/plugin/ChannelSplitter.java | 152 + src/ij/plugin/CircularRoiMaker.java | 83 + src/ij/plugin/ClassChecker.java | 107 + src/ij/plugin/Clipboard.java | 198 + src/ij/plugin/Colors.java | 276 + src/ij/plugin/CommandFinder.java | 705 ++ src/ij/plugin/CommandLister.java | 85 + src/ij/plugin/Commands.java | 174 + src/ij/plugin/Compiler.java | 451 + src/ij/plugin/CompositeConverter.java | 111 + src/ij/plugin/Concatenator.java | 493 + src/ij/plugin/ContrastEnhancer.java | 369 + src/ij/plugin/ControlPanel.java | 1092 +++ src/ij/plugin/Converter.java | 187 + src/ij/plugin/Coordinates.java | 187 + src/ij/plugin/DICOM.java | 1727 ++++ src/ij/plugin/Distribution.java | 175 + src/ij/plugin/DragAndDrop.java | 220 + src/ij/plugin/Duplicator.java | 705 ++ src/ij/plugin/EventListener.java | 94 + src/ij/plugin/FFT.java | 614 ++ src/ij/plugin/FFTMath.java | 163 + src/ij/plugin/FITS_Reader.java | 191 + src/ij/plugin/FITS_Writer.java | 353 + src/ij/plugin/FileInfoVirtualStack.java | 288 + src/ij/plugin/Filters3D.java | 148 + src/ij/plugin/FolderOpener.java | 787 ++ src/ij/plugin/GIF_Reader.java | 741 ++ src/ij/plugin/GaussianBlur3D.java | 119 + src/ij/plugin/GelAnalyzer.java | 649 ++ src/ij/plugin/GifWriter.java | 2058 ++++ src/ij/plugin/Grid.java | 288 + src/ij/plugin/GroupedZProjector.java | 87 + src/ij/plugin/Histogram.java | 186 + src/ij/plugin/Hotkeys.java | 198 + src/ij/plugin/HyperStackConverter.java | 325 + src/ij/plugin/HyperStackMaker.java | 166 + src/ij/plugin/HyperStackReducer.java | 192 + src/ij/plugin/ImageCalculator.java | 349 + src/ij/plugin/ImageInfo.java | 526 ++ src/ij/plugin/ImageJ_Updater.java | 192 + src/ij/plugin/ImagesToStack.java | 295 + src/ij/plugin/JavaProperties.java | 190 + src/ij/plugin/JavaScriptEvaluator.java | 89 + src/ij/plugin/JpegWriter.java | 162 + src/ij/plugin/LUT_Editor.java | 481 + src/ij/plugin/ListVirtualStack.java | 201 + src/ij/plugin/LutLoader.java | 446 + src/ij/plugin/MacroInstaller.java | 551 ++ src/ij/plugin/Macro_Runner.java | 353 + src/ij/plugin/MeasurementsWriter.java | 50 + src/ij/plugin/Memory.java | 164 + src/ij/plugin/MontageMaker.java | 282 + src/ij/plugin/NewPlugin.java | 187 + src/ij/plugin/NextImageOpener.java | 164 + src/ij/plugin/Options.java | 243 + src/ij/plugin/Orthogonal_Views.java | 1002 ++ src/ij/plugin/OverlayCommands.java | 465 + src/ij/plugin/OverlayLabels.java | 120 + src/ij/plugin/PGM_Reader.java | 320 + src/ij/plugin/PNG_Writer.java | 104 + src/ij/plugin/PNM_Writer.java | 107 + src/ij/plugin/PlugIn.java | 13 + src/ij/plugin/PlugInInterpreter.java | 25 + src/ij/plugin/PluginInstaller.java | 246 + src/ij/plugin/PointToolOptions.java | 226 + src/ij/plugin/Profiler.java | 112 + src/ij/plugin/Projector.java | 871 ++ src/ij/plugin/ProxySettings.java | 81 + src/ij/plugin/RGBStackConverter.java | 288 + src/ij/plugin/RGBStackMerge.java | 453 + src/ij/plugin/RandomOvals.txt | 15 + src/ij/plugin/Raw.java | 67 + src/ij/plugin/RectToolOptions.java | 85 + src/ij/plugin/Resizer.java | 404 + src/ij/plugin/RoiEnlarger.java | 277 + src/ij/plugin/RoiInterpolator.java | 105 + src/ij/plugin/RoiReader.java | 47 + src/ij/plugin/RoiRotator.java | 128 + src/ij/plugin/RoiScaler.java | 168 + src/ij/plugin/ScaleBar.java | 760 ++ src/ij/plugin/Scaler.java | 452 + src/ij/plugin/ScreenGrabber.java | 90 + src/ij/plugin/Selection.java | 970 ++ src/ij/plugin/SimpleCommands.java | 274 + src/ij/plugin/Slicer.java | 753 ++ src/ij/plugin/SpecifyROI.java | 227 + src/ij/plugin/StackCombiner.java | 159 + src/ij/plugin/StackEditor.java | 331 + src/ij/plugin/StackInserter.java | 78 + src/ij/plugin/StackMaker.java | 80 + src/ij/plugin/StackPlotter.java | 93 + src/ij/plugin/StackReducer.java | 99 + src/ij/plugin/StackReverser.java | 45 + src/ij/plugin/StackWriter.java | 264 + src/ij/plugin/Stack_Statistics.java | 54 + src/ij/plugin/Startup.java | 110 + src/ij/plugin/Straightener.java | 230 + src/ij/plugin/SubHyperstackMaker.java | 184 + src/ij/plugin/SubstackMaker.java | 229 + src/ij/plugin/SurfacePlotter.java | 468 + src/ij/plugin/Text.java | 120 + src/ij/plugin/TextFileReader.java | 18 + src/ij/plugin/TextReader.java | 168 + src/ij/plugin/TextWriter.java | 34 + src/ij/plugin/ThreadLister.java | 93 + src/ij/plugin/Thresholder.java | 439 + src/ij/plugin/URLOpener.java | 198 + src/ij/plugin/WandToolOptions.java | 79 + src/ij/plugin/WindowOrganizer.java | 184 + src/ij/plugin/XYCoordinates.java | 174 + src/ij/plugin/XY_Reader.java | 58 + src/ij/plugin/ZAxisProfiler.java | 261 + src/ij/plugin/ZProjector.java | 823 ++ src/ij/plugin/Zoom.java | 277 + src/ij/plugin/filter/AVI_Writer.java | 631 ++ src/ij/plugin/filter/Analyzer.java | 1070 +++ .../plugin/filter/BackgroundSubtracter.java | 829 ++ src/ij/plugin/filter/Benchmark.java | 105 + src/ij/plugin/filter/Binary.java | 191 + src/ij/plugin/filter/Calibrator.java | 486 + src/ij/plugin/filter/Convolver.java | 458 + src/ij/plugin/filter/Duplicater.java | 33 + src/ij/plugin/filter/EDM.java | 452 + .../plugin/filter/ExtendedPlugInFilter.java | 73 + src/ij/plugin/filter/FFTCustomFilter.java | 182 + src/ij/plugin/filter/FFTFilter.java | 442 + src/ij/plugin/filter/Filler.java | 257 + src/ij/plugin/filter/Filters.java | 99 + src/ij/plugin/filter/FractalBoxCounter.java | 241 + src/ij/plugin/filter/GaussianBlur.java | 609 ++ src/ij/plugin/filter/ImageMath.java | 521 ++ src/ij/plugin/filter/ImageProperties.java | 352 + src/ij/plugin/filter/Info.java | 26 + src/ij/plugin/filter/LineGraphAnalyzer.java | 72 + src/ij/plugin/filter/LutApplier.java | 130 + src/ij/plugin/filter/LutViewer.java | 162 + src/ij/plugin/filter/MaximumFinder.java | 1388 +++ src/ij/plugin/filter/ParticleAnalyzer.java | 1214 +++ src/ij/plugin/filter/PlugInFilter.java | 93 + src/ij/plugin/filter/PlugInFilterRunner.java | 634 ++ src/ij/plugin/filter/Printer.java | 154 + src/ij/plugin/filter/RGBStackSplitter.java | 37 + src/ij/plugin/filter/RankFilters.java | 947 ++ src/ij/plugin/filter/RoiWriter.java | 54 + src/ij/plugin/filter/Rotator.java | 202 + src/ij/plugin/filter/SaltAndPepper.java | 44 + src/ij/plugin/filter/ScaleDialog.java | 191 + src/ij/plugin/filter/Shadows.java | 99 + src/ij/plugin/filter/StackLabeler.java | 306 + .../plugin/filter/ThresholdToSelection.java | 427 + src/ij/plugin/filter/Transformer.java | 92 + src/ij/plugin/filter/Translator.java | 83 + src/ij/plugin/filter/UnsharpMask.java | 99 + src/ij/plugin/filter/Writer.java | 47 + src/ij/plugin/filter/XYWriter.java | 65 + src/ij/plugin/frame/Channels.java | 244 + src/ij/plugin/frame/ColorPicker.java | 414 + src/ij/plugin/frame/ColorThresholder.java | 1568 ++++ src/ij/plugin/frame/Commands.java | 192 + src/ij/plugin/frame/ContrastAdjuster.java | 1363 +++ src/ij/plugin/frame/Editor.java | 1773 ++++ src/ij/plugin/frame/Fitter.java | 328 + src/ij/plugin/frame/LineWidthAdjuster.java | 204 + src/ij/plugin/frame/MemoryMonitor.java | 134 + src/ij/plugin/frame/PasteController.java | 85 + src/ij/plugin/frame/PlugInDialog.java | 61 + src/ij/plugin/frame/PlugInFrame.java | 63 + src/ij/plugin/frame/Recorder.java | 908 ++ src/ij/plugin/frame/RoiManager.java | 2852 ++++++ src/ij/plugin/frame/SyncWindows.java | 1197 +++ src/ij/plugin/frame/ThresholdAdjuster.java | 1216 +++ src/ij/plugin/tool/ArrowTool.java | 69 + src/ij/plugin/tool/BrushTool.java | 324 + src/ij/plugin/tool/MacroToolRunner.java | 38 + src/ij/plugin/tool/OverlayBrushTool.java | 255 + src/ij/plugin/tool/PixelInspectionTool.java | 549 ++ src/ij/plugin/tool/PlugInTool.java | 56 + src/ij/plugin/tool/RoiRotationTool.java | 146 + src/ij/process/AutoThresholder.java | 1246 +++ src/ij/process/BinaryInterpolator.java | 240 + src/ij/process/BinaryProcessor.java | 256 + src/ij/process/Blitter.java | 60 + src/ij/process/ByteBlitter.java | 169 + src/ij/process/ByteProcessor.java | 1302 +++ src/ij/process/ByteStatistics.java | 177 + src/ij/process/ColorBlitter.java | 139 + src/ij/process/ColorProcessor.java | 1459 +++ src/ij/process/ColorSpaceConverter.java | 484 + src/ij/process/ColorStatistics.java | 144 + src/ij/process/DownsizeTable.java | 130 + src/ij/process/EllipseFitter.java | 346 + src/ij/process/FHT.java | 662 ++ src/ij/process/FloatBlitter.java | 141 + src/ij/process/FloatPolygon.java | 237 + src/ij/process/FloatProcessor.java | 1154 +++ src/ij/process/FloatStatistics.java | 264 + src/ij/process/FloodFiller.java | 222 + src/ij/process/ImageConverter.java | 303 + src/ij/process/ImageProcessor.java | 2835 ++++++ src/ij/process/ImageStatistics.java | 329 + src/ij/process/IntProcessor.java | 165 + src/ij/process/LUT.java | 111 + src/ij/process/MedianCut.java | 404 + src/ij/process/PolygonFiller.java | 291 + src/ij/process/ShortBlitter.java | 145 + src/ij/process/ShortProcessor.java | 1270 +++ src/ij/process/ShortStatistics.java | 224 + src/ij/process/StackConverter.java | 313 + src/ij/process/StackProcessor.java | 369 + src/ij/process/StackStatistics.java | 359 + src/ij/process/TypeConverter.java | 255 + src/ij/text/TableListener.java | 11 + src/ij/text/TextCanvas.java | 197 + src/ij/text/TextPanel.java | 1129 +++ src/ij/text/TextWindow.java | 368 + src/ij/util/ArrayUtil.java | 164 + src/ij/util/DicomTools.java | 169 + src/ij/util/FloatArray.java | 99 + src/ij/util/FontUtil.java | 56 + src/ij/util/IJMath.java | 34 + src/ij/util/Java2.java | 55 + src/ij/util/StringSorter.java | 110 + src/ij/util/ThreadUtil.java | 149 + src/ij/util/Tools.java | 465 + src/ij/util/WildcardMatch.java | 201 + 849 files changed, 157272 insertions(+) create mode 100644 .classpath create mode 100644 .project create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 IJ_Props.txt create mode 100644 bin/ij/CommandListener.class create mode 100644 bin/ij/CompositeImage.class create mode 100644 bin/ij/Executer.class create mode 100644 bin/ij/IJ$ExceptionHandler.class create mode 100644 bin/ij/IJ.class create mode 100644 bin/ij/IJEventListener.class create mode 100644 bin/ij/ImageJ$ExceptionHandler.class create mode 100644 bin/ij/ImageJ.class create mode 100644 bin/ij/ImageJApplet.class create mode 100644 bin/ij/ImageListener.class create mode 100644 bin/ij/ImagePlus$1.class create mode 100644 bin/ij/ImagePlus.class create mode 100644 bin/ij/ImageStack.class create mode 100644 bin/ij/LookUpTable.class create mode 100644 bin/ij/Macro.class create mode 100644 bin/ij/Menus.class create mode 100644 bin/ij/OtherInstance$ImageJInstance.class create mode 100644 bin/ij/OtherInstance$Implementation.class create mode 100644 bin/ij/OtherInstance.class create mode 100644 bin/ij/Prefs.class create mode 100644 bin/ij/RecentOpener.class create mode 100644 bin/ij/Undo.class create mode 100644 bin/ij/VirtualStack.class create mode 100644 bin/ij/WindowManager.class create mode 100644 bin/ij/gui/Arrow.class create mode 100644 bin/ij/gui/ColorChooser.class create mode 100644 bin/ij/gui/ColorPanel.class create mode 100644 bin/ij/gui/DialogListener.class create mode 100644 bin/ij/gui/EllipseRoi.class create mode 100644 bin/ij/gui/FreehandRoi.class create mode 100644 bin/ij/gui/GUI.class create mode 100644 bin/ij/gui/GenericDialog$1.class create mode 100644 bin/ij/gui/GenericDialog$BrowseButtonListener.class create mode 100644 bin/ij/gui/GenericDialog$TextDropTarget.class create mode 100644 bin/ij/gui/GenericDialog.class create mode 100644 bin/ij/gui/HTMLDialog$1.class create mode 100644 bin/ij/gui/HTMLDialog$2.class create mode 100644 bin/ij/gui/HTMLDialog.class create mode 100644 bin/ij/gui/HistogramPlot.class create mode 100644 bin/ij/gui/HistogramWindow.class create mode 100644 bin/ij/gui/ImageCanvas$1.class create mode 100644 bin/ij/gui/ImageCanvas.class create mode 100644 bin/ij/gui/ImageLayout.class create mode 100644 bin/ij/gui/ImagePanel.class create mode 100644 bin/ij/gui/ImageRoi.class create mode 100644 bin/ij/gui/ImageWindow.class create mode 100644 bin/ij/gui/Line$PointIterator.class create mode 100644 bin/ij/gui/Line.class create mode 100644 bin/ij/gui/MessageDialog.class create mode 100644 bin/ij/gui/MultiLineLabel.class create mode 100644 bin/ij/gui/NewImage.class create mode 100644 bin/ij/gui/NonBlockingGenericDialog$1.class create mode 100644 bin/ij/gui/NonBlockingGenericDialog$2.class create mode 100644 bin/ij/gui/NonBlockingGenericDialog.class create mode 100644 bin/ij/gui/OvalRoi.class create mode 100644 bin/ij/gui/Overlay$1.class create mode 100644 bin/ij/gui/Overlay.class create mode 100644 bin/ij/gui/Plot.class create mode 100644 bin/ij/gui/PlotCanvas.class create mode 100644 bin/ij/gui/PlotContentsDialog.class create mode 100644 bin/ij/gui/PlotDialog$1.class create mode 100644 bin/ij/gui/PlotDialog.class create mode 100644 bin/ij/gui/PlotMaker.class create mode 100644 bin/ij/gui/PlotObject.class create mode 100644 bin/ij/gui/PlotProperties.class create mode 100644 bin/ij/gui/PlotVirtualStack.class create mode 100644 bin/ij/gui/PlotWindow$1.class create mode 100644 bin/ij/gui/PlotWindow.class create mode 100644 bin/ij/gui/PointRoi$1.class create mode 100644 bin/ij/gui/PointRoi.class create mode 100644 bin/ij/gui/PolygonRoi.class create mode 100644 bin/ij/gui/ProfilePlot.class create mode 100644 bin/ij/gui/ProgressBar.class create mode 100644 bin/ij/gui/Roi$RoiPointsIteratorMask.class create mode 100644 bin/ij/gui/Roi.class create mode 100644 bin/ij/gui/RoiBrush.class create mode 100644 bin/ij/gui/RoiDefaultsDialog.class create mode 100644 bin/ij/gui/RoiListener.class create mode 100644 bin/ij/gui/RoiProperties.class create mode 100644 bin/ij/gui/RotatedRectRoi.class create mode 100644 bin/ij/gui/SaveChangesDialog.class create mode 100644 bin/ij/gui/ScrollbarWithLabel$Icon.class create mode 100644 bin/ij/gui/ScrollbarWithLabel.class create mode 100644 bin/ij/gui/ShapeRoi.class create mode 100644 bin/ij/gui/StackWindow$1.class create mode 100644 bin/ij/gui/StackWindow$2.class create mode 100644 bin/ij/gui/StackWindow.class create mode 100644 bin/ij/gui/TextRoi.class create mode 100644 bin/ij/gui/Toolbar$1.class create mode 100644 bin/ij/gui/Toolbar.class create mode 100644 bin/ij/gui/TrimmedButton.class create mode 100644 bin/ij/gui/WaitForUserDialog.class create mode 100644 bin/ij/gui/Wand.class create mode 100644 bin/ij/gui/YesNoCancelDialog.class create mode 100644 bin/ij/io/BitBuffer.class create mode 100644 bin/ij/io/ByteVector.class create mode 100644 bin/ij/io/DirectoryChooser$1.class create mode 100644 bin/ij/io/DirectoryChooser.class create mode 100644 bin/ij/io/DragAndDropHandler.class create mode 100644 bin/ij/io/FileInfo.class create mode 100644 bin/ij/io/FileOpener.class create mode 100644 bin/ij/io/FileSaver.class create mode 100644 bin/ij/io/ImageReader.class create mode 100644 bin/ij/io/ImageWriter.class create mode 100644 bin/ij/io/ImportDialog.class create mode 100644 bin/ij/io/LogStream.class create mode 100644 bin/ij/io/OpenDialog$1.class create mode 100644 bin/ij/io/OpenDialog.class create mode 100644 bin/ij/io/Opener$1.class create mode 100644 bin/ij/io/Opener.class create mode 100644 bin/ij/io/PluginClassLoader.class create mode 100644 bin/ij/io/RandomAccessStream.class create mode 100644 bin/ij/io/RoiDecoder.class create mode 100644 bin/ij/io/RoiEncoder.class create mode 100644 bin/ij/io/SaveDialog$1.class create mode 100644 bin/ij/io/SaveDialog.class create mode 100644 bin/ij/io/TextEncoder.class create mode 100644 bin/ij/io/TiffDecoder.class create mode 100644 bin/ij/io/TiffEncoder.class create mode 100644 bin/ij/macro/Debugger.class create mode 100644 bin/ij/macro/ExtensionDescriptor.class create mode 100644 bin/ij/macro/FunctionFinder.class create mode 100644 bin/ij/macro/Functions.class create mode 100644 bin/ij/macro/Interpreter.class create mode 100644 bin/ij/macro/MacroConstants.class create mode 100644 bin/ij/macro/MacroException.class create mode 100644 bin/ij/macro/MacroExtension.class create mode 100644 bin/ij/macro/MacroRunner.class create mode 100644 bin/ij/macro/Program.class create mode 100644 bin/ij/macro/ReturnException.class create mode 100644 bin/ij/macro/StartupRunner.class create mode 100644 bin/ij/macro/Symbol.class create mode 100644 bin/ij/macro/Tokenizer.class create mode 100644 bin/ij/macro/Variable.class create mode 100644 bin/ij/measure/Calibration.class create mode 100644 bin/ij/measure/CurveFitter.class create mode 100644 bin/ij/measure/Measurements.class create mode 100644 bin/ij/measure/Minimizer$1.class create mode 100644 bin/ij/measure/Minimizer.class create mode 100644 bin/ij/measure/ResultsTable$ComparableEntry.class create mode 100644 bin/ij/measure/ResultsTable.class create mode 100644 bin/ij/measure/ResultsTableMacros$1.class create mode 100644 bin/ij/measure/ResultsTableMacros.class create mode 100644 bin/ij/measure/SplineFitter.class create mode 100644 bin/ij/measure/UserFunction.class create mode 100644 bin/ij/plugin/AVI_Reader$raInputStream.class create mode 100644 bin/ij/plugin/AVI_Reader.class create mode 100644 bin/ij/plugin/AboutBox.class create mode 100644 bin/ij/plugin/AnimatedGifEncoder2.class create mode 100644 bin/ij/plugin/Animator.class create mode 100644 bin/ij/plugin/AppearanceOptions.class create mode 100644 bin/ij/plugin/ArrowToolOptions.class create mode 100644 bin/ij/plugin/BMPDecoder.class create mode 100644 bin/ij/plugin/BMP_Reader.class create mode 100644 bin/ij/plugin/BMP_Writer.class create mode 100644 bin/ij/plugin/BatchConverter.class create mode 100644 bin/ij/plugin/BatchMeasure.class create mode 100644 bin/ij/plugin/BatchProcessor.class create mode 100644 bin/ij/plugin/Benchmark.class create mode 100644 bin/ij/plugin/Binner.class create mode 100644 bin/ij/plugin/BrowserLauncher.class create mode 100644 bin/ij/plugin/CalibrationBar$LiveDialog.class create mode 100644 bin/ij/plugin/CalibrationBar.class create mode 100644 bin/ij/plugin/CanvasResizer.class create mode 100644 bin/ij/plugin/ChannelArranger.class create mode 100644 bin/ij/plugin/ChannelSplitter.class create mode 100644 bin/ij/plugin/CircularRoiMaker.class create mode 100644 bin/ij/plugin/ClassChecker.class create mode 100644 bin/ij/plugin/Clipboard.class create mode 100644 bin/ij/plugin/ColorPanel.class create mode 100644 bin/ij/plugin/Colors.class create mode 100644 bin/ij/plugin/CommandFinder$1.class create mode 100644 bin/ij/plugin/CommandFinder$2.class create mode 100644 bin/ij/plugin/CommandFinder$CommandAction.class create mode 100644 bin/ij/plugin/CommandFinder$PromptDocumentListener.class create mode 100644 bin/ij/plugin/CommandFinder$TableModel.class create mode 100644 bin/ij/plugin/CommandFinder.class create mode 100644 bin/ij/plugin/CommandLister.class create mode 100644 bin/ij/plugin/Commands.class create mode 100644 bin/ij/plugin/Compiler.class create mode 100644 bin/ij/plugin/CompilerTool$JavaxCompilerTool.class create mode 100644 bin/ij/plugin/CompilerTool$LegacyCompilerTool.class create mode 100644 bin/ij/plugin/CompilerTool.class create mode 100644 bin/ij/plugin/CompositeConverter.class create mode 100644 bin/ij/plugin/Concatenator.class create mode 100644 bin/ij/plugin/ContrastEnhancer.class create mode 100644 bin/ij/plugin/ControlPanel.class create mode 100644 bin/ij/plugin/Converter.class create mode 100644 bin/ij/plugin/Coordinates.class create mode 100644 bin/ij/plugin/DICOM.class create mode 100644 bin/ij/plugin/DicomDecoder.class create mode 100644 bin/ij/plugin/DicomDictionary.class create mode 100644 bin/ij/plugin/Distribution.class create mode 100644 bin/ij/plugin/DragAndDrop.class create mode 100644 bin/ij/plugin/Duplicator.class create mode 100644 bin/ij/plugin/EventListener.class create mode 100644 bin/ij/plugin/FFT.class create mode 100644 bin/ij/plugin/FFTMath.class create mode 100644 bin/ij/plugin/FITS_Reader.class create mode 100644 bin/ij/plugin/FITS_Writer.class create mode 100644 bin/ij/plugin/FileInfoVirtualStack.class create mode 100644 bin/ij/plugin/Filters3D$1.class create mode 100644 bin/ij/plugin/Filters3D.class create mode 100644 bin/ij/plugin/FitsDecoder.class create mode 100644 bin/ij/plugin/FolderOpener.class create mode 100644 bin/ij/plugin/GIF_Reader.class create mode 100644 bin/ij/plugin/GaussianBlur3D.class create mode 100644 bin/ij/plugin/GelAnalyzer.class create mode 100644 bin/ij/plugin/GifDecoder.class create mode 100644 bin/ij/plugin/GifFrame.class create mode 100644 bin/ij/plugin/GifWriter.class create mode 100644 bin/ij/plugin/Grid.class create mode 100644 bin/ij/plugin/GroupedZProjector.class create mode 100644 bin/ij/plugin/Histogram.class create mode 100644 bin/ij/plugin/Hotkeys.class create mode 100644 bin/ij/plugin/HyperStackConverter.class create mode 100644 bin/ij/plugin/HyperStackMaker.class create mode 100644 bin/ij/plugin/HyperStackReducer.class create mode 100644 bin/ij/plugin/ImageCalculator.class create mode 100644 bin/ij/plugin/ImageInfo.class create mode 100644 bin/ij/plugin/ImageJ_Updater.class create mode 100644 bin/ij/plugin/ImagesToStack.class create mode 100644 bin/ij/plugin/JavaProperties.class create mode 100644 bin/ij/plugin/JavaScriptEvaluator.class create mode 100644 bin/ij/plugin/JpegWriter.class create mode 100644 bin/ij/plugin/LUT_Editor.class create mode 100644 bin/ij/plugin/LZWEncoder.class create mode 100644 bin/ij/plugin/LZWEncoder2.class create mode 100644 bin/ij/plugin/ListVirtualStack.class create mode 100644 bin/ij/plugin/LutLoader.class create mode 100644 bin/ij/plugin/MacroInstaller.class create mode 100644 bin/ij/plugin/Macro_Runner.class create mode 100644 bin/ij/plugin/MeasurementsWriter.class create mode 100644 bin/ij/plugin/Memory.class create mode 100644 bin/ij/plugin/MontageMaker.class create mode 100644 bin/ij/plugin/NeuQuant.class create mode 100644 bin/ij/plugin/NewPlugin.class create mode 100644 bin/ij/plugin/NextImageOpener.class create mode 100644 bin/ij/plugin/Options.class create mode 100644 bin/ij/plugin/Orthogonal_Views.class create mode 100644 bin/ij/plugin/OverlayCommands.class create mode 100644 bin/ij/plugin/OverlayLabels.class create mode 100644 bin/ij/plugin/PGM_Reader.class create mode 100644 bin/ij/plugin/PNG_Writer.class create mode 100644 bin/ij/plugin/PNM_Writer.class create mode 100644 bin/ij/plugin/Plots.class create mode 100644 bin/ij/plugin/PlotsCanvas.class create mode 100644 bin/ij/plugin/PlugIn.class create mode 100644 bin/ij/plugin/PlugInExecuter.class create mode 100644 bin/ij/plugin/PlugInInterpreter.class create mode 100644 bin/ij/plugin/PluginInstaller.class create mode 100644 bin/ij/plugin/PointToolOptions.class create mode 100644 bin/ij/plugin/Profiler.class create mode 100644 bin/ij/plugin/Projector.class create mode 100644 bin/ij/plugin/ProxySettings.class create mode 100644 bin/ij/plugin/RGBStackConverter.class create mode 100644 bin/ij/plugin/RGBStackMerge.class create mode 100644 bin/ij/plugin/RandomOvals.txt create mode 100644 bin/ij/plugin/Raw.class create mode 100644 bin/ij/plugin/RectToolOptions.class create mode 100644 bin/ij/plugin/Resizer.class create mode 100644 bin/ij/plugin/RoiEnlarger.class create mode 100644 bin/ij/plugin/RoiInterpolator.class create mode 100644 bin/ij/plugin/RoiReader.class create mode 100644 bin/ij/plugin/RoiRotator.class create mode 100644 bin/ij/plugin/RoiScaler.class create mode 100644 bin/ij/plugin/ScaleBar$BarDialog.class create mode 100644 bin/ij/plugin/ScaleBar$BarDialogListener.class create mode 100644 bin/ij/plugin/ScaleBar$MissingRoiException.class create mode 100644 bin/ij/plugin/ScaleBar$ScaleBarConfiguration.class create mode 100644 bin/ij/plugin/ScaleBar.class create mode 100644 bin/ij/plugin/Scaler.class create mode 100644 bin/ij/plugin/ScreenGrabber.class create mode 100644 bin/ij/plugin/Selection.class create mode 100644 bin/ij/plugin/SimpleCommands$1.class create mode 100644 bin/ij/plugin/SimpleCommands.class create mode 100644 bin/ij/plugin/Slicer.class create mode 100644 bin/ij/plugin/SpecifyROI.class create mode 100644 bin/ij/plugin/StackCombiner.class create mode 100644 bin/ij/plugin/StackEditor.class create mode 100644 bin/ij/plugin/StackInserter.class create mode 100644 bin/ij/plugin/StackMaker.class create mode 100644 bin/ij/plugin/StackPlotter.class create mode 100644 bin/ij/plugin/StackReducer.class create mode 100644 bin/ij/plugin/StackReverser.class create mode 100644 bin/ij/plugin/StackWriter.class create mode 100644 bin/ij/plugin/Stack_Statistics.class create mode 100644 bin/ij/plugin/Startup.class create mode 100644 bin/ij/plugin/Straightener.class create mode 100644 bin/ij/plugin/SubHyperstackMaker.class create mode 100644 bin/ij/plugin/SubstackMaker.class create mode 100644 bin/ij/plugin/SurfacePlotter.class create mode 100644 bin/ij/plugin/Text.class create mode 100644 bin/ij/plugin/TextFileReader.class create mode 100644 bin/ij/plugin/TextReader.class create mode 100644 bin/ij/plugin/TextWriter.class create mode 100644 bin/ij/plugin/ThreadLister$1.class create mode 100644 bin/ij/plugin/ThreadLister.class create mode 100644 bin/ij/plugin/Thresholder.class create mode 100644 bin/ij/plugin/ThumbnailsCanvas.class create mode 100644 bin/ij/plugin/TreePanel$1.class create mode 100644 bin/ij/plugin/TreePanel$2.class create mode 100644 bin/ij/plugin/TreePanel$3.class create mode 100644 bin/ij/plugin/TreePanel.class create mode 100644 bin/ij/plugin/URLOpener.class create mode 100644 bin/ij/plugin/WandToolOptions.class create mode 100644 bin/ij/plugin/WindowOrganizer.class create mode 100644 bin/ij/plugin/XYCoordinates.class create mode 100644 bin/ij/plugin/XY_Reader.class create mode 100644 bin/ij/plugin/ZAxisProfiler.class create mode 100644 bin/ij/plugin/ZProjector$AverageIntensity.class create mode 100644 bin/ij/plugin/ZProjector$MaxIntensity.class create mode 100644 bin/ij/plugin/ZProjector$MinIntensity.class create mode 100644 bin/ij/plugin/ZProjector$RayFunction.class create mode 100644 bin/ij/plugin/ZProjector$StandardDeviation.class create mode 100644 bin/ij/plugin/ZProjector.class create mode 100644 bin/ij/plugin/Zoom.class create mode 100644 bin/ij/plugin/filter/AVI_Writer$RaOutputStream.class create mode 100644 bin/ij/plugin/filter/AVI_Writer.class create mode 100644 bin/ij/plugin/filter/Analyzer.class create mode 100644 bin/ij/plugin/filter/BackgroundSubtracter.class create mode 100644 bin/ij/plugin/filter/Benchmark.class create mode 100644 bin/ij/plugin/filter/Binary.class create mode 100644 bin/ij/plugin/filter/Calibrator.class create mode 100644 bin/ij/plugin/filter/Convolver.class create mode 100644 bin/ij/plugin/filter/Duplicater.class create mode 100644 bin/ij/plugin/filter/EDM.class create mode 100644 bin/ij/plugin/filter/ExtendedPlugInFilter.class create mode 100644 bin/ij/plugin/filter/FFTCustomFilter.class create mode 100644 bin/ij/plugin/filter/FFTFilter.class create mode 100644 bin/ij/plugin/filter/Filler.class create mode 100644 bin/ij/plugin/filter/Filters.class create mode 100644 bin/ij/plugin/filter/FractalBoxCounter.class create mode 100644 bin/ij/plugin/filter/GaussianBlur$1.class create mode 100644 bin/ij/plugin/filter/GaussianBlur.class create mode 100644 bin/ij/plugin/filter/ImageMath.class create mode 100644 bin/ij/plugin/filter/ImageProperties.class create mode 100644 bin/ij/plugin/filter/Info.class create mode 100644 bin/ij/plugin/filter/LineGraphAnalyzer.class create mode 100644 bin/ij/plugin/filter/LutApplier.class create mode 100644 bin/ij/plugin/filter/LutViewer.class create mode 100644 bin/ij/plugin/filter/LutWindow.class create mode 100644 bin/ij/plugin/filter/MaximumFinder.class create mode 100644 bin/ij/plugin/filter/ParticleAnalyzer.class create mode 100644 bin/ij/plugin/filter/PlugInFilter.class create mode 100644 bin/ij/plugin/filter/PlugInFilterRunner.class create mode 100644 bin/ij/plugin/filter/Printer.class create mode 100644 bin/ij/plugin/filter/RGBStackSplitter.class create mode 100644 bin/ij/plugin/filter/RankFilters$1.class create mode 100644 bin/ij/plugin/filter/RankFilters.class create mode 100644 bin/ij/plugin/filter/RoiWriter.class create mode 100644 bin/ij/plugin/filter/RollingBall.class create mode 100644 bin/ij/plugin/filter/Rotator.class create mode 100644 bin/ij/plugin/filter/SaltAndPepper.class create mode 100644 bin/ij/plugin/filter/ScaleDialog.class create mode 100644 bin/ij/plugin/filter/SetScaleDialog.class create mode 100644 bin/ij/plugin/filter/Shadows.class create mode 100644 bin/ij/plugin/filter/StackLabeler.class create mode 100644 bin/ij/plugin/filter/ThresholdToSelection$Outline.class create mode 100644 bin/ij/plugin/filter/ThresholdToSelection.class create mode 100644 bin/ij/plugin/filter/Transformer.class create mode 100644 bin/ij/plugin/filter/Translator.class create mode 100644 bin/ij/plugin/filter/UnsharpMask.class create mode 100644 bin/ij/plugin/filter/Writer.class create mode 100644 bin/ij/plugin/filter/XYWriter.class create mode 100644 bin/ij/plugin/frame/Channels.class create mode 100644 bin/ij/plugin/frame/ColorCanvas.class create mode 100644 bin/ij/plugin/frame/ColorGenerator.class create mode 100644 bin/ij/plugin/frame/ColorPicker.class create mode 100644 bin/ij/plugin/frame/ColorThresholder$BandPlot.class create mode 100644 bin/ij/plugin/frame/ColorThresholder.class create mode 100644 bin/ij/plugin/frame/Commands.class create mode 100644 bin/ij/plugin/frame/ContrastAdjuster.class create mode 100644 bin/ij/plugin/frame/ContrastPlot.class create mode 100644 bin/ij/plugin/frame/DisplayChangeEvent.class create mode 100644 bin/ij/plugin/frame/DisplayChangeListener.class create mode 100644 bin/ij/plugin/frame/Editor.class create mode 100644 bin/ij/plugin/frame/Fitter$1.class create mode 100644 bin/ij/plugin/frame/Fitter.class create mode 100644 bin/ij/plugin/frame/IJEventMulticaster.class create mode 100644 bin/ij/plugin/frame/LineWidthAdjuster.class create mode 100644 bin/ij/plugin/frame/MemoryMonitor$PlotCanvas.class create mode 100644 bin/ij/plugin/frame/MemoryMonitor.class create mode 100644 bin/ij/plugin/frame/PasteController.class create mode 100644 bin/ij/plugin/frame/PlugInDialog.class create mode 100644 bin/ij/plugin/frame/PlugInFrame.class create mode 100644 bin/ij/plugin/frame/Recorder.class create mode 100644 bin/ij/plugin/frame/RoiManager$1.class create mode 100644 bin/ij/plugin/frame/RoiManager$2.class create mode 100644 bin/ij/plugin/frame/RoiManager$3.class create mode 100644 bin/ij/plugin/frame/RoiManager$4.class create mode 100644 bin/ij/plugin/frame/RoiManager$MultiMeasureRunner.class create mode 100644 bin/ij/plugin/frame/RoiManager.class create mode 100644 bin/ij/plugin/frame/SyncWindows.class create mode 100644 bin/ij/plugin/frame/ThresholdAdjuster.class create mode 100644 bin/ij/plugin/frame/ThresholdPlot.class create mode 100644 bin/ij/plugin/frame/TrimmedLabel.class create mode 100644 bin/ij/plugin/tool/ArrowTool.class create mode 100644 bin/ij/plugin/tool/BrushTool$Options.class create mode 100644 bin/ij/plugin/tool/BrushTool.class create mode 100644 bin/ij/plugin/tool/MacroToolRunner.class create mode 100644 bin/ij/plugin/tool/OverlayBrushTool$Options.class create mode 100644 bin/ij/plugin/tool/OverlayBrushTool.class create mode 100644 bin/ij/plugin/tool/PixelInspectionTool.class create mode 100644 bin/ij/plugin/tool/PixelInspector.class create mode 100644 bin/ij/plugin/tool/PlugInTool.class create mode 100644 bin/ij/plugin/tool/RoiRotationTool.class create mode 100644 bin/ij/process/AutoThresholder$Method.class create mode 100644 bin/ij/process/AutoThresholder.class create mode 100644 bin/ij/process/BinaryInterpolator$IDT.class create mode 100644 bin/ij/process/BinaryInterpolator.class create mode 100644 bin/ij/process/BinaryProcessor.class create mode 100644 bin/ij/process/Blitter.class create mode 100644 bin/ij/process/ByteBlitter.class create mode 100644 bin/ij/process/ByteProcessor.class create mode 100644 bin/ij/process/ByteStatistics.class create mode 100644 bin/ij/process/ColorBlitter.class create mode 100644 bin/ij/process/ColorProcessor.class create mode 100644 bin/ij/process/ColorSpaceConverter.class create mode 100644 bin/ij/process/ColorStatistics.class create mode 100644 bin/ij/process/Cube.class create mode 100644 bin/ij/process/DownsizeTable.class create mode 100644 bin/ij/process/EllipseFitter.class create mode 100644 bin/ij/process/FHT.class create mode 100644 bin/ij/process/FloatBlitter.class create mode 100644 bin/ij/process/FloatPolygon.class create mode 100644 bin/ij/process/FloatProcessor.class create mode 100644 bin/ij/process/FloatStatistics.class create mode 100644 bin/ij/process/FloodFiller.class create mode 100644 bin/ij/process/ImageConverter.class create mode 100644 bin/ij/process/ImageProcessor.class create mode 100644 bin/ij/process/ImageStatistics.class create mode 100644 bin/ij/process/IntProcessor.class create mode 100644 bin/ij/process/LUT.class create mode 100644 bin/ij/process/MedianCut.class create mode 100644 bin/ij/process/PolygonFiller.class create mode 100644 bin/ij/process/ShortBlitter.class create mode 100644 bin/ij/process/ShortProcessor.class create mode 100644 bin/ij/process/ShortStatistics.class create mode 100644 bin/ij/process/StackConverter.class create mode 100644 bin/ij/process/StackProcessor.class create mode 100644 bin/ij/process/StackStatistics.class create mode 100644 bin/ij/process/TypeConverter.class create mode 100644 bin/ij/text/TableListener.class create mode 100644 bin/ij/text/TextCanvas.class create mode 100644 bin/ij/text/TextPanel.class create mode 100644 bin/ij/text/TextWindow.class create mode 100644 bin/ij/util/ArrayUtil.class create mode 100644 bin/ij/util/DicomTools.class create mode 100644 bin/ij/util/FloatArray.class create mode 100644 bin/ij/util/FontUtil.class create mode 100644 bin/ij/util/IJMath.class create mode 100644 bin/ij/util/Java2.class create mode 100644 bin/ij/util/StringSorter.class create mode 100644 bin/ij/util/ThreadUtil$1.class create mode 100644 bin/ij/util/ThreadUtil.class create mode 100644 bin/ij/util/Tools$1.class create mode 100644 bin/ij/util/Tools$2.class create mode 100644 bin/ij/util/Tools.class create mode 100644 bin/ij/util/WildcardMatch.class create mode 100644 images/about.jpg create mode 100644 images/microscope.gif create mode 100644 macros/AddParticles.txt create mode 100644 macros/Circle_Tool.txt create mode 100644 macros/CommandFinderTool.txt create mode 100644 macros/ConvertStackToBinary.txt create mode 100644 macros/DeveloperMenuTool.txt create mode 100644 macros/Filter_Plugin.src create mode 100644 macros/FloodFillTool.txt create mode 100644 macros/LUTMenuTool.txt create mode 100644 macros/Label_Tool.txt create mode 100644 macros/MagicMontageTools.txt create mode 100644 macros/MeasureStack.txt create mode 100644 macros/MoveSelection.txt create mode 100644 macros/My_Plugin.src create mode 100644 macros/Overlay Editing Tools.txt create mode 100644 macros/Plugin_Frame.src create mode 100644 macros/Prototype_Tool.src create mode 100644 macros/RoiMenuTool.txt create mode 100644 macros/Search.txt create mode 100644 macros/ShowAllLuts.txt create mode 100644 macros/SmoothWandTool.txt create mode 100644 macros/SprayCanTool.txt create mode 100644 macros/StacksMenuTool.txt create mode 100644 macros/StartupMacros.txt create mode 100644 macros/TimeStamp.ijm create mode 100644 plugins/Decarburization_Measurements.jar create mode 100644 plugins/MacAdapter.class create mode 100644 plugins/MacAdapter.source create mode 100644 plugins/MacAdapter9.class create mode 100644 plugins/MacAdapter9.source create mode 100644 plugins/TESTPlugin_.jar create mode 100644 src/ij/CommandListener.java create mode 100644 src/ij/CompositeImage.java create mode 100644 src/ij/Executer.java create mode 100644 src/ij/IJ.java create mode 100644 src/ij/IJEventListener.java create mode 100644 src/ij/ImageJ.java create mode 100644 src/ij/ImageJApplet.java create mode 100644 src/ij/ImageListener.java create mode 100644 src/ij/ImagePlus.java create mode 100644 src/ij/ImageStack.java create mode 100644 src/ij/LookUpTable.java create mode 100644 src/ij/Macro.java create mode 100644 src/ij/Menus.java create mode 100644 src/ij/OtherInstance.java create mode 100644 src/ij/Prefs.java create mode 100644 src/ij/RecentOpener.java create mode 100644 src/ij/Undo.java create mode 100644 src/ij/VirtualStack.java create mode 100644 src/ij/WindowManager.java create mode 100644 src/ij/gui/Arrow.java create mode 100644 src/ij/gui/ColorChooser.java create mode 100644 src/ij/gui/DialogListener.java create mode 100644 src/ij/gui/EllipseRoi.java create mode 100644 src/ij/gui/FreehandRoi.java create mode 100644 src/ij/gui/GUI.java create mode 100644 src/ij/gui/GenericDialog.java create mode 100644 src/ij/gui/HTMLDialog.java create mode 100644 src/ij/gui/HistogramPlot.java create mode 100644 src/ij/gui/HistogramWindow.java create mode 100644 src/ij/gui/ImageCanvas.java create mode 100644 src/ij/gui/ImageLayout.java create mode 100644 src/ij/gui/ImagePanel.java create mode 100644 src/ij/gui/ImageRoi.java create mode 100644 src/ij/gui/ImageWindow.java create mode 100644 src/ij/gui/Line.java create mode 100644 src/ij/gui/MessageDialog.java create mode 100644 src/ij/gui/MultiLineLabel.java create mode 100644 src/ij/gui/NewImage.java create mode 100644 src/ij/gui/NonBlockingGenericDialog.java create mode 100644 src/ij/gui/OvalRoi.java create mode 100644 src/ij/gui/Overlay.java create mode 100644 src/ij/gui/Plot.java create mode 100644 src/ij/gui/PlotCanvas.java create mode 100644 src/ij/gui/PlotContentsDialog.java create mode 100644 src/ij/gui/PlotDialog.java create mode 100644 src/ij/gui/PlotMaker.java create mode 100644 src/ij/gui/PlotVirtualStack.java create mode 100644 src/ij/gui/PlotWindow.java create mode 100644 src/ij/gui/PointRoi.java create mode 100644 src/ij/gui/PolygonRoi.java create mode 100644 src/ij/gui/ProfilePlot.java create mode 100644 src/ij/gui/ProgressBar.java create mode 100644 src/ij/gui/Roi.java create mode 100644 src/ij/gui/RoiBrush.java create mode 100644 src/ij/gui/RoiDefaultsDialog.java create mode 100644 src/ij/gui/RoiListener.java create mode 100644 src/ij/gui/RoiProperties.java create mode 100644 src/ij/gui/RotatedRectRoi.java create mode 100644 src/ij/gui/SaveChangesDialog.java create mode 100644 src/ij/gui/ScrollbarWithLabel.java create mode 100644 src/ij/gui/ShapeRoi.java create mode 100644 src/ij/gui/StackWindow.java create mode 100644 src/ij/gui/TextRoi.java create mode 100644 src/ij/gui/Toolbar.java create mode 100644 src/ij/gui/TrimmedButton.java create mode 100644 src/ij/gui/WaitForUserDialog.java create mode 100644 src/ij/gui/Wand.java create mode 100644 src/ij/gui/YesNoCancelDialog.java create mode 100644 src/ij/io/BitBuffer.java create mode 100644 src/ij/io/DirectoryChooser.java create mode 100644 src/ij/io/DragAndDropHandler.java create mode 100644 src/ij/io/FileInfo.java create mode 100644 src/ij/io/FileOpener.java create mode 100644 src/ij/io/FileSaver.java create mode 100644 src/ij/io/ImageReader.java create mode 100644 src/ij/io/ImageWriter.java create mode 100644 src/ij/io/ImportDialog.java create mode 100644 src/ij/io/LogStream.java create mode 100644 src/ij/io/OpenDialog.java create mode 100644 src/ij/io/Opener.java create mode 100644 src/ij/io/PluginClassLoader.java create mode 100644 src/ij/io/RandomAccessStream.java create mode 100644 src/ij/io/RoiDecoder.java create mode 100644 src/ij/io/RoiEncoder.java create mode 100644 src/ij/io/SaveDialog.java create mode 100644 src/ij/io/TextEncoder.java create mode 100644 src/ij/io/TiffDecoder.java create mode 100644 src/ij/io/TiffEncoder.java create mode 100644 src/ij/macro/Debugger.java create mode 100644 src/ij/macro/ExtensionDescriptor.java create mode 100644 src/ij/macro/FunctionFinder.java create mode 100644 src/ij/macro/Functions.java create mode 100644 src/ij/macro/Interpreter.java create mode 100644 src/ij/macro/MacroConstants.java create mode 100644 src/ij/macro/MacroException.java create mode 100644 src/ij/macro/MacroExtension.java create mode 100644 src/ij/macro/MacroRunner.java create mode 100644 src/ij/macro/Program.java create mode 100644 src/ij/macro/ReturnException.java create mode 100644 src/ij/macro/StartupRunner.java create mode 100644 src/ij/macro/Symbol.java create mode 100644 src/ij/macro/Tokenizer.java create mode 100644 src/ij/macro/Variable.java create mode 100644 src/ij/measure/Calibration.java create mode 100644 src/ij/measure/CurveFitter.java create mode 100644 src/ij/measure/Measurements.java create mode 100644 src/ij/measure/Minimizer.java create mode 100644 src/ij/measure/ResultsTable.java create mode 100644 src/ij/measure/ResultsTableMacros.java create mode 100644 src/ij/measure/SplineFitter.java create mode 100644 src/ij/measure/UserFunction.java create mode 100644 src/ij/plugin/AVI_Reader.java create mode 100644 src/ij/plugin/AboutBox.java create mode 100644 src/ij/plugin/Animator.java create mode 100644 src/ij/plugin/AppearanceOptions.java create mode 100644 src/ij/plugin/ArrowToolOptions.java create mode 100644 src/ij/plugin/BMP_Reader.java create mode 100644 src/ij/plugin/BMP_Writer.java create mode 100644 src/ij/plugin/BatchConverter.java create mode 100644 src/ij/plugin/BatchMeasure.java create mode 100644 src/ij/plugin/BatchProcessor.java create mode 100644 src/ij/plugin/Benchmark.java create mode 100644 src/ij/plugin/Binner.java create mode 100644 src/ij/plugin/BrowserLauncher.java create mode 100644 src/ij/plugin/CalibrationBar.java create mode 100644 src/ij/plugin/CanvasResizer.java create mode 100644 src/ij/plugin/ChannelArranger.java create mode 100644 src/ij/plugin/ChannelSplitter.java create mode 100644 src/ij/plugin/CircularRoiMaker.java create mode 100644 src/ij/plugin/ClassChecker.java create mode 100644 src/ij/plugin/Clipboard.java create mode 100644 src/ij/plugin/Colors.java create mode 100644 src/ij/plugin/CommandFinder.java create mode 100644 src/ij/plugin/CommandLister.java create mode 100644 src/ij/plugin/Commands.java create mode 100644 src/ij/plugin/Compiler.java create mode 100644 src/ij/plugin/CompositeConverter.java create mode 100644 src/ij/plugin/Concatenator.java create mode 100644 src/ij/plugin/ContrastEnhancer.java create mode 100644 src/ij/plugin/ControlPanel.java create mode 100644 src/ij/plugin/Converter.java create mode 100644 src/ij/plugin/Coordinates.java create mode 100644 src/ij/plugin/DICOM.java create mode 100644 src/ij/plugin/Distribution.java create mode 100644 src/ij/plugin/DragAndDrop.java create mode 100644 src/ij/plugin/Duplicator.java create mode 100644 src/ij/plugin/EventListener.java create mode 100644 src/ij/plugin/FFT.java create mode 100644 src/ij/plugin/FFTMath.java create mode 100644 src/ij/plugin/FITS_Reader.java create mode 100644 src/ij/plugin/FITS_Writer.java create mode 100644 src/ij/plugin/FileInfoVirtualStack.java create mode 100644 src/ij/plugin/Filters3D.java create mode 100644 src/ij/plugin/FolderOpener.java create mode 100644 src/ij/plugin/GIF_Reader.java create mode 100644 src/ij/plugin/GaussianBlur3D.java create mode 100644 src/ij/plugin/GelAnalyzer.java create mode 100644 src/ij/plugin/GifWriter.java create mode 100644 src/ij/plugin/Grid.java create mode 100644 src/ij/plugin/GroupedZProjector.java create mode 100644 src/ij/plugin/Histogram.java create mode 100644 src/ij/plugin/Hotkeys.java create mode 100644 src/ij/plugin/HyperStackConverter.java create mode 100644 src/ij/plugin/HyperStackMaker.java create mode 100644 src/ij/plugin/HyperStackReducer.java create mode 100644 src/ij/plugin/ImageCalculator.java create mode 100644 src/ij/plugin/ImageInfo.java create mode 100644 src/ij/plugin/ImageJ_Updater.java create mode 100644 src/ij/plugin/ImagesToStack.java create mode 100644 src/ij/plugin/JavaProperties.java create mode 100644 src/ij/plugin/JavaScriptEvaluator.java create mode 100644 src/ij/plugin/JpegWriter.java create mode 100644 src/ij/plugin/LUT_Editor.java create mode 100644 src/ij/plugin/ListVirtualStack.java create mode 100644 src/ij/plugin/LutLoader.java create mode 100644 src/ij/plugin/MacroInstaller.java create mode 100644 src/ij/plugin/Macro_Runner.java create mode 100644 src/ij/plugin/MeasurementsWriter.java create mode 100644 src/ij/plugin/Memory.java create mode 100644 src/ij/plugin/MontageMaker.java create mode 100644 src/ij/plugin/NewPlugin.java create mode 100644 src/ij/plugin/NextImageOpener.java create mode 100644 src/ij/plugin/Options.java create mode 100644 src/ij/plugin/Orthogonal_Views.java create mode 100644 src/ij/plugin/OverlayCommands.java create mode 100644 src/ij/plugin/OverlayLabels.java create mode 100644 src/ij/plugin/PGM_Reader.java create mode 100644 src/ij/plugin/PNG_Writer.java create mode 100644 src/ij/plugin/PNM_Writer.java create mode 100644 src/ij/plugin/PlugIn.java create mode 100644 src/ij/plugin/PlugInInterpreter.java create mode 100644 src/ij/plugin/PluginInstaller.java create mode 100644 src/ij/plugin/PointToolOptions.java create mode 100644 src/ij/plugin/Profiler.java create mode 100644 src/ij/plugin/Projector.java create mode 100644 src/ij/plugin/ProxySettings.java create mode 100644 src/ij/plugin/RGBStackConverter.java create mode 100644 src/ij/plugin/RGBStackMerge.java create mode 100644 src/ij/plugin/RandomOvals.txt create mode 100644 src/ij/plugin/Raw.java create mode 100644 src/ij/plugin/RectToolOptions.java create mode 100644 src/ij/plugin/Resizer.java create mode 100644 src/ij/plugin/RoiEnlarger.java create mode 100644 src/ij/plugin/RoiInterpolator.java create mode 100644 src/ij/plugin/RoiReader.java create mode 100644 src/ij/plugin/RoiRotator.java create mode 100644 src/ij/plugin/RoiScaler.java create mode 100644 src/ij/plugin/ScaleBar.java create mode 100644 src/ij/plugin/Scaler.java create mode 100644 src/ij/plugin/ScreenGrabber.java create mode 100644 src/ij/plugin/Selection.java create mode 100644 src/ij/plugin/SimpleCommands.java create mode 100644 src/ij/plugin/Slicer.java create mode 100644 src/ij/plugin/SpecifyROI.java create mode 100644 src/ij/plugin/StackCombiner.java create mode 100644 src/ij/plugin/StackEditor.java create mode 100644 src/ij/plugin/StackInserter.java create mode 100644 src/ij/plugin/StackMaker.java create mode 100644 src/ij/plugin/StackPlotter.java create mode 100644 src/ij/plugin/StackReducer.java create mode 100644 src/ij/plugin/StackReverser.java create mode 100644 src/ij/plugin/StackWriter.java create mode 100644 src/ij/plugin/Stack_Statistics.java create mode 100644 src/ij/plugin/Startup.java create mode 100644 src/ij/plugin/Straightener.java create mode 100644 src/ij/plugin/SubHyperstackMaker.java create mode 100644 src/ij/plugin/SubstackMaker.java create mode 100644 src/ij/plugin/SurfacePlotter.java create mode 100644 src/ij/plugin/Text.java create mode 100644 src/ij/plugin/TextFileReader.java create mode 100644 src/ij/plugin/TextReader.java create mode 100644 src/ij/plugin/TextWriter.java create mode 100644 src/ij/plugin/ThreadLister.java create mode 100644 src/ij/plugin/Thresholder.java create mode 100644 src/ij/plugin/URLOpener.java create mode 100644 src/ij/plugin/WandToolOptions.java create mode 100644 src/ij/plugin/WindowOrganizer.java create mode 100644 src/ij/plugin/XYCoordinates.java create mode 100644 src/ij/plugin/XY_Reader.java create mode 100644 src/ij/plugin/ZAxisProfiler.java create mode 100644 src/ij/plugin/ZProjector.java create mode 100644 src/ij/plugin/Zoom.java create mode 100644 src/ij/plugin/filter/AVI_Writer.java create mode 100644 src/ij/plugin/filter/Analyzer.java create mode 100644 src/ij/plugin/filter/BackgroundSubtracter.java create mode 100644 src/ij/plugin/filter/Benchmark.java create mode 100644 src/ij/plugin/filter/Binary.java create mode 100644 src/ij/plugin/filter/Calibrator.java create mode 100644 src/ij/plugin/filter/Convolver.java create mode 100644 src/ij/plugin/filter/Duplicater.java create mode 100644 src/ij/plugin/filter/EDM.java create mode 100644 src/ij/plugin/filter/ExtendedPlugInFilter.java create mode 100644 src/ij/plugin/filter/FFTCustomFilter.java create mode 100644 src/ij/plugin/filter/FFTFilter.java create mode 100644 src/ij/plugin/filter/Filler.java create mode 100644 src/ij/plugin/filter/Filters.java create mode 100644 src/ij/plugin/filter/FractalBoxCounter.java create mode 100644 src/ij/plugin/filter/GaussianBlur.java create mode 100644 src/ij/plugin/filter/ImageMath.java create mode 100644 src/ij/plugin/filter/ImageProperties.java create mode 100644 src/ij/plugin/filter/Info.java create mode 100644 src/ij/plugin/filter/LineGraphAnalyzer.java create mode 100644 src/ij/plugin/filter/LutApplier.java create mode 100644 src/ij/plugin/filter/LutViewer.java create mode 100644 src/ij/plugin/filter/MaximumFinder.java create mode 100644 src/ij/plugin/filter/ParticleAnalyzer.java create mode 100644 src/ij/plugin/filter/PlugInFilter.java create mode 100644 src/ij/plugin/filter/PlugInFilterRunner.java create mode 100644 src/ij/plugin/filter/Printer.java create mode 100644 src/ij/plugin/filter/RGBStackSplitter.java create mode 100644 src/ij/plugin/filter/RankFilters.java create mode 100644 src/ij/plugin/filter/RoiWriter.java create mode 100644 src/ij/plugin/filter/Rotator.java create mode 100644 src/ij/plugin/filter/SaltAndPepper.java create mode 100644 src/ij/plugin/filter/ScaleDialog.java create mode 100644 src/ij/plugin/filter/Shadows.java create mode 100644 src/ij/plugin/filter/StackLabeler.java create mode 100644 src/ij/plugin/filter/ThresholdToSelection.java create mode 100644 src/ij/plugin/filter/Transformer.java create mode 100644 src/ij/plugin/filter/Translator.java create mode 100644 src/ij/plugin/filter/UnsharpMask.java create mode 100644 src/ij/plugin/filter/Writer.java create mode 100644 src/ij/plugin/filter/XYWriter.java create mode 100644 src/ij/plugin/frame/Channels.java create mode 100644 src/ij/plugin/frame/ColorPicker.java create mode 100644 src/ij/plugin/frame/ColorThresholder.java create mode 100644 src/ij/plugin/frame/Commands.java create mode 100644 src/ij/plugin/frame/ContrastAdjuster.java create mode 100644 src/ij/plugin/frame/Editor.java create mode 100644 src/ij/plugin/frame/Fitter.java create mode 100644 src/ij/plugin/frame/LineWidthAdjuster.java create mode 100644 src/ij/plugin/frame/MemoryMonitor.java create mode 100644 src/ij/plugin/frame/PasteController.java create mode 100644 src/ij/plugin/frame/PlugInDialog.java create mode 100644 src/ij/plugin/frame/PlugInFrame.java create mode 100644 src/ij/plugin/frame/Recorder.java create mode 100644 src/ij/plugin/frame/RoiManager.java create mode 100644 src/ij/plugin/frame/SyncWindows.java create mode 100644 src/ij/plugin/frame/ThresholdAdjuster.java create mode 100644 src/ij/plugin/tool/ArrowTool.java create mode 100644 src/ij/plugin/tool/BrushTool.java create mode 100644 src/ij/plugin/tool/MacroToolRunner.java create mode 100644 src/ij/plugin/tool/OverlayBrushTool.java create mode 100644 src/ij/plugin/tool/PixelInspectionTool.java create mode 100644 src/ij/plugin/tool/PlugInTool.java create mode 100644 src/ij/plugin/tool/RoiRotationTool.java create mode 100644 src/ij/process/AutoThresholder.java create mode 100644 src/ij/process/BinaryInterpolator.java create mode 100644 src/ij/process/BinaryProcessor.java create mode 100644 src/ij/process/Blitter.java create mode 100644 src/ij/process/ByteBlitter.java create mode 100644 src/ij/process/ByteProcessor.java create mode 100644 src/ij/process/ByteStatistics.java create mode 100644 src/ij/process/ColorBlitter.java create mode 100644 src/ij/process/ColorProcessor.java create mode 100644 src/ij/process/ColorSpaceConverter.java create mode 100644 src/ij/process/ColorStatistics.java create mode 100644 src/ij/process/DownsizeTable.java create mode 100644 src/ij/process/EllipseFitter.java create mode 100644 src/ij/process/FHT.java create mode 100644 src/ij/process/FloatBlitter.java create mode 100644 src/ij/process/FloatPolygon.java create mode 100644 src/ij/process/FloatProcessor.java create mode 100644 src/ij/process/FloatStatistics.java create mode 100644 src/ij/process/FloodFiller.java create mode 100644 src/ij/process/ImageConverter.java create mode 100644 src/ij/process/ImageProcessor.java create mode 100644 src/ij/process/ImageStatistics.java create mode 100644 src/ij/process/IntProcessor.java create mode 100644 src/ij/process/LUT.java create mode 100644 src/ij/process/MedianCut.java create mode 100644 src/ij/process/PolygonFiller.java create mode 100644 src/ij/process/ShortBlitter.java create mode 100644 src/ij/process/ShortProcessor.java create mode 100644 src/ij/process/ShortStatistics.java create mode 100644 src/ij/process/StackConverter.java create mode 100644 src/ij/process/StackProcessor.java create mode 100644 src/ij/process/StackStatistics.java create mode 100644 src/ij/process/TypeConverter.java create mode 100644 src/ij/text/TableListener.java create mode 100644 src/ij/text/TextCanvas.java create mode 100644 src/ij/text/TextPanel.java create mode 100644 src/ij/text/TextWindow.java create mode 100644 src/ij/util/ArrayUtil.java create mode 100644 src/ij/util/DicomTools.java create mode 100644 src/ij/util/FloatArray.java create mode 100644 src/ij/util/FontUtil.java create mode 100644 src/ij/util/IJMath.java create mode 100644 src/ij/util/Java2.java create mode 100644 src/ij/util/StringSorter.java create mode 100644 src/ij/util/ThreadUtil.java create mode 100644 src/ij/util/Tools.java create mode 100644 src/ij/util/WildcardMatch.java diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..933d8a2 --- /dev/null +++ b/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..34914aa --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + IJ + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..af07d5f --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/IJ_Props.txt b/IJ_Props.txt new file mode 100644 index 0000000..a58cd47 --- /dev/null +++ b/IJ_Props.txt @@ -0,0 +1,466 @@ + # IJ_Props.txt - This is the ImageJ properties file. ImageJ uses +# the information in this file to install plug-ins in menus. +# ImageJ looks for this file in ij.jar. It can be edited by +# opening ij.jar with a ZIP utility. + +# Note that commands must be unique. + +# Version 1.53 + +# Commands installed in the right-click popup menu +# May be overridden in StartupMacros +popup01=Show Info... +popup02=Properties... +popup03=Rename... +popup04=Measure +popup05=Histogram +popup06=Duplicate Image... +popup07=Original Scale +popup08=- +popup09=Record... +popup10=Find Commands... +popup11=Capture Screen + +# Plugins installed in the File/New submenu +new01="Image...[n]",ij.plugin.Commands("new") +new02="Hyperstack...",ij.plugin.HyperStackMaker +new03="Text Window[N]",ij.plugin.NewPlugin("text") +new04="Internal Clipboard",ij.plugin.Clipboard("show") +new05="System Clipboard[V]",ij.plugin.Clipboard("showsys") + +# Plugins installed in the File/Import submenu +import01="Image Sequence...",ij.plugin.FolderOpener +import02="Raw...",ij.plugin.Raw +import03="LUT... ",ij.plugin.LutLoader +import04="Text Image... ",ij.plugin.TextReader +import05="Text File... ",ij.plugin.TextFileReader +import06="Results... ",ij.plugin.SimpleCommands("import") +import07="Table... ",ij.plugin.SimpleCommands("table") +import08="URL...",ij.plugin.URLOpener +import09="Stack From List...",ij.plugin.ListVirtualStack +import10="TIFF Virtual Stack...",ij.plugin.FileInfoVirtualStack +import11="AVI...",ij.plugin.AVI_Reader +import12="XY Coordinates... ",ij.plugin.XY_Reader +#import08="TWAIN...",ij.plugin.twain.Twain +#import09="Capture Video...",QT_Capture +#import10="QuickTime Movie...",Movie_Opener +#import11="Pict...",QuickTime_Opener + +# Plugins installed in the File/Save As submenu +save01="Tiff...",ij.plugin.filter.Writer("tiff") +save02="Gif...",ij.plugin.filter.Writer("gif") +save03="Jpeg...",ij.plugin.filter.Writer("jpeg") +save04="Text Image...",ij.plugin.filter.Writer("text") +save05="ZIP...",ij.plugin.filter.Writer("zip") +save06="Raw Data...",ij.plugin.filter.Writer("raw") +save07="Image Sequence... ",ij.plugin.StackWriter +save08="AVI... ",ij.plugin.filter.AVI_Writer +save09="BMP...",ij.plugin.filter.Writer("bmp") +save10="PNG...",ij.plugin.filter.Writer("png") +save11="PGM...",ij.plugin.filter.Writer("pgm") +save12="FITS...",ij.plugin.filter.Writer("fits") +save13="LUT...",ij.plugin.filter.Writer("lut") +save14="Selection...",ij.plugin.filter.RoiWriter +save15="XY Coordinates...",ij.plugin.filter.XYWriter +save16="Results...",ij.plugin.MeasurementsWriter +save17="Text...",ij.plugin.TextWriter +#save18="QuickTime Movie... ",QT_Movie_Writer + +# Plugins installed in the Edit/Selection submenu +selection01="Select All[a]",ij.plugin.Selection("all") +selection02="Select None[A]",ij.plugin.Selection("none") +selection03="Restore Selection[E]",ij.plugin.Selection("restore") +selection04="Fit Spline",ij.plugin.Selection("spline") +selection05="Fit Circle",ij.plugin.Selection("circle") +selection06="Fit Ellipse",ij.plugin.Selection("ellipse") +selection07="Fit Rectangle",ij.plugin.Selection("rect") +selection08="Interpolate",ij.plugin.Selection("interpolate") +selection09="Convex Hull",ij.plugin.Selection("hull") +selection10="Make Inverse",ij.plugin.Selection("inverse") +selection11="Create Selection",ij.plugin.Selection("from") +selection12="Create Mask",ij.plugin.Selection("mask") +selection13=- +selection14="Properties... [y]",ij.plugin.Selection("properties") +selection15="Scale... ",ij.plugin.RoiScaler +selection16="Rotate...",ij.plugin.Selection("rotate") +selection17="Enlarge...",ij.plugin.RoiEnlarger +selection18="Make Band...",ij.plugin.Selection("band") +selection19="Specify...",ij.plugin.SpecifyROI +selection20="Straighten...",ij.plugin.Straightener +selection21="To Bounding Box",ij.plugin.Selection("tobox") +selection22="Line to Area",ij.plugin.Selection("toarea") +selection23="Area to Line",ij.plugin.Selection("toline") +selection24="Image to Selection...",ij.plugin.OverlayCommands("image-roi") +selection25="Add to Manager[t]",ij.plugin.Selection("add") + +# Plugins installed in the Edit/Options submenu +options01="Appearance...",ij.plugin.AppearanceOptions +options02="Arrow Tool...",ij.plugin.ArrowToolOptions +options03="Colors...",ij.plugin.Colors +options04="Compiler...",ij.plugin.Compiler("options") +options05="Conversions...",ij.plugin.Options("conv") +options06="DICOM...",ij.plugin.Options("dicom") +options07="Fonts...",ij.plugin.SimpleCommands("fonts") +options08="Input/Output...",ij.plugin.Options("io") +options09="Line Width...",ij.plugin.Options("line") +options10="Memory & Threads...",ij.plugin.Memory +options11="Misc...",ij.plugin.Options("misc") +options12="Plots...",ij.plugin.Profiler("set") +options13="Point Tool...",ij.plugin.PointToolOptions +options14="Proxy Settings...",ij.plugin.ProxySettings +options15="Roi Defaults...",ij.gui.RoiDefaultsDialog +options16="Rounded Rect Tool...",ij.plugin.RectToolOptions +options17="Startup...",ij.plugin.Startup +options18="Wand Tool...",ij.plugin.WandToolOptions +options19=- +options20="Fresh Start",ij.plugin.Options("fresh-start") +options21="Reset... ",ij.plugin.Options("reset") + +# Plugins installed in the Image/Adjust submenu +adjust01="Brightness/Contrast...[C]",ij.plugin.frame.ContrastAdjuster +adjust02="Window/Level...",ij.plugin.frame.ContrastAdjuster("wl") +adjust03="Color Balance...",ij.plugin.frame.ContrastAdjuster("balance") +adjust04="Threshold...[T]",ij.plugin.frame.ThresholdAdjuster +adjust05="Color Threshold...",ij.plugin.frame.ColorThresholder +adjust06="Size...",ij.plugin.Resizer +adjust07="Canvas Size...",ij.plugin.CanvasResizer +adjust08="Line Width... ",ij.plugin.frame.LineWidthAdjuster +adjust09="Coordinates...",ij.plugin.Coordinates + + +# Plugins installed in the Image/Color submenu +color01="Split Channels",ij.plugin.ChannelSplitter +color02="Merge Channels...",ij.plugin.RGBStackMerge +color03="Arrange Channels...",ij.plugin.ChannelArranger +color04="Channels Tool...[Z]",ij.plugin.frame.Channels +color05=- +color06="Stack to RGB",ij.plugin.RGBStackConverter +color07="Make Composite",ij.plugin.CompositeConverter +color08="Show LUT",ij.plugin.filter.LutViewer +color09="Display LUTs",ij.plugin.SimpleCommands("display") +color10="Edit LUT...",ij.plugin.LUT_Editor +color11="Color Picker...[K]",ij.plugin.frame.ColorPicker + +# Plugins installed in the Image/Stacks submenu +stacks01="Add Slice",ij.plugin.StackEditor("add") +stacks02="Delete Slice",ij.plugin.StackEditor("delete") +stacks03="Next Slice [>]",ij.plugin.Animator("next") +stacks04="Previous Slice [<]",ij.plugin.Animator("previous") +stacks05="Set Slice...",ij.plugin.Animator("set") +stacks06=- +stacks07="Images to Stack",ij.plugin.ImagesToStack +stacks08="Stack to Images",ij.plugin.StackEditor("toimages") +stacks09="Make Montage...",ij.plugin.MontageMaker +stacks10="Reslice [/]...",ij.plugin.Slicer +stacks11="Orthogonal Views[H]",ij.plugin.Orthogonal_Views +stacks12="Z Project...",ij.plugin.ZProjector +stacks13="3D Project...",ij.plugin.Projector +stacks14="Plot Z-axis Profile",ij.plugin.ZAxisProfiler +stacks15="Measure Stack...",ij.plugin.SimpleCommands("measure") +stacks16="Label...",ij.plugin.filter.StackLabeler +stacks17="Statistics",ij.plugin.Stack_Statistics + +# Plugins installed in the Image/Stacks/Animation submenu +animation_01="Start Animation [\\]",ij.plugin.Animator("start") +animation_02="Stop Animation",ij.plugin.Animator("stop") +animation_03="Animation Options...",ij.plugin.Animator("options") + +# Plugins installed in the Image/Stacks/Tools submenu +tools_01="Combine...",ij.plugin.StackCombiner +tools_02="Concatenate...",ij.plugin.Concatenator +tools_03="Grouped Z Project...",ij.plugin.GroupedZProjector +tools_04="Insert...",ij.plugin.StackInserter +tools_05="Magic Montage Tools",ij.plugin.SimpleCommands("magic") +tools_06="Make Substack...",ij.plugin.SubstackMaker +tools_07="Montage to Stack...",ij.plugin.StackMaker +tools_08="Plot XY Profile",ij.plugin.StackPlotter +tools_09="Reduce...",ij.plugin.StackReducer +tools_10="Remove Slice Labels",ij.plugin.SimpleCommands("remove") +tools_11="Reverse",ij.plugin.StackReverser +tools_12="Set Label...",ij.plugin.SimpleCommands("set") + +# Plugins installed in the Image/Hyperstacks submenu +hyperstacks01="New Hyperstack...",ij.plugin.HyperStackMaker +hyperstacks02="Stack to Hyperstack...",ij.plugin.HyperStackConverter("stacktohs") +hyperstacks03="Hyperstack to Stack",ij.plugin.HyperStackConverter("hstostack") +hyperstacks04="Reduce Dimensionality...",ij.plugin.HyperStackReducer + +# Plugins installed in the Image/Transform submenu +transform01="Flip Horizontally",ij.plugin.filter.Transformer("fliph") +transform02="Flip Vertically",ij.plugin.filter.Transformer("flipv") +transform03="Flip Z",ij.plugin.StackReverser +transform04="Rotate 90 Degrees Right",ij.plugin.filter.Transformer("right") +transform05="Rotate 90 Degrees Left",ij.plugin.filter.Transformer("left") +transform06="Rotate... ",ij.plugin.filter.Rotator +transform07="Translate...",ij.plugin.filter.Translator +transform08="Bin...",ij.plugin.Binner +transform09=- +transform10="Image to Results",ij.plugin.SimpleCommands("itor") +transform11="Results to Image",ij.plugin.SimpleCommands("rtoi") + +# Plugins installed in the Image/Zoom submenu +zoom01="In [+]",ij.plugin.Zoom("in") +zoom02="Out [-]",ij.plugin.Zoom("out") +zoom03="Original Scale[4]",ij.plugin.Zoom("orig") +zoom04="View 100%[5]",ij.plugin.Zoom("100%") +zoom05="To Selection",ij.plugin.Zoom("to") +zoom06="Scale to Fit",ij.plugin.Zoom("scale") +zoom07="Set... ",ij.plugin.Zoom("set") +zoom08="Maximize",ij.plugin.Zoom("max") + +# Plugins installed in the Image/Overlay submenu +overlay01="Add Selection...[b]",ij.plugin.OverlayCommands("add") +overlay02="Add Image...",ij.plugin.OverlayCommands("image") +overlay03="Hide Overlay",ij.plugin.OverlayCommands("hide") +overlay04="Show Overlay",ij.plugin.OverlayCommands("show") +overlay05="From ROI Manager",ij.plugin.OverlayCommands("from") +overlay06="To ROI Manager",ij.plugin.OverlayCommands("to") +overlay07="Remove Overlay",ij.plugin.OverlayCommands("remove") +overlay08="List Elements",ij.plugin.OverlayCommands("list") +overlay09="Flatten[F]",ij.plugin.OverlayCommands("flatten") +overlay10="Labels...",ij.plugin.OverlayLabels +overlay11="Measure Overlay",ij.plugin.OverlayCommands("measure") +overlay12="Overlay Options...[Y]",ij.plugin.OverlayCommands("options") + +# Plugins installed in the Image/Lookup Tables submenu +lookup01="Invert LUT",ij.plugin.LutLoader("invert") +lookup02="Apply LUT",ij.plugin.filter.LutApplier +lookup03=- +lookup04="Fire",ij.plugin.LutLoader("fire") +lookup05="Grays",ij.plugin.LutLoader("grays") +lookup06="Ice",ij.plugin.LutLoader("ice") +lookup07="Spectrum",ij.plugin.LutLoader("spectrum") +lookup08="3-3-2 RGB",ij.plugin.LutLoader("3-3-2 RGB") +lookup09="Red",ij.plugin.LutLoader("red") +lookup10="Green",ij.plugin.LutLoader("green") +lookup11="Blue",ij.plugin.LutLoader("blue") +lookup12="Cyan",ij.plugin.LutLoader("cyan") +lookup13="Magenta",ij.plugin.LutLoader("magenta") +lookup14="Yellow",ij.plugin.LutLoader("yellow") +lookup15="Red/Green",ij.plugin.LutLoader("redgreen") + +# Plug-ins installed in the Process/Noise submenu +noise01="Add Noise",ij.plugin.filter.Filters("add") +noise02="Add Specified Noise...",ij.plugin.filter.Filters("noise") +noise03="Salt and Pepper",ij.plugin.filter.SaltAndPepper +noise04=- +noise05="Despeckle",ij.plugin.filter.RankFilters("despeckle") +noise06="Remove Outliers...",ij.plugin.filter.RankFilters("outliers") +noise07="Remove NaNs...",ij.plugin.filter.RankFilters("nan") + +# Plugins installed in the Process/Shadows submenu +shadows01="North",ij.plugin.filter.Shadows("north") +shadows02="Northeast",ij.plugin.filter.Shadows("northeast") +shadows03="East",ij.plugin.filter.Shadows("east") +shadows04="Southeast",ij.plugin.filter.Shadows("southeast") +shadows05="South",ij.plugin.filter.Shadows("south") +shadows06="Southwest",ij.plugin.filter.Shadows("southwest") +shadows07="West",ij.plugin.filter.Shadows("west") +shadows08="Northwest",ij.plugin.filter.Shadows("northwest") +shadows09=- +shadows10="Shadows Demo",ij.plugin.filter.Shadows("demo") + +# Plugins installed in the Process/Binary submenu +binary01="Make Binary",ij.plugin.Thresholder +binary02="Convert to Mask",ij.plugin.Thresholder("mask") +binary03=- +binary04="Erode",ij.plugin.filter.Binary("erode") +binary05="Dilate",ij.plugin.filter.Binary("dilate") +binary06="Open",ij.plugin.filter.Binary("open") +# Can't use "Close" because it conflicts with File/Close +binary07="Close-",ij.plugin.filter.Binary("close") +binary08=- +binary09="Outline",ij.plugin.filter.Binary("outline") +binary10="Fill Holes",ij.plugin.filter.Binary("fill") +binary11="Skeletonize",ij.plugin.filter.Binary("skel") +binary12=- +binary13="Distance Map",ij.plugin.filter.EDM("edm") +binary14="Ultimate Points",ij.plugin.filter.EDM("points") +binary15="Watershed",ij.plugin.filter.EDM("watershed") +binary16="Voronoi",ij.plugin.filter.EDM("voronoi") +binary17=- +binary18="Options...",ij.plugin.filter.Binary("options") + +# Plugins installed in the Process/Math submenu +math01="Add...",ij.plugin.filter.ImageMath("add") +math02="Subtract...",ij.plugin.filter.ImageMath("sub") +math03="Multiply...",ij.plugin.filter.ImageMath("mul") +math04="Divide...",ij.plugin.filter.ImageMath("div") +math05="AND...",ij.plugin.filter.ImageMath("and") +math06="OR...",ij.plugin.filter.ImageMath("or") +math07="XOR...",ij.plugin.filter.ImageMath("xor") +math08="Min...",ij.plugin.filter.ImageMath("min") +math09="Max...",ij.plugin.filter.ImageMath("max") +math10="Gamma...",ij.plugin.filter.ImageMath("gamma") +math11="Set...",ij.plugin.filter.ImageMath("set") +math12="Log",ij.plugin.filter.ImageMath("log") +math13="Exp",ij.plugin.filter.ImageMath("exp") +math14="Square",ij.plugin.filter.ImageMath("sqr") +math15="Square Root",ij.plugin.filter.ImageMath("sqrt") +math16="Reciprocal",ij.plugin.filter.ImageMath("reciprocal") +math17="NaN Background",ij.plugin.filter.ImageMath("nan") +math18="Abs",ij.plugin.filter.ImageMath("abs") +math19="Macro...",ij.plugin.filter.ImageMath("macro") + +# Plugins installed in the Process/FFT submenu +fft01="FFT",ij.plugin.FFT("fft") +fft02="Inverse FFT",ij.plugin.FFT("inverse") +fft03="Redisplay Power Spectrum",ij.plugin.FFT("redisplay") +fft04="FFT Options...",ij.plugin.FFT("options") +fft05=- +fft06="Bandpass Filter...",ij.plugin.filter.FFTFilter +fft07="Custom Filter...",ij.plugin.filter.FFTCustomFilter +fft08="FD Math...",ij.plugin.FFTMath +fft09="Swap Quadrants",ij.plugin.FFT("swap") +fft10="Make Circular Selection...",ij.plugin.CircularRoiMaker + +# Plugins installed in the Process/Filters submenu +filters01="Convolve...",ij.plugin.filter.Convolver +filters02="Gaussian Blur...",ij.plugin.filter.GaussianBlur +filters03="Median...",ij.plugin.filter.RankFilters("median") +filters04="Mean...",ij.plugin.filter.RankFilters("mean") +filters05="Minimum...",ij.plugin.filter.RankFilters("min") +filters06="Maximum...",ij.plugin.filter.RankFilters("max") +filters07="Unsharp Mask...",ij.plugin.filter.UnsharpMask +filters08="Variance...",ij.plugin.filter.RankFilters("variance") +filters09="Top Hat...",ij.plugin.filter.RankFilters("tophat") +filters10=- +filters11="Gaussian Blur 3D...",ij.plugin.GaussianBlur3D +filters12="Median 3D...",ij.plugin.Filters3D("median") +filters13="Mean 3D...",ij.plugin.Filters3D("mean") +filters14="Minimum 3D...",ij.plugin.Filters3D("min") +filters15="Maximum 3D...",ij.plugin.Filters3D("max") +filters16="Variance 3D...",ij.plugin.Filters3D("var") +filters17=- +filters18="Show Circular Masks...",ij.plugin.filter.RankFilters("masks") + +# Plugins installed in the File/Batch submenu +batch01="Measure...",ij.plugin.BatchMeasure +batch02="Convert...",ij.plugin.BatchConverter +batch03="Macro... ",ij.plugin.BatchProcessor +batch04="Virtual Stack...",ij.plugin.BatchProcessor("stack") + +# Plugins installed in the Analyze/Gels submenu +gels01="Select First Lane[1]",ij.plugin.GelAnalyzer("first") +gels02="Select Next Lane[2]",ij.plugin.GelAnalyzer("next") +gels03="Plot Lanes[3]",ij.plugin.GelAnalyzer("plot") +gels04="Re-plot Lanes",ij.plugin.GelAnalyzer("replot") +gels05="Reset",ij.plugin.GelAnalyzer("reset") +gels06="Label Peaks",ij.plugin.GelAnalyzer("label") +gels07="Gel Analyzer Options...",ij.plugin.GelAnalyzer("options") + +# Plugins installed in the Analyze/Tools submenu +tools01="Save XY Coordinates...",ij.plugin.XYCoordinates +tools02="Fractal Box Count...",ij.plugin.filter.FractalBoxCounter +tools03="Analyze Line Graph",ij.plugin.filter.LineGraphAnalyzer +tools04="Curve Fitting...",ij.plugin.frame.Fitter +tools05="ROI Manager...",ij.plugin.frame.RoiManager +tools06="Scale Bar...",ij.plugin.ScaleBar +tools07="Calibration Bar...",ij.plugin.CalibrationBar +tools08="Synchronize Windows",ij.plugin.frame.SyncWindows +tools09="Grid...",ij.plugin.Grid + +# Plugins installed in the Plugins menu +plug-in01=>"Macros" +plug-in02=>"Shortcuts" +plug-in03=>"Utilities" +plug-in04=>"New_" +plug-in05="Compile and Run...",ij.plugin.Compiler +plug-in06="Install... [M]",ij.plugin.PluginInstaller +#plug-in07=- +#plug-in08=>"User_Plugins" + +# Install user plugins located in ij.jar to Plugins>User Plugins submenu +#user_plugins01="Red And Blue",RedAndBlue_ +#user_plugins02="Inverter",Inverter_ + + +# Plugins installed in the Plugins/Macros submenu +# 'MACROS_MENU_COMMANDS' in MacroInstaller must be updated when items are added +macros01="Install...",ij.plugin.MacroInstaller +macros02="Run...",ij.plugin.Macro_Runner +macros03="Edit...",ij.plugin.Compiler("edit") +macros04="Startup Macros...",ij.plugin.Commands("startup") +macros05="Interactive Interpreter...[j]",ij.plugin.SimpleCommands("interactive") +macros06="Record...",ij.plugin.frame.Recorder +macros07=- + +# Plugins installed in the Plugins/Shortcuts submenu +shortcuts01="Add Shortcut... ",ij.plugin.Hotkeys("install") +shortcuts02="Add Shortcut by Name... ",ij.plugin.Hotkeys("install2") +shortcuts03="Remove Shortcut...",ij.plugin.Hotkeys("remove") +shortcuts04="List Shortcuts",ij.plugin.CommandLister("shortcuts") +shortcuts05="List Commands",ij.plugin.Hotkeys("list") +shortcuts06=- + +# Plugins installed in the Plugins/Utilities submenu +utilities01="Control Panel...[U]",ij.plugin.ControlPanel +utilities02="Find Commands...[l]",ij.plugin.CommandFinder +utilities03="Commands...[L]",ij.plugin.frame.Commands +utilities04="Search...",ij.plugin.SimpleCommands("search") +utilities05="Monitor Events...",ij.plugin.EventListener +utilities06="Monitor Memory...",ij.plugin.frame.MemoryMonitor +utilities07=- +utilities08="Capture Screen[G]",ij.plugin.ScreenGrabber +utilities09="Capture Delayed...",ij.plugin.ScreenGrabber("delay") +utilities10="Capture Image",ij.plugin.ScreenGrabber("image") +utilities11=- +utilities12="ImageJ Properties",ij.plugin.JavaProperties +utilities13="Threads",ij.plugin.ThreadLister +utilities14="Benchmark",ij.plugin.Benchmark +utilities15="Reset...",ij.plugin.SimpleCommands("reset") + +# Plugins installed in the Plugins/New submenu +new_01="Macro",ij.plugin.NewPlugin("macro") +new_02="Macro Tool",ij.plugin.NewPlugin("macro-tool") +new_03="JavaScript",ij.plugin.NewPlugin("javascript") +new_04=- +new_05="Plugin",ij.plugin.NewPlugin("plugin") +new_06="Plugin Filter",ij.plugin.NewPlugin("filter") +new_07="Plugin Frame",ij.plugin.NewPlugin("frame") +new_08="Plugin Tool",ij.plugin.NewPlugin("plugin-tool") +new_09=- +new_10="Text Window...",ij.plugin.NewPlugin("text+dialog") + +# Plugins installed in the Help/About submenu +about01="About This Submenu...",ij.plugin.SimpleCommands("about") +about02=- + +# URL of directory containing the sample images +# location2 is uses with Java 6, which does not support https +images.location=https://imagej.net/images/ +images.location2=http://imagej.net/images/ + +# Images installed in the Open Samples submenu +# RawReader expects a string with "name width height nImages bitsPerPixel offset [white]" +open01="AuPbSn 40",ij.plugin.URLOpener("AuPbSn40.jpg") +open02="Bat Cochlea Volume",ij.plugin.URLOpener("bat-cochlea-volume.zip") +open03="Bat Cochlea Renderings",ij.plugin.URLOpener("bat-cochlea-renderings.zip") +open04="Blobs[B]",ij.plugin.URLOpener("blobs.gif") +open05="Boats",ij.plugin.URLOpener("boats.gif") +open06="Cardio (RGB DICOM)",ij.plugin.URLOpener("cardio.dcm.zip") +open07="Cell Colony",ij.plugin.URLOpener("Cell_Colony.jpg") +open08="Clown",ij.plugin.URLOpener("clown.jpg") +open09="Confocal Series",ij.plugin.URLOpener("confocal-series.zip") +open10="CT (16-bit DICOM)",ij.plugin.URLOpener("ct.dcm.zip") +open11="Dot Blot",ij.plugin.URLOpener("Dot_Blot.jpg") +open12="Embryos",ij.plugin.URLOpener("embryos.jpg") +open13="Fluorescent Cells",ij.plugin.URLOpener("FluorescentCells.zip") +open14="Fly Brain",ij.plugin.URLOpener("flybrain.zip") +open15="Gel",ij.plugin.URLOpener("gel.gif") +open16="HeLa Cells (48-bit RGB)",ij.plugin.URLOpener("hela-cells.zip") +open17="Image with Overlay",ij.plugin.URLOpener("ImageWithOverlay.zip") +open18="Leaf",ij.plugin.URLOpener("leaf.jpg") +open19="Line Graph",ij.plugin.URLOpener("LineGraph.jpg") +open20="Mitosis (5D stack)",ij.plugin.URLOpener("Spindly-GFP.zip") +open21="MRI Stack",ij.plugin.URLOpener("mri-stack.zip") +open22="M51 Galaxy (16-bits)",ij.plugin.URLOpener("m51.zip") +open23="Neuron (5 channels)",ij.plugin.URLOpener("Rat_Hippocampal_Neuron.zip") +open24="Nile Bend",ij.plugin.URLOpener("NileBend.jpg") +open25="Organ of Corti (4D stack)",ij.plugin.URLOpener("organ-of-corti.zip") +open26="Particles",ij.plugin.URLOpener("particles.gif") +open27="T1 Head (16-bits)",ij.plugin.URLOpener("t1-head.zip") +open28="T1 Head Renderings",ij.plugin.URLOpener("t1-rendering.zip") +open29="TEM Filter",ij.plugin.URLOpener("TEM_filter_sample.jpg") +open30="Tree Rings",ij.plugin.URLOpener("Tree_Rings.jpg") + diff --git a/bin/ij/CommandListener.class b/bin/ij/CommandListener.class new file mode 100644 index 0000000000000000000000000000000000000000..bbea16c05e8b6ca5c30d2f7ede4da8e3e3ace8ba GIT binary patch literal 182 zcmX^0Z`VEs1_l!bPId++Mh2nGEPdzv+}y;x6rarElGMD^B6bEAMh1bb#Ii*FoW#6z zegCAa)Z`MNP%=cFYei~uX-Q^YIwOOch7Uqra7htR&{`8yh>?LSIKQ+gIn^yQCzX*w j1kFM{kQr=@42%rSK$kHvFaqsoWncrb7}$X%69WeT$dNG~ literal 0 HcmV?d00001 diff --git a/bin/ij/CompositeImage.class b/bin/ij/CompositeImage.class new file mode 100644 index 0000000000000000000000000000000000000000..592b0867951723b1c5d70f60e231fe905b45dea5 GIT binary patch literal 17693 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 ah?IPLXQB*zk#GiKzD^V&ae|6cNB$4Hz%6tD literal 0 HcmV?d00001 diff --git a/bin/ij/Executer.class b/bin/ij/Executer.class new file mode 100644 index 0000000000000000000000000000000000000000..267b9e83883d90b1b8514cf7ad1b112767df7889 GIT binary patch literal 9063 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 ZD+XYA#|1@)BK3*uBL_nHhklcP{s)W%{mlRX literal 0 HcmV?d00001 diff --git a/bin/ij/IJ$ExceptionHandler.class b/bin/ij/IJ$ExceptionHandler.class new file mode 100644 index 0000000000000000000000000000000000000000..be6c54544e7a1e7fb4ce0d36754b2040362411d7 GIT binary patch literal 210 zcmZ9Gy$ZrG9K`QmZDMN$C-DIsbg_$@qbRlr4vKqPgKY_^r1j(39DD#DN{n3um%D>I z_}%OMcmkNA$KenLc@akOXn9ZNxz0mkz_O+%49A;a zyU$E{6N!@Z$${YS%8Nrq5`su;X%|r1h-*Z&Oe literal 0 HcmV?d00001 diff --git a/bin/ij/IJ.class b/bin/ij/IJ.class new file mode 100644 index 0000000000000000000000000000000000000000..7856c343808e91b3c375d0670a0ed044818e51a8 GIT binary patch literal 63092 zcmbTf34B!5`9FNlUC-PcATS^VL zo#{ynkTg4!HI=hK&C-TtOIOsboX;7r>*rH~$(gxoc|B)?bRP^r>_*(WY}K^)Zxgj%-XuBDu7?I5VjK zqIyCf!L^7`5^isgMS!-!KDN~Bq-ynp{G#?`0_~(y^TJ6ggt{9$qLTn$CKZJO>ByG! zvT!^Cu!2;C-Y2chBvLntR6N{<5#k}7Y-%MUrW(Rc>UT{v&Y2rc)kotUTh!Cc>GPt1 zsfY-;I+9F96DU#C1S%#IvD!o@l@G_#`YDiV4>v_J&#loGKV7zkfv@Eq@i@o=G_@xq zo1=-2)XLUmB-~s`JDmnV1yJpeWLQ&cq-oQFM6$M{Jr->Wrz6W^9UG(Z6oC>8r&9Ha zaB~D$8JfogHMtD57Ho^OH2`@)c5O7(9t&@cpyNPWcuPa1Es@*`DlP)|MN&=ScF-Ip zDSt>Z(i}~Kndc{yiKJg2y^ctAv||vdl!jwTBE2Bd5pPzdK=7tSTU$8ZyfWMv1EPm$ zpgPjg*rX%Esn#?l1El_u&5?MzKAK8L;=pVc4XWU3bec$_R56B8&^>1!IH5g~Oh+TA zv|nE9-~~S&6^-T=&W5kk*@_KnBTbNk81UE@CLuIZC}B3Ypq0=M71LasLg`>S*^x>& zgyTSAvIc}p6NZXXZHYv>6%3S209Zxnx_CO01bXn7-~$(`eos)2C346@y?KqCpZ9?Y;J98ax?B%>`JTg#4>>^QaoVlu~XVQ8twQND($ ztDCSo0I(V*vzZfM&8)>`AbB7w*ph8z4$zcsd&*R*5S7fO3Y=|{Y!(~pvNjA(!IVdS zkHuJm;8;T?PmpXLs}T(3aucaal46f_uuhk4&eYYgZ3RcQNw$T7tBW>9(mq?@?Vvu! zFroLbsJD|OD`f)(I|aQ#Ei@-OQyx2wo#C?63wu+#RtZr64A(7VHtAU~=VT z!5#&=@PMbsKu>zYkm>CTOCeDN!|O2uFt^GKTvy3n;OFHD#v3c zgZi}6tx=M0tfCizV+*a9Ub{b6yMt zqY$|gb#SK^r0iW~U=N5VtR{MdBoFe@oGtpF=<&ae3C$iz z&5r&5LuMZ_AY8Pq9ekCQDK$(GLAjDklyhvsf}#wEQ07l8{3cvpk-*bUpxChF!?(Tz&);K@K?O#7ns5i=la15=l*+aeZ`~ z-=eAo30TM*Bwxapf*$cmXHG*EA|OlDBekrwYq8g3EaJ=f3dvV0v|#iuhqga3ryN?M zF*pm8bYX}%UY7fsViiA%sJ@o7L-OJEyX%dV-YKxDd>!ZoDrlf8p^K`U!|mur@MAdJ zpZbl)Ly2TaF>ffINQYX;`c;L-2!0$WIL0d_HV6}YL2(vvl@Cfj>I%6^a4;gD;PDpT z>T+m1fstc&)RK}sN?bJvI-~~EfCuKtF~yr8NC>O?EKm_@Poz@O#@N=7*BEIE zccdbrXgU;4DLk}yG-9$?2}8x@a|-(X#zpw1TjOo(2AfLDP!XF(-ZgrHFl75p4!7c;!50Is2#c_c|lK2NfP*&(DZ zE|B~}{$uTQs8X5vc}aoh$;*PUUc`Uu@{6G&dk{$1mHcO<0|LYzs-DZ?{Ez&93Zmec z4LHiCS792!uaNu~d>a}`bu^~bw1pIMU2pbR+Vm>;6Kt#CzXVB+qw3d6ejWc6WPqX{ zVX$V-DO8HI22+O>krO&1@utY^2{h5+H%N9Exj3BPB>ByxL+l7e5~#UbB)^s42DOIC zRbRB;9OO`|4))X%bx>=H&Y0c2wO1i&LE(7 zSAxF)w1Os~gC{mZk8Mnbw;~<|w^8tyV4HF|O^FzoNbr||TuPlXvsZ<`KsqRE+EkTQ z+5)ttCls{#S4b!oS-x;yNVf=g74ffdV>A}k(IvB~jVu6&7-&w^=&+W{{|>zd$(z3= z($oP{xYCyKILW z6G^yEpYbmx|B7VRgV(!M1)WkJ|Av3(@^3+noSM?XD#`cq@8KdfX?Hpk!7B7M{OBfh zgTW-5&Ss!N!;pqa#?-|a$W92u1{jd>vBy!iD-A(lxB!E)F$lwxMiIGsKx7}vNa%KI+99P)99%95XqJ>xJGV54-EpF_T#br$TiMT z7h-g%a`2TREePWECRXnhnjIHKa}h!6dVk3dXVtDjGYW;qLNa;ANFxM(GDd5ixLOzo z0y^k`%F0S%j04RSJK>Frj+aKM0Z}&FJ1|!+nEv(lwfc}gKS>znKoJor)tZPk3u7Vx zZwPOSgyu!#;pA3fOad$T!D9soXwnbU;s_!}Q28op8k93;_Q`apIu7MPE`u;;V3<7; zL+jQbBaB(#RIGOp0Yhp;Dbmp@G9ZjY&{VHP5F=Ydm7@x}lrRp}oi}aDPdp4NCDTW? z1|(h>)sXhWl$yMR5r8d|DWFD54%iFoc*Z$5O&r zj4r51LnOITBd7sRk9y0NQg{O&7G#`WGe&1HSWs>gHymj6VxU!6?C8 zkSL5zFidq|8hFJ{T|Bg|9DI@hb1&@xj!(e&@eo=!5vkB~#q;C9byEaDVL=891;Xe6 zv-M0>O>5a+feD0L#XhD0Ka#xFn$8_m!G~Ib83Y4u|5|nojzSvy%-k1 z?;^tZ8JJ>TEYXP4Q)f^=mqH_}1Y!IfOg#_A6nWD$#8*%zr3hmipsfi5m5EU4teFQd zu29yswhqgjXyqzEo5>})$fW<0Fd2!(FvwUUz7-8mp-!(QsmLV@<5!dr!>SvGKb=b4 zfSy%?FmBSSp@qW8AzH?Oi&8qZa%#gol-o{xnuzCT3gcF)u@d;0IvH^KK12qjUlT~k z?2-H%uthCs3S6WyLW1E=kTjDdjJr`_ep_R5Yl1L6l?v zLo_}rIe9V_-GyFz zv^9WeOg)(J^8&`4O%=vVSmA4!F(tGx91Cv&+f11?D@#etoQ;>E&!6aiz zqyx?`YE{kzLS}@Te7%kAze0;-vI{a^2Utr;vgbjdsOlla*KYuG@KOE=2d@(rC z#y+@K>@CD}OfF5sG{Md^r9zM;-*+!?oqDV`^llmLnzl3@a$~)4b2GW-YM%l{cOVVN zPg2f0O{`6i9PKq2d+7u$#A3HW19{(`+21U7%>m$8l#EvSVszusJWkHN5Tik(=f@Fu`h**xHzXJ6caw`GMc(Kn+Hg9 zFrR`jxEYe>Xo{Q$G$W7T1?hc+PK0?dAZbepvkIuh z8q{g#97HG3!T#A;| zoEbz7`kO5B!|B)yU>+&W<>m^oOCwbM#w3MhRkHxDXGb2|T_w%c1fd}AB9Mi76bKfj zbu`z6sPxw?odL!(*P81g%jPkFrL>OrA`l}UC(R9JShJm8_UX-bI(>EkEi_59nSit# z!33K;v&C$6&5bbPz@M6yk@F4YMNKRO7PW_yDQxgTWEK3<7ny1&)RH*GAg}B{o*6gW zT{F>xt#$9xJi$a1ryoW;FN+_P%!+PioRE&VnU-dUxf#VF1^LB;J{4(inL-*ZrvQL0 z(%h=%kDcLYnxyz7X`XDJ0trQbT6mOxD(FsaH$r!(nQ7NNJs;uzMkKdQZy^F4de$6{ z`4n|j6mJSBp&lSG;4VtYsf=t>Q)lB36nKUmUC0j%a&!66S=4IyPt_lBjAc$MI zGmMr8rUY1r`3q@oGp~epPS9-H-@Alm*=2B^fjC;21%|FRuW`*^g7p1`ryUx6cc==`oGJjAo^94+^`*r3m(!A9~7?tR{GPBQ( zbd6H9**-I*z1_USHQ~gTR;9wTkY{8SMD3PrCL1rz`#>Hn zlT)YnOY;HqK_C*H(%woEpqz41yDbsHG#`eFT~B=RJ83>j)G3Cqhy7D%#h>Mpa(zsi zduSVvfN!TZ@d)z?D09yf=9BOeC}0e|g2llM1>xGd>oNbpW;tR02^H6`YHtP$RuO2= zN%MKyp+l4*f4Bw;FijaRO7kTS-h3}L=Z?dYn)glo#HoQFQahA;dR=IanDbb>Go zRenR7ZxUnMv=0F)@*jX`J$Zm|xKy`qN%L(Q;ouGh3M8eYE$$b9Re}pitpQ}R=>L-D z`zAa^(zy$gYUy`uAMBTB2UIE0`#SSOX@2BGg4O^u7VnJ|B;RRLIsh2Mo(RaY6~iEv zXafN#hlp^V0niPi7NJQK^=oNFF7`_ETN)}sb@6Nv?(c0sT>roV2Bg{Xy?bYqAC84ALyC^o^ebKJKx*LK}LtIZay7+0+SCJbsdi zPI|)+c+f}!eI?`z_5~if%WBXo0H3C#+Hd_o#p7#DHtPL-!kPih^RITBttr@*qtIS_`DL(1PFApEy0g9yVU(jUq3LEWLFQ zw&R$$TZ>^kta{GMdm<~pt}g-#y;K?XmPl(U$uJDN3JW@?eWle75p9LEs;rfL`#E(J zYF!JuJ8$b?{;IFv97P#{C0`xLyj1X#*%XdJYVsk>A05boy|&6EVWE*q`R;5FdqvWq zR+^w$024ev+PjpYdR~jr#K3KyjfDS(l37EeO(Yp4H=a<=im;%nhOKOkAi4>Ogaqjo zuT%&t_;JkGtX9nB_!MEmHxx_!NHj*)mVyUa1P@|qi{4)CS#b-(2~W^Pm#eUyR9Yt} zkNQfI$@Em$X-la$xgG*#tNQw4pm*$ z*9^xx4^TGDBakkTtcnd4){jA=xj+0*SQkMx&smy3qPfEQDHO<@YJ~l;w>275J0SFE zL#4vH1Q?tUY7M8bgCY_~>C3P{I|o}Fnkx|y$JPq0OaL>WOr~;0h&DiAivbE>&ROZ4 z`OVn->_=bcYC?n#3h6bd6qfu{c)M~g*1Aj&XhMv<&0>iK5p->8)yFKPzgkvD zi?kqI3qsQ||3$g3{+@eb!Nqg^7cw5VOY0VXkFf55Ntx4{Zi`{e@YPZDFLQNEP zkHOWp?&XtR3$aFuT2OC3jF}D}v>qYV^gGUG<@;r1wfb0E?>+4$uXYM*^C0A>pdj+VCwJc8wkXghTEQ^1mv?nT0a|7Kv5hXvQoS_+XC)^o^R z4sU`M*d1kLVN5)=B&tS^f{`t>Czbr;{8JH-7UCR#__PBYu?^tpf8JGav}?ih%b;-~ zor#DxWzhHv)-NK6Fz{93!TH5{W{*Y#j6MI8w=Im#XYn11h`>C_3sbXmhvL#*%qOQIL!sg(hvJJ!+4f=5KAhSs1|f9Tib$IF6(Q3)9m8phAl5r5&r#8(AXI`h!%Em^ z&M>=zqEz}v?FL!Yd+1PkfN7z$Yi;<0*z5U!RRGr#2hy@Tz(}PrPwa2kOS?e{e-vkn ze0u{o!(J-wWi*U|nSB}nt05YTMN`h<7ay(|z&Y$q^$TBs+pDpw0#?F2&^}7qYssmn zSAi!EoK_2lL9IWYfl=!T+H<#r2Y!ko==@!9LZajsZkE9_m#im~H zh1RT!PMlX+mObN%$y!kw5ClofgrSYN_z{TRm8*b1d&?U zCxRHEHgw%jk!FdXL7>2AQ_!_#fEv=*@F2>k+h@8qF*%Aje`KFU!jW1MUV>qu z{?SzY9BH3R8D2D17l${2*ci~r63)Vp_w9RA5d*%OGp%xZWE!0kvM-XX4u@cD_+oTW zW&UJgUxEU3#DtzN0}j=36MBJDUaEVUx7s(LSVoKUuiah&P``500 z8#pe9y9VqSmG`_GJ4(Dgw~zph}^n2BxAL)m+QFHT44|=@ zj&Gq}Uz;2ILL53vMmEx^OlVq6zH``9`x8}sx!Hb3+E3X}lRryGUf~AQsaN8s=Omj# z+sOJu?&yr{;>gxa){Bx&qAU*wX7iF?mTWR5!}G?8*}RNbC7Y_s!)NxBNh7^3?Z5e& z4K5jCtO;7s;r5%-{yP~xsiO!w$W0l=2>RG>N&9Uxtkt)_cBR8U1oXRbfT<^QQv@++ zvojjU&`Ix-L#dRtE~|980%lb^se=!t{gM4KkPPj&JOMS;Ur+L>8s0I>+F*Yw?av5Y zM{Q+n_Ute0|GG97N*V(Skqhtj~nV01?XwY^xWf&+~dsLG-yAg3u5%(f$DcY*{^)C{`l6E1dlxfm14HOp9Yq>kN}l2`$0}Fzv%CPGn_5 zI3r+QSK`Q7X1_g-5vY~b+#Xj&`$Zw)jO46_jv2&YJd%~P`Pe%m5^JBUgSL7L+9KNB z)X|P@tIcXFKW3t;JhoE#uvKDcSk71QK|O-99qrm*S8?kCLl4b?MKouObPgoebXpT> za3|)f&N%5Dq!yaNb^h!Y%;=O#XM#^a#I*I!HPt%(j;-xyY5Lu1xP zQe!Gz2UHy9GdNPPxmnGI^k9Wk1yR8q8a6tmcX%8o9C%+clNLr$Ksa-OEE`{S~~M+nEjwhF~7oTj1*!5lv696`IO^rjifTkz{^7E9N|Ch6*Y}I!mZYh|20{GL8M*gz06{Ig(IEuHd|_*h;nxM}o+!F`2-X z3=Clu;jH59Qm_fdBSQYL<|-E|M{D9`0&X;0$9hjwB8hpibk-|=#9|xXg>x)%4WZRT-3`(S(*#xOT+N** zfCa#fPz(W8PzPQ0r3gO%!g%874h$U~xEY>NfV~)p)FB0a01NKO*hu!@g^g7)Y4GVL z>BMM%0#RD?IfxaPPJ)yxZnw~J>?l;QgA*~wIZ+~JN;+w$gZSOo;l;}1&Zuw%b*NM) zm4b5Sun%@8dA}r*%Hf6Hjx!3e=;B;XqRGg>pJk_a#8Pa49eYe zPIpeA#?PY0&xQ&s=ou}c06Zbp2rU%3&iT+9zEn|y=bY!90R6WGC^xp!pDmDFW9wAP zK+OPkzbpZcfgN-%k@hQaFL~QHE1R25n8K$2K zS-HyjrR%`K%S~4OJn39xjRnNuEY+>+z{jJ5t%OdMf^i2>|9a`%;M@qLZD}X)z#GQ8 zI?A|NI@?voR-HjO3Hw z2OY$|Y~{OYZ*mdUrI;5rZlW{N?20imT#YpFu}fT`vM~cDzR6_!=_o?6!iH$PI^GO_ zkOuXHboMxZP+nO^NvTO>Ek$0?TU!()!;XL+nUv?C6IDnw!8X$FWcVh|pQQ6Fd9HMN zV0pMxg;|IppO?-H1pfd);P3ce4tc{$)`8ATl=n0}y)2zqNV58AhYN7lVi5#vu438+ zkpERWf8hsErPrnNH>xyH70Pd14>-F4od*1O>Ad0mLpk}id5$RN{!n*tcF-4ggGQ3s zN(vZ+15^T*v#61GsF8Pj&!lUAs7c&yQqW{kv%WD!TgeZItZI{i^PzM;qLv9wKwlOL z@;5$_&Zo|2Ky5lvPaxKWDTt9d38QwF_;(ugF?kUt6f0arD*?wrIA20K;#lW7(D_hz)0K?&lH}y=} zJkXsc-RT4m=@s0cgsnO0)_^Yv zQ9%dc!jaPpBO&2#fSBht43GnwTl=JyP+I@2ghFx~rQ1Y6xvB7G>=A%R)!&Usw}oy? z=!c7BqMI>cQVQiV>}D{4_!EO|BplZ zOcVtH~et-Vl-il z_G9%$-NMCKhhssy6HD)jPW+@>)kH+CXyF8``*R?1j6Nz4;r7pA=XDHJb?S~xTrdPL zDC)XbL5XBZrQvs-8$D|T297@o*Cl5!Tf$$6a#-)TPa(R~*(6}C z5l2#WLpeEDb+7}~{jGHGal4473BMg2kIqIH2|ATL+jG0!`&{>42&~U2wD{79wP5!) zb2G{6{QwD1DEFXrchJU758+H7aFP~mL_ifP@H=c3bsr@!G3x^Qt*L$HY>m^LSVI#@VC0X|ead}Cxf=yiUkC2V+0%?0RfpLg#`yzT zfph}*jL}@19FWUsbT3HvMfWAGaMZllU!PO)&IQ7K8T>L`xPQUV!NUD3K-0IRx$f%# z-$$8()qTx<12)EeQ^mEA^8*%_T=Ko{KcxFlT2EAFE*j73N#~K$TSH`gsL;F8eUC() zCg;%aIzU6Nn}^Qcm+lAdzX3!Teh9WB=$&cAE#w_j^^c^h)+|*Y;Vs!dsP?B+8$O+& z@>C(sc0h1py21dDWQ8l6E1j(@KqYMHoQNmJg)3PD!ejz7ra>#z^>l=5 zVwuYHmb52|L_b%^f+@ZVOk^D|Df-jl+G5i3%0xf`w=Kf{Ls2XSxnf`@7Ecn9)4fsz z$-EXvQ{>gtniC-}-0TW?T)9IHszK~dq1%MSU^r*|Gcinx66(>4U{VAuMYY4oJHcaQ zw@4S$7+^k)G^V+@3sUa`Rud>}`HD^jlQsHi%v>N@fJ8mJScWr#5ycF^(^x6SQAltg zmVz*!SeeM>+FywAQj9T13o!xyK2{opD8~bgoDdTMXmhlsMTkjw!j{>D5CF=;3B475fjdn!p15H7d6sq zrqjzEV!pIqB%6n+12EA>XGIDQA@?xszHy}WFUYH?lVTy&rdj>!%>9*W<6}oG<|hcz z08kGBcy=S*;iJ8(T6OUwwjK#_Bs7-x_;CRIeX&A{m85Xgom%SFCVlaVfQ8i8O{ii= zNwJoSkz1mO?w48zRK+^dDAOX2k>Xe?L-QGBY!{*1fY=}!T@gmI;&*JXUVtN4QZzBJ z9()JHZ^O79QV6JWt2KeeHi7s{0K0#ZmHIV`R~WuH9qdU7u?erV3gN%Dkp`ya(vEh) zJ{AH#xa%*63IT`0_1F15krta>(LqeD?&hLBBHFA;(MfRA9a8?Pd=6i{REQIS9s<5H zny5^-wc{2|Ax;K@C*frh5$04WP7|l=7M3QlMuR9EjS{PNiL5d3M^0(b0%wY|Tmb_> zMkg;vinA47&Cn*fol}@(1t61 z3QeTD$d;{XO(cL_Ph1Rv6&H(3rC7@65TpKFipxoKCGEJ#P)4ye5Q+P7(IS|u1q8vN z;sUryn46oRhWzxNoy`{F8d#7?)XR0!u}QhmLJP`K(hUHCIwo^Sh@-oAFkXT$Ew)Q> z3k{lr+mwM~&Fb=6e^aYLlP|Sa4@Bmu3Xm*Pder_MQrym`xZ*C1Ek}LA!JXI;Anp`9 zQ2-)}C-@%fHZ8v`wL`bB!y#l0&~mpF_lo<#lGK&r`o8at=;^Z)+uSd$>EspTO1$2N zoDiwnQ&Ay;>>=vv0eb2N3aGzK7+X9l9&^R-VRuo?2o*r=e4q78b=WD*=bZ95)r}?KJVM6wlEmaGrno z3Y!33@j^!68vUmkZrv7n^Hx52W*cvWw#&h&*zR*33VLW8>1okfEt_;p;?6Q)q*S33Z2fF?FvMUG)%~u*U{2~^^|74=gbxV z%BAQaLqAm0_AX8O-NPom-Q z#3#htpNKE8xGes|Sw)|0)|2yeWuW{W@ud`Bk>FtsIozy5)Z!cSLsxv8@fix?6=E;S zW^gP793nm!3=-fNK>!|4=Gw_#psMJf*^z|1V5AyJi~ImoXb}p$zs~l#fK*S1Qaye1 z2IWV=U%BHy`4mKEBb1uoBL-ARL{zqDHVmN&6w5$={tgXjkPHNAl3qmj8LJ(hOIt)> z2+CHbqquRG2ZqYPFazjtsB(1*(jFKt10!fTh330TbBVx6&fr%?G(XY}SfO@Y03A^` zE7UEjq`IXOu$)8jC7QSE1W)-5p_Y!gk{n{>wkWO+qg#>Cn182Fa|oOCB5}Ipx)O(7 zD8huf?o@z;dxQ|Jgx?w%Edygn0nr&5 z+oD7O(J1Tq_Q*yNs6czix8tFbu!ASU&SV*wLK&xff;Uq7Jzf+Wd*R1 zl!2Kf;N+z&Lc2#|@h+O)>IDuCRJj2-JLHo3ijVTw&^aK_0BaRY3DF6|9+Ut1?c zfE>DwQ4kgmomGh^FHjw*aRc+f{<-cJMq6^d)pCZTnEyv_eZ`0a3xH?Zy{_6ENnklWHKjI-00lNG)AR&q&ZRa~ z?W0iV#Av$+AedxTs;Bi}i=*q7sceLfiVy$~)rQkym4NsWeC4PJKr*dL^#p_KRFH$3Uf7_rPenFdP!WIwW>u=EGm%}tYNg6P3q2u5 zM^~ZtowC(&5fL~aIA5N5BD% z#N$wwRHWLX0j|?p2-2JN_A^`L2?pQ&z({cZ@1np1 zJ+7G4*?sIUFSvKpFGcuK-i!!53@4Q?G{VgeI#UttcYuNb&ZW75-)E-;l;iJe2<(Ac zT!n)eIGWY$1s)ANK}PHk|0{64bqtI-T6szao+gu;adcJOC-A7a)D8S8W6AT3kk%}2 z;JHk=z{j~x_X5ubULdeuMAbfwJ4Gh~&jkKVMP4p&Aas3lFp&QjpdBk6foEXb1Fz-V z_NCc13XnGiq0e~PIYfc*y=|x8e1Xl)wS_8@oU4rIDFXilRmjH-DLqF_1;5|=Ejj;} z;!ofyPi!FXaYNu=GVs2dJ7D8}ige1q(Mi5nm%TI;o{tf718|&lpvA|W?|cP53Vcdv z`3$_AbH1o&?RA+SiU3?D*KZo|X^Q%-UW+$-h(P$#}0n&;=Z9?Y%2 zKiwZfUZ&?+z^-SA4_q&hSBxw`K`|jd!1-BEO0S>SAIg&a!Uc#uV9H>ZuA2vT!s*%% zJ#U~FbiF|tPAVL?^agw6DRttS8TiLkO{2E37%=QKs&Rnz*1_Z=wgSadk|jeA;AXqt zP*TJI$zP+$Ezsuqo3qshlR^vac?WnQ*Bhn14hm1U7~% z0y)Wzjl!D@0I*94D=FSoU>K`w?ZTUm#CRf_LgGx|6k)CA1ZFfA-SVgbRV#XA`mm`r z(p*RT5(_T*{Xq>(DCjw_6&|#kjq_iT7>tuQmryts?yq;a^s4E02igKzi)9w&nWltS zgPv&{pT3;Un~&GrU6D?iw-93L9RW${;aAc88_h0~-r~%SB3fpSz`*EA5o}`e8oZ^h zx5QtU)>k3tvKz3UCMmsTw7fMyL9Fr+OrmXNPG;M=(?VOOz-ZxAbphG)q?+w763IQ@zvlpj44XbOBJ`aUNfpQ8b+jVT5ORKXN^I zq`9xOP=@r*^3K*!*WprIGT`7DrNJ|GDIN9C_0D&_^I)fXWlB#418Ij`Jqeufehj-b zQFw4$J-sX*f*UM6IIjV^PH3xsy#zzeUBeBeWr*47T_zFG-c3MXF1;&g%B7;YfJEPT z;B{K1K$v%>^se%*2K?!SLL`YoKGOXgmkJ1xz4-}u2+*o${jdOL$MOCyi}6eTcmfRcN3TqAtGEc zk>?L(Vm3;zV{t^EVL(e*wehf_Z6#Y(id_p2{<4WliSXbe2l6nIzquuc z5EO0-cjjB}9)kkKjdm2s#b@%ygiN|CD%;BuiVRRN`XqfHI90TCHAYdc7@=&ntVNvxf>VQFAk0t>-F6YFy zuY#Vao4-i!Rk~Ih_gSd?OizR1zD8TN7L><5)G_aM>HW=n1Ng=i6l|ZS5C8PI?%M~< z^$_W#qs={_De~U*{wck;XaT8TgpMC9ZBpwbSY(b_@G)GKSXT=c0LHO?2s4_7XV;ha z(-%I7@9~9}`D}sf!7CrE)Ro#B=eLDR96j#?#3a2BypN^#3EdfoD>MiJNHWIv?XdYu zQHhy9L@NC?1_?{8zp#Z4=+-dW9N@NRZV>ZUBsy@EkX&XYBb-J6L7&ypYNZl$Wz>7J z78}jLvRHqf#sc&qEIbA1`&RfZ!H#f@5=ZIq@jh0BcbWIz5AXf+-Ur~lm<`O9AB6Y8 zdHF-|jBm!M_OT;hwO_)9XUmU3`Tg_WN8T7(j`pZuoaIr!I0dABafC`o-P(>KFG3s9&7ORKGZwqki!L1NDmw<<&24P*%VACYbug)d=br%%pzt zX(#oI({1V(UxZS>^?rRUk*jxH0LBDJm?}+TU@q>6_s_dpW4?mXV7Jf z%LZMhDijrqo$T`M?5AaB7yCu=l4tCEj_P`1xSSsP+T!O6dP;fY*A zv99xEUW%^tbY6gFWBHUb!1oH`#0WdnLa1#2_^TATdM0eI+50aKPJCrix21Du~9BN%_tr>u6UqZY@r;! zsi%1uNAlnRqO4uEi@iyMHPm1gW3y1UVH9A0Xa7LH!2R)W`IVRB*Ff5!vYqVh@}2D6 z9qjEc_OG4n!(2;)n1vE2;_peQdoqAKMYX!0t_c*3Mn2kLp8%aOk?hkfa5EHW4C~uy zAOF3J|31WjpXHkOR5PbTZ=Ru=C2#_C`<@TtSicDipxuSM{NbIQSL45+gOX|-1=JWR z8orZ#wS#@t&A!QX*Iz063ox1sp+tY2ho2(0PpJ@uL)ccosj29WkSof9oOg4xCjgh_ z)fe1RghPF9`}L1RZzk#w*v;LwyLh0XqO81&d%0%x=&u3Nu2oHibaB5aROCfyl#q&@ z-QX?*=P2;`t^a5919~=p8>;`hp!tCc(jRC(JNn7~=$$f*eNe7d&0BZnjosn8#Q+cT z!9H-8V<@y3dmxnG{@r}YlCo|-0%$C+=;ouUtciFlvF@U`G1Rn{(F0(2@hw`Arwc&u zf=qNn3hspsypN59_9i)*uAAlKILR|Q;NumJ=!vjppym)>id++QT|R-Ap*@l(lkt&2 zAU?q5!+0o?6vF|ovU0%HbC8dy9yO~6R6o>Xz^4i|MZN>s5)u>g7~ZIOoVjSO0i3su z9k831uMP5vyZ9s!ekuk#6F*)2kZyixd5|Bj6!X9x?A^y-`Rc z{8;oyEo}($rfk>UJW^$s*n60%iyx2TC3ZG@JKL|y3i23$+|G*VF^)%bCvTs47cf>s z-}U54^}C0s)$enBGjd4Hu7+}DY!8U=IGm#=aOCqTxCT!{;r!#zO#b5MqV0qI_6Zz@8pSI){Ce2dS&Z)8dT zhEMt$b~Vm;l>rtgcYY$&sfE^;^ON~0c)FDh=cl5s4Y(Ynorbg*)YIvBA{JT9JP{hY zmyLFr>GFm9;PY9~y_x@mgIu2cjtx+ME0Ih}qgRRaBA0J=nV`>(Y5p(=q5}b6<5a~L zV>RB-$dc(tKz;#tfG74ao8PsA9lwnY4Dz#;1{$*l{>AYl;cM`n{M>GSewD>RNej&N z6L1R=Rq@Nf5<8D!KBv@6wy_VuI3>2vIe)8iz%|+tDK0N|y7=YY{7SIYV@STbi(j*i z?cTw!zmnb4CrhDxK1xz-Y`q?Vgs^{53{u^EE&McztCVr;QRRcSTRUO5ufS`yT!KybBYXfIpKuh={>#pXO}dbM1@GZ&c0K!s?ZC$( zUuAnCCf}pwee842c|Uvsa({e}u>z~(M}oQ5@9dJk3ghczQPv= zhac;UgQLV@9qT{d4Z3?-smm{dNUT(Rj}J2@_u;$0PDU(F1K^ZR(sBP@HWhz)|9#NZ zeU{MDF^GBey+2th;zJ%W6;SE*gegaPEgu zE+q6BWC!{4nJl|k){B`ew^!DmGg-x=SKcexyy9MYuljlD{zh_Tw4xoO^vPg0j1OVc z_*f#$q0bol%}qyRL|o;XyxS9?3MkHrc`V>$0m4U^bPE93 z!v7fC48QVmCcF8+9%oKB{}_7WQ`q1CcJr?rcJO_Bm{)&0y|99 zkn35DK7KiXAHsqhGiW}Wjo@=wDL<4=;njOjKu~oc=t;M{e5tgNeFMtFu z#2n)Y%q{9LpIEF$tv#g8{-`lVV?PiC5j3{W7z!wYZmzJQ>nteGwXvDHF@u1P3 zeMtt5boOO)(AbiFIWcIQoP9YpAGXtj#+liivx3Gs*_ZQz#s%4z9|w(#vM)ak8kb~W zE(;o$XJ391G_K6PTpcv7$-Z0{G_KFS+!!=&&c56dG;Yhj+#WRU$iCbaGi7lX#1voEg% zjaRcTuLX_2WnbP58vn??ycIOw$-cZ7G~Um?{5xoTlzsUmXndA^`66ii*MBMNGQN74 zomLMA-7deYEN7%X!hL0ouYsjKiVfmxA&W<|3cijV!q>9}{1{m6W7!6N9O4HX*oi!h z56w5S%XkyCdNaF&N7!z}6Q1B3*-N~Y{ewr@$NYGFfqE0Xh8W+ExA6mUm-A$v;D_Sy z`yze9r(KCW_(Jrli!4!jds94cnFU2)9^I@0>9uLehUAbp9=MQ zn$eG+ZVcyVAWnOxF`fU&sOD!GOZeIN5Y;(GD?b+>YdX(3lb>%~%r7vm<`?3NJU=$z zh#HUbi;ZXaPmSmK&x{xOr8%u`{8{TH;~6H5@03m=ty9E|Z;eO4W5r~eN$u?Q)y|Dj z2c)DY_E6Gr5{71`lu8@XY;rpj>N%%d%b?E~;w_S21>Jfzbn7pnTd#p`y_SXeb!;sE z6&ug52kviBFw#5`on>d16SLZCqOYxnLbK_aCBbb&TMi|+9$)11A+ARgq~7-1&EH?k z`7Y!8#h5*tY)x60X|2f_Dy=YYh7Yuz74us#VZBv>Kt7Adr0JTNEI>Kp%XfauvsJrD z+rdBG22*7QasyRS4Fr`O{u|)$4%HG_TLKb(RRESBx4`)ga5)n#CZIb;t4uJpGU_8AX1!MX1?hn5A+5}4&VBet=ig?D7`G>0HC$}e9$ z;!1{!_&Wnk1apqRpvA~;b5uhGS@$k;48pcB?R!{oA_ZwNVX!EW+hvyKFgy+D94LoVsWHNfq2eVv(bv}49tlKo0aAyAeN9=&5C4y^E>nzkl(TXu6f|$t~p^J zprJ`8$2(r(c2gQFNHdVezXk#cp}yB+t_IKc!$4wX-R9)-$C-6^dC;6f?zTCt9*?v9 zN8W&!L%Ph_b4Omqrr~++$S(8nnYiNIxQ+$Nfu)&N`N$zwmsvw|PH^~whVl~IuZRJa!Ko zu)}PuvWuH%I+!AuGcC~Ct=P^0Y?UY<;+)%UMlPBe7!nvb>hK$xz~7kxUgdS0o2rVa z*-F(+m)TY&)l-S+GE+kW*RbhB0^Me(PLoL6&c>7!?O`KJin`1bsnn^u0KH;}c+WA> z?{HHZRvW-=ufyB@8$|UDcwcXVnf?y)|AS57|Ah8=3;y2QtOl-gJ%1Or;5~K>f1ic< z2VkOqv(x#9n7w|)F6AGy%aL{+|D4^*zkp5n4||yZmp#V6WKZ(1U>m-MZTJQu*KgSe z{5$p$|DJscov_#7+%*h703YofY*>6KzSge)aON1ecdofz?nc%p#JqvkmjK|y5wZu4B!wb{e$cJn;caM)vPyAlc)bLND3K5_+j zS;V{mxdF^}R+$%~6_4+aSvakW74dR*fcayTlS+76S^vQI>=4(4CjN$%>_d!NKx*j! zNWunN*Zc`%%rGw| zd{8_P^L0H#8NY*lhWUlkJ>c4(;uY~a{Dl&_bUW0Mc?F)X$}!1s0K<$COd9*M0meu+ z#5jPBGD7SiW3)oPp4E=`H3;blGD8C#=Fulpd{w&+jgT1{x7)mKZP2_P0y2>QbRhd? zmwDr2^tT-`c)-wY{<_P&oBGakG;g+ z3>GzJvUXz@JJC3pU2RmcyNx-l+nCFC8;7xnjKkR;W1d2zp4mR`lWrUs~I7J!2W zn7e*Rg>eIq{S8ketY*Btu$?`JYJXPM;&^%mDt^ug==35V9e0-%yCbNDioxcqq$9}y z1V$E@+?Xq(yPF9kDM4LChR?D%Y{B*Wz9<`EZSX;;vQca#g38qCdUW-g0$!@hf7{OL zko0#22MqeIvSJUmy373Mb~YIq@2Ik)g66&5=6mEnk0^h{ybf!ko*S^$DA1pgkv#u#;MqOpigF&4AgMgyB~EMd!yrR->983=bI+h{Cj31bD@ zVyt9m8mrg^#%gwnv4;KJIEr0mtYz2W`9@&7h8XMR=u~89Q#j5#GpV(NgH$R5j!ifMj%>0B=*yU)p!TeOECD>x~ zGnKZ5%`-non#nF@D2{(d)aWj zaQ8bfiP(p|1C)oQxnbiRHo^Q7AqBlewh+JcZwOGo2ksGtKq%HseDqaro}%1JV-r+S z4D(0C_8OS^@lUi?Ou>&Uyy?kAZ1&|JQe0oLKHK&2k45 z%QaMVS@z>>Y|wIYK|-YAyu{R?RirgaH{W@8MOl~CZ#x^_Z54xCyRAV~Gl&+Jz_qmc z<+yb!OcXQDVwQ0>#(WMm?z!OD^T4I&vpL3vtlIc7TV(tMYUU!g+PD~jo1ZE0b!cb3 zHB{lkVn`69Zqx?q>~W|6 z`ul@F6^i%02KaBI3x=vfuGYxBaVl|~QK3eraMM?XQMn2e%N1fmeKea_3h3AKiJ9c3 z8b62jy_^M%E5HiBVB-zAWyY0knsF7YGOlK8j9;>&jBD93P+uF2U$Ik+>lKQ$i+!p! zTA_%sQ>`(0!WLz=RUyk@(^y#^VlWl^j*SOmzGIU$ZuSRm@Vy(Kvu*`$Xd!a~tR<0i z+}w)cBf2bL3d@)!7PfYjSi?t*?6xW@U}?LosW~dE5Uqj9gmDXoaVsc&8z}v2R$|=F zMjO9jrG}oXX{t`JW(hC+FXU(z>1}y_DWF3OPCN>leQBILwuYzoRkHsqM zAmyI~@WmIJC&B^2oYJNx19k;;G4D#aL*Xb@*+@D}CG`g%S6A6(yvv%u+d63NE~~D}*=^pwwrq%1;_R{( zRbfYCm(?)XT6TyiamvjuYxyApj(3$ut1^Kym;`J7Fr32X*idj5JJ(v})BI?x)-q!^ zkaa&)!2_(=c#sV@9s=zjh7$iBn`%4?(fB=^XFLW)zK1O}9)|^f0?PbJ)?_@zPB5Nk zn~guRlZ-#Hvy5li`NnhXBI9{>nel?+3%z)Fg+d-7`3h@|;tPvi>d)>ayU;pHA$cl` zSZncQVRPGZ>u75oklDnhD`GfUP&~+5Zykf>gp=6**0I)c2mz2IHiKcMy-6L3DW)#8`_1);`zxQ#va@~gno zU%^tZsnKQhGFlw~_HDG9{1!b4pcRF8wKK=|Y8Bsj2W`I#bNn96@xP$d-)F;&51`op zo$b#8_BJUgCF}(lLPEoUCI{Wect$UO%Cv{Mx!Y>%wvs`sW2e7~ZtLRA0+02xjDA9?OI6vQtG`sw+G(BN zZC#NY)Ld*FV#X)TGyswD8Ib=uD>J@ela2qdnZ|#?nqPu7zhdi+uUVt<4Qn;NWpRus zX?)K*FvXyWlXd_%`@1E|SiSWN#e&)4&&2R&WBA*wD}C%Q01pyv2kc-ULe=2!K@h3D z`GQ)5F&)<5bhC&>aqBv;Wi|qYKn&WCFD$>k3`VAwXECv zHFcq<9eM~O5X;#=ZwSKY4Qj);Q&@|I=|{abHOFX#nLeaEX#EC$BphBm-Kjjval5U% z*OrygZMx2e#>y3qb8^yoG=B(D%A z1(>nq;aUY0&9T76I7~DTf`>Mq%`i*Z;bs|YFw5D|_||O%v74AVSq(&+qBh@J18g-U z?3`!AtlwLYVfYPr+G9PAC*lE2Lk}1|7W~g|&Z4aa119l1kdu%hvW0c1p!Gx-&3*dk z0b9VVnrJf-rJIFt(!p$$S;fYfv-7ZfV4mcmgHfu_j5}~J`XpcI0wwMP@E7J=iTm9^ zd4C{!x9Z=z86Xsn?x?NYHHDfDs6+&Yya5AFuE*g zJp;1>;^FC8WnlW5a}mj{CNUEtGc zB0I)<9ujIGjIqLc0f7qbOATY*p#CncB!8#k1hhr(P;Ob!<<^T?#aXXd6;koTM9Ybp z^0m#iY=C(*#A}@bD^I7BD7}>R-^q_Bp73^9or^h;tri!wFzJSxK!#>aBqLdpV|!9o zp#E&VjQSL&*a{d)phoSsUcqXm^=d-}Nc~sH!r$=U-?7oe1V!^Ay((~RZi3OmoWN`Y z>h-)Jq?^g2aFmZi?0B}`vflO?9FqPW8|hlF9FDnz^^SSG55{wlYZqYD5EyV+0LYT@ z7o9u)3fjE^0ProCiDlyvZ?xW>2+;5!fi~Fs;7&Y#c!-U^2#VX|?;K{|iDtADsin1( z6`5Or(yb5x%qh&1z#%8IDds8Ql2h4S^E61W-g>CLuo~+NP@MkNSpR_z#pZZ+uo{Ai zZDanO!+-rv4DYM#M!j<(O@!s~x#7`Pz~aGO*4KHc z#=!5w>)xEDSHU3jTnynnDBkngP{h6tFfYVV^wvd<_3>Gd$3s_>uUiDmn!~za0BH=s z1J&?ZVISv5f+oaa-te=#Z4M;Y-;Ep!CkdOhEN$k}%=9xb(j};?S8}wc;MuNH`lPn7 zt>Oq5by$GDTjV!it{OvTw;kxR2lUkTS3rP&k>&CLqPqU%6syM$?QCG%yX`@T9WdA) za@o~-A03{DW^BD(qV>pjHiA^l0e!du4gX(d-vM7$k^O&e-ko-nygYbGNJv5_KnmSR zLdS*>5D~u?1OpCI8{bJwTw1N)8`{kz;`=?!1@3c7436emu&#$IL zm!#;9CQD?&t+zvV5IwPymB?6H^upDo2-()eEPfWc{WHk2(td0eX5MPh)Ee49b_X3C zTT7E-chZruyXdCa-FCoCXFlB_YHf)8D4HeuS{}1M9WVOfh-24|7X5M8rz5Fe3;>o2r-Kp=lc4I4Z=;M6}Ahy*qo_AG zEu*e2VrWyd7!H%87_mvzhnEAKj&`T~O-E{O$$AK+mZC(g8Kj=ZY<(o+UkNG}W5j-V zo=FZPuEenR7h!xIjk7@IP~Fb8ep(#Zmfjx6V4kpzxjBPq6STlTS{#glaJRJ>XTi_y zj&(!wu1331)#;J67{3%I&a{}=`NXRV#i3`^ixN>N4!dP?9$6f{e~&I%EehdIIBxfTFFd-1rmILG2;2JpgoU^JY(xX&C08jvA)T6bqyG13m5 z!@v$L!s2`fs&!QE?lX*A?=dVPt~4O{sk*7uv|c_AmrKynq1q#sOVfdG5IK+v!_GG&G2( z%I6I)PnfkN6K42Ev1BW87RXjg>?ONIEWralCvYUib*&j3Vu^59Zpd7b?#drBSCqT* zKbb2wH6$r+$y{+6B}uU~>r(jMZCRJX7nf&UvPmULu`+Y1d>{a@x>P*99{-=fXhke* znDnr_za9V|;Z=B@0LD&USNEwG4GoTc3vuKh z5L*99<6!We1mp1GFbf|KgYX=ff-j8yNY}yGw>tJSZHoOu&&GbGcVgS=@3H^TuGkK* zU2K=v4g9ya?|B3KoD8q?hK6VcQmy!s1GjZTthI#3q*PA1@pvwb#&9$jA?ms0KXiYi z5_fLL+=P)7#IY_UtzF@yn+dA8J1ril_Y1{_Lb0i0-r}@)XimK>@k`|KY4Py2b-FSh zN^h#7NT0yI*4rq*NSFJ^R>1faRF=MSzlYpI~T$W)wAer#GiT4yTZKy_s676$Nn zc((H)P{pAv6;O9SL4*8uG{$dF2m6I|h+jlU`c5Go3ob)6Q#@hGnvb5u+breG6ddUa zv5cC;dffG?gvN=da3qnoGEzK^I~tb3e&QJ%xha+)u?x~-%-aw0Q^J#&dg9qw>xy#~ zT4SNEHk=wAQvyM$^%K@nAzx|V0*+kc*ayspbbZZ#YMw>(%>_DE=$n>3}27lg4i;oecfTMS99DYe!e6|jl-6UR1i@!FmgU-H5 zY|Eg*M!NV1(lCE4jq(qM7BrqF_>DBlpGZ@Dl(6?F(R6<@9qk`Rv-~ME+drI6hbi?e z7)7pGWzkI`PsCH~`Pl@lK?pCM7_nA&v)ew7*`~G~wj^mg%=feI5&zbMM zW()N%%1j)t_TTmmAg}Ekzt4IDwPf4Afn40S zZ(N&sgRoWFZ{N7N?Hf+NwUblI=o{VeCjaDED@Znr$&_&G*5lqFcEWU%)p)ZUx8Z*NUtKZX#Fd-Ou@y@?Hb zYa$a?xJMIa?@grk(gf8+y~v`mYuaM7C3@A!{OFAco7O*^jm;-B&tvIr9VbV=^H1eQ z8HYnADHEIrF5@`mSVS35%J%TA<*aOj2WvTjiC4lwSO5cWHa=j{FF;)YSxA?K&|C-{ z@pt+sWf7)bhucF-Dn~TB)EC(i;)M0X%5t}$BN?QO$LypeYnSt1riA|r75cAIU;j1x zO~;1&Bax05kpy*-onXdnWy7&JTdYtG$;i&Ki|b6zbO=$Lsx}aO2jX_g~75HTf3fl;grJvlHX$1SC?VcVn1(dlU?2``^){{=euM|KIeM|2=)@|47@}%IN>-Y5xZ=@PG9R{O#}# z?C?tconB{um)Ap(S0y~Jzld3KnCvjys(E??0)`3oCRebIo^IBJSUl>SQD?ZET)nQl6l@vgfBuK_1k+YpRMCNX$XERx$l4I z@Lc~FsQX5qEKdoEz8Q4Jj`fD}6{IR|M-q;losy@mqiP&9KnsV#r9-LTB7SJc(v-p7}D0cyo<$S?|;Z81$P;pa; zie7C|QDRX6B*=?HBwU)3mu0$btsDXwV4@4)-VJc?PL;wXayfYSis0QVf_JY7-n}At zC$iuT^Rw2>As0q)zAA)s_1@uJ%y32@Tj6}g9=&wH?E~PV9Ezx+D%f6`Av@q!N5HL) zfLk2_w>k^B=vQ2~N52+FkUJWXV;r=%9k?VI@s&>^0hij!c?0XWTCtoG{QfH@|uu>3gwcJ zf(qremV%7DzST>NM1ETi~-`PZHN>+WWGFM5|qq~!zas4gwnFQx8ip>t~s#kEh$jU5-4El$bJ2>=mK zC2Tt#%bI0+4=mcCATbn{=V1^uhhu(^fXQ$q^%0|JKQWrdiZQU8??AUIlF7}4UwkQPzZ!qOevvCpv*a*VoBrF;aorq;P1+ezcytmz3;y1ZUCySeZ6 z1n$}1a6KSo^Yz6y4Z;FxJjc2m$ZL=V(=a}|xj~l7%Cvm+%HI47@ZxC&fJp`ze%;Bx z_5|1v3$PyCTIZ;>A|!Xqjg`GK5NqdJs_fk?pWF+`#*+|@lqV)oS1}Pr#6xIbF^MLN zLur~o0HipK&KFYvs;P8|IGnB%)3QKzeKa|)4<`q5iNmRyM~f`T1RUD^EXW)W%I8qC z=?-L-(Aecu^68LW9uAu=v&;A%OtFoPQaJago1O8f1Mz5f#-rI84`-*5&&X$g^NnnV z(DoZnOZ~rn1KZ4n-`Ee6gQtBiP+!?Q0r6)mGsW!rej*gSlPn~i43^8A+#GB*c zS%ZD+UJ4r=AI>&Y)vM25o0|58g4baOb8oxJuUM0 z($h21Q>VGQrhnU0^jW@)DdJ;~rsw1fVV@sB(@g&ZY)lxayH`rSGPYFq!JlzOBB55H z=uj`5y}l0REqe7S!Yud;WUjX|>xcE3iFqW&e99N+P@#YpE6$@H;sUCHRlL7gkVT9G zqA?GM#ylV#GrD2BDA1{D8?xny(OSuRC`4TWEbm4NcQwYqKfv7Vw0y5E@-9J-F124~ zu^cbLi0eG&9A9T)je&g7Iv9s{EAtCDd&QMNmeW|Be%y*IgMB(%NoXXvE0d6_?Cqsx zR_z|QTwISH+>k{+O8zw@A04f5Er*+wH!of3hXx+T!H-tlO}d^4Lgl$SeAY`Hg+_) zBe+}~Nklf%a2d7Rlb-PPQZUzVGPGkYAmR?dX)WZAJE=n41-iK#bmMaA9M7+fI#U~U zrZ(&hESPq{Of$XO(o8XEhM5=kh;{w-I1-=feRizQc6NBFK%uOZ{3oj(N%_5vUok$X z9ChOhKe!8$FQ6hvQvPHwNKwZM(l3}0*u;28krXSb6uAhI+MkA|kyh`L+^K=lBa#sISM`n;mt#r1I{ zNfhP!$1bkV<5qCm)jiOD&#Squ&zJCX@IPyQJg>x*D4g_$|B|nt!RNE)9FH5SB%XZ% z4F@IasCqP0je91g4yZ24D~Ur3+u;z@YJ50{DiAmF7YMj-VkW)?4*oXv6z@RJewPM` z_h_hipY|6Y(0K77#Ey^Y7+5}!7oWki`B$1RzJS=V4Wh)~=xUfum%&QBMtno-#kZ6e z|DYGdKj}U39epVNMW2g*(>LOK`ceEqzlt9%Qd8C{zCA+fc8f}8IomCIbAaNn)Im0O ztC&6y!+Lwtn`*2&7^ojaTh%zl%WoY$V^CbZ#;M}y4yS~1UAWMrwy z)omD#Tg)7H-G#}v8%z)qn?(IhnL|Y|^s-vvgqq@znFwkZ!rkyzrr{hZ0C!-9`AJ6* zU&pkXIuyT-Xhh;^m!vw<+H=$lm|NI&mQ=^I+O;-WB^gyXiYCAu&Y8K1+Uh$gmvcD5 zDUQRPGH{R+ytJCNjxKk3tl*!V(3_}9sW~;^A*a{iZdyznb(e=b4dHGE%c7Iy) z-0lzYl?!YJoioGPEF`JPmvAD=08q;X)bb!6=hHqiPW7^YhRXzvgEMiWY)_MAk`9L< zaJnp_8M2s;lO1TLETLJll;+5e*qFJD=E-t84`#asvI{MeU1^2vMi0vFv|09~hh-%_ zCVSBfvNydb`_Ox`iawIn^s#hKG;8mnPt--AVGied!_E_wdq+UULa!2(FdXrC7vl&F zpU$&BnjBF0@iw$0h5&so8N#_oD`>R3Bw7&mpQauh8MTg1s7kBL zm;?%Hx5Q3gM!GhwuGo-PsC>n_7pByI9HDDSGRB&`j;c!iBH0%-#wNSxs0679Y6+~S zNSot4UUhC-UAtkC^onGWSiSpAhhmQCDOA_rbldKmBdWZy)!M3Vil{QP7~50@IfxQ+ zFcr&vsDs>>%H$C0B!NTIU1NALr2Q}fZhFp*#qcI zT)#jbjF9&sP^!k$-LlcnE$7C*Ck&=6qie%Jo9=X!_)2kQy&-gvx<#%92oFSp>#e|x zKv2y91>+<*+2N>j#^FuQHrIW-9E|#SMj{=2uw%y1t5!;am^bU zhiqa4nr4sAn5r#4jL=0@Px@skwVeN8a;?D9Q1zr*nNq76O%9G~P1Rj)hA=$u!VLKu z$h$iOHkceLochRV)L$L}LB0u-WIBzOP7!phW{e#= z!LZsX1V<>Dl*%0x^P!q!``#I0_s50Z*RT{~>1`Wq4y&hvy_5NvQ+c#{(9RGB!g?I> z`k@sv?=V9QwgBE+yRp5rI}7ce{9m@KdEeu(zYhc0?D6f3{-)KY1|Q3QXNd8t863#* zE~x$B*hrvPEeMe2dyi#63%Lxw(mAB%sgx&AqjvIifcFeg<(bq|&ZXY+EXcZN+n%}T zqrTCI`$i+~8;%(LP*2zuQ&XjP0V1V+@cRPG>s`dF)v9_U3)%;Gu)}dUj5>hJf-bGL zbWEvd+AK>jx+s|b80=0j(jj9)8<8(0D!Pd_ARs;0#;+qU%6{Jq3r{NSp&a%h@(HEY z3vHBsd6|9JEr~BdDt0Q}NPwQNv5MTWERJR6Pi;Tyl(|rmkE}?!p%uwszvfrN#-^Y# zwraBpxMNvw#?xl@Cx1l zRYJWkR$*zAW#uXL#u9{W!Iy&a9Ca#Ir+L+@MXlKto=O+=%eG&FTmK^`pI~n$=GY`541#Ri*i`T9xLp`j-#; z8H`>j^{ag`UZj`O_xo3Ji#rqAU=^s`LSF4^oQP;aal8(DOg2&shgX@Gpp8zNoF(Q7$W(;LyM zs(B+Um0)V&8hb538{P;@C1BDxVk&_a>kY9|m_#{=3XPlrtN=Z{j_Q5PWlcxZC%Q}{ zxsgt!cXb6QEFkn+cS2l19z8?ztT8X2KBp?PRg`171*!?$#;-K;m|`6kV0p z-P5|VExXtXcJX}nbhqQ=Hn8)vn+@K5fCbN#ZKyy1s;_by`S{qds-*7i5`%DrtaQ{B zFbOqm9X*C~1Z3HBjr03)-q*>5CK1Pl+i{`4eLxGGF2U)*@Ogvt3voU;JP&Yw8qVv& z^IV+I!1)mOI)82=P8-~*#OX+!4hx^=^L{uV;cjzhy5MwFcsmd037n6%=Q5E$EX%`q z5p@RDz6440PoUhFv7o*JdE`}4?Q2veUkBB`0jm8osP->3UcO0Fp6h!+$T#N!3=0ydwYinK7tc25tn&$W2Q z1AE13Jq8lsR>UEu_5KjfQR679YL<2JNu+7Z@374M3$`kx%**eoME;0n?&pY1e8?2+ zx^uA)cA-MKj_z!3JyjnVetRPRGS%3EcHq>FoXc39SuU){2>tivPJC<^*3|3_zSe?v z8Z(aNEcTyom;VXd-WPv)5j1EQk?+!G)Mr_S73_XWX7yKu%B$hczQaZ`D=C(dlpfn= zYN%XNDlcjS!iww<(DT$7_uG{aQ3aWy*)U+`N@Ow!j2<7o!Lftf_2GzAN7uO+QjpY* zD53$cIbH9s%8G3a;|j++w|r43FvAzmpK{ue)gh5;?yC-zt4gT7!gdF$BUP)isOvt} z>xtlAS?J=J#uD@k30n>tIS-;iEUKto))-Fk2{lPQ$qM#CT2F3V=N*mi^`a-Bbo>PVj;c?TKKwGtc@x~YoiH4)mD_XyD zB`zROU0-c4V2n%L7)x68bud-w8(@mlH?oG#S$EZGeN&^I6%LOG^6M{H5yi%F<2`0^K#YxUa303t5gSiHR>R*pBjt(ybs1&H_kgi zjrR^wjb4+Q=*@&}W0soaovtQ(XRE_3U0MCfn;+^=Io{d&7El-}t9f(6e7*+ip*i<9 zhU&1DcKXFoHnl`A)yn|RnUtq*)3<}@n3D~p_E_nRt0p9m^|}x1J}`3Yt>Iv=e(~8I1#nUv7^^rS(b_ zw6S-%N|sG+3Jcdnl-jnS4c@i!z2n=mN;M7h?g*6NZGx^p9lGI>bg-HMoE#I)J@m=r zdf0C6p@1)V*ck5o>ZtRX{RPr`O-*bsMTA4D`L-#?*{}tC z_}a=)OlYtxOs3vzi+-@FS#N0MimixuLLREEJ&ZCt=;}r&TGdr4y_w6m^0(YHu*#OY z5W@LIP^K3G?-zqUE`d07DN0BzqJ7lmP|B~!8d5_D<_<8T_tBhY%Y=p6;;6P-s`P^r z6287qr^3$u4xMFLsZ*QIR+Xi7Gh4Y-O-i?9I%K0T)K%!v)#%VQFv2W>RmTNDI{mg& z*a5U@qsp43jHRt+;=bMG{r*w5fT+uQh$OxQ2iMSYv^rF zE&5AV$@JIF`r8K8qQ3*Z{u^w(qMBEuQtehETPT|&;ZpkhX)tx^pPCx<*4-aLA=AHN z`JUjGt{vzTk`j(h>Rpa)tY_Qxc*&x~rItN!Vs_?trFF(P2wg)-jgyQtNHC=aI?iarv00-LTW~8J zLq1KRgG^v@p=+E(`6gs0(${j@G#tFS4A4C(=cB z+$?J^Fr6&7X-B7-&X_Tr<=Vln87|HyNf5VLW!!=6q=HcZd|4>~UJ})Q7>QSS@+sb` z!xXn+{kjZ&cI6`+(^29MOH2F$uW?vKOi62H*%;@_0_8bZTIT_vn8ztP%P{dLR67l_ z5vu_ws(!%5GT5pcBcMizhIf(Bh&A|{x??8j&Gi^_B4-H0d`=E(Ktjy3EM~B)Y z{^nu!C>KXzGgHybQD|mb<^xUc14rV{7*ui?>FxwcGXq!p<5`F$?55>z9_OAt9(Rgy z=fupNS?l3Iy{0xr{)I! zfwrl zFlknVlg+GJ2SvBsexG?)_pJ= zt6-U_rct_vM(bLdru)({x*wga`_n0U0L{|_X@MR@m+HZEncj!4)pc~U-j|l?A+$-? z)0?`1KGZ|$Q$39SsfW|gdIbGPkMxWl?d9t+UYXv{>#q0rdg%kaee{9eaD9+BT95S( z(Fc3epkN*emGKna=*`s=y!m>fcbPuKyIN23uG5El%k^aMPJNhnkDlVasHb|b=)=7) z^fd2VeT4Tf-INpP={fE7kvYZssGL$gBWJiiI%l*#CTFZZE@!fynKM_Pkh563z$t4X z&AGCbo8(++=QOx3uJN4CK~b>dtgXsP>U|Inkk_7#QCycp{LrqbU1@1iA6be&{8J&5 z@uGP5pf`jc)64XBjF^4}XJP*b3+NmVG#+ar2rCo>3tG$|EY@ai%G`M$NN1g!V>l`V zO*OXOod8~OHms0&2x>6*jNniP9NaeohI|}}lzE^cV{){ijQUg{8E$0_Q@uWey6H2i zM$e_e`Yal*=g|RrK26l;!WefR9ih*sqxA(@EcVz|poAj42S;9avjMgIa_OL;pP=uc@yO-mlQmRXg~L?1b6qJn1>H8DOu*%28#^EohlrD3<-3 z7qYY&n|`G)0)I!^k-iwff~c)81@&J>hwIDfbbSTQ)r)D7zS087+10NIS6reiOxp7Q zIGyKh!OD{ZU&fi{VY>z~SORP1ImGiiBDB{$(h3hm0fY-qRdp+eXq}ejD#XQ|qn>5d2S!gy}ZK$ewk-bV*A?04Noba;43*1arHUMS6($r#J zYpO19Hg7Z=$f;E+^A1+Q_h9a~l`xPHjw%?u70-IejUG)V> zs2tjKjb2WJ^a^mUl{8tef!uHhIM!M^Ro_Wx>bq#6cJ(ow&-#*R*q4OsHwNhq23hA& zIn~+RgBT>$O7o%l2-9~EiVCwchzqru*i!Q`dT}PozArGJ;E3^DPAYE3K(N>aD>mdh z^E7_{LLH7$=6%#mGoMB>{r6~vf%^O&8sT8pVw2`GFotIHIaFEm#RUA?HnBDWV_51a zEDbUqPd2vbW0`yW4WhpJk~8Mb-`BGc-!6G&v-x(SmGI3!nrfQOcP!xZwqN=uhZ_lV zu+1(;;G2^Hc{iL^A%ETOw;5~Sv+!OE;T?-ssl`gt{D8HEL#jeVLQ(UeKi`imlGb8- zM+1(e*8?{jfSZlf6+1sx>dn9r!pL+p?Wfb!s2`@opr%dJkJ3!&S7+-d=mL!$4fGbQ zEKk$z`f*yRpQW|>Il5nOrS#I`%T{*n`Q9ZFG$Z2W`d{9Xsn*)WEr$A#uHU|NdL2W9?#gq&R z;I_k7Y-R_L&cSkMqBb2Q7_}$R)pTuyTC1k%SAp8sfZErA+BbmOKLfRY0czi(vHEQ~ zM8AVIJJQTPK%7Su-KAM5q%bHbg74n3Nd_NsjwB_Faw&QW?N^WSn z!V5Mrh1I%To0!7-MsCo}E){$P>j-+spJFdFQ_3IlDUM><(yBJVpDL=8K~F0Tszkz` zp=Qg9I_$R=^a(F_v6ry~LYi%l>);qT(nWAXszgbhfSVQp)QOc8;7^xTh~B7Ec#IS2 zz}c-6w*e8AQOGHrx`?n02DtlzoxB}!KFBr;Sti)W-jT9SEmHO~a*;|@F?3LIUxfg2 ze0wHJBalX-ll~9vpgXYMqOP#sh55UiYT;uTU_2UUP-zz0`k^SHdki%7nFtX()hIg4 zV2@B}>6fC6^)=AUZ!~#y7iR7~CQkR60@`2_wAr+?z;}FpO9cEa!M>Q;4E!w?4GjD( zVSW&fEE;@xDjy1l01X0O+Z91Q_?VpOQI9 zSOeY~LcA@)YwRyc0X9P|Y;2kER%M$Z$&41df(80A>V_i*FZO=o1+(=*MuB~>s@ehb zY_!@UQS2HnoGuiYuGHCdqd}%SjWs=}$@HWXOeM`Sy=X2Z-Fc=D zU1X~05>rh#m>ODPYH6+MOAnZSw8^+~g|0$sDndvqLP#n?NGd`|D$ED~41+^0LilJh zjwCf<&jL0sDw;)yz&*&dHFyb*lkD7kC;UI~4Cj@@(i|KXObI9LD?lmFq+xE-o?IjV z%5*T5fndoLfUE$hii;$}sldSrY%&5TTN9Ptf{ z#yTvT`NN`_Kg^Dh4Y9-Q2w6uMW=F`(Wtim#67a7M$I2tLsr7@b9W|I191)HUg<=2| z7+)QgFn!o~#01+_!QFMso)%`;`*8@e8&3(-NQGtsm70lEVJ6wB<3e~kMZND7VX9Mv zsZJ55I)zgw7dzlER$K^=?P-PWDGiV1hCPj7xD zGUh1q&5X=zG$MM9?P&aCd{lYl8ituyZ61X=4zJFHg5tuo>{s#n&tX5KG>ne_%j>i7 z`pJ0x6#Mls4I`M&au3Mv$h{2MJG1S8Ps{50$f)O$2Le4`lj*f}W|*_^Sr=wydyKu7 zu5mr~;0A?ILv+JNZ}C>T3SLWgdLNMtj$#)a#CKSpf}^c+nayGNS}2cau^sk(4vcQW zp6rCoRB#MzSk>IHY44?ind>(O$8+_j;H0K1OqG+dYv?+J=<{iFa0<@QnG7?r7C>_z zNpn6J$_3ay<9AeK7J&3Fq#@=aI?ODD74~8}-dqO8vIv&EE9h*q7~KB%w8UIvp~VVO zK$iuxnK8l3Fi~?3v2%E!s8^U0%wkxTDcp`nO)w`oHN@%|z=+ABFddvWz9l%TsR}VZ z_>Zp7%dFrI(l-FoH%7?yVJP8H6-Ms-;G7WVGte1@Ykz17&TVQA&L59`{u-0P1?gbH z1VCKDfTLrN(ZcmybaZ-6MJl-1O5nj2JU^4c)o_z8qY-?##2yaf!|SZ`6w-N5?0Mrh z`QqF@4quYV;07D55pbdF;1AOl#k`7|RPaBmcdxCgN(Hy#W2s>2ld&ajp?*7Pbve{^ zgjSiAU|FlE3u+?tG}xZX+yU0LmZq3HX_~nUYwbF$vv)>mIo^^ z+&o$wtb`rOLm9P;gH^$5fT0`BgoB3dt~E3vSQFdKKwqV7eL)=LNL+BVRP1~Qvhnc$w_kM6r(55R`m{-TY+y)X*lch$qe{W-J& zC2)bxjg}Jp4K2YYkk>7?>El#jp1=~Z1u%Sy zI+>@T4?jcw%(FDupg6MG3J=Ni5C~p?wDuw-w3i^A{fQQum+3O|iUpaomMroX1Y0rA zN;*1tK6nA6??+RE7lW5TIrZ4lighlhMYkuB$lnEDXRt{H$W&Tb7XzUG6ukUfOa*Pj zZq1(o=)YuRDtLvaT1>qfycRZ4hfgvOfL3}YK>T`JWPL~q3V&7%Y4B$7mTlwhsEvKY zHee3pHr~nFdCf78Pchp*%WA|#jby4aJ%UCU_k++#Q{z)Hq$ic&usjuf@MQ4G#M+Jz z2cPAj)Vx=YtEu3N)w@5hei{~+>ZhOBfLA3ZT0gwrj|SMkj_hp|llcOu+lJNsZ_t8J z2@c0qxHi!I-FC^jYzGC~FkM-q=w>G_o2``Xrb9#-`8yF+L#KL$yCGO1#2(fcPjT5c z#5x6k3%(2ix&WBtnbl`w@YPnzZwdYmTK#rY@D+R?aa;p{{(*g;^Y{=X`!5SD*IOrt zpy0XrH;C+e$RR&K%KV9X8y9cotY3YQw$lzD;EfB8_PCHBPhup?C*mynofrH&?8$ia zgHfBXJ>eZk?AO1~@IuEGcH)D(F#jnFLy7Ps+%)1T&~Wg>UKa6~4Uu!vR@QYK__h(d z8q=gme&7Hw?n*(&O|de&Kb*q8Iz^c|W+T7?M3@GKf&iHuL2g!W3W6W4BoX!o;17O6 VYdOKs!7un5pyJL0`2VXk{||7l37-G} literal 0 HcmV?d00001 diff --git a/bin/ij/IJEventListener.class b/bin/ij/IJEventListener.class new file mode 100644 index 0000000000000000000000000000000000000000..7fbcbdb50c697563fb28b9bb1018c303e737d597 GIT binary patch literal 381 zcmZvXO-_SQ6ot>VwP-Ec)|%)JbRrWsP=O+ra1GUVNDyBPgwTXS7w5nQxG2;6YD|c6 z@CIMbyXWWY`{NV99WD$U5XM<<`H${am35G9YE`PrK$kGglWk%ZN%?N^Yp&9oa5>}1 z^&)-=o!I7pqu73!hMwyX$S0Wgy4=)BSuc~KQ60&E+}uy?`F}TBC&KxU(UWh_UGWQe z;W}E9W}|?;c;$xK=Jt0Hq<4E|p#eLzE77v8Y7M}{AwRi}UUL%afO?K`*Cp#)9M+N`@ literal 0 HcmV?d00001 diff --git a/bin/ij/ImageJ$ExceptionHandler.class b/bin/ij/ImageJ$ExceptionHandler.class new file mode 100644 index 0000000000000000000000000000000000000000..9c8e523e40a68332cd5b9198cb95f858f66a4bc7 GIT binary patch literal 1990 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 Q@rA_;JzHF8o$CwyA6Ure7ytkO literal 0 HcmV?d00001 diff --git a/bin/ij/ImageJApplet.class b/bin/ij/ImageJApplet.class new file mode 100644 index 0000000000000000000000000000000000000000..689d84ddeb72eec0dfd6ddb0b5be76e36c12fdc6 GIT binary patch literal 1168 zcmZvbT~8BH5Qg8=F5R*$NckuN|p`sO)0tyJGq#{Wb#k-~5l*N9z-DE-?OcifN6HN=bV{m-kCFd`s>?w05LpK;SsoI9Yhm(BWrH1lu9|% zQK1NQ92oD6sNrrzYx@GeMXO*r%L3kTWJf?*D`rd$UNq}yf{&!6RWP?I`90HqZtUeu zVv@zQk=rqBORnpiinDK(1p>)SiQ`1d1RC1Wp&}sABt15DbfQb3C2Kl~LfJ72X_8yR zk$>@V4MAL2(Ie2@fa>T)pWG;?_KWYWLe|y&TDpd$-~oZ2aPn%M@yL#fAs)I%Iirw` zrW~8`s}(DkF>OCaNUGR54TBhyq;Y}nI_ZKtInrZ7$0WjR9-CJHc!A~9q zR%&tyBLjCPNR@v9P)!OWgP?{FOk+S!X|c6t7)*t8PJS^+1rJz#XhBM135d%ToL^d$ zoa&aDlgh{-gkqx}$TBuY21W*Epu-p#n1K#pWng0fIfNZbbAV`|3Qi!&#J~jrlrb)@ literal 0 HcmV?d00001 diff --git a/bin/ij/ImagePlus$1.class b/bin/ij/ImagePlus$1.class new file mode 100644 index 0000000000000000000000000000000000000000..21882364481c475f18729f86bb4e425959d6deec GIT binary patch literal 1101 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
R}{i+uy*044b1t4iLv=p}lN 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 IcedsJKi{Rj+W-In literal 0 HcmV?d00001 diff --git a/bin/ij/ImageStack.class b/bin/ij/ImageStack.class new file mode 100644 index 0000000000000000000000000000000000000000..31a492dfd7627c03e05f65447f0cccf116bae4b8 GIT binary patch literal 18090 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 literal 0 HcmV?d00001 diff --git a/bin/ij/Macro.class b/bin/ij/Macro.class new file mode 100644 index 0000000000000000000000000000000000000000..987fc9cd5ad04bd5912984a962dc71ccbd26c231 GIT binary patch literal 4568 zcmZ`+Yj_lA6@Di>o7pTA5*89#E+quWWdnptOUo@KY^j87fD#}AMVM@cWMQ)#c4xz- z&?{|)7Hh8xVht@VYSS7(7i_deYeB2^Qng-CtMz&O;p0F5@lf$SGn36OiRJ0+H{a!) z_q^vlXTH4u-?!cda2x)ip+JEzy0gB;h$IslR0X9wjb5WZX2d(|*R=06BbI`(Esddd zYqo_NSBF-FSKO^25FXytY9*ub&czCfLy36GGUC=oBbGLOD3XI>wjyH0BWBF(P~f$U z_L!-l%rTI*qOtmwMyku6rVX#ro=DQ8vYD`!L}Jl+)M`-Rsj1zlKn*22Obr1A<8Al( zr~Md*2|CJ9qM&ezeXs}$ChG8GjDnJIG;Xd=cek6#^$vzWI1w>o8;xXC-t9?0JeWX7 zR#!Boz#kqWhRD=Jj~OTMnvtQ`ZuX%<0sT~BriK}L96I#qn1w3hGIytq7)|GyacEv# zyIDhx0-a8yiTX8kXD0oq#sqoP>6nc<3W@|qQ@7Dc5-Uf;BAD&%Z;M6rZyk5hDPplaNHA(nXNQU zB9_z#16D39*cSp%rB_eX1 zMOR+aG};-djvYF56#LPIof>E%r})=*B~7D4$1cQ}?}(E#XHvlgCT%z(XO?2?LR`lN zP)PY3+#cb>ZUVG>!b|~}(qRd;J~Q5tYKvN3Qp3GE_FymfCC%xXx?V>59XXR* z5AHEP3+x*@z6mNzWBGPBoBKpduj}Z?8=UKv52aLmUdtGTm8`+TI3PJ2kZhk(Fr_9m z(yuErmSZK(>G%%5OIK{gPIDI%E+g5<^hqNM`7wweX!!onSJ%E7 zgt)_qSkl(t#ycc1nhKj1-{nxKmOZx;Kh*If>A8h5Gu~-+`S4?kwOUp#398HgrwrM{ zJiE+&?A0CS4kH~KN{rVIfi)RPu(H{SBl67(`S45rEGojU@f!^v4z)1*ZeV}lO&!0* z@3?{$=c#Xs zlZ~FF$)_Jzq};FIV;z4IF{lw#-Bs^8NL?aCO-Z4Vi|wSX;x;z zW4tb950gjEd4uOMY516U2RF$P-PwT2n8LRrOvNqk;eMJE#J2`7K@SF|Wib61iU(0m z7`0)we`r(}%plk@`!c+q{@EGKRr`6!W8l^>gr+bN@!g1Hcy|f`&oE|K# z&!BC|@#PqGP{Pd(j6f^Mg~1tj_o61UKXRqhyc zVQEaoHf*OmA(4M8T*G)}3^5p440{=^V3;vF20iGpL9`D9rrn5QDJ?b-#a%?Pj3^pO zN+<`#j4X;NBT?u?p=Y(b-DVT0R?oq_fX)oM13eiek743jl-m>Own6OM6!4wL{qKmn z9X`rgUqBOsSVieHQ`cc8V6}}x!jiZ>P(+&#AmQ@f$B1RI(eczA3_O^@!^cJVhd4Nf z+99Wpi=s5;G|C5Yn5FO}_48EVOBp>3%vDimHQ`r45kylR`Y%5NGrx zesvj=kzB8iKzGr1jE$|ENr|%$^e`#Avt-aLUco7vmyzIb1WFuZ$MWw^yo%Qx90e+I zNbmel-36Qu2Zj3NaZHqK1TWxQ=kexHHkxIQyJW6(X$IeBhQb96!MZ~pWoiaz8>XIs zclN;4V<-(yRXZ}sT)FIeW~p#_Cyg{536I;2uC#eCV?hr&bZ^)mH1ipsnVr%`gqtCV@g z)yeB0ICzHaN*M3C7=s?$7sGZ$euP~UylH!pCa1l8+rzTo%iQh9a@>!VI6&DQc*Q-%8_4T8f-`uM7lx;JKRAkuypX($r*VmU zKE$*52+#48`T{=Y`me;#@d0;=`lh3V>Mv7DN~;P#fO4SE(h0WqGO!Qx+ML6GY=R^EvP7Uh# O8#$1#_}}eapZpIZ=>O0F literal 0 HcmV?d00001 diff --git a/bin/ij/Menus.class b/bin/ij/Menus.class new file mode 100644 index 0000000000000000000000000000000000000000..0bf68710839f8145810f60dca1d758411f0125c7 GIT binary patch literal 48849 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%0A*kb>*hMa^ryq4!V6oalM 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 literal 0 HcmV?d00001 diff --git a/bin/ij/OtherInstance$ImageJInstance.class b/bin/ij/OtherInstance$ImageJInstance.class new file mode 100644 index 0000000000000000000000000000000000000000..7c4dc4095e6976a12c1d756b75fbb5989ad49955 GIT binary patch literal 318 zcmZ8dO-sW-5Pg%@Y>l<81;Lva4|*{dZ=OUHQVInNy{~bI8B{&KL2SeIX%QDfz5uZN;+#|$>lJ8xk^wX_FFMp!^wjqB6QlwS28#9-_0?xc SFw{7~Ax7#m&8b`*rGsxAo>9pF literal 0 HcmV?d00001 diff --git a/bin/ij/OtherInstance$Implementation.class b/bin/ij/OtherInstance$Implementation.class new file mode 100644 index 0000000000000000000000000000000000000000..477550a67f0fb1bfdaf99d67abd71510594577dd GIT binary patch literal 1947 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 E0T26=7XSbN literal 0 HcmV?d00001 diff --git a/bin/ij/RecentOpener.class b/bin/ij/RecentOpener.class new file mode 100644 index 0000000000000000000000000000000000000000..29ac4995602621ec5301feef7cb6ef36cd9f28ad GIT binary patch literal 1381 zcmZuw+foxj5IvLZvaoK5gan8;6mLn0#w%WectOx;5Cx3li!Nab7Q+UYjrsxpgfCY4 zG z7VbCI8XkiqW$eS_B_JXVlBsb{=L)DrP6PIz- zz?HTphc*k>Fith)vxZ%x)NKH^)Wn}2%3W@)Q|vhi)Y676!8I5m#DC|`2#5bUsSf!=m-kbw=D z_3i4@DSNFoKjv(`;Z^0Nqh3(Y)=@sLw8Q~Efhd>G6;UsN4>rQp1(P>BxLblo1dz^c zz{(AOMEnbS=5vYO4Wy>>O$@vbTz2!@$1tFyM#nB#NFs&-bYl;1_lH5S7yEc(xF8wZ zMV~=hBTRRI*dVDGB)WsBCtol$zkx&L;oJwrn>hRl8I|<-Sad{-j_B{OKcCW@IMc-W z5Ha=|W=bE?-objMihRh$5`(q+hjW<~nIk#%;q^*o8R+Qxb4yIXS z8aHqk_n31JuG9Y>wHm|`%yOsm9K#&$b5~)U-OztAaEAe5V8}p02tnN^Z_9e GJp2t#QzA_O literal 0 HcmV?d00001 diff --git a/bin/ij/Undo.class b/bin/ij/Undo.class new file mode 100644 index 0000000000000000000000000000000000000000..976026cde0150357e6dc7d0520cb664c3203aa32 GIT binary patch literal 5308 zcmaJ_2Yi&}8UMe@U2f#fAgm;yfGC8J5E&vvkdS}`vrGaAj_{Fu9*38^>kWpZbyTg^ zjf0^Qt(t;}CfFn5D77`XcZasMt=+A)cG=ca?f-ed0}h1d_xtYsp7(wBdY>;ZeYW>m z0Mpb253&?I!8KJ&!krNhTndG2thH8E$O;FlnpUl`I}$u^Y-(Fn*LaqKs#D0TiG9~Gscvjt*woS>JaVXO69v>)FK@4|t*vWI3-Tyr zw=~rWikC7Qs%u)B1n?>N*R8e^Z4t3mtnRV`c3mx%4@PSu(G3bC>gkDSEYe}e<5hKX z-0T36*la_P#>*Ex=RA~*%Tb3Fa#$40QI~DSlQFxh#tH>j#jHdy5*DObB; zRV|Sq4+=Vi@o31}&_bu%4Z$$|Os$UGwbqkCD4B2=6lN+~zod;!awBW)7@1@sh=e9* zmk8c7Izqv4Ffo_@DVwlV!BrFKw0#WTsTw1YBX3hQPQZzjT^|hFjmfT6cC5`>6|xnI z>LavqsTB(foZiei!!YzC8>efWhDtI`tPaK*Dp64UOu_EBok&K>qO4AK5BqJxQa@$R z(U>cg?wCzPALe1c2i1fHcdH=0+7*ZI9p+8S^d#;vqg^uXwYax6SKjJM(uE?!pO3^312`?3z_Z3 zIn>0kZWY2dh00^GJ}v_hxl|~Z9YyI>-R$Fw7Ru!sD{vlznus{YvvSJ5NHW_`cV3`z zp_rZ>iUj;viB%q0^iXCfG&(?w-2pox1;PBNlXSR9V_TX|llhT@F_C;}xuSn7r#*@)#B$j9p z8c8TI=t_;N#8={p)`Zn@acl4rTMoXeag7{^rL|5$OTNm{bsAs8_0Ex?*M;YANZ2eB zI=VKedON!l{{-_Ujhk_cTunNX}4*N0@H6J?)2aeg_7fjK{&1v z-gj$k!e%Bo6=dU~Iu7H5jmVF!;`V#kC&?_mE3)p>7>kqTxLf0XJU~yevWz|ogUXIB zYBmAsv$QVQ#j?)|@+X_Uk5`9rSc4T!_mAATwbo8|DA90-#v|BC`P4AIEp5Z(SY|Wx zV;3Ix;4zwz)-;VRw(b;_`!vciku5Xg^gl1Y#gLh%VA{;A(So%{qYM+MZ_)&JFP`?` zDLR}?mj$`CuE7fP5{PL$BS9&kS~ba7%nm!od+}}dUBgru%o^>*cWCcu6PE94d{5e1 zL42JRZ4R!tL)EdEwIMEc4ruI`v%F-qlOe7Sch<(Nb#n5e#!Gma9-xB{d0?<{ai#;L zV{OD7p?FQ>b-Y1Okm85}8)2x9Q9bN;Uc9B8gnLK&;`ez69HV*tpVD?(_YXCGgm*<- z+Gz5DIX&vAgg7caR*2F+*7%7?)l~XWJCTF;HGYPlGr{O;lPr>8CjN%K*$i4Qb> ziC;OHm_{r*EZD&lCAZ=?8ow21`>j>1)%cqTbgi=OsHEyYG(M5e;EJ18u?zpw_%}YK#F*X1 z1my{@8YAX_#(xcMClf*{C_05UvUucLpF%~H(ke@siHDX2I}@vA)1{SLHhtV&WCsJQ z6SC{k$}79M+?^jx)Y?(O-l+Uqqsg&E!Ap-@bpPGNTnD_Cunt| z8s+2&wPcYr$PnZ%tF1ND3!BSidX5WSL`MnG6%3ojTAzk^87MT7@uz#?yG z20V=L@C>+^a7hMS%KL#+nz<)RXYD~MT@RX88ulx|VeK+h0n` z57FI7_Tcr>!kli*>c_gx@b%%+%_t+r6|QZ_<$5(G6mz|H2yWPP5AyhW<8IvAgH6=1 z2e+3N`noZOyMYvLKbkq6NX&34)jL5@8Y%{mSK%P`V5#`H2Ti4gJkJ+gXJc+@AuW*z zm<A79VgX{gB)~#v-=1IySMzN^$7)qh95sK^37<4d-+{0?q0aj?NWm zQB`PFQ#m+K#}YM*{x}oMO`eofYvuyJVX|D_9>i#7%KIq7LrlReavF(;nTT2Fp_OAW zNRQD47&oY3hXpdzu_g5B zR7rBZmrsgMhVoMwcc7CUz-D|eVtfOP>1wJHWc=1(Auh&Zg!m!Sg%*Ud6cJ;XxJbO~ zM)tUTDcAmkx@XT|fY^Z4o&9+7Y@>gL(f`>a^`~!_6zyZhW*S1L=k>wnS^GAOAymTB z+k(N|2-GtG&++B?9^^P3plSy-V50DckB`q3J~2MerB``>jL+wM-s1B?A9@E+N-oHk>Eo=Td?Bxf{K3va{_6EFwuj57B#2#=nd%$gY2Y2E<+{G#DZhVAI ztozNJMz*LNeoExyK28zctl0b27=AgNj0e>OJfte|u$qh>H4WR<4D3*I@rbJ7=wFLn zYB2}km=mTsb`Pp;EVsEKAGoy>nv1=ug8kX@8m%IPhK zbs*I+2SW~Ess}6cO0qC*Ef@RnNun6 zUjFKt{z`5Yq*M_z>$wVU4NIw#O2v(eVsEiOrAAg3yNYwTx^_}2Cr##~knhi97zTJ@ z?B`|i0xyUcnKCc&0(hAX{}nd(SD8Amp_Shn%kT!%XsIhEGE^bBKxt!;O=)~dbicDuW+mcqW@e@-St67*S~=gc|h zKmY&TzxVt8KfHGQxx)aa$deX4f`PI1)yre4OlKtCn29uh$%0Q%$ie#dNNdy#3GDhM z^-cA27hm33ziNR%>N#b8GLgUeeml3<^|%Bwb3{~2iL|jbR}Bbx=Gj#RQEn##m< z`A}zdXC@Y}UX~jtC%ZM8h^8W$Xk8=|p|q>WJeFvQZqikr)f7H3BbJC|W(mAw$1SJW z^OG&n5WFa}F$}|Ll*O?`bV+CX+GwgNvNj%7m6FYo`0_|9rtj{cFVhwy$w7;I*^n~$ z$JW=6Tdp)mE6s9_Y>c(2E`hdathFs8n8YcL3XzSO>X@3ldVVsVOf^uN`1HQEap;Xv zdSeCUpS@fsSD?y5C0VOGFR(El6KI`COG{(4BSHfuQ-X8G_QR;}Pyqzz+9*KC!X!a) zZwKr5DK=~r>Qjx4A{5ft&FzA+&rp))pc=GP5Nfn(7U{HwXZblv_RT~ zld_v@V*mm{%;)ID8i&S2EU>W<=QEC)lZi|umY}D~@|B(67D+Wmuk4H_nxoUltqOvH z;l23$c|lxAI+G_Uogj*p!4ezA%3v8|WnC;D*RhLj48{~iV zZVOHRG(zj9YGDwP19QaqHvpXlo=sH`UtN9!+ExY-%Q9@(5xL z^JffYi7sI>jVX_%%M+dP_!%Lr!sTkbl|g9iPY7Wmnr*azJVQ*_R3^P5rg7M>0JK7@ zU_w41jNMq(R@K~IrKvrxoVHA7Qk~6=o$^F7Q{I`5wuBG^abx|-VX--#iMHE_gCS55 zNu?s2o0A>P;fj34m#$qOZO%;3cX9U+gMz;6){w5rw08)` zW`8sjb>qZ0mLDu!BN)~b7LMZcI%DycXex*=^J902lqWkg<;ivBDS~gbwmgKZak>6n zXXAR@KubhAI-&_;^#~Hqv)oA}r|XIvZEVFh`XG~ZJ_?46?Yo3|D=|W*BlY4)I%6c) zEZ%P8X57M@Nyn~Y_z`UDb@&@LHsdNrMQb#(%=Js@$$?7&V7_XxtLoMhuR+}m5JTu>CGd!!qh?@D(D-J;^{vawrr3{sgDitswP*T#c7 zs2VOZaBtyZqFJ6O^EPLqIdwvK2%{}@oh)r*TQb$7atM#=oNt{x#}te_I*;p|e>i#0 z!gw;0Pv=RD4q+djvG6ogmNKnibxdw-h$L8oQZ~MgXX#IhKEE@SBI=kb34SC6^`B zF~vMO$Jig8hP2iY*S2l);u( z5`VJsU-&agu(eAxMcX^vc$?CXk^Ab_PfYI8K-jB4f3fjb4I_(T)Wm4fu}>9G{w5gd zg!3ujTZ8cLHjd+j6NJVWrZIC?=6u+vZwrc0{rbeZB)NN7VD>Ke#Slgb+Sc}-V0 z{D~z6mIN8jiX8FI)>xu?VNyZgaf2-(t?L%6a5qESmO?3V@W{=LL3xR5h8R;`DYj*h zhQFqm$wN~rgEE-(%%c{Mtd!a^R86P~%yS!Z9UpE>SWaV^<@i!&n(5C=98RsBv1)gW ztaiu{d|rXyNMq-s&y?T{(9Ox5i~AZD{Rz z#<@RJx-)H=C9`Q67v!3fT1~VjuKJvsD06L@r%Q`+OHGwgs&%$3(8REFpV$oRhx2V& zq#t_Ln2N$rV5;rVRNEnoZE27tMEB-Yl*o}QNrF-A;XhBTWwu--7aL2ZiQ2lm`3!{9 z4Fe&0BQG|xfi@Qv{RYSxK(Y2#N_%p#;&4Zjt$9G*7Ujmre9r+IyBIEGPgi7G_a<)a zTp03Y2pr7l7&Dp-Stg2{@N*+eSGK3g&>QQRWC^E+PYDi|aNdyqg4fa>sxY^3A-60+ z`4X{sQ8d0Gnu#?>Le!Q0^e}A^hxG%E&VF-;bl7sGP(O>wrewHxb!Te<&=G_GJ0xRE zry_F+C(cb|Vv%?(!fw7Px=Bm0Y_w&Q5UYaxY|xJzjyytU$fb0(E!U`6p_Wu+V|TCU zZ`Qpm4Y$q+G0-tuP6i@MI#l9fEQ*Jr;5x#JF>=?nAWDkIO%MaPyDzsA~@vDqTnF ze4-}y3Swz@k3yn-3AfrE(>c-V+ zyclCLMGD#@o9be%v5cmRIjfn()+4R^R!evF3gf1)((@Gm*wwU%LhoZXN->mMqXegi z8k3!==BQeL2PFCX$qDKvoWZU}4~;@N4SodpX7SB~ATJ&=m}laEIqCRaARC*-yYZR8z&6 z;*iaWF(xO*7*`B3#d(-!#2sS{?lYo&!4+*eDfmb!MA1f;)E>Z$9`26PQ@N_faI0`; z=D3A&+(J2SAHI9THk`%30zQMVITaj|(@qa%c?(qx!l(PuK--lp<;Z?qR8i{Rk46>S3w3pciK-bi+Dy8777ui0Q{*`` z-(1vU9xlXuEJqzK#R8+&EJwJUT5ED@t;wmi##M_-;ESa1Bh>~{3DA5i$a5%sCrBV* zF+D^qq8CtDSWE+4f|ZULq=<7hxVBavMd2Z=T3K@G0bKS%Wyuv;tlbIU(-qHRa`&fo z`l({&vzX(%g4W_Ey(S;tgPHp5Z)40f=!mzc$+$secce4XHHsm!z2 zJn8~0p-xK~WXqTi7crYIW~4V_9GWnh8qcP#_0)6)^|_Q9t>fua2CKQfUxOQQnX#jj z6I*k3+=>J(C0utdlFVq2u~ZPgqJrYTk(b)0!?6wNtM{0UJ<;c6?Akoi?0ZbcYR+D1 zNb6GyX_Ic3P#`CZPf2Xu=MwA02A9}ImsmZe(gf)v*?L3F$%+0YbBL*oo7|k>X^2bq zZ8sR2%Y1t=piI5;Fv>J44&myRCD$InSIV;ZY8G2eAgI$GWQdf_?7|Rr;|O1ixlL-^ z;*k-$eD8^mbwnS1Ez;=3P77nFcV;B$izLpb?Z?xM6KKH6G~ry{7t@57pc89pg&ve^ z&Y8X0D8gej=GoV%lH<`3{Ds4U$N1+2jrXtTka#t}D{2m`$l|6MWxEipph~y)h*M$= zdlfBlH8JKI2H&-Y?NDc#%hpd%tNp};5AVlNq!51_zUkUB#`zjNgOvX54OK@Ftjgl< zeX@^(4kkFPt|OW2N$v(7y`gIrbto-;e{)9el^(}5N{V8MO3-(`EsFx#F- zIzKWrQnd;>@t`Gw=Uoo5z%YttQ`*t=U-eP;$PKjG1GD(Er^d&z#wWOt|029J9D}^7 zcxmd^@*2)-5wCJy%Man9l_N`i2k=Nk7b*_nQRnS3zKuA9$DOw)_*QfXPdaZ;HAt?=ueJ z1J3-gp~{!VM=ZgAsN#fbUX#mwEt9hN&m~?CO0CaZ;~#$nyT)hn-!%dL{!fj?-~X)% z^7oT0j@1^7A1vIsdS1lD+E6&8W(pmYz)s8{wW*{Q4mEb6dVE#}__8vvc0hQ5`8?A7 ztTGRl66zZsKz*6zjhs7#Y{{V37xwXOSXRm^$Y&J!96^X9XLK>g_RHB_XgefhR^|(T zK*rSu!hx(*?L@&~sqR8xuuQDAG!itBG>#Y!N;)f(Sv-x^t%FiS8G0h=W$HqZyv95UW>y%a+y)<>%u7WI7c6cDC_z96w;@Kjx6;m z-5w0op$k;X-MXe*;s~Jh1-cAcd$FUlV$xbmae;T z1(P*_dsv|EC9>Sdl)s;c+z((ocJXO99>E?wg}po`dXNW_5Amq+VZ6ordpvyo2wnIS z9+R{21UJf0%3_|gHsUGyBKFB@9;G(p8EM0}CC=m1Yq4LhV<>Fl>F72blyoVtN5P0j+f*eyez-OEAm&KIUL7Pj}PDX6yjCSnRv}} z4u0U7j@La)@I%k#c*E0zH$B_%mS;PD{1thflU~)3Syu z{ItvMhFgHvx!G{DXrrxinbM=Bt~H%%0qu3ATuuo>w9;r(=?Bmr%j61)AV90kl(qb7 zQQt!PVgpwzNWYEKkE4ztEmBxm7&?whZNn^Ni?v`Bev05HSad@FeyB~Fna2t6@Qp7g zSQJnG;B`K5&fpUSX`M&#K0$s190DJZwjo1m(xS9bxsIPsP?%FrG3;dB37Xes*G&dE z#v^?hGmn%$#jI(eP(ff=8A}@vgqd3E|J?}Gne#6H11Gap>WmE7W zn}UznJb!E=$!U?bjUGkz6u|hF=D=*@3oY}rnG5eSl61FlGsX0g+6eiwe1$@*e@x{b z!pbdQB`2p-t5L)9o5%B{{+jk(D?RW>!`JBqJUPA|H-bg~+~m^e+3;oMy53~|Y{>N5 z@agG8pT(?jy%+zb-^Dyjbr&C|X4-c&5g(@>Wq|L~2dyOyRYzg-#T1r(RfKxe;ulpF zTUEr0tZdt-0Ng#pf#Jcjowb0aQc*aEa?Aj&^~acdf2VDaa|?VzH{Tc~9*h$&v(Rtk zEO%td*{aF4RSD)Xgmf>bUf_Xn!7-FF#)i-nx5%xoE>*m%-94sh%JYN_$g@JXCq%v> zx4A1%)dsN}?CBx%+aMK2S>@Uw0>crPSWXOIFK5V*e#N*w*KJlBihcWZ3Fj^B7DMC? zY;##o;$6iF(i_#>m#gLT^&gqHKImFkV zY{|+!MF-{HTWhRR>s~iIxQgp7rbr`cOf3ymIWIXvFIMzK{v_>)ftSh<^)u+8QnU`0> zIKs#AR(X)We)iO1Q{w{cuot>d3o%+A;%v)g(O~$GQyfbkmLmHYM#`sTEc#cBzSfts zdk4hqey#rqE1-jlJz-F}U%C{n^TVKyrgRQ2oRG>713eJaXy#w{Tv}IBN5!bKDxQ_D zBj8R^x(-(mDfggQpNUozd{?lsESot(qr}-RIRRHsoi8-9I@oen&Z1|z!8@9G@i?vG SV2dA~@;MrX-%^p~iT?vS(=4_C literal 0 HcmV?d00001 diff --git a/bin/ij/WindowManager.class b/bin/ij/WindowManager.class new file mode 100644 index 0000000000000000000000000000000000000000..0de8a34821cc8d743f85efca32946d6f16f7e99a GIT binary patch literal 14811 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/ColorChooser.class b/bin/ij/gui/ColorChooser.class new file mode 100644 index 0000000000000000000000000000000000000000..ef77b9838c727d7f6b8b9cca90c6c1cf94b9eb16 GIT binary patch literal 3651 zcma)8Yjjjs75>gVa+Avq_%)Sasb@F3C-YnRFh2 zt!TAiYf%*ORmDfOExt=@XM&AZ>Z54Y~Pl3j*@O19=ShaprtGXM-~MDMKj)Iu))id(0Ut0C#JdDYCt2(4nlceZkAZH5 zT2jA1>m12hcsI5vteJK4y9(}v=R0lVb#&qm3%6jSyxnPGJ#MO_zIqMZrLeZF2OSj) z@5MTDH=VMR4~YHU7S>{&N&5i{_h67*@`eKvI3Au5lY1>RpixXlENmB(S^*&umF2jF zCU1GCg8wh^%Hrb*lRCniVW;up?XgP1`bp^ zp=tL3__}NA?5ae?Q=S2arN!6mE=dhH8K+V+vnD3-0pac?gH&i)1BrCb$>&TgfO={}p0Vmo@J|#s ziDMRy;{@3yWS!*|lH_ucz0(G~%@0|47$0K<8GL4gaK6)ZQZv+i%W{aS3yom> zxP>~HCLY134197q`c+mI9>png=%gGeVIu_G+L`bJUR4_h41Agur0l1ZMSZ)8(|Fv% zXYd4Rpj*}TlDS_+-i`HCJN5`ZYvFUE>QK&oh!tZJpSSP@@zkWJUSxCP%oi=Jfob4N z)j%ea*-R=mZD&oK$5$+T6GUOF4N$jUZ4bM=5q6R5yfm!Zbzg(+UwDaS1ONc%D!xeyxXt znM{hB{TW`QcMEnl=fpDwU2Rv*%DCi6$-*KoQ_mE=UoD!eAbQ0@)a&9U3roVMk#!bq zH%(xQt>u=A)+_Sx{2@DbxwWpO;r6OKyls^*R>*h-zcKJ@h4!*&SGBe9Tl|h0AX>{5 zvPq&Ur&DzTP|VkZz5Z5)H|HNw-D|yH7mR8kH_gKx(>!RH~5w(b0pXWya@8Hwq2sW8oZoi#xQO^+X#Abd8Jh?#WH-RM9b<&>b zgj^x+$Q6I%cbMZybP*lV%jha17VR!#YtK~}FW^0Uqdi6RMSDu;UY$5+)>0> zG*ZOw=!Q@c;{q+Hp>5IpY$Tu-zO}KnMhLW>&sz!ZHg@?en zJWxcso3H5xa|sKF8q8oYxQJ{Cg=lvPA1&f!aPT5pX>qcxgil<->1YX$oeHQnnJVGT zQ`i0}mS>ANw`_UNvpi4BFZ-6~JWCor$^Q#aBCIa)au+dqYducBn1V8d}8n zF4282JO@Z4(8*AAk@hG?(TxM>fy2N&j4kX(F+7etSjb~|23uLq?gWK^S2+HKJO0Yx z{T;n{gFm$Xg+9EAyLAZ1m>8tk58-CKj%RU^0XWT&f1lzH;JNbK8w{T$NPs6a%a1E! zdz0N58OA{JU&uUA}Y*Kr*q*9lgg-AuSWOzLr}CqV`5t8$^Q>_Q)1c$G-P p)Xi1=o^dv)C{aO>d+YHByynMw4T5jUZ;GI54{*aD2|0j2{TGhTU+Mq= literal 0 HcmV?d00001 diff --git a/bin/ij/gui/ColorPanel.class b/bin/ij/gui/ColorPanel.class new file mode 100644 index 0000000000000000000000000000000000000000..acc056aad8fb6c1e01adf727ab022bdd73411d46 GIT binary patch literal 1825 zcmb7E-%}e^6#i}!vLW44f-!BiQ)#U>B$m`zf02SMK`<2%31VrhvLu(}!e%#aHbs4M z?1OJUI^(M^zG_FHV`uyW{F@xd@9u^W#TS_g_wM=mopZi(|M=^--vQi!V;~}sQago4 zR~5>x?RqPgBW(jQfsq~Sp;fT<{6ZiT7}`^HzbPOp0`aC)jixUU+jbouXBPX`^Awb5 zRTCIJmIlEoj^9*{@@E9n*?%kNCe|6R?AB!xQH+`}krIezC)S4{@QjHe#P#i26GnJD zYa)qL2F?qd{zv` zvt2fkz_6}MIdZw%+LGRywPj0zu|>CL+3S|4^mk7f^P8$ejEg5)$&!aUbfBl0V@Dd& zU-9I&^gLOws;?-{WG9Y0F{fJ6=_uDJ8F+=s`c?H6N#rr9D=08>R^t^&bfh0Nm=I31 zRU3>j?GhW`>bAn9Lv2gxxn%~-7d)%oRJBfNVm*mB@Ror$Nsc~(BejX^cvTxrnJD2V zvu`WgUX?Y5GYCjSN@51L47@YQq5zX5Tp0iqu11dpgJjgTY?`n%S(A0o+6%q4xMrdr^j?d@ z-iwAN;E8dOfWsWMW|jY?K%`bA6Uo)F&~z{#VOD+iaLH=-c%7`eU9TqRmF90M09tOk zZby2PIuXXXj!~{v3=zGZyhZeG1}FC?IJu!bhxjvbn)V~S{XkueXE}F>Gr1JL-^W;P zw78FSF29dVF83?OzRm7_r^URd!+SdS6z4X^o{Oe&fyzT%+{dL~7$$-j`TrtQ19usC z4Jm9O!>L@xeN5s3W;uo{)Q$&m%XkSd^NRrV142;xb`@Dh5|}`)2Xl(Cv~Kh%@*5hH z#&U>jKT{Uwj0gJ7AhsFW_G5ERdU0lG9idM=sO}hXGLY8U92rE&gjW3s@g6#X;{V3f z{0~e5ukqCnMj%oglOD>WJd4U4;PreodQ{>8-VTM+;ZfcU&-u-`J$)gCuYbhwV^n@Z z{4o}n^VhUDEiN74gM91>s@nPqHl||v^RWa!(uw*3KFibiI8Usu6)`HdbVH)S2#e$h zVDs&5aeNM68kY}78}s;_OYWhH4u|POp-bsQe1pB9%o|}T{l-0FL$_Jzb$T>dXoOK_ g(d4M3^uJ2~9gcRFxj2G9F)d~dzAb{8^oXS^xk5 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/DialogListener.class b/bin/ij/gui/DialogListener.class new file mode 100644 index 0000000000000000000000000000000000000000..88d4184d0db9263a23b304f0fa60bd79dd17fdf0 GIT binary patch literal 191 zcmX^0Z`VEs1_l!bPId++Mh4N$EdBJ-OnsNk#GL$epUmQt)V$Opb_Nzk27#=^vPAuy z#JqHU|D>$cHRj)o7^40n*$%w&jB);?hEiRC5wj^QD$ zWvO{3)|yd_3|ztarA5i9Zkai$j10o4*6M+@urV?)GB5+3$iTn|bOkE|8;Hfg4kVcv FH~`MUGr#}< literal 0 HcmV?d00001 diff --git a/bin/ij/gui/EllipseRoi.class b/bin/ij/gui/EllipseRoi.class new file mode 100644 index 0000000000000000000000000000000000000000..5b33cd242805d728e786f83505d7920d90978033 GIT binary patch literal 8454 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/GenericDialog$TextDropTarget.class b/bin/ij/gui/GenericDialog$TextDropTarget.class new file mode 100644 index 0000000000000000000000000000000000000000..5acd2888363ba196a0e90b69ceeb58ece3ffc4ec GIT binary patch literal 1092 zcma)6YflqF6g|@}Ok0)~XemCS^3Xnz^?_)DF)?obEU-DW#f{*j+C zg2oU20DqM6&bDc3q6r^%ws+>-bMHNKw!i=U`~_egPdH)>OX|pJ9V_RB^kt};RVBQj zwOp4cx*7)Ux(HiRb65=XN1`hn@mV{r?>fW7=dNgL88TR!k{EKe0no7QDCxPI4C9BM z=mrE^9G<&E3mpo-b0|ZnO2JNqG3cktSNa*lTyY2=(v=!iZYywQ3UN%@NFvP;FP0if zOksv&nxQc0;y+bvWRYO7J33T;t5-P*hFKdN3G-{t#u&yKGBxGPz2i>@GOUXOPcqEZ zf~N2qB2*^sZ(90;>M*R-E}!7mIrDy)%syDNRYS!$%PfLj~|lJ{TiHWsnO zkfgiXkH%rxxD0PkTTl2crx!V2zi+@=`^0uFSyQACM>cceCZq*cGjMF!Js1$Q}C z8L|;abh>@glx?j7-^QA`txTI_r*1{_V?7j25_L?PVMh%z;3t(kJ(Cd7{&ibcJa%-O%AEkQw6i&BW*-N6-r6 z5n5w(l0=Hu35s4(Y%tU1@36~XkvT)ITscGjG=g5Cbp{sYHem5OCirVi@i(|i`}tlX zuHiaGL=hFDIdKCw`}6@}OdGlKH{>fnFn^BaFG%dJo}+w97*G>w(moDTDQ&R)Eme7k yEPo$WDfIFORdR4}k1{jV)qOl5{)d!F{|kE@6^p`k3S)SL4SJg?8Ts%r5`O`{JO&d0 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/GenericDialog.class b/bin/ij/gui/GenericDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..e07710153452f734241a92cccf829836cdb9c559 GIT binary patch literal 45118 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 bH8_@HBg71j_>1(zhFg&JgRgKz*`c2SzK0$W literal 0 HcmV?d00001 diff --git a/bin/ij/gui/HTMLDialog$1.class b/bin/ij/gui/HTMLDialog$1.class new file mode 100644 index 0000000000000000000000000000000000000000..aeaa5022f77750fdf44631cceb762b898294fc87 GIT binary patch literal 634 zcmaJ;%SyvQ6g|_#Mq@OskNT?CMHjV#QP7nlhz}H^2rb=D)3HuT6G*1&Z@Cb`g&*KY ziFaZ^uwo!H_jT?)bMD9I+dF`LY+5KV%*%n-A4%`BeeIt~9>)Fpo&}R(b-*9^lb1fq zsPCP0QM)q6aZgNvVcNk2iZ%?) zIIu9wU^H}r>&r;ojD}s2w0Soa40C=Q@UX)Zsr@`?Y9b7a<74W3M8qY|MEF)DcX2Wl zJ%(Dt*AMgfqw>Uqh?MvJrgLo{>dA~|RsSnuuv_sc3B-leyxbq@c64isoJTeu3S5h4Y-%xO5h&MlZfZr<2(>%9taM_K1bN!vXoaC;JUN$bgma eWDeBOMO5fCu#^Fpu|l_rRa8+U%ch(jOWzkoX@!jd literal 0 HcmV?d00001 diff --git a/bin/ij/gui/HTMLDialog$2.class b/bin/ij/gui/HTMLDialog$2.class new file mode 100644 index 0000000000000000000000000000000000000000..48f55da2ce60262b3c7be3ffa8a2ae33a6cb8f3d GIT binary patch literal 760 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

Tg$@xEj%vZ&xE+jcO{et5L3Y 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/ImageCanvas$1.class b/bin/ij/gui/ImageCanvas$1.class new file mode 100644 index 0000000000000000000000000000000000000000..8f7f08ce874a5074c1a65b11f0f404d9780692c1 GIT binary patch literal 2119 zcmZuy+jA3D82_E*B-@13l9nb=C2Wk%VxK1cFV;J z`sCk0U+9A~K08Az^5Baz=Ug2*cp1lMXJj0|Z zo`#4(r!}1`2Uad$)yw9H?#$}7o_!jmz=mmkR?h{#W#`7Ns_BjEwMznVzhc#T9w)`Z zYRw^%&+2xMw~ytazJA5ejhTk8J7wD>d4lCN>$*u#L|&kkX(RAzu2*mkfmBni=vt0X zDlMT&*CGwI>~zevND^a-DX^ufWX_t7pBr_9nt7a24(X8PSpE?KqigL=Xhj=ik0`81 zDv2m|D70gzK(vodt}j@Qc{-?0aTlg+%D97yrP~v_XEEJiO74w7W;xguS@!IsLPWj5 zNunPE8nR?!b^jE0V-FQvHw?2DcDZ-WlJ(b1(438-Byym!p}AQ9zp+u+&xynh-7!p? z-5*dmh{2{C>TBPcZdQ5epGc7YQwmSx8A`{h&Iq(|wqd44JE&10hj2v0VVaede1&H* zOi`3gpIS&-ef`T-;+7_H49{s85lA+wLR^L8I6?j7X$?D(=Vvb{}y=%JcrOizRvl&Ph(ZU ziwZCCJ8_k%Nrjg~>Kd2!6@^#v8s$z3Bh97iSQ%e^pp(#HXqci>{1;m&Q7D1ZjhmMP z-KN~qef89A+Th%oscG8fq5erOb6KIntxc3n+w@JUSQ68)HPD%8Ym{-^b?qtLQ>bzt zTr=k6OB}4LFoVl9F%{h@*=Et531&u3ClJ`y@X5zx?H%)6Yd;l_TSGA7F=19`{A*N~ zNfvOm$YJ0ZWW8*b1lHBB%&0rdR4difxHPKIG&J8@nJYn&6rB-atUM>#>dE7B< zw`MuzQPZ!uC50OtyQ=b~DSEUnY*pnQ$Mi;QoyZ!t(@IC_*2YsQz63z3gYQ5H=?a99 zZa@g>0)&wEpEM%$wWEXdHxO1NS|hkCJFtMx?Cu3*vRfChDcdC%usPcmTR?ZVE53kj z*^jaPqfq}*`i+x5ydV{PBDp{+1u#6gT8x6H?T$QWlY?` z6AW+T$wdqm2JRs_u!y4{LPifai06qAr}vQRp4&Jzhc*JmIcSm}4a&x=b3{c3W9gW7 z3m3^^<{r+I&RKdq!6bosj1YC9PFnJi59G-87H(ZutRP(y^P% z{^G>%Tnt@oUc~FfkCJCOs1d*WbF}jN*0h`>rT9(kZg5#CO!1AhHiyg|SSfW2ZOo=` z+aesUY!qDClhhl?uaJ~qqf>r^Zuu>G<##wBzsFJe119AIROAxgmp|eY`4c`P{J#7d zOY#@|C4UtO`J2ed-^C93N7$XQdIuWa>BZmhXdVRpm;Mtqnv@8R;sJbm6~!dJK!90D z+P=HELed!S(@D5Wyf~IHjBA8xxK42%L`U~Sq%|BH77wvb3~MMnM23EuR)T*oiYS9j hNOTC9+DJ~`B;^R+!rKHT?dm&tm#`#-#A8H%{{pn*^V0wT literal 0 HcmV?d00001 diff --git a/bin/ij/gui/ImageCanvas.class b/bin/ij/gui/ImageCanvas.class new file mode 100644 index 0000000000000000000000000000000000000000..44151c355da615e04f9509b32e371d403084445a GIT binary patch literal 47610 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 Dpl)U% literal 0 HcmV?d00001 diff --git a/bin/ij/gui/ImageLayout.class b/bin/ij/gui/ImageLayout.class new file mode 100644 index 0000000000000000000000000000000000000000..21485be9add26b5408dc4604d97820083191665c GIT binary patch literal 3661 zcmZ`*Yi}G^8Gep;c4v1zyX#BF$u@BaC4g^@OKCT*?bvA@+PJlyi&HyE4c)PK;+XW5-{u2k226NWs@df^ES>Tvj_R zB!X4eLK3YCnJKU0&a9Oe-P$Qm$%z@sJxo5d%pb>kL3gU1xwmt8-Md!o7~(6MlnqWidoCy-OH znq?%n$)>3#kMXJ8a=8 z#F^M7A!QO1_?(F&3h740rX9a%;Tb&31j^3kItY`ZJ!#=N(T){eZ@K78Zc`SfF{6-R zK$DeK*C)L?ni5`7S6~{#ILT% z_lp)5bQp_O->;S#kMGpz|3mEBu09nD9;KS7*MLVMUMaiMH`1XVf0R@O+Vtw#v^U68 z&qX0M>pM#qrk#~~U$vLn*5$Qw&^=O*Bw;^q;ZyiDQ|mb^j79&{l^Kio#!zy!>$Ch;YF z#l)A%xBJJLg|CV?tLCmUt6;>L__{*2E<3watCmWOPHo=vi&M^`TQc#DX6l9llF_<_ zZ%IaJW;7jOww>5(7QQX6OeVIC;8hDHupq7OWxwVe)vRl7{loef^u*^&9TD;2}P{ zXc^QoXfwfKpQdgn+Ob=oX?^TLH@|2}E(2Ou*o%+Vy^M1|!k=oGjPVGtwTXSfVQ4~O zpH7Jico?5x5DNS8$-1#8s3$J(GnQ?Nhs3VO6PS>A*av|LiiZS+m| zZ{d-#NY2>6iGJJI!e@u0+2|Gy4##Zc29kZZaOf>`4h-DFcs9nzJ9v5%V@|!rSKSbiFe61VGHoD)w_bZxze%O%1@vLzR$K-J;Po*8vVxE%YfgX3~<>;&& zbp=O92Y$^^Bf1d7gdR*RNpLfS*CtZk#A-irdXz{V=ISJo%Ja-)x>XL$tLViq(5JyU zsVT7$g0q3o;|l~OiJ#&nYPHbnn{X(_s~_`N{?upsxOZrlZ3(HOm+!A0(wL}5nPHcszP>e?n4!(+O(NnOz$Fz%vF%Gn*1 zGj#_$iPp1&`t;2@S1RYyMLlaF1CUF{dP>iGqa|&kYPuK)IpErbJ6vfvSaLu ze^B$vupwvTY^-Dag*{Qo#k3Ch=J^X7!j9$(Ig>c=vST)_zx)x}@ds)batY17;iUMZ zl^uDl->7A4Bxgl(sYoupfj9f@w4{;AW^@|a%pdW!x6mr@S8m{7e}tzzVfeW=k@@Cu zyWN&;=l9Q$)Nk46mSn$$S2xg+%>)kXJGONbmTd`d@;NTFq^#26(8<^-t-^_uORcAs zohEd}zJ9w^u+f@!%1+o;q5C#oZ*c7zI~I7J445LlT)p~vY zJWO`UtTAv{&D2*Z^$NbDS>48oCetZKyIq>WCQhpyzN`0E)o#3@nSLGr!8NYp%;}%_ z9^VOO_BVW=mXply54wk1ndR?v4_S=yUHpJ@Df|^TwU0FOzN&p>nDu`Akf&|Ty&FHG zm3G}PGibd>cuf<^!~qk>dE@Yg+joz%H`CbEBT^~Ka8!!G|FD<+br<_(>kiWCl>J|{ N%8mD9;?;tm{2#&T=nVh> literal 0 HcmV?d00001 diff --git a/bin/ij/gui/ImagePanel.class b/bin/ij/gui/ImagePanel.class new file mode 100644 index 0000000000000000000000000000000000000000..5516de2a35b2d4401d4d17c4f044b3fd4119684b GIT binary patch literal 1126 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 I2;pD<2Ujiq_5c6? literal 0 HcmV?d00001 diff --git a/bin/ij/gui/ImageWindow.class b/bin/ij/gui/ImageWindow.class new file mode 100644 index 0000000000000000000000000000000000000000..cf14557e189595f412adbe8c018a5996392fcc97 GIT binary patch literal 23752 zcmb7s31C#!74|vzzFG4^0!bi&1i~H?c5IZe$PU3IARwEHgk(sDBr|a)VRNZkEpElF zR6#^k6kMu^WQgM01zWpVTWf2p)!NP0)~dCxDF1ivdo!5?{QLj5nRnk^&OP_sbI*Do zFMRpL(?oQd^>rU9ras{f;~Lt-<0@Nf8$v6>k^1N+A6ZPjHq>sc9ap=lZQQ(AZA-{U zUZ%oSU1(z{f|h7qd%P+fZwp02F(3JvihA^L0Csf=G7ULpm!)mBZK1AyHq+o9{i>tw z@z9FKP^dZCjVVxFS#!aP$~nsxFmWYQ=Im%B-c}oFTVC7T9@1FmWlTZov|w)K`~}OT zrkKfEFn{J^DSJ>}jxv+4YSHWq&YHUl1gSC;H`hiQ#w~4&g(D49{8WIs!a||3&Cxn0 z9d2m_iK;+5L$}4v?eQs0D!hRyBUQW5RH1vWRHcYKyEd}1R=VegBjL7iZF6;PLnORD zTvyu`jz+MMIZXaYXw!;teOn{uMR7qW+|by@5K8k$2uC4i2}F4^%=TXphvS-2&H=@v5C z@#b)SD3(OUT3J~$TWE-_vR1j}Y$ngr*)yx=Vg<|Qu3UD3p{-~0y4pCPc(yoCYqKGu zwZ%Ls1Gq3dI&hme$2WA<)5&gf_QTMU!O7?!tU=G#qII*1ZJNP^5iUZOp;b z*M=eUYq2*$m$n!$s2h+;)H9q{m%hGOdn970<#$UHf&J9FW`QsrdtEYWIhHs(S|17! z(|nswqh26wRX7r=X>VBM*No-$5nP*>*T%x~?pAuIx?u83PUfpo%Y&q;+NOnc zmcU+QQ!x#e(%CkRq~SqYO6T}!8SnuEEsllO$8B0pE5Kw8p|<($;iZ@mGXuprGHR7g z6P@wr+BAuJN!|H2O{U>ey1=Gsg1Ob!7&e@c&Xf+9fTo_daGSu)#EGnHs;+G{3-J-E zvU?s3rrsqfZZ~H@R>agOD+)6W@6mdyV<01%sKrOk*r)XKWK)EqSUFa@%t0x)WYozt z41h)#+7zSVpvt;t;Ov}s6u<^yIm5RHENx)Gyeal*%@iP0d{)8#f@LEpf^4PDq?+l-mIv4Mlm zlu@e#bQNv$(Kpk@?(ArDG-lJ)bPYCTQ)9SIcA)DFm}Vf!be&Dt)3>l=I1Tfnu~0)y z&blnBXYWzVWuhBwy3qhN)Dj3c+jL73!YokGfe@q}bgPfPgXv{o7TRF|@OPyV<#kwgCblO9eTwk%ewzxZNJ{tH)Sm|U=s_?e`iPWW+#GF7YO?QkuTJvD zL-dI7=z1SL+Cv2@>2Z1jE5;(6d6@b-D5iwGd!Bss zWDodNwOgX?Z9zIpPYVXmF!eL1BsJ1OV2X2+p0nwD^nFZ)y>&)14M^Kpr>?p-0@W0A z$NbQyWAr?>r?wtkP@2|zcC@868iAOd;vOVX?_HI`3O}~#1^Nj%4THO>cPQjENT<;M z?(9Fa>F4w!s15)Q5xUg%kdq*E@8N=Y$);b@uRzlj2=GUjb-*<8E8ukN<^ub*O|Q^z zaIgUEtk9M&WCooCB(2#U{HjgAqu;~uF-Hr=&aznT#!xI?+l;>Pxe*ccps`i5dem|s zz23FehM}Fs302>)=}r10PCt-1l^rwUBwSAJ<)ZW_oBk{)*+6MYC?4J_IQZzVAeYPj z3t*5%8(_30=ifnZ+w^z$>{i#-#iBuahyLZGe~JyVVO)z;jjI&x+Zqeu*QR%+cSblq ztG2DKu^Or`Nbk`HK6)RVfe{X5m+fr&4}Az0P3=~9(PPX$L8dN=pLsa9b~py$$|5>x7LH7 z0D&!=Jq*iM^l5SeOqWy#ay}emBfx$R`Z$m-p-qd;8KP8qv-HTcxtDp{BEYk4?#+Dw zJPmsY(~`BVD-0ofax1=jBb*} z6xlpbEP-rb-pzm%K*cr>rn8{p0LUm$sT^waFxghSIa=4WG~5h1(7Yvph7mTy*nniO zYlJ-*ipzYXY%Z0~9%FIJ@Xc5u;Ii?1`xp<;PQPp2o1!TESOhv)KqAI}pr(ore%&#AO|fw&l6$4te^NjtM? zO#2?6Wpfp+cc`)~)Y9s%+@OJj$2B%Dl6H}hsqhT#<@Pm;#IV9%ZX5xE361x`Sk(cqB6$E(uW*hQ>~&$anHKHo8nT;mujzEXDj z0-M)Llc+q$jD~H>>*Vxc%3&@k(tV~h4dakih#InaJp-jahG~1aCh0=Z-H9#(rps9g za9BG7yJm+RWe&2~ruL9lXuvtL5Yd>OQB7eY<4MLWkHkCVMx!tDIozfjPjd4nM+H|HL;P?=M9zNs>9qNAk%s<_wQ!3? zn?U>@g+<4GWWi6_G@Yi%VH8zkyi4yo*98xxoN}~>HzZjCdyA7{;TQg=2tWAX$&;Ng zI^$FfgS|%2%2UP>gx_F8huYE|9MNb_C8VI{7i@l>f8?F97LGjr^Ug32DYzvc|1#+S zbqx*hOZ>9Uzn0^q+oG-V{u`TrEALiwXnmVh{LbdzO9j^5))sA%s@HA)gH(B963z&s z=1rj(3?m;FC~6Vi!!F9byVmO>3~>ziK340>rVR6vbn5-}vD!_Qk@axQ`ldzpe!>#f$>>%+SQZgAmAVGL*m*a=O z6f(ik@d}wlKfSNmR!V#}QSoMYP+7|BQy#cWSWA0bxOv<{gzzTX%BK*S6BBA?M8ZvB z1Y_!#fq2IDI%yqK#FkY-fJrsYi$>b~%7&a-S{sQk4aLIiy)#N2Rtc(r$_9F>cNe&H zJ&?pV$5y#2&pE3CPsr1?!&2kee5#+V@hQuOJ}^%1@27Rf%wc97*igAi7yLu{)vn8tjyB2F8b65pyqP4y{|K{&=Ry+PSl z(-cf9QPN8rqp>!FYE|tzk-d4R(g#>$y_#XGnX*PN49kW#m~u^OwyowEi!0!7DYI)8%f=$?kT2|!7FXGN{~jJ!%!m#DxGVq^MoXsLK4|H zcdB}|##R@owbDCMAKEOtXMsYnnd;R#Th%G3&_KL>UEBncWtk~Kp0*)|e(P=3pb+o% z!U~Vg#Q6|Fvx91bYWAt7ZgwH0MhxBo95vOVA_mnt;fAmoGker(zru?3*s){%Dh8?z z@hi0GAybNFc^hqIj2EHJGQ?h_Zi}t9s*7;sP&XR_VqMG)MfZ2Oxg}H^Z;yq>rL|27 zs!P<@edV`D2w=5cMj@#-cb+h9h%?Z_oTWXsZ z3#UOBdP?TZksaEhzHO@=3h^ynKN0nPOXeh%Z2I2#Im5585H1#UZ@1O%B)Woa+^_Zk zz$hUutGxp7E;%K0PQhJ%bq~50ZwAxe3$`dWtB4B^?*~?Aj9C|kl)WD$n^5kQA4GZ5 zL@CQIeb-hG3q?KQ_>%dv{OTZpnZhB6CZ}6*3H+#0()qBhI;3-EI9?e+$O3L;RXe~R z0ZF8nRKYAMt~zHK`m=i6R!^v-j*N380eoyEYkEX*Pq8Xc2E+@*C0s0mQRJ?aA?;E@ zJ9CgAXY9H(;fIdRj!^L-#er};hH3~8aDE#Y6EEN#rhI{N3jPAPV7u%9?BhCfZj8N; z(@^JF_#j@roC^4}P3ot%`kA=P!s3EuWn|Wtwou$V<3>k~`PEBU$XVVQ-@n>a_|>l= zU#e$$XO4IE9ABMivrCW+$NRF!?kXRYKOLMovC3j ziX}UQ0$cYNaDD{55%37r`*k7Hn&66=OKK`>=2sLiLms6VlBFS3 zjDiG3LiJ;VP&n%lliM=77;A>WiOVrVdwXO(QbF;?PHw85{3Vq?siT^Q8G zdN_M`^Pe)I%ePm}?c7-Qf;eVR?fB(?RPF1PjR5;?SEp_b@IH~B0~ zG1=A?hVn_K09E*sAcw^oUaF_t`b<3o;{+tO##L6xHW`aGU3fb+U12;Ec+<0NJ=+kt zJ{)gFLREHquC3=uoo79Axj3&l7%m54BQ`om1R^OW+;59WO7cD`TjJumML=U5G-&+( z$@Y^r3qnh+hq&x5J<{ogBeVom?ShM0!1ZQZC9{STl4e=D`DU^GO%I3zy~5TjC9IU; zL^o6Qt8IO*EG#S8`5Y7dL{#yhKHt`B3?X2LxdKMRgtL&|p6Ju-lF52Q(aPrLP(y9= z%veKv3!LRtY|5{(;!KPfJWUddV^Ip~T7(nmHQiwAMzIM+>ee8y8*Of0R~v&seob$% zb(2h;?QDMc#ui&gWG$Hpk)^k=ZngD=5=?Qzo}0rh?JbT2Ev$v?Xeo(0)9tq2s5d!Y z5Bz9Jh==Nvo;DILDc7bevLl?oMPKC8TLC?dk+$G^1Ms<6UxICO^_OIut8sKCkBSi4 zLYe&QHkA>a0Tsu*sf@qTe2yEBz0Z*3wo3$OgJ(B~Th~QvW7v)+eU+`hp_d7;Z8nwA zDDRnXyUGVu*V+1d4Tn$IVy?KsjH$f(=Xkzcj0x8{f*Pq!-3dY7r8>Jc8x{71t&bYX7lv6=w`l1~ z(Q8lH`f0avAw2pCXZZEAkQJc4nbw5w^!MNb>hJe(0l(6DOu2xdIP!&`i?v|IjHTdj zftr%@;@3ZxS=={gmY>KhKb5fH>XS{uE}#eX&u#sp9B*U`B$8&d=xq1$O2&G*H#8pt zM}pJ?(@tYrT5+!2FhWScuV027NE=a6jGcj?9niHiCk=zNgF3}krkTkc3e(B?KId3= zP4qkPWRGE3H7N9&tzS>(F&stf-Z}~DH}oHU8po(VcD%K@y&)WN5=P>KEHbfs1eq1h z3F<%Tzxec@9YyZ;TrX;Q9K1P+0mW_oSD{!hM}8-3MJ~K;>%U8rBstPnRM?XO=?enU zoV5tmVOse0zo6XA76?!M+t%+%gf>tg1z)$+O02h!GXNs1EhN)7h|2$u{;y9%N%v2k z1BcpabJ_YMInR-0xmFet(` zFe9=0uJs7txLcK$C&OwCpHxNTFceemVI+~l>%%g}S zkb%*Uw%)eYN7_tSI9e~b$Krk~7pv@na7D4->Ic-pF1QX#GA+kvBP3*8H+16>!Y{;92aQ91QV=C@>8hX%PG*@zUkW0?8M2KVkf{3#m zv}}&I)yBX;UTA((fShkdLbu79Zd+3&->K``(GH&srh(MYvaK1yw?VTRhH<2Nu5Ha0 z~_MD zO?h3=zHTiP7F(-etypVpno6UQ+qBl&R;|pD8;M4o+@{X=ZIUz!f1t+#-~`lZI_12*nP0hT4VrOXpom<;S<%r0{b!FYKNHBU=iGxLs1Pv(h>0eF&Fqvi?w z+B{+E;;AHshi88pn})|YypK|s2I=joLD@o@0Q#M2ChhA?L2IcN zeK8rWGdo&`@=#JIFhu!?eYjhQA3h;Cx$p9XX(ZR$T#V0jG4_>~@eyk1xzt(Y1yy9J z&aA%FtUkjx4zRET%z6-b`$4OvrDX})@c%;r5MpK4t$S!#sgPuwyf$q=C9Muo>rNV4 znxJ@sw)D9)L6_~MT#)q21YL{A_5|Iup9VXnYdh)NJ1G+{yG(V^>30*byK_4BNI1gg)I*_UHnPIu4%!Pu3Nl(gM$6_Q{W(;4BAx zp`gEu@=6n&V*vZg65JPW1)~#O*y~n+E0ax0@E|k{DfgBg=HcbOGTp($1#HQRvOFKk zd0wz4dXxc&1IK^^9z_WrTiVGJ_S3}%C0X1t8q&$9?V>_?N$}|vo&wJ?>IKviJY_$5 zD!hmI%ws}(J~P2H_fw|B^?);nALBD&#hquCo{epI9(Ws?^eXG*h4`!9PrbXZ;!!Z} zr+h*wUhEtdUPw#8mh-@H10WIyL4b^euq#1n0{%B2mv|O{aVzOuoX%FN!iC;yx|C|@ za$1BNvx{jvosHY7OX*HJ2Nz10(}T2v4$?|GOsnWcT1_w0dGsosPjAo~T!p)U-UsV{ zgd_JkLE2Lt`>CEYDa5&mLKIK~u39zn1YBwU8g1YTYUVSkh38R(Yp9i1(1pB)V%$t| z-b!tJDJ~sfNgHu9V-sIbTX+X;YRdl3zix_f^~j{sV2}cj#*V5Xsy$;`QbF3Ta_I)upKeqG=_WOVZdN1c z7B!i^t)?M>JCnYn=FqKbA>F2GXs232yUhVV526!CH+jHO62*X}q9iIo(LR2Jmq1u* zdIMKtmVy=*R<)UzLBM)|`y_^X#77|a(-^reMn2${&C9WR4~cNPo(7&k^abHw$fYJ9 zsGXEgX;nr<;kKfYTRrR5@V?&TGy*rT=%D8~%|W+*{v7{t8t-Gj&i(>nZtnF7c~5{3 zys2qq79X#apT(aMX9fBP10tt!Z_p83o)(C?OvvEH!tT$L^{6}(yn34RTr<5-eka#f zSPVAD0Sy;eom}s*{P_izdl(z#L^gSw*WN?E+((m4&%(7*$`zczaT zz?f##sl8mPALGq}dS8PCh8YYWWz~*};0Jm#Mv*GR@P!R^~K-@e^H(cbU7UYiMD;r`dD@R5N?D zsNRPf={EBJrCW0KoqU-fz*}WdMBaGoA^zr0Dmcp5tjgi*j_|k8H^JKzd~+x7K>0Dg z4gDYEU5EI#a^E4|RqjVA+s5o@*p;1)0@^lPcy7is+uOQ}veE~Zn#trRy0-K%@2>CQ zJ*Lj3QU~wNnjCP1^sN0iH9G}Qsd-fg$u zapHHtYgLs->T zS=x7Z(gNuP)gYjzLdnRhMquh-s{Zy9@0LQm^WHpPeJ4LwgyNn&zZ4UkfMz)&3<-5J zU>Ef-&GY5?MHwAs|0*dT;U_xy$&_J(dz{sf0C&Q?*$at%7Yx_CDWC3zT)7VhCU%$( zz`(g5^7aAR23dS9Jw)Gvgg8Kl=n;Ah|2vFJ;~lv5oq$w6La$RNy@fmW|DY%61KgB9 zj$8B|dWr|o(>xS6c+2TouB7K6o4yAb^#hI|8h0Tb<8R>F`wsds-$pO+1GvThB>fDM z_2;;l^dh9yFZeTh2?pG+l!so1B>lC@p;uHP{YDL=->Nb6s+vT5nQ*Z>d)Lle&ohtiF!-d+BZU5dB>pqJO9#(m&PD>0jzidRP60{;fWy_mEk8 zUuz^dvgiXnivELC$cK77{a2qxAL%pbV?BdD(TnIaBbm37EwU*one}3R8qyW+f}X*+ z#t2E4ug{R1i;&EN^aLZ1EgFKH%QLW5J#;`F;Ac_tfkfMGchhYYL0>0Tk(zEb(oNW9K9CDl* z@$B;%jTm}*#us!3^Klxdk5g~`8R_AJ2IcxkBV$JV1OA~SdZC;xpiPu1oHL*hd;`&X z5EYKa?G+6~Ul=+RI~|jBfW#J;K-x3q#vn#8AA=TmO!#r~jy4Y%y5^DY0AU1+O=~+H zT!1Y1+?>Hs?)aJ!JuWIe!auG81ZV|MS@LXD=JHRZ4jN)xNmD~E|17$_+{&|dlUJ7` zKIDwtlsLXS0T^E_)&ekIS6$E(iUYj``tZ&Z~D4IHPS6s`cV z7dqC5PwD({b_3LxgH(ncoWwRQfE}_Jd$@(O%(70yo&$AvSsvIKZ}Fe7u)eTXWPcU1 z5R3WGpqK?4X%hbhB}YT|ChS!&AHVbk4b#{!0OJiYDzE>k}A60k`@uy-Y9^Xm5kJ44EkdZyCkMI{2 zo-$MeIjd7?Krjx$prb0VDo17LT$L$xP1}#CtO~0Ppj01N%&PBbP|&{?ze3a1YZIzq z4Af@X7(hyiADDM}AYm>{l01Ba+86BIk8*L7zAsF-V(w3)AUGy+A)U@eG##c~B@e$yGvocof~urL><%8*(oIuYe{gwmhH;RS_tbOSc%pD-ixB zm}1vpJ4exJYLF@h;iuCeH5hds7^Ge`L=8n<9pDTl4-Q+6W4`eF*b5(B<>PnoOY|vu zMtusI$#VbA*%Z%H0FeP!xiv=(>&B@E5^BV>LTexGlkG^TQO*XbG3Ayh!m>^^9-1%@ zd$K)8ot9ALg^o(B8iP7D*)eZs$d;VBht6#$*Q*>U=qJdnjX!7JO@Kq^%qW&9{$A#^6z(P@(Lxduae(EP3fv;a!x8?xxKd zkUe!D4HUG-=k-gpUyuFt^mUrpaFCea$v`zqVCB%?oy_ zTg>dY;y2&l^?n=PWv*SORW`t#&+Mu6cIPwici>%iWk2mngI}8OJ)-V30FvM?H*a^# z@O*#I#w$(Z|NC4M)8}%iaZ2p=0j^+I^Z-r){=3O};8+ksPK2-yK?gbI<}{c&>OObm zep=bB9z5@^cSg7R2U7JVY4wIu>LIw~IqH%9oIz*vWLg6kGR{*VL8j4N zJe}_08Pv%$=_t>F5i^@!<~j5x&&8)A^XNZ3ADO2G5Y?3kq%Y)=Q0)`A3LnT+^Eq6D z6y@36&P(ux!BYMvFXJ7ky%%m-2e06#c_p;oD*h?H7Jr}5#eLS*Y80P`8>{E5WxNKL z3NKJMp}d1@)or{^J@QbQ$}SEfjj!NdIqww49@GbMk$?54eB}c^WZDR{idYx0ohjd zJ!yp^Y*619mBsCln?FEF^QEdl{ZJi4>kdR+pNDzm;d^0b{RryL%N;P%evFbIhSP7< z3m6eJvi1t;6C_6zR*I%q>pQ?bKttT3k^$GPmPQ&nWoCZM~j_0AMIf3{$lp+*PMXP?bf7F2>{-KFt%{1&egA_V0Ka6x0k}& zfF+(#Kgm@;k1oI_R#+oZgst9tq=q!8$x*+ku=dkJ)cz8+dD0q%;*^$`rDZ&7UO`KN zY3WqIz1AIPTKh>&Qx6Mpb+{ROmMl0l5LGxa=rR9 zwg;&yx)v64KQKcsZBu_m$x({E$?N@+h9VqLkN+1G^~xh9*C-vjVY4JgPSj^)r~2FU zBF{eZc&3jm0yVnO72}xj4U31bg%Nd~LBla9`?%bR@#-H24G(1+F42ZdV1mND#qm<_ zBrPHO3G5)*sk0I3RR6?*??|Y3t9#RButS>ar)L-A8Lcn3iteECdDb0Nc1Znu7Y#=| zau*FMLt~>=kKc&-ZL-9CiCVB-KIk%8OemOd0KzvyRNMqpa>;ueA4 z{9&j1yxRbT9$m$GLKNpQiI+U5^-jtzu%3kzs-XfsmUOxA7y`2rm(c!QRM4q2o+pGi z5i!l|)LFo*5FPdOqMh`BnVmLNcniGX-|W8os94xHNB4p5mb&sBo$K&$1wtxCPGjFw zG#W@ce<$^qJ_R1vB+vtrqe~OI=%_AURhpxR6nKv4VG=S)=#hfULsBZ0v3krZfpkQV ztMC{26MFnEX+|hnPeSx&7iAzMa~uA4m3Hc{$r_Ita|cU3LstI~W|@Lm?_qxO7}XY- zLPAg7Mdu(ex{DT>;F7rHUIb@mc7>WG`mpC78kZJjLe1WLsHhLrmmlkq$PIoX;%ez> z5<XU6m%9prxY^tfb|eq%yB^uNlt}#;eg*x19>-%M7VM;@1aF}C#~eYw3hEe z=<{ya$oC*jxsM)z^nHl$!xhv6^f&^7--omPCO<@<@pswchj{=(iG%nckL5?X9G_F2 zftc_NJ`9Z|TB#Es89vVI_z8S_ca%5qQ@G#yG?L2C;8Uw-c^f~+d-;1PeIH*Q{ea)* zAL8?z=lMPU5r51-<}djLrTHi3fV7Hf3}@cSmE`jKdcJX;=kl|90ZIy(-^&uL)WCnY zuG9;0Kwc-U&ocVwGdkZbJx+Uc73#b^mPYAnT?1dDnu2-}9CJT!L%`N?w(CH%6_nFw z282cq50D4G`}zVR(WlQIfvDyr@0&(lEI!DG46~q1V>EcglU>|6y z4LGX3anA&aT36_e!!zmM9uPT~<}mY%X7xS%TYU~C(>`~hx5k+9$T=LZ9(@cke-rkU zWQeeUq#{N;ys&(9La)N#dEk-@y2O*?l>Q3h>1Bu~`OfPVtoJuG0HKtj2vCpY-@&5! zJ&ohn(q!%Ul&Bi7*P8IM2YTNLFAL=Yq>M(!dRXo%I%|b|(nj1~Y|;| z=+HAJfdf>Lo6*ZlEu>5wuVYf_v-Dau#Pf*hzot?ITTyYmo(W&uQ7juT9yyq9l9Yk*u^6qJ$dB6r{EF4 z+%9wn55rUbvpV&aJ;yqMDM@v*T()#ZC>T*t+{6K7d16(ZWaV+sm`}(?En? zfrB(h%d<<|*6q@ol^@8<+Sah$(_EgDmxB(Ccau!ih}SH6=H=Am(Xnza-Gvy000KmypHTQ7wRi}*Zmu+lUVd7IG?QZtp8s*wL!p#pS1 zqK_L?25nL{U8=I^n<^VtQg7sP`p{j7O72s+^q|V4M^#^X2GM{QQ~{!s{Sj##Kp!Jw zDb~T=h_YfcT%Phk@JbXA^U?>18%i8#Fujeq;b4fok@S|n1tl*cun*~P>m3O7Y@j>! zcTg8V?(Ak5@e&Wbl&;gap_EBCQy*yTUa;Pt!EJJ@LR`SYrvf97LVMU(y165O9L390MEvh1NcO1AYli-JcoJsRA&i+;VkT z4(^WyOyMnpH*T-OPP~G@*VbkS*G@y>SNMBXUZxji=k6tMcIkBV+g)y1<*)%3mF9U8 z`c7jq=zA)BMaWO+oBV6nPB-a-F{1wSJe~RgjD#F5UZd$?yjO9Rkr#DS2v>SI&y#1} zb>iSreZ#84q9ghtIWdmzoG9U2RgAj?gJEF~LEL93m8fAfRt-lqXar4FBWboOp=vdX zEDxCi4!mdx+ehmTQq zr+$1V1v~YVJ0%kbVO(YQqn~54J%inVxPA`K2$DCY*x^R}Mex^zzi2}Lpi5Hs$Kpwy z4ziy?If&cms|uQ^rXVFYH4VA>E^_$@RUb1aM92hHlyjU0f-QmT^C?{0K^&2b6AVtq zM9k4Y0!|Qaixc_<7cIPC4WgD3`e)`2NEcQPxy}TMX3#J-lgiXA8n2vd7LLlE;9?|r zSfq+Ffx<76HhQ{L*OSCXOVj*nzFAmL&Hs}ArAzB;-OZh0%;>4W@V;YY>`xe;yU5M^ z4@+O&ubkB#=ps5U19UB?4^Sj^77bKYG(}a@nQBoQac8(oA)29otzQ8a?xLoEj`}zH zw+>#2%Q`;XC1z&VJbyJgGcGO|_Y*sAxWfmf{CiO+VzuA~ftWA@psy#vM1^+8LNd+z z(2plbzl1s=0mRHP#?SC}>OYuTiB{v~E%T!P*0reV61YHayjU%T-d#qe>KvM&mVUWYPqAM|0j>&LYs5u*E^Ri`)Ir^WS`dwUF z!Fib}9!1wuMi_^#4ym4cBzZ!Vg@jd(YM=tuNQEklC2jy4HmB{`@D$3!U6f_dJf!_O zFkSWgDU2R;H*F>km zZS*yBYM>ou;`HOQF^vqInuOg+phEvQh)w?kgi4*ouf$^WSAviO#FqZwSZw)95W<3N zS^pcv*8c&*^S?n{^nV30*z#InD7kT`C6plnrRU2MmLIHSWlqOWwnsR`l3+r)Wh6tB zVJ#~MtktCjoVq+yXZ1^|Wv9A7s*xc`ie{%0iAKYkj<|N}2BNM@rx#gwOAw-PSJ!or zWH^6Kxy1215>~NuS)#GrZ>~0!2gI8QbXr4qQnmCr2H)Cr98qC(K|v6AMH1Gia=BO4 zA#ZlV8eMJ|BCaPjc^P@Q(sgjZ#9iDsYuuxq)zsdOFPfe*RwQ{SRT)OJd!8|W!@BfY3@ zLLUBR`m4HyK2+bPPY{^)s$01?g42EQ1@;iNi!0Rayg=>dkh+5#@#Xbp>Q3IS_VR=3 zZhi)zS07XR3{zg|uv_ws^Yi-G*4My{9{!#swyA>O{*)zoWewBwyVe;95?FKxmztcr zhwkISCg<*@M>yY_47*Daa0Xd4iqXGZZ9ggUQ$1H;Au!+LX=vuT6Vw5)k5unj5`qa>_^{K3oVxmJ z;fi8Ube|zhJ!_zeA`V#ddA2)j5}tyrrxdpvdk*^nhW#)N36J3j2SAcmbH&mF0CN$d z{{TeiP66o0a2{VUfE>Fz(+~-x5Q7nh5*+ZQ7Jea9>0-!KiBk739Z*;V5oj%ta8JTI z3*kL}QsNz5h&o2;i`b4|P_}xB^3*S3A^ghB<%F60r?#elYHRwtTZ1{wvCuM`ITjYS z3;(p8i^&p~5o};`#Hum#cg<~L^XfP7AAU<2>QytR!wz<8PVd0pV!l<=1oEM8eBm&YcZei!b%I8@a~@G^_vDvck?k1%$;>=;F% z(O|kZc3KzgqIKCw`+knt)9JYIi@jTiVqZ__bYEW|MjW!%<1(7H-snW!x+(Z$$aZZaOGb6;Ps)%xu{26d!XMBD>OaWMeF*XWUx@FI z5UBWs9#Eeek~y~|9<(-6E#|XmzcGOs4%)p|J%o>fSlnxZUK-)ho2^Y2QcH*goMUZ4 u?AZfTdA_w3CC7&yK%Ort7j`n^Ch2jMtc$*&4VLvO*?w&1C7424m;N7CVEDZN literal 0 HcmV?d00001 diff --git a/bin/ij/gui/Line$PointIterator.class b/bin/ij/gui/Line$PointIterator.class new file mode 100644 index 0000000000000000000000000000000000000000..576505cfcc3340ebcd4caf2e658e918d9decf8c8 GIT binary patch literal 2154 zcmaJ@TXR!Y6#mXm+BC^&!BQ}Q7m!PnptMv}u$5a|pxO)2QYl{OX?jRco1TQzlahKH zL}u{Gzo0WZIw~qCp&fB#ygcFE2ggVMhcoK8_DR~ngPGZ9t-aRX>-K%S)7$@i`89yu zcr^lLV7)WlmMuDM{f=vIJ(hP|zt6Wl%g=iegbl2ow&tz2oaJWQh9;)%j2}V0fo5$j z`cAH`dfI?0?J{8WP+1Vg)|#WGI8h*$LYlu_kgh8}H9&tlX&OIkK-9!~T>rXJBJ( zt#^*HlXd49%Rpo8q0SgWXqU7cSSXnqjk1hKO+?Tj+G8f7Xo%qn?2TZLfuhZzctac~O*99NXHBdLv@sJchzmhF zgdFmjBoEXii4d8x<_7H&8({yZIiF$Q%FGN{h3afw?QBk_`gQl#dB07wv@-%0v}uKi zgZbfNW~w)5&)TlvTguo4-^sf%WY8W#wmQA#`!Sq_Bcq>Yd&$1O)TnSEXJS^sh|T8b z?U8&?nNc7U?@mvadbXU3z$157pgHNz6$^#D=i8G*1$jxVqNbd4XgAPOZKvj8kqidr zO<0&<{^5Ml%h-nBmd)+x} zZq6ok8}E4d*vv{oTy><25;hWCC0wLz7BXo`Drrd~X+LSCB}t?u38E$Gp(S~tC0U^* zIYAj=p0!xV@4eg&FpHvGo8EC939kFOHgRoP#P)0YT8iJ898&M2MSXxY_v?a7*nx++ z<8?fOoeU|VlR;=>J5x2%cPYJywsdP59qCjVyAyS-*}c(3B$eG8PbZ>@=u9G>h|gq} z@c0tC-YR21t?BF7e>q>q!E_>C)w?A|uf#Z#rmeRHec2^E^JW=CvOlu2Kb~$LE@MQ0 zN7JD)POWrui0=sMLxj~wh^mj#q^_{5Pq0CKigxuGy3|!1Ro5_}KF1051t!#Wc&dz& zx`Ef!BHmC-dICq;9eZ1zz$Fcvp=|qVRi zW`;wYtHfBvhE7T2@1ibzC3JHr8M)NE1dI)QzQq_3x zA$}8?u|?RoZsP11%|l$r7ctdOm}Yid)lnoK2nmct|j$`FI(l zBL5fh<$0~=N$F|i9W=>huF<#uy(kO_M!22Rhq{fJ`T_T=ABp-;*rR^N0rd-xsb4X! ze#7hP_nHx3tQm1V`}-Rg*i~V<%Z>_a4L5O)qn*3OT&f1HKWbt%1hG~hBuai|O4}%9 nN_FWQC<+2Km+6oPCvP-_^I9umLCcr$GNt?#<9`)+1=0Tit~->c literal 0 HcmV?d00001 diff --git a/bin/ij/gui/Line.class b/bin/ij/gui/Line.class new file mode 100644 index 0000000000000000000000000000000000000000..ef6f264044e847cde585e3d5dcccb56e4c183496 GIT binary patch literal 17254 zcmcJ034B!5_5V3{-prfjWzWEbB+4d91`vpdhDE?c6eLL5Bd83MVKNdjF_}O@wQf{U zlww`M4P2mAs0wC?qE=fg?pE7sTWc-0{%TuoZENk~!vA~jdovjj`~Up^pZ^~}nRnj3 z_uX^u+0VV#7ruDxNg^7rer=Ls3M4KZ6YollnVo2hnPf2eorZbI1e59>$HX%M^Z-!Z zwF2SC)jh5m9Swkb1afuP%5RVU?H)Hzx65yLt%l{->`X;EQr#G5Lp^}3wTb3bOMOeE zEgr)wpNr1sj!0a;S*w#>ow0@O^~tWb6q9f6%nRn9F@0YBSs-F*14g2uapCL*Oy0@S z)XXee&(vQG`E)_U=#Za=(r}Z8G38$vSrZx43Up%{BdHdPPM{I^ zCKYMJH`NU_4W6lg`^CkVo;2ijA2Ee3Dx*OHjk2hm1~C-~o91+_UJ>h95LwY0V=A7V zj7C})MLH7l?lgKPJJgufGx4uxksou^S~P}E0@>9K_@}8%wIn*3@@D7AU=qkl3fEmp zVL9Mh4cB_6{sv;hKR>x>ip(`uD4S~0H1dG@R8M}fa4CX=^Y)(wKBn(7M&#%_`t!~ViAYTGRvZ~X*L*U&w}=tEM2%X!(PrDi_W3S z?1=i-MEks0G$m7h&7yfSC3nl)0*e-E3X`qPOQd13MN4#dkG!30(b7zJlQf)X(fQKg z*191r%PopXix)F!3Z*e>QL{99T4ITKi^jXsqPR5qFz&2uYr>)n^?cC36jqQoKNX3t zYK*jJgca?G>5dXxFh@zF1R|8CnX4o5*xc5x&PlR<7qyEB+Jsjf7IhL7G>}}may}-D zwJm{m8r3sv8idd_7L`yy26kK2bL7A#J>u&YmD(dNvFICgDN_i@+7c@h(MT$hY@3nn znBTQxZlXKZ3SFqKIad_=a*M8@E5W7?mwsj!8 z9HyXF(j&)-dion;?Hw_Y5Nj6dr^wG{i@rr$Ad1ddN>2ertN!w*MjYA&+Ay=NEjL

-;Cf1f3(3;{LkZ=*X* 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/MessageDialog.class b/bin/ij/gui/MessageDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..f5bdc43900af24eb3f9671bcb2aa7bfbf5907cd6 GIT binary patch literal 3902 zcma)8*?${X75;8C(kPR$jSk|c0 z*sep%R%mGpr7T_2LR;3fP#}dyHZ_!`EWaO5Jo3mZe*kYl`0k7>jU9oAW6zy)&-u=| z-&yYb`QPun3*c`2LB$qNi z3(Z9{>vW$=JC>atvI>rwHS;RM0B#=PB$FORCvYmVW7DFn|vHA#@JgZ|EBlK?-Sa%B#yR=?XF(y!7W@^mHni&nFXjPFC*iqgX z$gtK+=^w`h83(H?6i<4`8^tbZHL2qgK0;(;nkAg@6+oXopR==Uy`Qxv_e|NzAC7SDgQX|nlTJU7V792J=f8p!9c%cZC1xN=i8@^$3dEQNgvH7=h>TQQ|Y{Ea)>qhV)Z7S8=%1Fb$kJjdjxGBfwvR1QKJO$ zMIBGNu@x@cOU#fQgCh&`Q)d3WcZ%qc&BmWF@|Jur2^HtERS;+@Pf-8l=Ku}mj69Db z&`>E5Z&s2|$8sb+QE$uNo@|JD4~b;WxI-o41CS$y_MFYqUi5RFlRdJ*joUR;t4ld# zd)UaiA**=V-&$@gD!xTax2t?XkORZLkjBjXjGdpC^Z)i@@yhXjd4H9N{I-toNFvo~ ztB|t`GC=;eSHkyKsb|O%m@}8lw@kY)&7Lw#vf|=({7}UkoNMJduu)0JkK|C*IFD)P zT{UO%V;w)iPbp??QrvGZv7nSV=b-Ghw{%3%AQ0OeX4x|jH0Po5KuOj<-^R_1Y4Bo{ zJ+Y!=RpN&|{0liQsd9cfCyA?Daxdn-j9uWhSdB=pDA!L}g)qMYbK?O@`WFpHy3jqz zXpSEbl~?IezLxZpX*+ApSW>dx)9*K}Dl(b*60XL!&!^1+%dOQ7pEgHi2C#?x1^5$$ zimiMqa%}MqQsmI*bBm;w&pPUDN0fheP}qwwe<1cJ4jooj(3n_5^W+L*MYMPQ3884m zi0DyPvG=}UOEB`nE9gxW5${oA${*1j zdB_79^>y5Z;`$Lp5ou9u$O=9Np*x&HXuey+qv39QsZdE z5$wcWXu=>`cv;2pAli_j#zWYJNBKGQ7(V(SbX}KHQ0?8Oa$&!X)BL_%h$*>KS~6DVD2e z@l_^YuD-@q0PSKczK(COF!l%q&oPc5<9Hk|;6(L3kk>hq!$E9)N7KcEiO@-E4-V|j=F(1c}nYn()D-oJ(>!9 zA3rETy}=*@L`WPe;`(tG$D4`vhPKzBvcQzUP26a2&|Zbc%-*;Lb={wrm{x>nc`tGA zV;A(0{$5ggj5Hp1{daiCW&a8K{~7%Tsr7Tb&FCZn2cg_z76UE;uQNI>Zc$zqY@t9z z0yMl?4ttDmGHh)<>|%w6dUxj}-JD`rr+uMl3DuA0@KnS5!blbO`kO zcok(ZQ34Yztu^Yg*Z+5l2AQHWKB!m#BXXtJWvDTx3wY0Ye4+2Hbklh K#CvYue*YhYX=U{Q literal 0 HcmV?d00001 diff --git a/bin/ij/gui/MultiLineLabel.class b/bin/ij/gui/MultiLineLabel.class new file mode 100644 index 0000000000000000000000000000000000000000..f24920a38f0af47a86870ddfe15dbc51a257594b GIT binary patch literal 3576 zcmZ`+TUQ&`8Qlj-3=(53V*|!O?4*eU5|$Q+obojY16v*)~>->P21I{cCEhk5A-MWrEgtn-Tj>zNyx@%Y0k`?bG~oyefD>b z`1}9<@Mi!|V#7d#LYuQTlrB3%$#S;jOgXMSWi8uT13`uDYu06J$huM*nzY=@R#738 zL*v7+->y*aHr1ySMee(*z$$ZL=Ab>U#JJ7BW z=hG)@uG?OO8@B&=k1<`6-?BvVKt}FUjMp6#XyRp z*0G>~MY}Y|MG>*mCXS+A?i>?qQc6a_zGOKfuZ;7AZuQMp%__bMz%_9ZECIDjIjmG_ zI$v^DuSrkA#3cz9rATKA_NrYd*r{1snMgM;+EFKGyG1AOjv2VjTB;(gc0_O$uNb(- z^{kmQX_Yc2K7W_Tq(O=% z-o{U8*i*+A7Uzzgn_HZio=Z$jB_?JQ(H5v}^zxd$A|?JNere)Y_%-cCo$F#N3~#b(T~|@3^)Tr}&W|Vh1=7RRr>zFK zqTujRY8aW#mkTTQaYqh|o%N^cplHL*JmI=_VKQqKi+0h#ZSHjs5KT}pA+LCtjtTJG z4)e<$mRHD5tXrO9y5$+BTW&786e7@m2!pPsaK;Ftm=5SnM`N{)DI2F{*F>7+J@m!1xVB*)iI~QDd{$1FA=cy><6r z0x_WVctmU2O(lA$#6$e-<&n1!1Jq!Y?Q?`A+QhunRj8AAMr+fIgLoDb1R5i;=NK)3 zNsf=8;lCI#(5p<&w^KunlS)6I z2KX7oFop>J2`unF#itr&+ov4D5*GOtyUoDDGAR$?sZEr3qwziLFtGBhf$8@U($`M4 zzkze?>MBm_RRvnKF{#2TznPk6#G1eXA{}M19ORFXLmEvx=W$kb0b2Y8P;xB!;?~8^ zJt+2J#y-N>s~FNYfka!&NAv zKkVp*puIW_LB_C-AC|{y$_XyVo2g_~g^Uckt=&ph+zJ z1;Rbs8N7`zR~e*2*`@KofzS=?<-3j}ab8E;Lc`rX8~EDwkgmp=rSUdl%BuWl&8}Wm zzRXOE+ZUYh*7x+)v_d+mGp zKJmx-Rw^~5E43GayG%m6DuahKcjYp07u{&|-|%&rGIlW^4}4#Td9HbYZ=&(P;LT+J zE&T8ne!78Q-0O-AvpD=GU`^xoHoRyRZ`8*dVtM`sztyGTt@cgaVs!bP@p~Sg>%s8< E0Tfl^5ww*C3@Q8fSWoO^e(fnY!Xh249e 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 F{{fXBJdFSV literal 0 HcmV?d00001 diff --git a/bin/ij/gui/NonBlockingGenericDialog$1.class b/bin/ij/gui/NonBlockingGenericDialog$1.class new file mode 100644 index 0000000000000000000000000000000000000000..2b7d6c17bafd06bf3a27c9b80147fd5d09f7f0b8 GIT binary patch literal 776 zcmah{TTc@~6#k}LyY0HrwSZUdtXM0tkQft#@j(Pa(n10Ro~OH$?UZ#U*$e$$CK3$~ z{s4cJ@l3l0Ud%qsobQ}D*YC{lKexXCyu&LS6^1oA>PETj9&7bE*1?rj(Vo$rE^O2d@?11fcVMCA!oeJa)i$opo>byE|2h!K z`E-_6PX|1{;E6PPVXT^d88*tw`#UtCmE_97b1d3;L6wP-OQ}LV?sLT>k+@jGGDCxh z;ndDB-|kIl<>RdI-8IcQXF5*;aUc!HOaEKfTN6r??5iNwDJ`=vvY`%bY%r{pti#1i z!-bO$_4wa9+#^MwVK3%sDpITxc@~`qsCb@X6YyGwOZre1Iu0Jvt4rtu=@!Xm=gkdf zI$Jk{e&Er!iQ_!oHF5+Nu|T&w!A(5I6Ve!-Vy;N@o}}@scfO6>B7`Sci{ILQav7;9s`Vvi1M~ literal 0 HcmV?d00001 diff --git a/bin/ij/gui/NonBlockingGenericDialog$2.class b/bin/ij/gui/NonBlockingGenericDialog$2.class new file mode 100644 index 0000000000000000000000000000000000000000..0706442a125fd78fe6078504c868c7e77e9ea4f2 GIT binary patch literal 731 zcmah{(N5bi6g_UUCYWq=z<@znfmNt25}6QhAOtHNn$+>Kse3kw)fzH;N>cJ&NFWdo z`+$8E;ySIgmu-y{`}+FaO+8>&8K=&#{CR7t4g4Vt|jg@EAN1hB7TYVw>;LN2u9Zl3bahCwsgqx1HZ+ls4eJ$8hbA94m9F<$Qo>5VTwxhyYMJKQ#_VZk>Tx~JMi{B{F?$ZM`MLBz&5Hh`bEs~8uPSbSis_h z;h1L2>9udcAMoZ+JumW2I;~Z3ERAs!4ywQ^)~LFS8r7Guj(0RW literal 0 HcmV?d00001 diff --git a/bin/ij/gui/NonBlockingGenericDialog.class b/bin/ij/gui/NonBlockingGenericDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..97bea99b96095aa1a2c28d67a19244780e430965 GIT binary patch literal 3297 zcma)8X;Tze6g>|HT836u6~!egDE350jV3BaK?Fqy!98NOW~RlKp_@$iFq+k9HnZ=E zX8(}N*H}rAidCt~7eC~u?D8yjVO}}^Fx%ZxX?tT9C&+mT#*p44GR4A;r zFD6IxcJip}?9aOCG20nEXgOBSP9Lz%tUIb9rqCQ2&Dv3@w8tkD7Nxk`KW>g%L)pC7 z$%TULWZXi^_IyF2kJLqT(oC8KKWR-`j-Na;%Ya9!_t=i@?^Rgb5!z-=XEb@j&oP|N zji(i2J#NN|qY?{s)L@Z9`y7TPH+?yC+~UDF6zX*}piyCgXZZte#>^6}V_5c@u|&gS zg(^wlu#Tl-RzHz*eJkys@J&DO?Kg8Z#8{?dIa-NfJDzVkX-i>=q%F+NB89osrEJGK znjasrawpA^EKM6zZraSAHgmRoM~gB4g6%1+nSq(F07R_vZ9l{bcr52eA?{sp3t_ET zkq*hL674$HVx45aK}QERDrmMhV5W0!U^6VaCv-e4JNme+AJ01u$=0x$F-4ip7gF2S zI5uI6RAQ?_Z^r{uc%Xqq{Rnnw*sjo26ics&{F;2wfsIq3ywRgdz6n(6AtmLpZFVpDKx1FY~YC2vQ0SW+pScWfay%$v};ypVRRqp5g&Qa%jjpWyqN^PkwX zrX{o?D|g<_jazhj|C#QVaee#z zWd+3sUQwDX;Q%YgB{yT%<)NJAc@o5m`vN~=T`rGcT*pO>Nd&HriJ&(NrZ;%R%E($y zhbL>bT zmKR4-SpJZNfMJ;>A8}}LeiV%&j^z9&IzE;C2F{wn3D&{(BU+`{pX>NS#HRUfU(R)W zsmu)>H>CzOysknZUOQ$wO?kC#kwf3;I4g(ZC)|84ZS~o5HLNV7VHSUzB`Q|5-{Do< zlQk()4{Hch$^QgY@RwtfPsl9dvz9wG5I42_dV*GB(`+QvZRkej6zVFc(PaFBmRnq} zzzRN>5Cf43>bINb(Sah|AyWKw|b{=kRSs; zA(pyD5B(Ux5RTJ>#JZQ!ffs!YEsS*;1-301n znf8N>2?W4OcYM`4;?=N%L zQgqiscYfq9hKi<2`r|z?m%rf$`|G8@KKeUIe~0Gvm!IWN$taBcU5WBbP)7x08ka+* zttwMmqe$y6Y9oogQet=&TL-+TV@R=u1}Lzj3}cXD8iK)^m)al^rN5M+hC+$hHrR7l zaV?5wcN9&vk(kD7KNCFcvuf^x`YHid2i?R)_MY^y^zKz$k8qNF%ZVyx^!B|l=K`2e zT2&E@!n?&1WTGHi6H|EaPaL{|I{x*s@ncZ&&j|TtiGomMCOc__(;&@0$dpCdTC932 zt3Fc&ifl>!0(^x7QMm2JaBt)5Qa6NIZx8T9>lOGmcz%cP`EKO*CcfpTD|q5xO0>}! literal 0 HcmV?d00001 diff --git a/bin/ij/gui/OvalRoi.class b/bin/ij/gui/OvalRoi.class new file mode 100644 index 0000000000000000000000000000000000000000..aba14bb238cd0b3a0b3300f8b8b3784ec065acf5 GIT binary patch literal 11663 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 ZF9q<(KI!Lo7@=0#FW;i(ugDYn{cl*NFP{Ja literal 0 HcmV?d00001 diff --git a/bin/ij/gui/Overlay$1.class b/bin/ij/gui/Overlay$1.class new file mode 100644 index 0000000000000000000000000000000000000000..907d4a6874c7ef378de4f090f458cee46b40d8a8 GIT binary patch literal 1177 zcmZux?@tp!5Pf^Sa-|$?DW(1vw5_xUQb1I&glGsPO@Jn%B=Cu&-IT@SuDQDs^w04# zA21O}G|}(=QO4QpA#JtE?#|85y!Uoy(qF&7{Q$6liUx%tC63C?wkX$5xNDgo^K%+h z@*bHdX4x|BX1R87#OuC>0fuzoZTrG1SAFi9zT+}Ph27w%46Mp9;2#PvKTG)=y|5LA zp%c@}I~^8^To$(QR~T}|{?PvK(hh^V>NNNu2w2A;;?hd%7(zUb2r@c!j4?!tr5$;5 zONWLSLvlmde5-wYz}OCrj_C^d9XeGhTHf_a&d-r(?a=hL_^BU&@5M2W zM;ay=61}lH9wSF8J@JWHNC(woc-uGYA2!X_HH{tMB#NbLqPLEyNXVj&5)8^~aw19= zt344^E0-Tr;WRvDn7nDsd)sTbT8``UMy(~gAsl<{w9Z@7h+`TVh77UYWOQ0doD6e1 z3V6a0-*(z=ov#a79z7g_8JWT`xGkEt>9<|FTKE^HN5T)u>+ve`7WqKa>FhQClw4$^tvb{hmB}akIv>XH)=_oB7 z!xWVuYn|dEbY_gH3nY!{3yc_77&Vj&WWSJK!EM?TGDayOp%@sY^A7oRLZCd{#XUM1 z?&Cp-S);7r&iIO~x=;JaB_4i8R6UEFQEa3Ovy7B7i?lNTAIy9Z%hCi!T?oPVp$6cE zKJcuhF?IvSJraM3$#Ztz$F@jpOUNmeF1AAWf{-mo=u;RA+2+Hi#J*#C_X;yV39wU= z0jWZH-31iu0HxlL+H#m*pi`D2YFuJgs_9Cm3t2~nmWuhndPaXZ((@cI0`E9p;uTqP J79|Y*{s8pD-z@+D literal 0 HcmV?d00001 diff --git a/bin/ij/gui/Overlay.class b/bin/ij/gui/Overlay.class new file mode 100644 index 0000000000000000000000000000000000000000..833e75c66abae10786d121b9d8705627b8e881b6 GIT binary patch literal 14839 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∨qs#mmlFh`S$y8`yBlU+;&&Kk*&<^2 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

e&>lK|^6K35Jx`0E(#r6jK8zrutA| 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 p5y5=5i!%J&8CsiZbuMyPk9I+3$Tk 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/PlotDialog.class b/bin/ij/gui/PlotDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..afc1c2a943a5b057f8942d12cd552b477659a49d GIT binary patch literal 19632 zcmb_^d3==B_5L~czVptT$(v-7Fu<_L04iAs2x`=zmL)7vk{}64K*b>$!a&Hx%tSyd zqN266sNJ+dtEg4#S_K*6u5HDw)h=3VwOW^IYwKR?hVp&ReP^;j>aTzN_`v(#_1tsM zJ@?$_ocj(>es%AIM0AAS;wQya7+*cPwJSb(L0ckK9gnspTK!}(1y@JUj81Ngwzp2M zU$HvYobr>+6v{OA8qSX=Q?d3~CsR(t?8fB_XVlG|&BQfKd9xDj$yBsGwK&?=71J1w znA{mlY8sZ;FKDc(uWOJpX0qqco;$m)T5{keZ(hyvg|i##=P!~rQmC1n#@V$C=Fe!H zEqQQeFIhf+_MAq^1(^IxmM^TCJI`cu(YR^(%=*U0`dX=WnS4#l8|xQHCeQQ~79^LC zT+{Mwt^gh9&7M(RQ#ZGPsd(A^zR5SFI^*rF(;yI!)8z~58*0o9AiDjEme(<KSp12V^~=l5Fzj1*`w%h0%?BgT}wHr`$vUEdHrGu8sx zE=6;%x?xpf9XLZi6Mk5^EQAKnqfx(e`9Vv@_P8ni*|g)7qKnY6o|L zwrDc7WPWr-tc|H?Fd>>2O_>(1On*+IJ=G9D3p3?8nC+_g!dP-nv^kZ)kU^81(VmLW zXp2XaB7i`4vNhF^zl_(Inf;u$Xe(y#9NXL$Z;z*rXVN95iy`D$iI$k3W(-hiZ&e+d z1&SiJ$U-Q3%ps_+J7ax38L96`#i7Cu%|(kDEiIAxMjj4fb%)H0x2~!HpNY0EX%!I) z4xNa;i;}TOmLZ2~Mdi?QC2H$MRU-@06Pc4lQ6n>rKplc6_?jY(i4KPrnI6J{Lnn)h zMkJks&C}%47>YQw3^SV1l}b2tDu|#{g-3rmHlgG)BSaxfkA9}U8Qf(GGEE@zoVrA2 zVlqUlOJgY_sklqyXuLyfJf=O_tVO0NE2xBsMFiE5+Rz5UodIUevVw7}f}Ist7xWRf z0A?c9+jbpOc=meeP{gw=$w|B?okYi2XL?qesT{?!2megjKs;Ep-7rAsSIZOrflmPJy2?*mA)9{2!lkPT zias<6>mW*K{&glgVx6gYEGd1iap_uVQjl$u5xGOxVgEG5Qbxfd^W$sdDTv=sH!uy` z-}=q$inq1IIs-JFZglA;+73-PQw)3kN~S|I6Y6(VOe;Nb?+565x>+Q63+zY#E*S$` zy4a!HuxG)9G`+*6pVOUCq-aM6>=0AMV0{i4Cd278A+f`yopd)gNh;xuft_122xopu zqb4KOxpJPxo3ldSMK=d%7u_R_-^+B!AWRItocFu*06mD+O2$%M9ofYX_bS$aai*0n zl~Epc=@I$`mL=NK;`Qh|U`O=!99Walo?p52DD6Sl7ISpez)H?qCEFH~o7mfR;Glhs z&p5ep_LdRdCVY&ZSYrB~@StbA)Ml{srdC9?)udeP`N zT>2Zm2}*6T_SV!YGpQO8!{1#xf{v8b+b&hokwW=jF1;gs7Osu1iJ484td6aWcD1E) zQ2U-s?~66)J(10sB>g^g=_BbkVokiQZQw3#XpeTlK%}JE$1eRxnhnb~%ZR9dn@?T( zj6R1o$K-0(Mq455E|{JmR2#DDEx{JpOJDivOSJ4??$SOSwUmS2o{@$5@VW{JG~K=O zX@1@h9eU*fX0}}R37-Y4;w>?Abk6APOss>&*~NaB9nlm&G^PH;SPPbh1G0are9p;4a^GGOg#!&R@h?V2fE+52WJh^&$ zfkUCBbXhg@g2%c%j>n_212(!AngeW8l;uFQ!IVvtriZwEsOYr=9KJ+mV}B!c)~j>x z7;<)i%Xp%n%Q2@u`&=7Mt#Y}7Ct+gI6;K1Kq`I_P7&^@5DLmC`t(Tim+pOV#!CYg!Dr$J;*nI$|uBeD!W>nAdY zS7apMVaW(2Agr~xbeOQ#%`07Q6$2>N(ikj<dxr8$lT6&i^ITyZlpH4I?)+v zj5n{r-~fXtiMRKa01<|~+DLq51TzD;O08NOZ*~~q(202X3nqd&>2ivJ4E>oAfq${~ z<`Cp{F0W@mz+k`SoeTd0d$|IQ264@nEghs#mi}xkA_NULv^f?j>4>k7wIxeo^7$N> zH}bg<6Ei}L7^2PjZ z0eh)vXDWFz=Dr`@EYMjhJNXiqzsp;&Yu85Ml9}maS6+tkGKy+Un&|uYT>d`)0BZ<= zcqB4=vSbjIKX4P69rYv7%`Cs6o5NRfx1WEEk>zYS!R4)d738rJNPWs-0+{=ZGK!`- z{1c|}WswNH1whRRT&~oH$Vx1J6t2kHXe!#j2s_1ED~*Ri9qO+;a4MnxWl(HO~%Y3Mlz9PS4T$z3m6$YHrchJV(eJc z;k46G9_vgtcg8zXr=2D{@^+W+5TWHZcXf&*ERL*{-|6yQro6=i22w8DY^Te2^RA52 zqYOttFDB`oi^IDi9dD~dq^EI#0(=kO=Vt)WyaDUt^8NgPXX3?Enp$nZ%N!BrLt@Gw z29g*=e$_@N05l7{Dff=!@UO5$GZSqsGU6VWACrUJjwekK=J15ePYR^Y!^W+SrlO`l ztii9b3j(qWo_6^e{w>rh+0hoq6bIAMn$kh`t4QegF8_g_h3bI1w+W3o65BkhR3I4x zCLV5FJN}bPQ>n~hK*WmKa4$Q(Km@!zp+u^Ad;>r*p2V^J8Y(m|GUkUeh}_-$EW6SatRB$9E7R0!pN zx%>{l3#~B*HW6>eE~^@Z>>!JD;7GF9-UqFID)b?aqDB~lOnfM^qBlU~@V}utnIJ?e zQISe?1o%Jvsh>ZA(!&sW0JApQj;#-i@fic=3Q(=77Dq_*pmdqhgS{?IqzdW$mCO4S z@T?qO29m+%<0K!5`Wyu$>xWhd)&ee8maBZq#@vh zfPH4F&~p`XX&O0>8U}e~!%1G0Nre$Yn3d?*5E1Ow8*8d^6tJnPKhaU6A%O<*>)YBq zw_d_3Sa>zYRS`87wH^d%iY4n3o-4;RVGsrAH}kA87CUM@bW+G0Xa>rTIs_b<0VN!i ziJPGUtY<7EU;-kT5*mepa@v67WF4>~>$AYrzag8lTQXSHx6+H}+1|#18CsvU2I6U; zcQe9oYuk~^iMF+QmIz~JtgbnpQKyzI#h9yN(H6u?kr!ygh)((pS-cHZyf+AS96qwB z*$}45WTTaKJ3Q)687{dN1B%9jvNZRSIfkN_4z!z~sh6?aCH#br0nbomO4~Bi(QA_! z*N(S%I&Q|S$z<$yk(5j$4)p1VN8?E@Pll~o+hJ4))k89~&gx2y7&Gjbm6(%XP4H91 zPviaSa8ZMWu4+(?kQB774wl6`Y|P04XNA>bmrBL^?^a7()g&lBAh9EF5rx1qSDhl~ zf|zL`Ff);8i$!5uSe@poQz5E752`M)$V3A`PaGE@VQIJ8Rcrc_X#$yms#R-U)vf?Sea&sK`1{9tGJ99w zfZ%2qME`Aik=hloM?k;9LELRrysPkQQK^AeF_^A|L=&Fm#5&Q|LAE_(->Ix`1Of!zjl@QmDMRUYz&h3gfAc7hb&bPw5 zS3p98VGZNaDWJ!4Z--{_Rpjg7mB{g0S8Wr?%U~jTIRvHrdRN^bvMb7z&srr%m{E8f zuIfft-6V&rZ$%r<9TC;fTy?X8DRv+L0}uzsV6&J}n%?HB+eIH-G?hJja=azAN-BQt zsyo$Pvh`76j?}?DPNOy5YNxC2rtg4voDA75DP+>F+ASL@*Nbvx&MStp_Pdkm4aQ-0 z0a=z{`b#%g89&)&t`cI-0U$YmdLN3z=rL`q*>Mj^n!ZIRlk;f5b!W_EiPdkg?!~? z#Pe`&9v7)OVG`ER2%)4@lG&5EL`Y9su&{o??1ha@%TJu$R0^ekLH*WMzmr2X;KlrA z!3sO-4_IpIG~`S-nK7(c2{@2ob9n5oJ~{F^z%|I^ej= z#*!TcoT$Nq@bFhxy(&Do9uJ;9aMbI4UK5Jm5@}?OTZzDgf4J&x^-oZ5 znF=dDv;@ff05+yM>K!P+L>c`(SG})3fF7=lulI)jf1{Sn+3l#0FoKa3co4z#S^}{4 zF<29$FllD}{OXC00wBvZd0f2K)S=)rbT+c>-&r&Qoxi{YCPv0N>Px8R3aE{v_L)_Q z#@ijuC>#grIa+%{61pW7Qzk|n?dvP?XlcJEwyZ@h8RzI6At3a1E)Xro?`Nt@91Uot z4qG441-j6$gIRSnw-GgP3Dk+@?`yvI^2B{^y>rgRb1tO!Vu0GsY=Tu^TVqL6r7JMy7ABmNkl!~vE z7Du`IXo-h7aIQ==#L-n!eOA0fs*iQ`aU$9rIN3>a#qQ{D00u3eCl|m#_Iie^XNs=p zOQ?NOd;E;9SRDe1juumBfKL@PXb$FQHgsgAxioh4Jlrm3!fSBGQt=9$!g6;Ed@d(Q zw?4tuC+hh)L4@$YySBk^c;+mHb&xnoHrhVTuj^p5Q8e(wiG!yEbgf>1<4~W(bZ~F5 zP{i$B+e{ls2S>DS(2M*Ur)qJp88vq?Cw0ZTVy<4SPlo0KOC)gLfSVCSjt3mG3%lA) zKoNmgM>k>WW)TmZnW)S%=*c+Pk?ZxTqP3@q)@ISgpjsiQ z^lQwx?}W+5V_lsR3@GMwwwXUNPIK=k4z`K+Oo2X`1j69CZHZQV%#e&D>WYc1h$H5n zTm#k+u1iT=S%1^j=je?vKRAi9!w72cy)(pJf7|56_!36qReF;?->=WZUNYSGt8=x% zc2*L`QJP)o>WlQZU?M?RB7y@fNd}f;CRyfX$jqk`{fJ^W1{xAwoz1a1h=s!p_Nmk) zVF0_JrX4qzvkm+o^BS7`MJF^g1p`I%@yso%!_zHVfM;IOLOk<}8u2VBTHLUcmIQ5&$WkG4iV!(X zh@37&Rx~s@MJ;HxGHBl)XwrqWslLf7q|57@e1){FzR516JL{YLh4gU!PFfXoJW{KL zRGW}$7g8NUs#8d%Fsxm4CZ7JH^&opz&=Im0i24jkkhg;oPz+A5Z*mHm>d~Q4$;%%M zVvKLM(v`@ z`UWjB%TbBvQP==SQ$D^D5ke8K8-0(yj~oaXWnz;Ob&{A8E-JlY^c3{>W8{0v?xxG^ zZIn}1zMFn@7jj$D^kXUXZ=-;h{fT6Qf$ONyB=T+`r)b-D8hsyK-?W34Na55T`sr@^ zS<$U&x;;&I)s{a-`S;PTrir_0_jYnk5(D(ql~wGf`?ivGS4CNx9xAKgH2w1AGHVw- zR*|MB@b9T^gyjtv&j{f@E`INoKDV8&+ChKnCO-u97by#_Ptz+^mbA96C0E|T{B0Bp zTYD%Nw$k+aHROb`es7u98>F>wKrjSQ3OBhWvQu(J;fW17t zx7LYi)kLksD|3{PS6Roo=&85Xg+PGTIgRLZJ~Oo#{#;FPNExVA>Bd^bQd+! z-RONUdOt`f(-X9W-hz()imr|<}Tw{;5pAv zyY^BJo>Ovi_R>fc6=q(;?a!z%w3iC~w71&NBP7KmhEK<+O1^*bwol$4VRMU#{0q$D z@~B#Q-_4OdnFTxE%OBiLPVg&im5F-FL~F_ro`5IPtvy`2CTQy(o?K}Zx4TboyUL6C zaI^_oLA#ibO!HA#?rA7I9)0^2>fjKa=INumX(TGE&4Bc-wGlhR=uDdDp!@_WFSPJM z(0mk>m4ViRUEEk!b~iVAOr?3T$L!KxW_?p8+J;$s%0%A)X3f6IGK;;?RSqs}uz{gY zMca_?0H*DJg&Ag-XPEWv$86Lv>+HvDvoPBX;$X^S))=EIG`=hdV>HDi-0R5+VdB1! zuhK5hnx%=RF3>V1WM>(Z64;3Y_O%*|nk{LLRodRUQp~GxuoN?n7hf^se8FH@C31Awg+V<1R(+Kdb{?$EF(YjwzYkm3stkm3LV;B{v8r-S>VB)r z*eYuq4e@yg8IDN{rzDzlRi3WQ*Odj^DH-;M3Z_;C%R+Y82^CCAb7y5?He;=qY*A)T zy|!;yI5(7U`Za{zQ2yq84iz>IXoR&Jq)}1h<^m2CfcxwAT_Wv+`TY!^TOKU%l&)N6 z5Y7#|;k-1TnVDxUsK~@+A63ENkjg?KVtQ^)^F>>!SehRy%_WbW5JC%>@E-n7Wg*hR zzI1TGPstx#0MU8t-z%$psP3iVf>2?aFCS0`5lP(-!@=IV!H$E*9ehQVT^pQp%u!UYP@i(qXJh&lW4q4YA|^FLAW-TfXg)=t?JSH&wY0R_2Wvsex!3L66>_#xZn5(Bs2qJ; zNX$`H4#v@yx!dUobjY0~!?@v`5cc3>lvAu+ffa6AfgYZPJJb*uo=|ROU~?{qazoB_ z`<^N+}JV{?VxXBOffYsOzT;p+^HcK zGBUJLhjn{NbVo2^*d$<;9t{~{2cw5 zKSNMx7;ROf=qgoCSF0oGC+c{*MlGdl)v2^iHPdx!HG-|{=mvEW{Zw5^H>&ICCUq3Qoq`jho@dck^tUbG&em#p8=%hq%B ziuE%6)q0y=wLYfTe8cE<-#B{1S3-aDO``X6;cyT>NEa8RZxWdxa$6cngqYzQKR^2H5s)zs+=EEhanYE zNAfN;1?_Ux@qC+_stzYtE#=GA5h%-3r}7qPokRKRbUIsAB2@s5Td9r$;tfLYn$*$Y zsu0?Df;tANBIwjpstUCQ(5#2mG-(HYI$McHUkHtvua4uZ$cA1tVnj)8rW*APr2Np2 zBh_@I9B9KA;QmO|zD@6`8R+YWX1t+hN-cC_x0;2r9A7ydYWNF44_XZ605subRgJ#6 zzBhTinvI#;zIS+(nuC-d>t3MdqOW5Y&~a*>P_c(nr9tj_b~%kzH8g?p?bX<0CqM={ z_FC+`6VWbUchEF5TCRN#9c+5I_A6ADDf?qUrl!Vg{6JubkC{{Jx%czn%lS4y@?wJ7xxt4)3F76zJcR{i@Ed z>Y4CW(N|RAWfx3G=~rl|>KLVmgDK|X;HOlqKBurV8leId;cuWIu$PLtU|1j+2=0UX zF>r7izL$>k^F5za(a1S?e@P)c@fV%&{FKVSGGrJTspl_gDDzMZ;tbmdcW^&y`>-?e z`ZEv44et!L>xuTk!^|I~9$!*ROMIw}CKX>xLhXYG1s|+;QA|%GpReGhi9Ghf@(;#^ zsgyyKr1oJ?er+o)tsuhBv57vXqxNFwj@}2w+HcMW4GF@#!;cO;A1`MD{L)<9aX*4% z>alLhnfMs_tlN;;-_b*qpchRFYK9Xrs>&))tCNoLnWulaZ@B-;t>n|_t`==Ya7S7x z34EwUaEeAq9&U~aBuLMtX?5x@dRJ1$kJu8*Nh{+X38}EVrCRT>Q1Z8Nx6RR(kOtKqSvMv$esuCT%gLqnoCw^lhV?rEkzUDJI(XwN8rcvumnEtFMC_0{3GFZJdPILGS#Q z!UYjNck=V_2@t3Y*>FcTRl>b{&dVvu$(TFTPt$6`#~?a({Nz|ZgR*L9PWo=*_-|Fa6IJwHyI*L z5-r8+_hxKM#y0AdR=fH@HG-{c0edD=F2gnh5|8G?a1c$wF+7!yDCCRdKsIkrV1Pl*RcB)yYY94yV+` z+@-qtOm!`9Pc`r|`FN zU;7>0zg~jN%kS!?yhSJYQhh#Ormy77^$q-eeJlR}PW}(|pZE&>GGD3x#y{5Ya<~4N zx9Yuom8JP=D~Eq#74kLKFuv9r!`rMAv~-MWeIux{s{TRptPdWd%#u(}$-TpUJOuxdS$0jnreJ?b8S z)zQ{ZGhnrn&NTqm!mmX)sC(6YxHg+fNp(L`emG1=ss{j=a_H&acF&^SgQne|(e5E> zhXCr4>S1ZeF`B9#L7BxV8n1q#CIIq2LmwJ=Yw@%6j)Av6ew|)6@Yd!x=|uyG{VD{& zDFD_{=TNl?-a6`HI>rQVbJUykfPtfy`ha#AIO@|rxLN{7Z5^UZ)h}_!;n#EMRP`&Q z9K95<^-aMT2%TKZ*hF96o3-=$^)R&D(;odTB(RIC>8@)%&IWv!uF z10;Rc*|_S$Z`=TRHv)tT==KYcIO$&?hL)$^xLrfeQyiZhL8n%FC1IJ}}-3 zC_&D^J3Qr6V36$=F#F%?fp@zBlnxm<-d-w4e**`{&t%iT-%fV1de$7AS!{xH>=?O7 zD)(-XvQ7VJn*IsbNn+440c`c>V)YmAhN(xrf{I?`g=23pDz6zTZ)B->k>$at91smA zxfl~bl8Nm950U&#%>RE0qg(xET&&&%e~878?ooe7s#yI?^45NN#MZ3=4G>rFEf^>i zAdUm(!yCQ%#Ez4~nRPi%*e+Hd;-(L06hCSf z_}@mx{{hcZhA?B6%RZ*I({!)rrm%hC*L4p|$m5}@+C39JiP%UpgU0r z>2Y|D_J%Ij2P0#o^2~PX$^~8E%hXRQuGu*gx}vh$-e;>c$%N%XH)_IPR&O+ z+7d?$%)+_iG9pk_1c79b~&adem z{JMUg-_n2Kzw6ieA99}J+A~k=ROUP#r61RmaBeWJ81Kjge?FqkCgh{}AOu__z}Ct9~tTu;VmmN{w0(0Dg@ zCd@C+8ax;K>H2*zs<7E|TFCo)yhj%Nw+gua67Z0Kg#uN!b5?yR%3#m5m#Fk8%}46m`D(!sfq1^y!m z?|@NH0m?W~w#&*NqmX=8kiF_NUm6tmFOd%p%tlM=!|E#1dP=cABCRX8QqapBgG@IS zRLI5q>;9Iso>nJ&pd!0>j`wy(T}6+cUN^B=&#DLx!6gw=vv<&W<~{}KxjX0rI4fE% z`4AJwmVE;M@EL=3*F^Ydb$Fa~jD;sIy@JE7{41}aoZ;5x_@o47I4r!L__{=_Lj?$f zo|V=$RrYsx|x4AV4~u3Wg$&!aHQM35>eZ1AaP(tHX4|}mk!ba(dQ^IRA!rxhiZQg3`?*aO`c*y#cko9 zrj&?^9PDYz=#nA2eYmye%AZjFa0}^@HK@#f6jBa1Pj~c*J|EK}{PL1ZiGZO$-x653-73XB|u zyL*JgbnlG~*hgCxHlunUBL#X#4H%;-qXzT_>|Z5@50r34L-GTg4EZ>UEK~$Xj#B4u zJQk5L|Hx6Bjvd)Ih&RR2WUGS^93Nu_EA#sF4i@YkEa(j;6!lq{n9dHHk6Gb2@tI*4 zVOW_}ad}#wT|04NTAypd9Bh7ApK&LOy^p;DA}TeWeCptSQ!MTswK(IC`6ypstiO%c VN`FUs>r3=^@y4%-bS>Wa^MBI?7*+rP literal 0 HcmV?d00001 diff --git a/bin/ij/gui/PlotMaker.class b/bin/ij/gui/PlotMaker.class new file mode 100644 index 0000000000000000000000000000000000000000..bd34bf932c4b973ad269b400d21ea94a2e13d561 GIT binary patch literal 186 zcmYk0K?=e!6h!A&ZL~u57(pIIudYTOPEI$0AdC(PBFw7A?yt2bl$;&g*fn~4 tQATsWN4~1|@y1Ni0~_l(FMo5(w3HJF1AYZ$ygovX^g>|F4q=jf6d(E-EM5Qr literal 0 HcmV?d00001 diff --git a/bin/ij/gui/PlotObject.class b/bin/ij/gui/PlotObject.class new file mode 100644 index 0000000000000000000000000000000000000000..a9beee624b7547374565411e0e847448e5e2c313 GIT binary patch literal 7364 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/PlotVirtualStack.class b/bin/ij/gui/PlotVirtualStack.class new file mode 100644 index 0000000000000000000000000000000000000000..5fc1af8fcaadef50e15480ecb4510c61f5b4e402 GIT binary patch literal 2705 zcmaJ@SyS6q7(Le(78n5r!fKitvKfcO2}zR{XaXjiQy?S|D50CM6(b@nSIA`PzWste z^)2mmrZ0U+W*XBeGi{&x(BITad#V#<(>+I{WHvjvAfQazc_WH2nlv;Zs-i_8w%1P-&1jY9Z8S*It^or3 zG}Iv?SDhME)HAgyvuMne*H;W@USF{cnUbB;tp(jN<#({DxT_}9?U{N3OlDno*UZb5 zBddm4Sak(dJ)f6!Fu7!{-RRbTHd=oe_Laj4+{4sy%2GxuF%!g2KbGWWsdj0AI7 z!x1C}!gMRpk@T6f^kN}>ZDozzjSk+Dep4EbVo0FD+?cd2+c9`MoE*GGSCVvrrd`SK z+bh(xUJ9@_6byIPyk}Sh+(G7^4-_SVr!?07H33cDcI=$7v0*y``=tRfmHe%Ac3m$R z)$`FP&f*Ofuan9bUea&^Bdk^Oe5A-L)E;Uv=!eQt`}R#XAD+9!;LUYHaFOm;!|%m`6vA zGOAbD2`U+%Gav8p2CoN@lUZ zAPuy7rqa$@AVD*DlOj?IbR5i?+HPT^jylvC5S{lmSj6Zns!h_Wok7aG2ljS+E-b$gRliq6?( zCudwT<@oE^JHd}ju;^#YhdJ*<{No7qRve&w%VEGN%x5FNN>IFe1F3l57PQnhnwMh3 zTZlbDdn#7{1Rbf=FX-U=vFD?U&rW&-n&`Cw?ex^cB>T~VZtf<0Z`|wU7chW0qY7`; z$5n{ySFk^bp7ZhpdKshrXa%oUaA=Y5!SB$JiggE3!;n$im?;=f!#+Mc7@^aP*hwGs zP>tA&V>nL#Y3_z0dbm7+lR-d}mnAK&sdxp$(?frueyD;okHjPI_D-B$qKgw-A93xk zLDpK0(~40KS%f@};Y~W05sCF5w5Xg0Le5)1tZ1e;$=RaB8CUWZocj|KGW?}&B$mWB zCYOe`aB&M)W{y?x)`%iWk99vpQ;$-?%!u+6_IX?N57F48ge$l{qVy<_W{&-en@pZR zgSf`Av`lmo#4u?B4ze(Zcyfe4R7ni-Zx|^idlVNjglQ5rORBCj@tZu6+=5*((tBOY*t9u&NX_qiu2=!fzYV=5+8BuGT;U&*-&A-DxTmKCP^ z0d$`m2Ih34S-s?@f|Z}S4tXFV-u_uHG0nddBqm@~?bCsMo#bb)@mP&Wd074pPX3*kbIk5JV|DBxDW z#9H9vJnf}E#ZI^REL`zi_}#@HzO?>bN1}nKYxGy2Jj4h7d66`nn$xjMIVyXoG3j_%ie(thY-xg zLPUwsv*`U~E@R`ON$9T7kA>D^l1M_+oT|L{gvmy0Y=>NauCy{Q2;1R*>isdnIZCIp z1;U087neNL(ecqjK&Xedp)*oizAL^?WcK;|%&NsCSV%o9 zw1;cB;o>@>!CHrJeB8t>R!|CMIE2IgxP?2|adDThxpo~N_pnRwOOd~djampJrv&;! z@IIwQ7R%SlI?`F$`Pe$f$gz%-G*^1|R+`gv>f;gi7%nT%FL3RIqsn7Znn*6BHqrUB z9NYZRn&A(%mf2Av@?7SuhcCvf0SW@^9zSl4R|_7am#e3)Rfsp(KZ8H`f%dnmXOsU; z?g8zg%eilJp4!3{j%*`>CBsXex3zZg^e46l-{CuFxV>U&vjR{b4H{sB_Ajutmuz+1 W=lL2QpvTc+haV#3%xbjE{QU#8Y=>(A literal 0 HcmV?d00001 diff --git a/bin/ij/gui/PlotWindow.class b/bin/ij/gui/PlotWindow.class new file mode 100644 index 0000000000000000000000000000000000000000..24abb4e5c6c7e3161a853c8c14edf1b582a1e544 GIT binary patch literal 28603 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 TW&Tz>Cm{{sA`K3k_`m-TYM-2= literal 0 HcmV?d00001 diff --git a/bin/ij/gui/PointRoi$1.class b/bin/ij/gui/PointRoi$1.class new file mode 100644 index 0000000000000000000000000000000000000000..0e3749bb4184e5c78d4eb9ca3cc9c4117b512cce GIT binary patch literal 1347 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 N;YZ#qj|I2n{{ZfiLfQZT literal 0 HcmV?d00001 diff --git a/bin/ij/gui/PolygonRoi.class b/bin/ij/gui/PolygonRoi.class new file mode 100644 index 0000000000000000000000000000000000000000..b99cf9339e0f1f8005821b2334d370c9b6f4f430 GIT binary patch literal 40172 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 ePvajtDX_Z~)dx9!`-Idq%fHn%yj`#N!e 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/ProgressBar.class b/bin/ij/gui/ProgressBar.class new file mode 100644 index 0000000000000000000000000000000000000000..de3f2bfe700ce558d4a10b522ca1b60f04eeac89 GIT binary patch literal 3793 zcmZu!>0eam6@D&rXJNR)h=3ZHG#fHpR2syEpcr99MsWo}w5eQ}3ycgiWM%-D#_Um> z)+CLIf=g3lYtuG1+6>Z|Cfz>u*Z-sa*zdK=bKW})QyYGA-{rjTS)TKp^WtCr{l!-R zn(=2ZiZqnRPBe~A#TpN!lA|d*oo=^MUg#RiPFT}cqcxLh?64BkR$7C*KN9Zk(x62& zEbB-n(itm}>9gWfwu|~sYVdUJjO^;_l~sj?l3{hPKNii5X)v5smmM1&%TNc8Yp1Al z#u0nQaspr6N@sdw6E@|$HF(ow$r-xD)lm(_Lsn*ZY;Q73sc*_iT+NVwpB>gd&mpJ`H*Yee%PFXAC@nS`DsHeP1y&)EQWg@?zAZ!3$GE z>Hm6aU@cbi`e@2JCCAqrsKQD=kAGVfJxf!xKocU|U|{PEc6lbQ&yOYCKyw6I-WS!g8Gh;Dw zs-pY<>DMay4;d(dM`pbS3}@D7V3{*JVxZKS9W_wq%)VnF0FMU0q<%yAkUdOq9t$nS zy(?u+j>U%4twfZN`Z0(hFDwnq^GR7eVqh3iBAB)_Im)akP^7k69StJ}MnQi2#HwS{ zD=EtvIALI1iK8iNMvx(qMm4PX2Cs;qkex#)EV?zkXdnU7(;W&Q6_)uXb2aMu`X8(@ z{s=xZ1}ahE$0?lg;xr*8q7&9|D%luGklm9hn_mOp!%HN3jP;!_A(Hf{Xn5Jc3elk9 zRRgaHeZ47r(uyTAe!PLVym*r}rZ9?IiP6TMQ|XL7Vc;Cj6T9K5RLV}MlHE%PB&pQ9 z+wC|AR&AMt6-g8rCRmABddw!odPddyvYrf!bgwn3=;6n^_>mXyE!K-NyVuH$8F(L; zXuukx89fxKkBHYF3QkGsF^(^hMAT+~ArTT*Jhv#BvC{kOQ7aRhW)3`4lTjg??NhWf3?Zw!1c z{lnefy`y7~5PIIg{X8A_Pg`-uN;h^UXxI}?W)36?T!88ir0fx!nTz(sPAeP=41}4I zL^_sCw0iM7)^EN%@&Z4;#MjcHSoa2tq#(C?NO9HQlblKo+nq7#0D*$XWxZ&_lLS%T zi52WzZg{xza#aK$*Tn^EKlj)Nl#E?KjqCwxWCu_q>tBtmel@b;s*#*lBdbG=?5t{J z6ICO7rW)A-)yQtAM)o>2veT)NeU4)ZePq1+KE>4^XiNO>H}B(Vvp9=2<~@Y6SZ6*! z{lIekRsH+VeKcm#L{W1VTTJtFY?(((7Ta>u?OC*&t}Mc4X%?OG=`yQp=CS*(GQ5}H zCN@9dY3PKim<|T`20TNM8rZovu|e-(Ywl!I-a{u3P|}B5?Bk6m9QUxFyYkboVARx1 zfGlDlY}>hT&hWH^Q)^adanP*EqQ~@PaoF@_8De$77tLb8tOsQVQ2`d8C*_kqS-S zJjs@o3wk>xg1*TB@o zx1qb*su%Fixw7FJ8Imn}kdUv^XIk`{Irx~m7TsK>&*Ot7aGoMJff*LtN%nwKtozes z=^3oy^WK1$$lbFAK{VxqXu@UM7Irq_3TH*s5W=hqXEikmjdT@633T7{+P|<$bNP5- z4p$vsp^cY>jUKaM9@ie_=&QuwwE~WM@}@nx!qDV3&FPvYLq%5yiE(xoAyX>$upZ^N zUwsGkLC%lqEbbqRy`S91YB|I9dAz~p;_M)Qd)N(@uyLBvn^gT4o6k8C#?=TfnVA=|`w;Ah#>< zix)4H|0+@bsj+}N^zAlE#Lac$Cdb44?XThI7(L~{nrh?FReJdbHgckrc8y_0BZSs|h1 NL6@fYM`iI({{{6-#vcFx literal 0 HcmV?d00001 diff --git a/bin/ij/gui/Roi$RoiPointsIteratorMask.class b/bin/ij/gui/Roi$RoiPointsIteratorMask.class new file mode 100644 index 0000000000000000000000000000000000000000..b6b9b524e7090978677118eaa2238b10333df6b3 GIT binary patch literal 2231 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 F{vWlT8@&Jk literal 0 HcmV?d00001 diff --git a/bin/ij/gui/RoiBrush.class b/bin/ij/gui/RoiBrush.class new file mode 100644 index 0000000000000000000000000000000000000000..a347addf09b032ca47fdf1602b3f2918f85790ce GIT binary patch literal 3195 zcmai0`*Ryt75>&*?aJ!`3d9yIpJv4DH zmvMAx3R>2r!{tK3oXF=C1X70%DNxc1LL(>oMu$`VqYCQtxk;x#Z)MJNgPC^}1WVJ_ zBu90&m|vi&0oRr}?{puhV5(T?rPi#So3o1L(s4$xeuGtTxU9|;vve3bkjYyG%Q;BX z@x&PA{gl^3L7M>`A$i+kpx%4iY9Q>r#SGl%y**&SK#0%>tU~Tsd1fMKk9uHQ28tOo zKW5sN%zdGDz;zc!06PqP7_H*rK?CXZ(>Zex(sxqRk(9ZTex$kt&)QK2kuE3xzQZgbB$Qnffo%|19rI( z#y&izW50q%UwyP#%ukrMfsdg_fiabH+yWS}o|m-76X`Gx;GmAj36Ux%twOdqKWG-r zsT`F)jwi?q=NEv>s?JLCp={>4 zDYCwhycX?E5Zw%lItnx*Tt8)C78h7I^QI-`6HiNBoj}QeQ%(6`F>6g)IlC05!#M-< zxJYKqY_{LBGt%Fi;v4gsW^}Zo;CTa|lFf~!@`PiX8KY`+pw9OLEWZs-2?1mA?%$y%IXWiD+@uju%Yh>D-^>Vq0m!;ia;hGz} zf4aF%rQjnqTGe;e97}5v-!Sk^e2cLcZ3R1PMr(Wim(~C- z8~Bby(U>XDE~FfG_C(pqm2`ZUG9UlYoSAnE@Ks!qpqCWr5{JuiAAVrqhhjo2`E?(h zx3bPOb4jKikAL8@$PwF3uKN;>(nHurNCEx_5#pDpJ8!O#obB$&lieM8uDc^o zb&hqyfIG5#+>s|WM~%-Gw9?up&U!h@XLDc$5p@Ndo10e# zJA5mn+(v1eX9gd^!|p7INAOWPQFvGT&XhkyDtz|z@>Z=QNFj5rA^FPgnYFzbDR0yf)1`F$ypcL$x94RQ!-5co@Gk1 z*pBnqfeYBlgX}@Pz)SQMJcP@LvyT#ZgPL!m6YpRT{zgyR=;f%#r_Z{Izv9#M$rBlW z!Dn!ZIcdQk@mYM1v2|dTHb2j}_TnnOz`OH`9}S9*zB{PbapDfz zbPU|V4jpH<+(ATXX^L?hPg%cuKA@r`bewf^geeO4Xi@E5JRH^D!Qv7cZ)5RPR9nSM zBL4#xld+Bn6Z_=~zJ4;QMS~T5dl|9Yc=c2)q6YI>uJRdCBkC%?*Q0etwFG&}J&1f`Avh0u7-@~vZuf2m=i^-#-LlUPZrG0;T6R!vM1($iQaO(~H zA`(2?)t(*lX1|qLEIagKP>D#_a>Mo4?*2n6radO}Ror}we!R?YXI8*#fxnX2HOJT; zL+pg(%=a@m!uK(Zx-1;@Vym%`b+Jpfvo3Zy{<1Fi0PXbn{SaqA-lB(~+XZfpJDFhR Pf&Z}J6#R}#b$I)~%l(%c literal 0 HcmV?d00001 diff --git a/bin/ij/gui/RoiDefaultsDialog.class b/bin/ij/gui/RoiDefaultsDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..115a522c8e5c4549be852f4f68f399fe28e4a668 GIT binary patch literal 3553 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 OC?(Nf{zZcx{QG~SbW5E8 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/RoiListener.class b/bin/ij/gui/RoiListener.class new file mode 100644 index 0000000000000000000000000000000000000000..335aab5bd64578d94bb43e772f1a5cac331cbd01 GIT binary patch literal 361 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/SaveChangesDialog.class b/bin/ij/gui/SaveChangesDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..6178d301ea04338e46f35cc49a9b6623aa5dc662 GIT binary patch literal 3720 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 H;h6D%d`U5% literal 0 HcmV?d00001 diff --git a/bin/ij/gui/ScrollbarWithLabel.class b/bin/ij/gui/ScrollbarWithLabel.class new file mode 100644 index 0000000000000000000000000000000000000000..980c1b8e7e6d4f53e4ca766846a68c314a579b14 GIT binary patch literal 4908 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 z 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/StackWindow$2.class b/bin/ij/gui/StackWindow$2.class new file mode 100644 index 0000000000000000000000000000000000000000..3bd8870edf1e1df4259a63c66424e7088f90d661 GIT binary patch literal 825 zcmZuv%We}f6g^HtrWvN`kd}s09xVizq|lVWE{IJ8Qlyk3g3`4!u@VQz6U_r8{tBrS zi3Q6p_zwj91mb!+&`PN#+t=5}=iG+6guuJ=~FE!+pdiVs9W$D zHvR>;KS zOFedD=QEtf7nDDxm@Bjv2?P8hm62WSKmSI*q^V C;I-8N literal 0 HcmV?d00001 diff --git a/bin/ij/gui/StackWindow.class b/bin/ij/gui/StackWindow.class new file mode 100644 index 0000000000000000000000000000000000000000..fdfcabe2a1d970fcf71277423b442d0b5fca0f40 GIT binary patch literal 11461 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/TextRoi.class b/bin/ij/gui/TextRoi.class new file mode 100644 index 0000000000000000000000000000000000000000..d779cbbb4578c522dd1fde2f37063a8cd567715e GIT binary patch literal 20750 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/Toolbar$1.class b/bin/ij/gui/Toolbar$1.class new file mode 100644 index 0000000000000000000000000000000000000000..3db2893e67ce3f89de8ccba523fa7f78dd0ccf69 GIT binary patch literal 975 zcmah{O>f#j5Pb_a1mh4&2>qgI)6f=3!J(grRJlZ{galQjAP%`LW<{JBn;M(^u1Y11 zdg!r-{-~-m6e%T0sipNid*|(&d9%NMfBymC4c;da6UaJe)*y1Mj_M0 zeKhA+sLmZwBZr>t`eVl%v{iWO_f5KTZz*fY(?1Hx9Mu=tEdOW7X4CUjaNtVHACtFa zKi%GVHp?RivN<03EYi4XCq^p`lk!#x?bEkLF<;%8B2z6+k*(II$W^}~|7Dham);4= zh91W~!px+6+{XiobbC>lpY@p(d9Pmmip&*?Kd~`Kmw3uZT;T;rT3K2-O}oSntHi-7 x1>!^3Vdx3u^(2aV3N_vMN3}jzwPqYqB;qc?7#es*QKJFA#ydg=6~d*^^9N8f*Hi!i literal 0 HcmV?d00001 diff --git a/bin/ij/gui/Toolbar.class b/bin/ij/gui/Toolbar.class new file mode 100644 index 0000000000000000000000000000000000000000..8f7d9ccee0837eb49357d631cf11b91148b1becd GIT binary patch literal 55230 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/TrimmedButton.class b/bin/ij/gui/TrimmedButton.class new file mode 100644 index 0000000000000000000000000000000000000000..beb580fecf9cfd8a25d0a87509589929297232f3 GIT binary patch literal 1174 zcmZ`&%Tg0j5Ir}q3By1jLLfX8e8eOe#s>ltKzVAc1gr{D$Zkk3$%SO1lZjaVi7wo_ z)hYx^t6Z{h<@dNy`%VHCQo6{br~CBj(~tS_`|EcA6L_M)#n2=6^g>PO8>z}D+d29Hf329{#*%?eZ4^9-SA+QF3!v!G`QU>1_8*d~FODtWHL zjdl$RG=|=PAY+?tVCdA~g-=Bnf(p6`DH-4B)dM2apQkd0X%x7n;SzcXT;O)PQrTZH z^Q)Yf7zU#;N5WY`Q!uF_gnk8m41LXZyiNTxIZ!YR1T|XSW#K(b%>h*U3kok@f7zmWl|X3T`rVwa!UH3~?GIe^oO| z)s_LR@9goMos4bMBGSU-YqjzYw>FHO5@`&kD>GfMmm&P$LDD`=XFMHQ+sN%_jF*k7avd4Eg>tQ&6>mvf=xl!Sa+XDzn^jRU zlM1Fuw^R8-Kwijs6G!O%2ybw3#&d$WWh5|8|I*TNn{-J_YIPAV zf;+fNnn%%xdldCh7BB$!9}L5zfGLR3F9a-*^$Y!!PlIlBw?cTyH~0-xFXG2|aEN65 z5FPotJ%5aaPZV}RO7}U?UPqc%PishHSU#uJDIzJo@guCXCWo7nz30iRPKn2V0mVk~ AX8-^I literal 0 HcmV?d00001 diff --git a/bin/ij/gui/WaitForUserDialog.class b/bin/ij/gui/WaitForUserDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..040ada740cdf4c66474faca8638ff1a01e170f00 GIT binary patch literal 4692 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 literal 0 HcmV?d00001 diff --git a/bin/ij/gui/Wand.class b/bin/ij/gui/Wand.class new file mode 100644 index 0000000000000000000000000000000000000000..2d20f47496be9227aeee81d6443c5e60a36e3dcb GIT binary patch literal 6183 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 Ia`5;60pe41ApigX literal 0 HcmV?d00001 diff --git a/bin/ij/io/BitBuffer.class b/bin/ij/io/BitBuffer.class new file mode 100644 index 0000000000000000000000000000000000000000..95785bb8b7a05be587efebfe564c6300231192c4 GIT binary patch literal 1251 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 literal 0 HcmV?d00001 diff --git a/bin/ij/io/DirectoryChooser.class b/bin/ij/io/DirectoryChooser.class new file mode 100644 index 0000000000000000000000000000000000000000..86bcb2b8850e038c69035ba5cd5885873c973858 GIT binary patch literal 4307 zcma)9iCiBZN&r6ssnWnK$rom^XPd1DY;L zy4j|7U$JYNwsuXTMwo^+-L*~kru)AB6Zy4Czw_Rk85GPfC~v#>oO{mqedpZEvoAdT z41nA59~C7EYR$=rX+=6s+lXc?`%K4#Wu*;Ug-=1%q&}@j5_)nx(myuIaRn8z{6q!8 zp5pjW#x|4VZ3@aVW+q`MD8I{0nwh-{RyQr3);yxX*I~ts07{{0@MDF7(x&E-3MjZm zgNpJ1s83f7GqnL^Ts(+plFcba;_8V{fWjVf++y>dO<((p=bQV^h3 ze=1{I$+Uu+rsk!oiR~5*ThT;o&K}VdS;Eo13<^t)mws7h1;Hq!p-eCeW4nqL1*;q; z^qEX#|Fn_J9LgG5LqkLmt1{D_W;&&3q7%atwxP!ag`FC92@1_iP*{vve{y)jOc$+g z*YGN_+VGNA#J>+dMxWy zdop&wFcK;{8OHKaY}y$>2M&lv?^e*#bhBWWp>9O@>C(`RdnB6Vv~|khibr≶cj# z5QEudQh;e|9#PSor_(9C{imabBRCcP3{rSL9ZAnnuaU)61<(ggE`u6|grk*A)K&Aa z6r>Ct)-Zx2E(Rj@AtS+>rj~nIi7d#bu4abZhfxj3@H)?ht&i__VvEU)V-nzrB3K+{ zMxC~mI;baO3BwNHIPMj&?xRfH6qgH^iONc28ls@%AcGyY^<+A3*q$Q=4cD9BFz$GW zV?xEaXxwRH{V5~q=B&Y#a$O}tHR5_UQLJ?przk9^UfA~phX5uqB{5f2u<5YWQ_?^x zV#Zi@T%b{5lP|}@p~My1zO0#u8MYrJv$4=gylxW~nK|$pFM2QmsCr8DrH;K#=dMOV?4r?l9CE1SJn!Cj4JsRGN_c0=}vKSolm+z$ckc5&CX!xLHDbw4>(pDmyF$VO^ zgzO9-*6P!hv9W;L9b6jw3D8zrJZjk?>Qg0&7g z%W9b0L98Gy?k{oT;Zm7TgC!Fu`+j_u8X?~aV>^M_F5g_893sRHcP;lqVN^Y5S*<<2(M#Hxx4~=;gq7UFMd`H7~@jWIuEn9Ry*}G0YH?2fR zK0nZKRlHWxs}#74&<`1U#E)lrXo>5IG@EK5txp?=9mo?tvt_^@@a9Pdq^K z?&k=*ITSi_T8J?E{Z z>eiu?BV9Zt-a(57HzMrDoqPk23KF?OyC$^qsRV8OS`XigG%8V{sC0q$kb{o(CcbUq_dN`HuM<)&7X`!5 zhg9Q*B!TcRhC!7c_`~C)yQ~tUqcVOu=PcVdI*)-X2!w@XqsS z3eVx)t-g?qykFQ6dUE)XM^p|UE2euEj}+1!WJ=%}u$1^$Vn3>IfGpijP7V@)E`*5v zcHG0Xdk91?2DxD!#Q;v?5Sz##5a5*@~i(q;=nB&ETe*y8)Z7s??o^~C4vsJBEudxUeqJ06OkhrK2Y*Fk$j3CFU&T)=T;1rK_ zHXG0kpB}^%0ZekEOyO~EKbJTnC3T!I5t;%ampwv!cmh8r>?N#$NAVMfkkzE)r+o5p z)mew6GOoMAA*q~e9K_G?bB^0+$1iwSopRhH<#-3m-Ex$YbpHce|A&n#;(Xmgs<`dA zgi#8md;)emQm=QA|0OXK?H16!gy&|#+O0Z|l^l>?3j6nRdy=+pqp5XDZJM#nU_JNS zCY-@-c)-bAbJy@P7h(BOvc8sh@TB8KGUP>47rKgS(FdDhVh(?vg&Jb*6zN&Xi6F>9 zD!vvEFIu3^0S^uoTp(Cl<;UMxNp6?jLV%@#luL(g7Jns>4k=v?gjOixIBoupe|R>V cd6&_$e6Guy(CnX%@xSnI-sR8O4&M3me=fcskpKVy literal 0 HcmV?d00001 diff --git a/bin/ij/io/DragAndDropHandler.class b/bin/ij/io/DragAndDropHandler.class new file mode 100644 index 0000000000000000000000000000000000000000..d87ce8bd5f21bbc874b03d5aef937987b3c040d1 GIT binary patch literal 4429 zcma)9>sJ)#9e&>3h1p#Ox#&XHTT!EOb&WN|ENY^l5e1V7!Gf{vzz#5E-KDdGqBXr@ z@Ao#b&{m^qLR+axyetwUwP{V-G_{xXcG|z7A9_wt&*_&Q8~VI6JFuWMeqeUyeSeqd z_xvvJ%UkchItgGS{-VGmaF5X!F_Mu6Q|sB5h&7nW{#{xk7S~M$K7l2D+F@-lk~(4} zdLpf+mPmE$W=23j?b~U@_4?jqGNqdWi<`2gcW3R@3FK}z5{9)^U|IS6Vij!yzWQWL z55kK)6$(@VZ+S&q00IkCSn9f8P^4ho045xyiGF=`JGwxS-nO| zf|@Ttf(mqLiN-_yNz)S8ItN#Mk{bya0{x!&0!64h)-8Nsquo^U^wxXO}RUO7fbAH7HlG zR$!%cc{^{}@+vA&NpL;7LL+Dz@Swc0o+<9O!9g+X;vc1=|ElZAIH& zi)FI;w+|Tcm~Q$}&z6eK42v>1l(Lj@Oy3g3!`LDFcBeon13b%0T}4|SHer{FM(k#d zOC6h{Myc4Oq8WQRDzyH7JrNUFTHaWZ71=B#b$&c1;4kYo%#;-^lluFlev3fOogB|= zM*@06MH}`L3E7vxXSvM~Dm}DI4;^znWcKGrDm9-}aS$IQU@Pf_A+V_Yc9psyQ}kgK znk3L0PxdJ25-6OT4L?X*VRjl)mbt}`9%lAHF#ljx5S=ho^x-M4r269q!&-YMS~BgB zog)Q@1QyOz&NkE3j>_^0B95d?F6WYu9JNU=rizrzWo}$g^jN(L2IRRQJC&!3ERG;7 z?2wN^6-Q;WM&~oY~J*&nH!U3ZEuDT8uR^ ztIFI|a2lV$XBB*gttqj%Cs_8Gq~dcDO{tM;9*A2;iymj&7|Fzrgq%DviTDdDzKAbz z{;@0`(@dTPG9WVVW-b#VvvvYsQSnuLjm)KVtA4OR$qqQDVhlf~dar#K*)iCq_siTco!Ld)+G{3{IOjy0f|oMal}M&s zT8<_xZE#vw!7FZA+toh3!J)_QxYrpFw;l5CIbyqE|8|og*$gDu(U_jDyia)9hVnWx zw_myC;}%n}FgumbB;cmtj!#HW+l3tp2+s*h_a8L8VO zHrw_h`=lDbQt`H&gn2BQX5)}9OU93ztRhPvv?2qhQ4_a^O(%Y<;&=ExFBBW3*l{IOEhLPhN8}f;eFO+j;>hIn$3VUsr zB{T<=(bx()U5RCUUyFNf*Odf9qtmV{5XNGF&mfcqw}(Sz&Q6!l*~bQpiViig{aw~wPC?4Q6R0%`0iel(58!+!4sT0Q*!KpF?G z;6tN0Unvcdf)}d8zKosDXu!5pYTJoPI{|7tD&atJcN)FX;8n!i!@0iUxn2k&Kt+!XhFCjjUyviCYP?_n7w>_98X6JM&XYZ zjv{1B3?o164;M_}6CUgzQbkA&<)4{47xuq_mog!ZAa@jJ-GHvciUuk#AwP|wVMHX{ z&qsrDAskHO%M`l?YXV=FEIkv=vp0zDtqD)-I%VL$d zD1TW;vb;~RvVcmKY8C6JhIO-!H`WOMgT9aZ9OUfg86os~~<)2(^FWon*p7jRKFnt0Q$elMdO zwX5I9`}j#*!euu3aXiD~8)y5ThKVbfV251CBY2f-d5mHsuHrSa@h`G<4U;G&@)P(8 z*8;@8lCrNMNb~{xlsx1S`*vF+D5U;1yiOlKBTkjQA{;&NqN5=B4oeN>W~$zoGEZ=kIyn6bZdM zewQw)Dr%x-AX@yk;rYSykhlH^3<|Jcq%!eQoB4&~}9 zj+_y!;m;ezoiAbuU*0PIbsE1JL6QCbj{TnF9rMl1&|-T|9$;NR$?7?XavZR0$r;_% aZY}Z5`Xg&z*t60HPq`f8f1+0p{`@~SdRC7B literal 0 HcmV?d00001 diff --git a/bin/ij/io/FileInfo.class b/bin/ij/io/FileInfo.class new file mode 100644 index 0000000000000000000000000000000000000000..4b40373c3b265d33dd4f7104ef8571243b737033 GIT binary patch literal 6124 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 ai$CEn5Mivk8&Ys9)%vhfh(@3+5&0jsWE&Cy literal 0 HcmV?d00001 diff --git a/bin/ij/io/FileOpener.class b/bin/ij/io/FileOpener.class new file mode 100644 index 0000000000000000000000000000000000000000..037d8543f0c555bbd57c36b6e353d69196ab5275 GIT binary patch literal 20595 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 XLa26NfSu_B;Oc}7!1@DesXzQbDUwcN literal 0 HcmV?d00001 diff --git a/bin/ij/io/FileSaver.class b/bin/ij/io/FileSaver.class new file mode 100644 index 0000000000000000000000000000000000000000..8093b35b0414677640998be7683559d905374264 GIT binary patch literal 23766 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�p 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 z z!i6m;y8|LjY9s}*Q%hH)DO-)9Ts4-$Y8(w#<7udxK%>+|Xy7C|SDj85Vn?|`odJ}c zO7~%(c}UHm7t~A~L!U_>t6B7!noWPhPV^sY9(}LQqGokAyTE#SsQElxE#R^0Jf5f) z@-%fmpQ(y@zAE8`Y7v)#6)jO^e1STPV`>REs40Ads^A^!0xkb`#NDpARKnM3+tX~k zQB0ntu z&T$Tbvj}H;6K_`!Kq>%+b%#cUAWonRJ_UIu&WQ%vDfmBc;DN|X!P(5!JV>*rqlizG z_`Wo)m3LEW`cW{0W||p?3%vZ+c(2L{Lv0ITw{OuZniWB2q%yz21Tt5ol=(ilvdqn|ii$dST?>b6uwJPHETw0;JmpNPm6{S1nJnxdaU=%RRrruHyoAJ@;2@p}?DXma6AP>PD_nH)-X(7p4Kykxi1#YL(nYDOuuO=|~B*F7_9dlV~-^DU_^@ zt(CFk3axap<0`f)9-%ZjZtZ9wm{Yt&+EyDuHeK4L_tS!%WLF;}V>;doX(Sxdxj_1O z*z5)uTdr=~o4-zi{O*80Qd`KUZp8-jHtM8qrxVm2bP~P`)K9rCkpYOT^9Kq9|SV%&@@}$WA1Xplpy9>49g$HJ}<>^+AcTI$%=9g<9RU@5)y#`zz z#f1*N4TZ{)gtmKEhte*Qkb2qO$Q|gakp{I?M;+9FOGIvPjxX){4PL{(Wqb3k1`q2F z`)zMto8+p-vA9HO?u3T!f_CEFUbUP0Ds-UkqEpq~Fr#~Dg1U#MD{P+CeKbeiPciiX z%;{cQs~!Z2eTeoT|JSJdxZ0=J!6vj%CD!37XB~pHpGRR174<;8svT(F7aMFl(0mY% z8%4+5h+kXFqfyTz;q*Ah6tg#-qC6HUAGYv|bnv-fuUweC@*;<*g*R>+bjY?CD#ET6 zA==#UpsUy(bZc6fQqaU8%zF0q2pJHHO;hTg#| z3|>1~_V|`*v)+g|7Jhef<8*()#)U_Y^D<|mv5HqiO*UluaF@BwFvq0+1nBuQ1otya zRe!-|>vPIce+2}60XGL;B|_e4^>+yEKd3}~4fy#dtyJH@GJFd+)_>9M>R%Ap@8HHd zLQkq6w7}YLvvy=1h9J}=gt<)xCps6R<^t550IZjnk1AlfZE?O3{?hppDWN@8xnR!=D)_OoPDSHy~`Mf-wg-z%R;IhvCN%&!Ro4dqg1T;zRzy6hh7k zfztM01HR=?^6tUkGx$5^q~#iA@C$?R;BS(Xe>2j(;&sBVeY8nP&3W>1{}@UH;S@zjn)swt8BT|9*_ zRi_?&XYo}3L~3jzH7k)SPNbrs?z%TrRGE>Hk=dm; zJ~Gl$j>^m8hU_iiy+(4ne2mf>cDb!#@GKipRJ^1g&c*Kogbn=*0!_SDyY!3TI$Y_B zpR4Xdzk8EusCYT>D;4B6ust%)qjX~-^)}9@K1PY2IQwKG9G^J&H0_C#p9GK}N5FwH zyaV!3HlfdxvGyRBJyv|d$q1`~Uk6CaQTWv>{RHcrC-|rH1QQSP6{0j(LYT9QT#n=l zlSqgzsNja_$1M#*%Gd72s0fzR)!mmXxmnFy`3}aC}_ke�%7#&Q~Ote~03N}6Y^ z)-z%MX2jWvnK+wwfU&TKeHe#4%bkwZ!>`fUqi4Z)As90b@=hSlozv1?guU27-i@of zid@+)jO%WIRkjPub8p05P-s~0CK1FYet2{97mbusgpFDgAHX}8l6erTd{|H_2V`bA zKjI7?Z(-m$E+wC_hEk2osIzf7_8v6y74aP8 zm(GFt-_O4-@*d<@MDkxP!XY?3#(w=7Xk;tfm(#?rN8FfoMuiQg-*w14}<+0qW!RwIqjcLdts;j!-!cld`z zX0|umL{oCV8NbIC2wsHTcE(Y;1U(tl4X-$Sij<3{;&%Z5 zq~-QR{L-2n{d*{%M+ah(5wDGt9;aYqn)hyG5g2Y~HwsHa~Opn6L41KNirc75l_4|+cANdOUT}P+Jj(uwl{wRbo;2=4hmSVe1!C@$7PaWPJ!_!<8^@7?kw2tj<0D#P{DEqWkLe2BbEEO8 z77p9Puq7@Ww8aq)%m)7*ORm7G*YiKHga%F-8uT901shhzUxPopsXr{^Rai_94aC{G zxa9CFp>(3&v3jV6viP5<=fz1!rsG-_@EbsW5d9Dq5eMyEK}AY9^?T~{Bjk@6QEY%q VZRks6ov|ZK#d>{%`xO7@{{h{l?g0P* literal 0 HcmV?d00001 diff --git a/bin/ij/io/ImageWriter.class b/bin/ij/io/ImageWriter.class new file mode 100644 index 0000000000000000000000000000000000000000..4778a7e3936e97a02d8ba46385d53fbbe1abf86f GIT binary patch literal 8697 zcmcIq3w%@69sW*o)0?EXr33<0C~cKTn-&TP5h--8Jfv7aY^}&+lD4;PFimQj0wQzM z!C<;6zP6#;5Qb)BICay))PYVX^TpVF>)dp@;a=*t-`sr6?mPG1+_Y(nG8JfY?s@#r z|M{Ko-16uL_Z$Q;9sg9}5EvR>TM>>`)U=0M_4DH4gdSI+2;{8|tqWB|LebWWrHyO# zri6f_CCvU>bN+&GM6Zdq#3}`bbhgFTpB<02#`Vt5>QG!jtPsfV46O@CTk8^`rV9nK zW{0EUM3um>l1u@m4FbyCShMbh6S*3^7$V>-DQ)n8+G$YXmaSnLT*%^Xwc)6~q^rGA zkJpD9Bf5aEHr5o1G=$<|`D}G6iMDVjS6RFDGLIT-y*1|UCG$!fycmrF6=MWkw6JE8 zhEd2DaCcaS(W-d@IfgxpLmk#Ox$C9NW>$w2#ts6bOKJ_z(WOhf5*=NMx1C_ifL56RZq-~bwwFErDRcQjh8VGs+b`#Jhg3a6*ZiOnNqy8NI)%F zSzS{j{UT7nfW*2eXT-q9ar=(&20cQDJ62ZHzl|3s^iG*g2|dc)JHs0((bZ%sllGaF z9OZ-g%-pS45>}d7F;9aBZWRjzMyB(Wo@T;TcZDO(dfbhL0v@RX`(_n+F&_-67iS6h zt?dlC<|4|KGZt%Df~B-3)X||wn+3*{)Rgv8qR$SMZY*OID&)dCxv*Yf(mmWy-GB@HuIeSEe-mndo9>&5=P1X(-0Y zbbF^+g2Pt!QpBujmV#hS`|Tud2*(p$p@#;6xTC&J@6=6O zq24 z#}i?KkX-i#4PV4%Og_1zRTX{1XFFe8*T>&O`#F}iBgM0<`&Ki={T*nkyME5s#wY10{v6dONE)?m~Rb16) zQzL0);(cAiR%|0!q!sF&EvZH4oLN16riGe9N^a#(BQ1^);JITs+N^JwmTF8@(Ue-b zAQB6GDBdne+o*VP3z8~srGRwm8B#QS58vl8(;`LI$Ic0@uZbq;K_;3~vY@m^x_~I- zOtdpS_okdS%}Fx}L8Eb}h961Kd7AacuGYmo#oV}y$An>*L?1nFf={+&2U7IeAg z@Ij9$B7VwBr!>As#ly5GwcOtEQ4K%Cg9N73NZ8?JGYza>`&zU)MqFoVI+c+$8;~9aI2cA|GjU|dgMV%4=y%hYMhUf7D zL58I%v%K?cJ#Lh!K{#rhV!Mp$M7*Tow-S^M(5k<tbE;CS4LLYSzn;$GAd>NmlGFsp+SelAVx}bB(c~ zRL@6shV_!F*-|x(MIogX8LLD#zj#Vf>?v$192jR<@v>!Wt+R?-`R>95^Q*iqs094V zmi?#*x{@d@@VnOdl|AtIm4fD=tE`+I-8fZtR|N{T`jxG*{W!hAuk7l1Hm&Gns%IKJ zl4_2^7#diJlTgCKn~VYksbnR4=Cl7S>L=Y(W%$0T!z79xF4WU2$4I2>zG6R>^{mhGCZpTP8m6dZ?3Z{iTU?p{`tN$ z_oJpjS<{VL-`U+b$Mnd#eC)>Z16UQH_0#Uh1>IP~SzE2)tue-Evr`mU&%1iM(~daV zNT6|4Z#+|I0%JZAr_v2oC?#A?z$&UPjSrYg*~YK7jbDvMG|>zVXQSCLzKHWZaJ=CV>5N*k<_65bYGy@* zW0zp2;gW@>{A+ z#QQi^#m0A85s-&yrr#XraHXvBBcAHv_=;)Ege_sSQNAU7nkz8EC5uD#tM|EQxs%vg z>{qvhM!2s34!k2=#rn3OTFj;G+kzf*)s;Ii-5JbIVqCf3Gb8BDcl)#b-fQaRx3b3X z&2w+Mrhcx%cN^-^%~ki|Ts%yfPmp=b^<+z4@9_lq3*RtiM7OB=UKFbox=B!k!i2Gj( zPY(@LRR|T+Ra~biY+OKK?;#X>GrCC-fTAAm!3$8I1?=ykVuLNn8fd}0%yr0Kt@ z*g~S^Munaz2wm5W8$K!sjWsH11yx^3f2|?{uO@zej!|972(9S_K+EkIEw^K|+yWrA zKLB|x0OApyX`c8r0qD9E0Nrq60MtYmG!r&-2Bn2X(8^?QBLIa77HjDgUZgm73EDqF z0J`cp07M#-={IMLX^{(vrgA6Gn(LO7EAnfWQtmRoT^1x+fYcVuvO4mz+ESS040@6x z-x>7!Ed-j-7R==08CP7-aC{Mj>t8u7%t*(>~-0Y_%j1RCW-#$CrB-<&Nr=Lmm} zrQNQcYm?YG;La&rv-QZ>x!-uk&Y9!=cec@yAe(YY`K}LMNu%P8Jc@x1*2zUYqT@t` zP9D+;o=jb=gLTBA^-Prw#Ji0+p9yjSaZu;=w1e1kDe-U%Z=~CC8NNg0xrJxb9Xyu~ zviAYbJ<4IlSVH!quV>+r=bYBWB`SQOT>_BHSSA zuue3R{YnJgjXaqWsp@3Q;zn$l^gtD>u!C6YBrB-Ljch3xDQa;OTQ0uqsxLTfwN}wL5@~brjNK}tp1i8{ zP`r(hI|;VtKOqs<0MN0>P2p%HrelfaeF~0lUcqymsv^GmiDomixU^4q#8vV^9H)O$9u8?v3UHNw+62 z&w9v`52N_NO5L2z@{lCqU=m9PlxEDdctTo+AuuIf3e1pZ*T}#OVnVuBE+uaVGt9x< z%4gmbGh-%_0|e;B8O+q# zESpihX)}srHlujO%Kg>U`3KBo4?k0_d1Nq>Wq8SJ7FvvAr`?iyUZ24z_6^J^Qr*mJ z%y}1{_u&%!&g$X$W3!ReBzf=TYh|(z$v>ZF{>ghv6LV+ojmR!!7GD1?WEF1P$L4MA z+o}qDx9>)l@Aj&KErsNs9H}Z$BWhdKk8sBhlpFm1$Gp%KcH?f}0bXYcB+r)(LjZe? zTg^QM`?!sHzYNf1;HQMZgFGb%L^Oof51 zpE9(3r$@3Vshg8CK$)%~#%;wjolm@`XBOMXq(b|;ap=UT(4%zjW7O+$QlTeEg^rL4 zJw_^Y)Ue9Di~XNei0>mZ%@dy{7221gLWfR_3Oz{|JjEbC%~X7bROnfrZ_kkmJx?n1 z0&Dg~QlY1Ldc9=0Dce2=h4V(>!)VzBw@B5#zk5{GfaS7i}058*;S6B$Y z=f(e3mcnaHwLdV`{>Z{OMl5-q`1mLC??01x{)G(Vueg|fU&0&2$2WQ2zC~<&jY#?r z!~d&@PxOObXnRt4$G+JtM&ccDGu+x(D!m?QZ-oDQ2bT_){ZK+>f2zcqPeqf!191$k&BQVcox^ zp#zJ2HHl+`iu^+okM3+CyQhHrQq1t?HxQjfq$&(lMPWqb(G}z-Vkele4M0zLF_xZZr~41S5&X z!ElddP$p)H1=p4&)^}qT*OID=rUeFN<6WVy4YegYncNF1tL86gGH5uHN9dhasG}1M zxa;Q4Ypg`ah*U?ky*+NBgKG^s0rSb=2$N&}?0SPnGwDL;2zH}ZSKm}sSKDaNiA(PU=BPkSy0tfxw5Kp<+93#I!spOfP5GtsH$zMtX?^%8NGE46_pJ_hscb&`pVjs zv#YD6EuxCg?Tm(6twt-1?PF4K8ed}XMUO8?F|Wt#2Dn{_IpVcdUBM0u>kp654|dP7 z5^F6hGC(uqOFBadt17SAqHOy*b|iY0o2VRW`0&w*<| zn1jL_5SBi;N)AMG+`^e_)7Q-o9k-;uW?)5qP=01O7*EUtG^!%);5Db(M$`ZWz`$55 zGCK@DU2^c)bMEq?0A$Q)4TmD3#7rDvLE&PcXl}I4^3YtW)Tsg+#?19Gt37VgJem(F zcUTDsg=u8LSEs8gtjZv!Dw9s7aZ)?iq=|N|#-!8iTAfJ+R4BdYnN(!c8cp(3p-zjA zQ34y~1}(uRrWUt^5(YH`JCn<#?rRKnM65Q0mg6W=q(zZ$(xgc?M^~FP*{)q<(iFRPtx2cbCw-ksXV|psO*+%2Z8fRP-op(h zon_a)X;P_OyUC=fcI_6ET;!J3ZZkJto~t-vYXW-B6o0rsAQ7enlRsMW@RY_nGu<>V?82qBd@sMidl& zZ!K(*CS+ifVNUFM-Z z^r%jIZEdXyw#K3+^$9Z`XltEqbUhH@K9l;DAV97Nby-jsnB#e|XqUJcoqm8d!Bj_2 zsC2#+v0|ZCdl`cshd$YC%xr)mFDdcRW3>rR_4X!p~vaY_H)gJ2K+XLbl z@fz$bT#K*8_AN&{-VmoJcHS3?`!0ru5&qva=~wg?M5sh%wO-f~T{pC4T_%EfhiO!G zD3J(T#Qho_MLuT+*573ZDXx zE}Qt!q(2C#bZlaQ748<=M<#urW=ZYOCjEsT#unEG<8yJr zYhA)dN>!xIf&}1+s$|eVO!_B%3dyAg*#cMn71IBiN&lw*huja|W5qQPo~G@$s< zp#sQE27O`DA>kl9HOZW4G;9SUqK99abOfFr@sZULiYLUuk4Oj3T)5fzbn%L=F3~C` ztpng1DSG)8C4pv6EEo#Z7>sCd&^H$@*4d3oK<*Crbc7UGJ>)!>k+V5lVuhgeqT$TB%w%@IeSIWUL&vQLhvf9@lxiopo(7%JVwBTYVm zM~RI|K%9uEBsd(>ZZ6B<(R7Q>V<6dMcc?Ssy_}=8fsKvjaWKT}gNhU!AF$x4cr1@M z`4pZ2(;>l&ipww&j40+(t@?0J90s(BCz^a37XWi>B*v|47tyEbwjDiBAHwRd#hJ)+=`k0H!gi zNYiDCGfgg&7YrazQ9V4B%XOXxL=ViX=q67WUC?D*=weHToo(_=J_ihdRv(MDT5+5q z(@6zYN*vubDJ7PHeylp1=juF%DIW_AJ;>w=(J2Y!YC;itW1h+L2T8$o23G+N#Uhk* zO|Iq|2&FqHxG$)}>c@LtEIO6zOs?niuvHj=#xMeNrb+21lDe?SNe;~ww9Mc}$UxA7 zxK&nPWb$G;lFVpNqOQHh>cTZg7F%j^GtCk=3Dpq7a+6nx0G)A>9`JF2;A5pAcPNGp zM%bs7`vk$adAN<+bq2$uggILy+hg#DrM5M(VkUPm)Ie_!t*Z};LL39O)YgU+%xGS1 z@`d7wR8-hYTxIgryumiR;62n53)=Uz(e~V3 zRxsWZvr5zYrsF_(qsiCOI3TGVHm?amg@>=>>vi62o6I3zGkJ@kAzz$;3jZgUVNYrE z%Fec#e1pP_#92CjlWFu(AlDtmS7j_Fdgcr(Ypb;*uGi#>1;;yxj&yqstM7`72UQ_> z%1+s$hT7nqA%SD<7sy>9+*i3mY9cguTc{(Hh=YXH+-kNdQV~58Dh6vIOf=@ds1Mi( z2U{%M_XNsg)3o&X&4FVFvIcmz$~9JPYzV1N*J&nVM*gnJ-{bGwP!Shm-#ESYA*TB^ z4?n_tb>3qex0E6Q;wC@JeUL&5>4W#@DPIHD^f`|+{RZ#DxqU@>Qd1U;mxF%H!#^m3M#(LT@`@CqZ zE0~aZe`oUV#cZ1560GhZ^gkLClY6y1u5lreLzf{CktzOc@?WSBQ-q_D4m!kU2xjRae|7r54GG3SQh__|P4f5D3*8e{yf6iY(gRF~?seuL^6Tl1v2g@?R zcQYKAD^1;OcQAM}yU^VOH#0R|JyT4k zZF7j7r%2dnw3ys4d{AzI9oWSF`+NFaF3Sr7PT+$Nf|iKeE4f{b`iOI#&Tg<5E8 zMPe`9VJp%h_-@hyrdF(#fYjO1s1VnnbNHwRNYT{{D8 zhT;`s%;lZAg}9kFX=QNE+F6iF!ScCsM$@$Ex>jymNp%2CxRAt6ZH9I>20@MG?V-{! zL)surcaEve(q=;r(6P8x5$#dwpP}+|6@|Dqu{PJ#Dn#QmJ9-dS+8PUnBPz|DEz(?s zdsr-vumcEm2_62(CDOpDYRl(hpAnU6LQ~=nQFCt7wJI1*tsVl^&I61J3KuFRmyGQ| zMH{F%3=O$FcZ*e`u8~dJa#LF&2W{YLtL`0!c7Zw-8369YxQH)RvQL?(L0u6WpE6Q* z)zcX5iM3i{?|`gyZ7z`s=wv9lq>{&zCfOy_G|4TY97P67D50Wx&^FWBS!fSSYv-Wt zO>5_(otM@giT0?pc0O8T(%NIuK8eN+uIEGh7M`GOMaYUR9OI zs;Uw>RaGLRs!C)ERf#;IDv@DBsY&KemB`zv5?MP{B4?*cWb9Ole4Q$hty3j(b*e;u z4y8wOlB$I5Qgmb)R2x}Kl$ns{IW!A9oK3_&a)L(mxYy(8r#X9Rfj4WohO88-3%xl5 z)oO39bZ^*8wce3V)m!h)PmvmOvY())W`}oiW3$t{w6WRcUDnv__O57b*1Z=rHXGhm zjmCRqq zI`+~Uv|VZKb&r652VIP3Ic4MN16EF^5x~+ooK+!BK>qVo>j?dS`L=Ea(w(m6zJmqF1qwpfp&tk1i|PLks%o3KR?b z=qeP|eY62ZZ69q!QQt?KP&D+>W)y4sXbXyUeY6e5jeWGe$lpgd7X|w0*8Q}j*?apw zy3?PeyOXrLpYE3}X=z)Ug<~qj(G>yQe#)l+Hd%}WehGd`;e;n)Ba`usU^XqFIaGt% zDymRh^4Sd2cjyBiaPvM&?x)9^z5Dmk6Ey+vlNf;iGIUCko(_1QMfIwU4uJPS zl71ZU{uJ#EY3&!$-guq#zohzKL3@+*vugeV&CO}uucN&sNxuwu-$rels{I@`){5Wq_tGj7_M5=xT(rMj@=jncKr zdG~QWczcudY##EZ%>hShiqA!6SOw*_RQX=2-0tOmIy`14&A~iZzx z$IyUJsf9kHRysm$Y>>snshv;3hok^?^4a)uG>=yEQhYHAQkdKEl_)|HUW1QAmr^%h zLl^O8it#pz^DUI%JE@0v(;9w|)-v>)AEWjBG+oRG=@NbsT!~pxI>;}jux#Q6gRm87 zUV-$1#t!J97@b9AdS>wuz$pr8eC0A*#Bno;D0sqs-$h z%kHI6zL}SuMes0QlCV5FOO;*#OzRbC* zpU+qOT!k5ArPOxm{;M-NFVp|zJzpLOJjyNo+*+O~uLY7E8dyJ!!TFhZkS9(U(E z575H>T-uz=@qFh#?h(Nzd0kGW%vVmCmgZlv|J9X|0Uym<>o+=|w1_}Kw}x|8n1^I^IJK6Mv8LwDhW(B1SJ z-9vBVi{6LyEhyk_d_=g9$KV1qfp&2T-Os1v<~I$W;AYZ;Je$7D7H#Gzz7edWN1$MP z_+ETVdzc>OXQ>Zrl;k&PAHPfe`~mdgk61~RED6^LWDhtD{)o53tHYV0b`yNPMh5?e zZ-&x3Xe_SxxAJXpNaOeg-oZPe7ZdmZ-_CcyB^L5Cd?#uK1(2WE1*v$blxOi>sAXcm z_wwC%*Tk-G=X*fQq9-?b3qe0Mls?Mbj2P_5+E6Y4g<3M4L=McGIDllU9 z@Iw^Psp&tIc?5CK*S(AxZya`t_Bpu^Q4@w|9->^GulpP#VA#G{wt>)ALJ&GcHlZlV z57`z0=J~xHln^VuLs{@!{r&uK4cfN?(zQz4(-55mjEj#0bl?Du3;_21u5gm~qp~#s z)!F~ZetxQ1=Gn*3B>9Im0Wm;D0T`p_dTF=4VAg&<=-80&rX;3|>1{0iIi&4+R zgg8a5oX0cqGcW*6JyxxX571e~SR23nWv)CItid_Oc`n5hXqS|`^W91Qd9gfa$)hsK zuk_NGTz*aF&*e9u=4w`n(fG}yVdr%SHHn^v?4H39{19;Y5zOqfILqe{h#r7C9fSw_ zF+A5#;FX?-CwhU}=x1cn&nW>9wVqz4%jgv--m3uBYfzrop)hYyFZ~ix+nb>N3az*B z^ESOt??8=yO`p)a@MG`M=kz{&*l#(LKEOHtUa3MmIKUaERKdmLlmak#sZe0oz|jl^ zL7uc3nc@Ibs(y`M+JKfPBI zaQ5;05~17-S0hmxF!upMW+C0$$1PG`)yE%7xuK8$fMVka{^L!%C|gJy`}j}LJ5{~5 zk3W{mrau0wl$$~Lo2vBjKcw8&$N!RYOCNvc_byKIzjsh(z|qIo^wP(E@6r?j9D(qT z-@7bDa4Eu1{oWNRf?E;x`@I(^f*HukRFnt&-c_j~x+2`>_qGfS$x)PR{NACng5kNkW)DXb}@qW~tPxI)#nh|&yfKLzB{tUD67l8j`Sm94-G`=N|qrc&T z@^|=Vj9(7lMhz$NDpywPEJoG)=FI|7?Z_{$HbhggHf zchD1vUY^58*caJNuOke47jXXrXCOu7!Kda-1W+cPS@>j{jqjYpcp~Q@7dsqQ(u;I$ zE-yq+Bj@o79*Io+2}s_LLW(3GUrR>wCVb=Eins3PlaMDLmlmu&no=Gw->Z3`07!H5 z&XgY!i+v*HW*nx)fIFaXOv}m18J;sd_0O448CoXtllB#RHeQ#D<<=sv2Uo^^Ei0)F zUsB}F!o_j0k%Q~wU?UfoNE^kvqnC8Ft{vA2 z_{j{2Gk?Hp$O~AC$hk)|1cmlBSx$)fOvRK4TT&Qz*$1G_fSrLsq?c8M>(A9jC$+IV z;cHMGm()%^+h05OcFORNO=_ps7ALiW$6e)4N13aa#^*cpT<{S|&EKz;LYwS*FS&Aj zi>`JtIx+m7BX8|T{sWh1=jl$H;!IJD zJVQ~6@^rb#~(`Mqb7)$|%aqP?uR4KmR^$&e1bm13(f&C z&eW<=!=F4+rdESmhC){dWx70v;fesO!<40KwEz<`dlEe1lY=NGNCG_RK;rVa4j{0W z*&zVbkLiNr0+bHn4{zl+K$?c!tc@gm{dR*9qTLEf627R&!;a>0KfW7w62XjIcdy!j z$A%3dJU49!o#g~j>dtbHbdS&)Mmj^dqh}W3u&?N)X+monpt%(7RG}@lX=YIluz`U& zBi$pNc+pN-1ah^dNo^Tilw>fr^^zBq^A*Jq1Z_JCj(O_Nw-mr=M!dTW09Z~_c?E)j z^QoFIpryQ$R`Dt*dk~k|7JRyHg_5_?^@zi7!UcF2cfcZds>7ZM31I(&hrL^c5u%a1 z)v;?hpbgqeZ50lC3dOV_LJcRZKv-)*%?=rINOyfn^K^d7>C7Er4$npkycIKOTHF5u D*7P>m literal 0 HcmV?d00001 diff --git a/bin/ij/io/LogStream.class b/bin/ij/io/LogStream.class new file mode 100644 index 0000000000000000000000000000000000000000..5004228929a0a5a912583f57db27c0b8bf03885d GIT binary patch literal 3831 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 literal 0 HcmV?d00001 diff --git a/bin/ij/io/Opener.class b/bin/ij/io/Opener.class new file mode 100644 index 0000000000000000000000000000000000000000..c4492eb5201018fe9a6306d34a227a60c4ea6629 GIT binary patch literal 37268 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 QfkA1?8Vo6i)WM3+FB<(RhyVZp literal 0 HcmV?d00001 diff --git a/bin/ij/io/PluginClassLoader.class b/bin/ij/io/PluginClassLoader.class new file mode 100644 index 0000000000000000000000000000000000000000..01f372a792cd80553074cd2ca181cc4d374f7fb3 GIT binary patch literal 2783 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

ew^qp^aV$8dtXM6zQzNlK(N#&C*HPTJb9NOsZE zdj9X^=vyqc0p4{*GTVaTuhiYoP~&2ipGI;f&)7yoV$>6fMxRVzuvl$AHGYt$4k>7M z^?C}>Jb7qhH$BTPY33ggb~Z<%pz;z2{cTo`vxO$sI!P}Gn7Jqil*&XC4 zExpeEb)4UAy{uW23OdF<(x))2w`~?MgbVb96)Dm?YN;oor-FZb_~cSlRKWR+pWn|g zSNkuUheJMUW`fp6Y2HUb*Xo$}3Rq>H$g z2fQM#Q=g0p<`(02qs;*(cpBM7~`{5h&MO?!?-I(BHx zOf56B@ITz8$=Alr%y>WrbLm;k@cw(xcpPR#ixYI!6ODkCH2dBP}nwcyvDAIp0!(DlDkN z(@}+|p|ZQ`2SoJ|&zheS)yK@>r_A7Go^79zt1Aj!SVbHypoWWvg78<>J6UqH_^b>B>J(dbWPNwL|c_TU>-yGaI`9XR^25Jrkt)0b%={g;I@Kf zAKvrwko=_l<5@=6azu7`XzY`RxcrA1mcA;nG}Q+E_?$${)W$iEh*ZPEuuV8wo<^`| zB-neUNNcr9(AT{Fjx_zAQXS`U`Avxk!UZ}P7wD|c($4!OWoT2u zefSVJzL^o+AHOF!O}=PnS&HW^g>A)G$ZZ0*@50Zg`U(7+x5f$lrdWe^a1RLaA<;cz aV2|0+|5D;{DmV$m1&6i-KBf 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 literal 0 HcmV?d00001 diff --git a/bin/ij/io/RoiEncoder.class b/bin/ij/io/RoiEncoder.class new file mode 100644 index 0000000000000000000000000000000000000000..535883c264b72b2975caff87327ac4de08e6c6f8 GIT binary patch literal 12573 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 HKm7e4$QS;h literal 0 HcmV?d00001 diff --git a/bin/ij/io/SaveDialog.class b/bin/ij/io/SaveDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..3d51d86510e35a37b1add106365832687d0f0881 GIT binary patch literal 8504 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 literal 0 HcmV?d00001 diff --git a/bin/ij/io/TextEncoder.class b/bin/ij/io/TextEncoder.class new file mode 100644 index 0000000000000000000000000000000000000000..caaffae4417485826e45402d02ffc4e511dc535a GIT binary patch literal 1961 zcmaJ?-EUK69Dd&Rv}Zj%V}Zcn9cCwavprT?wqpezJ2iG(z4s1{ixpv7LmC;=daU7HvJ0!4Y zJ<+Bl8lFc&#Svccze}$pi4>{uiheh&Nxx2G(y>#=3pmQ&jxL-L zXduB+d(tcT+_O^Et>c_jN$!^{J74f5`l^l#=n-fnI%LmUY-_Q~PK6TLY?9ZCO}WUxUQY)HUZ z>CZ-ea-;!oa@Hf#@T!itUV?i77nolNMjbDAI+ez3ei z{a6zbS>+&Pm2a?ypcG+U|}^MKEfoWFxlK8u5rrlG2ad3+4DP`s{%0%^W0#Y&nTvt z?*qh^uunz&yjX%E)Lp^zVi^a~phBp)NrjLfV3{eRn?d4B2+^T!vo0uo^li(et^do5 Ps=v?{;N_-CA%N09B)FuD literal 0 HcmV?d00001 diff --git a/bin/ij/io/TiffDecoder.class b/bin/ij/io/TiffDecoder.class new file mode 100644 index 0000000000000000000000000000000000000000..2162547e8441dd09aed844f6da8af0d53be3f74b GIT binary patch literal 22529 zcmd6P33!y%_4hgVednDwlQ+pElL>?n2#W|I2?824tO+C#%tjJGK!uP@U^FB#i-20S ziu+o3(INm+uCYfTCKKdU0Q2ft=1ic|L@#)W-=1`ec$sv&-XkZ_RYQL zuIHZZo_p?{{`$~kPY}`Z#xY(}OzGh>CWSjDHHKST=Y^U(T0&i3GMW5mG@acvsjaDf z&7|7ZXM~!2P*hXCbY;WhvWm*8@|BIt>dKk8k|||wM|*cqQ+v;nrncUYfuRpEC0ACL z&M#lNv~phK0u-SoIc`x^dCh#cjG5BPDjOSC)|J<cUBjJy2?|^tE6p^DYd?KX%sIRNlLRo0C(83WsT)4=hiN+X_WFI z!0nWkK(@AOact;eC|h&oY^Y%DFSD%V6^ zfu7anjiuGK^>qs-W44P=O)QxrC8sfYDzWY|0GBMpz%`W%R=Rv!Fi#NrF%}V&QC4DU zr7H)BD^u*d1=z+brQlZ3WN}SH<@}oRd4LH*WDP)&oz#k|S`fBo{>r*qNQ< zv}Qim-3679aTY_c+IoAsk*}+&Z8SRDI-ny)eQl-D)e+8P^42aXudgayW^|k#>H=Nr z>T4nBjg=Uqv#X;M3u_5?L9sfzHZo;Y4d`1#Pgl5o4Y+BxH?0d{%-$}{ufpw2!>U|u zt#4{?=~!3V+#KreM)Oe9Iy6se39as3Qwo3xPe)~YPpAt>TQkD#;htGc z$BrM2^5BvQOR$W&K*>i2bvx8SLZtB%me_PQ6ZvQZo#Q1Cz=MTVE_CQzIu95$wX`&Z zI-9zhKpm!WN11BE;JK1uWX^YJ6*WojLWfop&`+xhw})za*R2k9HG+|#O-5Bmb5q-r zrmnES(L%FlZ5RXyLp{6}p z>?_~U9O~=|!*Y8GDizSuC%tOpH8S_@4sE8Jz(R;viy(ccLu(20uU5ktsnyV3wR)=j zIUdr4ITp8fhu5@+AZqSh3w`Lc!=W4LCJ(fZ4&xs*7%dytc66bQEhATTh)($=JFlZx zGYK*>Px{{P(7klGj~=9lz4Xv`=^P((|`F{6R^vaep(FXg}R%&!dm*FFj)1Ga|(?vr7uM9es9xP7)ZX@mgG6%@^qmL#xu7q3k`Oma>g5N3mW_8yGV;1A}I{6vVpQy?h+g zkRx~Ivfgl;wjr1D6o;qs@u09)U!m}K_A1wNo_^HzPvT+-7*G2zOj%;{NkF)i#{E|ebc0K?WkC$%A={zRa^LTyXK zEuurL`C0F9gH$E!ss*9&nzcPPFNRFDH??=bT?uu`2umG4MPt|tXT%m~$G0xr+~qc~ z8E&Xrx6bC%kv1b;1v8-g+PoS?XQBv1G{f!9Yi$P8R4WDn4r*OkMnA*hGevj;a1+9- z;e9VYgGjQG2`8{dZ)>>6wdX1AbHB5Cvm4t6h|mod4E=8igDc%tuu&dUD z);IL7j%|B3KLFX)8m0BXX81VC8m`jD-sVRjN20Qwwc4$~Fh50;+Aa|)Et)p>p`dQS zOSc(rjb9Ivu-DoAGcZzIq`FX7L({s>wvf$xU=!WEl-7wp+Wa_XtnXN_+sVW>KMBM& z4rSO(^zXjtAHe}R*=iR9MHZzvs|4anp=8aa@wxd)I}4y_{{Z zfy1ta+g-@3ebtJ2m<`7HyAGH(&Hr%d46|nMAmi7Vak1zGtV zYImDBj!Ka^lh>~e_k=3DmxsDKYy}S{2j`03&Q6>dLM?gU5n)+4t8_zxv9OqqbIKe#LCltaa#!PRZE_&vVd;k>Tpk9Si4L`g5*#~Wy{jiy0wVJ zl|t%r95r0(oUR1jkl9B#{0=>8&A$4q=Z52dwt@$fn~3od8dieI$1)Wi1;fcxG)7g+ zmlno68sOSkSy9oAsKtdI`SYl)5E&{tN^fbGC#IzvM$lCV^bWV@$>BDywW)h86g6+W zt@2?~CkW~V4*i=xu~iX#ii9(7NnURP!J%2MF1c1PucI|@D#T44=cvg-bx5^{lbY(# zujtpdI)SP5DA>Wti>;-|*Vwf&ueV#ow`Xk#EbS0eYsO?T)&^qH;TVVOk@jVId* zDr^>Y_naxUr4DwV`68(Bi_Rp`XI1Vfl(~8yQ-5I%iJVkAYN5!9PdMSON{Xu-RV}W9 zKJv&}0EKkE{J9c^?d9#z966{kEQAfQ)SEDrY(s;)!&)1o8Dm{HbcDNYbqXet>BUbg z@!3j(K6!b0(V;}l;0gIqB0Dl{gdyi|Tdl-q+S`6+d&l~ATLGgKIZ)hYgxA%@eT9)NWozhbVAFsO$d^|1Ee z!#X7CX$^OE!yC0$Yv&d%K0#IJ?^L{#1~JA%5R)mTqV^{JL=~mILVl?y*<<>T$A!&IOh(GEd^LuwiRl10i-*1P}hp_z5eH+||7b{)-!b z)5qY_uCAtyRq%m)>UY%bRjH&n)sZJwe{txibhoYEg&4rZ*y?@AZi(J! zv2uhASpCi65BTp`hx)stK2-leO9Xb0IPk{K$jfa7r^arF#KEDNDZSv%Tt_!a?h{9S zs{R8~_jG9bFpVFd$oAMEM=A5F&vDT+i0s0}FCF!jKD_C`xv)q|zj4$7x&f>U4+L>- z^({0WZX2S=vi-R(m#q#XJm5zA@_IV3^FoSUZ$jLX*9>+HC2pStrd;pcHcV*Mnx@XY z)uEpCh>t~`5ADVnmScDg+qD7nn>t;gIR+~|!V?^P?wD>HK3osD5&uHO2oUnmlgL+I zhlD6GJWnzloLr0)Ik_w!JVNX^1Z;6Z;9@Ik1W5$PTzUDffMU)J$k}n83nVs*f1V2^ z#EUx_S^H4O$d;g$*h%poEqioX9>KF?jeY<85OErV~=ALck5XF`Y=+H!*a4i9 z0H*>@OMv}=(-Ytfz<~rf6L2sA&H_9n0Uin%-enx0VSsWH;NgIC6W|emM<&2|fJY_3 zqXCaefX4zxG%P;;IKamyz~cc=NPzPJPfUOd02d~}MSv$Iz{dfeoB&S&JT(D69`FeX za53O%39#_vqy)GG@bmMUivcf5fR_S3B>`RrczFW60`RE`@M(Zg zPk>iKhH<;4Wf^x{I>i+dQd4e!bc&OmF2^m6P7&77DNc<#g#)Wo1o(7{lNZuF$V>-y z;_q32uE0E^KRNlksB7YLWM%YLP25fEtMVuI(f9HT`sjj3P;Jon@ppt)u3Df_2L)RM zg{mh%H9!#>X&5c0i_j*|9gZ%hO@J`4UIy!&EJV>I$Y&5AMt8$Y?4?WbKa>etmq*d+ z!5o6tXhADKqc?_&&R&tfhpt*SaTi_FN7oB>N@F)nFK;P&o-*`p8PRlRG@TVqi%n$i zxSfUsP5;mhxqknKRm1)EhE+MnhE@0HplG-hf(%r_igI?n2H$-$L4ooT(}87fR0{4=64FFqWSX zpBS{th+&d;pnM8_# zKt3T$j?l#3Fg$?}S$dJ}AVdd{?1Vi%3nJH@z`SGP1jvmOAQ&Zph!qz3Esek`{c@8y z2(Pu8W<-T9qq5>l)fs=@#-;~ixp-BRkilhCs$SH3%(4OEeog{83Zr-l4hHcs%#Zl@ zp!uhuL?~%v^q=ge2OdH`k-`@w3~5J)Byqh4jkFis5}7P>DZFz=PI_hKR!Yt{`{<4c z<>sVUR|{xpFx73fJCpXv0w13lUtQU#f#u~xYY3WX(`C?|%R&AtK>jOf99<2Gx(3_9 zb&#>^X%78BVL3kcStVi}N8-yr1Gci3m(wlUYnO=j z!#q4c%_EtS{q%yQ*=9ezESW41N($4N`sfdm?6v#o%~t@j5k{%Y72qOB15ZIJ1^0d0qU*+Kbq59HxTR092<3tg*% zyw^hFm(fpPZSRK#djNsH2caYnL4qEJC4B^~AH{z^1p)4deDu+8a3}u~?b1xDhjf6F zaVEX2*lfEItFmxM^zkqiZzG7ZrQoA_{!wVgctaLypc<{g!>+2+IO=$eUf zn49pXz@RmCY+ z(OtJ_6U+?GT@uW~U+m;uha#Ey$~|uDT6lQ$cPhS_CL31;u<>#k!YCnBj*w2j5 z^n33n^O1tWKK2#{*w@cau_x%+Le)X@c3M#A_x7{j@4Jg;qBb+=>F2CsD`;(@!or}( z?_CwN`Z>GAo9#{A&AHbW+du zqG>F5^e%P5P80vZ2`G3DWZT1&mQCEvlXvk60Y0&xOA7FTPWao;v*0Lj*>kYIAZ{6q zasD#E=(2pJ+Xdv8W~dyXkn6ns8F=oevGf*v^|#?~zeCmZu3o)sY-{4HuhA}x2ucml z!&(%2<#V}Sy@&_iV0jKwAr_Mj*+2mENNG3!hPl8M^{QAW*e$)4ipyToB;KK3)=`I^>q=Tm^C~bZ!Q6 zr|H}j6#aD?>>SUeE z@HOE(qrkw(5h^J#bvmWMqtpBXOQ)j?ygD6L0KG!Hi~_$~kyPOKp`;*~ycaFbGK!rD zWr7iB1s#n&pIpDm;h+;t-gfvO(bayKxLRiq_b%JT>q}CCj=7Ca3_5=QcA9Jyr(&S& zlqfB3=;sYfH>GkgHRxcV0x>b!DZBVwAp{9E&qvN3pUr{scNRNA$Fq%2D!}e?J0(kE zZ>9W#pac6*;G%fpHGRCvooHuqN-vgz)7LQ5-@slSfGs)*jr^%uOb75#*~=#T*~8g*pgbDT1h#PzorE}{j~C&A z&L(#FYEI#s@S5gMJkHsH*7xIi@=iRzc^nULp23To=keJ1B|I#C3+dl6(&s#k4{?t2 z@^IX2<*H#k0yjXpY6_3UO+}t6<58-T$EwAAj5?jiA)tP&+Q{P(HlBdcb3TH^h3ZFK zq#oi)>PbFMJ3A24mpiDv7Hw}pE=4^=$MB7O6Rd(*vAHzz5Y?It$8jVdpA`LP9io{y z${e7DUj5I8rAuM|w=~Vmz3l&nvc4wkAWh(L2kAHk^M{L1*No$2VCrq8Jy#!b;H%|e zPb~3@uW4uWLgvM^2l!T4_Tl|}JIr*6E%y7)1p6J}EwJD@s1EQ}!usEhzwq*S zN4Blu=x)%*cgDpL8y`#_7lVY;D2-2~0zQe>@pR1|*9U5kv4`3@0+N6|@T6QvRdTMQ zDmh`93BhjVd$l)|hOJZ1v0iMeW&C60+-)s|)VGvvVk@X~588-iy5ul!M2dqNM{<6G zX^6MtPY%9A_}-O9`1c`F$$WxTh?$8``mDugMZZ*}u;UE*g5D5kAU%vUh!5j&7QWH= zhTxlkZy3Jg@JZb?e8cfI;Tw-{7QV^&%JEIXSA}mnz6N|V@GZxOy@Ihf@Em++z_apHJ`0eU~ z0Uz@moXm)4(|aJnKe&`W12SZd2gHr+HG9NGW7VsGSaw8wlvnKP&d?Huz zY_3La4gS`0BVPQhfU#Q5^&G}?tWK1y=f!+JFX2mgDa_X?P{(Dw8Sk#Pq2)b%n#d}g zCYe7HlU0sHW%UDylN(FGYdcyH6EdYn1nb|hm{ zH$K=QCoVCzADXNB_(OQnANr?sZl+O^+d{)!)BI1EW+`y3^KSlV54{Et=93aD+w#ll z6-VfZH(5rXA%AqRV^;p~+jxY@kkI13jmpeh$*}kd`5p@nH)+ zGA?YP=Vkew4)B1VfAMK>sw|7&DuT%r7w+&<23CpXW09Z^0{rdP>OD26h+8`*_D}V?(i(oWw~Zck>*#lh0fn}rzbxf%*MDh$kMA8HAgqaT_<>+DaNhOC$JrZG_TfTo)X`_l-Ed5%ZyZ!7)jwvYEUHN|{EV zN{*Pf3I^*C`h|b%WLpTly&o|z0_@kYN5fr!^E7N}cr{>Ws}R7Sq6-!UO$;NjNOHt1 z&DTP&QX%v~(-rxC<&T)-K_qj`VB(9!i_@Km22B#LegF*`867nsqdMZps32SxI6D}7 zV6Db8n)eC;Z4fF5%1DSvkki;3J9K%LF88=tJA$=wRnYWqrP0w2!$g`?66j+41K_bg zNjGYhM#idWZ(}KjmELYs1N;Y>lOx7#7l&aH#j+3&m76S?)d2)1sePM8mw zVNQfXG3`y&Dkr8hUt7*HqZml1Lleq^e%EmNIp~Ms{8+a{81ID$jf;*nFG4vv>Bsta zP-ad*&6llASfe*n#E^sDpl91xpNY^YD3##fr%u+H@AWB!s_2S+Vz4CBr)smUxRa?q zMAS?CYzyz+Gx;pa;x5YLZko(Jw19hQ5q$4ccs-rY8>pE#(po+ThnaJ!m(Qc~;g8=2 zH}WRF5U;YokF)PZ5P?nnAl%AF_%iP2%XuGPiI=`t@#}Ci-{x!JOLB0a#@DU*Mo zviSxzgKty|`6gA(x2Sr)RjuaR@$zi5>ft-od3>k3h<~Uq<1OkI-m2~f{1d)g?c^P5 z7yn2-&Jpz+zE{1-_u*;HPt==yzj}`!z~ht$)o1*WI=~MbCO={%^P@(Pe`@6LPU9Hv zH>UG0V=nJDs`zKdVt&k6!F!C=yw^CBA2+)A3FBOT%D9!EHn#FJ#&&+zxR3W4yZJfe z=lo0KCH|H1CjZ*_J3o&b#NU`{{97}NUoc1Ti{@B<$vl={F^l+Bb29(IJc(a3EBSS^ zir+9B`AxHh|75P={bnz}WnRc{o0sxm%p3R}^EQ6h+`{i6!1TWPWB#l8DF4lTj6X0R z2b+J(|1e+Uf0}>ef0>`~N1hD+*fW9u?U}@%dZzGao)h_V&n*7Jvw*+!)bdxJ<@~j$ znZNOL@d3|ye9&_{ABIsP&wYwLyOHiuhUW?8@$6HU=SAi9ysK=_N6P2+{L>(m76PBjsxu+X|k6>Z@r_6 zt@qW5)<4xr*2ikP^@W;Y9a1yBtY&+CYK}LcO1;BWnKw_(^-e^3yqf2osmi@`)O_y( zwZMC_TIg+3CwrTDy?33e@ph|P?|G`udx=`)y;{|KZ&nT3=luXaE&S5xp{ChuL=QD< z5pa+@BSq`1uk}&fpkC_}eH1sz^4_2%I^v;4-ue1aZqW*FhK_)E>3r*beLTmTc7*iB zbN2DOG*m4@4|#T0>6WPrsK6~Vs&JXE+ZijUT*;dQ$2^Gw3cr;hpV^2U4sGOk@VY}S z#vwn|^CIrAR7FR{`=o`Vxr09h)OMxja@PUk^%x`I=$*EuCK{|>LqvsLL5a)S~i``~{Wc4+r zBj$jHbkv5P%(OIr+R$&Qz~r=?uPJ%N&{O{x<9fy=4I4Hz&*#t1K0tFu`cspJk3CGI zurhe(@=qoQs$e3bdNc=zsly0yAAurerlu!lW~OE2+1W#UN$H2-<);lA0Z+Fmh1f6h8%_{*fF}vq=YD85zYliiL+a{dq?Dvj9bK>@58MCk|#Ff62b3_z8rXdn)Xx? zP(t7xmBr#Ol^I3F-k{g-$mz(NZj9X_h8=D*E)-vQM0N$abjhf_E3r7}^|tu!w&J8< z(q3PQPyfY)a?9zOHP{*hmuYZq3|yhXGh*Or8f=S!D>c{-j|G$urW6(XHB!at!IWV7s^XL&Xb;Y8 zBMXZPren`f3aC!R=K`v$Bx5l7dW0mYLK1a0vf=?DLU^l`rlT}FLvsEjNahCEleQgt z8S!yBLTy0$IDOow&NYiujp8)%3wRDlpt0&yny5~rsp@npQ!D9YwTi0LYN}Vwv`n?oY89epwT9Z%TI#~{;SK5x zx&W5_4%J4_sde;`>Yz_lCw->QqHk0e8;IFE@Lz_gUc~Yd0msYtLbZWsAdY{sI)|65 zb2$v(W`p`3Uxw$(x2OyF9(5snmhW>P{GGk(V*Zud#BZoe)F^e9ny4;UQ`Hq}hPqPC z#*_3ib+uZcu2J>sT6LI0?N^VrQ;qCeT>SooiZc$IETh*^o@~XOB z?N^)e@b(UMP~Bl!Vje51+Sd8+Jy2n_neq?M=5#u6t zwQ;rjv2nAyZ(tWSZi()quzQFc=`c3R+*^i#aj|lrDh)Tz(R-6g6X9LSO{_&zjb^nK zsh8@FdAKf@+u1fFUqSZ}y6-Z^s=IN6i{GReBh+@}l0n^j)DF}-pz+=69^_K!OFmxj zz^S8qr}2Z-t$u`BKlh=Zgw)gd4Vtea*fBG3yRuZ>i?UQz#*OO7$fc=rp07vo z8zb@LN}d{|8xtvCkCGv~DBkRiNkgc6GI?bS(HoFN%W;96g)rEa>{a*Se}`}@!g3uk z40}?l`|n#C!<<}(g0E-+BJ_X)x%e-Tn|lz5o$w8%r8$sD{Gj2;7#d#&Oc2MTyF_Dh zie}};o+)ITxzLeZPaBFnC31a>rz9mtF#;am2#wG71TA?*f^)&tbUK`nJzt)soqxmP zO%?|&oHSmK(6s;7P1?q~uEH>h-Q24|&o1?oKK0-uG3CN5HH4tl1LRc?!tHs8vhbUL zVd@bYsUD?q>ZdeG?W9FWm#aQHP4!c&+C`Tm{ek)!ZBdVD!Dbn#QZ2Q=xi>PEfy~>FQakQ2Ss_pMy31 zC9LVMU`>Bbn~+|so(J=PLz~raX)n@!>P7madWrs|UZ%fctbgOq@*+;36k*{&h-Me}u39 z28~8KQN5`tq}UK0X?)ZLoo3!4ORq_Z^%q7l=bxZusOUPlND&13Rb} zcvr;49UjY5Ksl$_6Op%;5OPmZu?2y7Drg3+=%((1Q-TPF+=k_B@^H`t8G2=4MfxF? z+7B6e3o`UJ1=U|@gnEZ2sCVfk^&ZVYS-JYFUflv0gZSzy;;XBO%Rxn44yHxrAOkN? z*$K{l;$?j9nh|YpZ&xj3{Ww>uUGo-2d zJ8a*F*j)YryZ29xviqbbCysJX9Oaz2JHh^9_}$tdG>*pq=Z?z%=f2B-fd6~`e-8eC0semp{(l87eGUG91O6WX|GxzPzx{vA zfBYox$Q47uT?4;ACh;)vzZz`d_aqqj|9@@!ua278vpovTHkRBpWj)5P(8H%!T zyEoD>X}sZ~;|z<64KI}%HqAGZs2aCjjYcw^W;k>K(kqMkkE_A{xEkz_YwrHI=DrxW;LpY__|tIf|Llo8H4umbz=jvh%r?cPJT;5DXt{z+81 zSFk>5W@J&SF+>v}%k35?zz}SAO$e;2K)t?8y|GLExsR^L zp8NKkkx4h*NeEJCw&J2%M=NfOUX#3oYm#X9fO^jz1i_LB;x6dm-qWkUmYCV*J{nSp zSK;b|r4ka+H=4K%@FdM~2b_ZBLqBe$Ck&1%{1fAj!~>(maEuI?nr-gnaVA3kNrL4M z4_{l@uRhwGfNY@@JWj9mtA9VGK9jfV5^6y|L*~1)PkjM2aS@O$*cu2#xixfspZXdP z{G=4JEQLGOgK~}Lb}WOFFWC}Lwg=Zy9z57bhP}_>cE^J&&_B5IJeqAHsFm$mwo`op zipSo;2Gl_bgW%3RA;6@*I7+)iXqWBjH|RTyKUz}*$d}}Uz6M@m4Lr%k0tXLkAZCSO zck+RLBYA+PXqxbv`#v~!WaN{LgI}6aKy!^kT3{4WjWLN98^_U7V+x&TOogdCo-Q*^ zplgg`y1|%6w;LzYcH<=akx@eT8q;aNF@yeT%%qRd_A_HP9WdswGD_LRRYRsRkH;D1 zTxeAA3C4V$V=UmgMkOya7ILF;GOxr{M98S-vyB=){#jXJ*EScL0~dVbSrP+p@^ zO)-{e)zFTRiap#|!2g2D)ekpRp^;`l{qReUCT&hl&O`v>Vf8t#>Oy#gEoUeXUrzT$ z58`5fW>M;4yuveeu+xiQDdVggAXs}gHFWGj7)b;`<+K_+NTa;!v*A|eK^l(219;<) zrq~wT2kK#%fFz80@5CoOcjHBqk^XR|5h%?xg40Y>&O?(X2CXSzG7_^zGNEbSEN>pP z&dTzZW_h#Bo3;>2rkQ$+n)t-F!@rvdiR7>J-$QGW^czFgUNX(f%d$eHStkBYGxN|7 zm%F0syNqFZ5Fse|!}_k($bs5QNx;DKkP^gJk&WGRJ`7x4i(Nu6!U)Ivj8kE?Poscw zI<~--RA8*a*4IR3fUAsVYBpMEtr4OQqm|A!*3cz}`a@o`jv_nMNw+C)*Yq)bKjdKN&Wqb&U^Qsd+xdCoO`x= zU!ME+(RLyl&mk9SOqszAqk@r9)xrAuIpNw!T_EZrgUPeOzu7-3YE+Nz3GbC|e-spsrSxGClj$Cmp;%>f-G-N30VU7~Q%*9k~vORCnEmsXd?f#hV$ zsG426XijIahe@jsV%@(z5Ttga zjae6Hs*MI4W5GxmOsy_sGB*Wc{&IiJZ(;NbSK9sRZH=;wCjm&TY=)~s!P>wg|GGd3 z7}jErFHRRO=4~EUIQG7>k7hbHa5&KNjXyOwM)9_4R=$7^sbeBGF2JqlN06 z77PbtGnldpyQ*u%awcOowlN+0_n9=DikS4m5zEu)TpHz~kgdjGkSLZDgl<6jA zTF5g^%CcIeCS^-&3e7gDTnxcok6jiW3UIDT^MsuqTN;9~KtTbVCS2(l&YYIW@w42_oAdNSHUpC>!%9VGLx3m3a0c% zF`E^^x>y6WUM@o`O-*V58PlloB~S#6U^VT!=Q*9T&8%dz$! z`&l8gE;cFOnuWd0v0BY04YFEWOd4RdwwaVmeT0WgOuAI@5V1WWV^J_1IInrrxTWbT#Et_0)4VY2e3e1Ydi4oD; zbte6gt`b8Mn)3sG7`4EEWYP_EBdkurE9RCn_5Y6LDk?@Smnk=ybhD7kR8z{VIHbai zjNW3>9vSth(G+`-!M!HkDud~2uuA+~z&|tTb^-TP;MulY%g7xj?N5%xH{3-(PsC_t z;VVQa$a8@167N1oT6ddtC0*sBUo!RSYU^dq!4MSSreDKel-9k$CU3|eT_1>gV-5bW zcl^Avk>1KkQ_NdkF?X&P`UZZJcS|r7@}sJmWrDry~~;c0D@K! zVLH0WFY(eFi&)tHCU4ZgWlDNFwa{;c&wJoCwz5@Hu^my=+(P%6bU!@+&+s>5sXC@} zQ&Fw_Gj&8aH?~EaDkL5<>0xTc=EfoxX~f*Z?o)t!kREZ-Avh+YR>cC79u?m7P-H!x zk!Hk;-2ZNZh&y3Kab-8Ib;0%G0}@QF3^Nw71L>eXd3`7M+FPW=&?8$*$p0Nys~9RcXkNKpFk zn)IGbcSSY_qM$CY_f7ghWJzy~!V;q~WOf1n!=w-CpI~HD=VYv{R(yVJ(!Yd~J3cI` z{kKV8DvE z;(%vGO{>c!ROy)I61gnzmwdmilX0ZZomzo^D&DZeWGB1g)2bRGQP^W%(xy63v*x6k zoGx?huolJ)J?4isr_~1op*j%{jMYY(WyG;riZ$3Fdra0?cXMy-Mlf940Q2H(lXEy1 zh7`EC*)L^FCzp-;;VL)x#eUarLXFpPi#+yV7qFzVW;?Gqt(bt)#jf5|)O41P+_^*Y zp0o5gGT)O#Fh3q*(ic2bv{zm}0#VtyRh30(Y_l5<^~N6X2xyvp*cWJ8v<6GyVw2BR zRuv7@iD~dClSd1{xjq^QgcWG4$>Rj%SO@tbnN?A#>@%hWzPmxMSWvM*7e~`WNPKz} zcCDk(Xwt=2#Xh~d7^N;&C<>fqaw(%Yw6=J5gFgzgMc=Vk-TltmymFK0@Z5N=Dvva) z6#QK~UT(=dUy6(>*{}sBFXTlK6y6ps#r_(F6(eTL$ay9&<|WXR+VI7QRKx$Lm^#(X zOQ8-gVk%c7Ec3E1(KsHsyGfLc#?SEzlP}`ScR+b zW>~69hKnL$SnYSoz5ohiKi9f=U8e|W>CEIhhM9Znlm@FKLQB%MEK@#Wf$Z0Mlh^QC zEC`^ENQs@RiZn-S19H^B)RdZLq_{ZBC?LXC`=B&)kP|%@dK$UWPfO{iqu(Q?FQr`1 zlzuPFLA|Wzq9|5xRJ7`i(pbGw7^^pmIQ2%krrszD)f*+DdZV~hZ1D zi9@{+<7MO0$gV`i!E7sqEJvOy9@@|4jVK@LOj8 z1ill$_waiNKk0Y<9r1z8oW11ITgU+Y`7Lz6&*PERx(uMZU>|vWo^-75%XAW$bWCrd zD|{aB|A_7a`cj_*tN5G{#W~&MoY4Ye)_MuJ$2q-d<-=6z47F0FLMagZ`JI+-A-Cvh zKG<5)K?Cus?zLwx<)U$cGk5Ds@xT9r42q10+Wq^Htw@&In)9UfM$8jh8h>T zQEROp@@f}KB~H;U_|C&i@tui=;@j=6j3$DX7VD)}R%W#{_w7b~VVWv1V5* zZ512Yy1SLOKY;Dk=`teLXcpRDGGKr`aB9gy`t1uBE5Jc>7>u(B$IQ`amB1h;!6c{9 z2-*SY4OC1w(J0!B(O=OR)QMxMjmFUvG=Y8x?5BYJ2bxT;W5#isifUpSou(Nm4QA5E zR7#)0{Jy5yFft?-n!`HH<20Jjy{Lk-XaVQaLiW-k9zvBooECEtE#WbAK2M^hJe8_> z7A@zMI1AR&1-y|~ax<;sZM2$q(1m;rUBtK28orm-@)K0UN6635(>i{YYWW?iO6#=+6r{_cff?d>+Cussq{Bb7BsAbmt!2>_7}IEqmPy}7O9$--=m&6o1C*-a zHctGv3zOO??<}SpE@Bt59pqYw=2?s+K^h?JIRC%HbB*sD$Q+z)49?6xixSikr*FtQ zihc@6E#NDd1vr9D0u}Mq#!ZDOWJrlq3MAYuoT7KNgdJ^$6?S;6uc(c#O)G`%OoTBT zcfojJM)+;;mwIg|UeShKesO}^$Ym(QHFAu-Kl~GG z{1^bZnQH$8?btr6{Zq6j*zMcUhWjX-eUH(BmFQ1ANOxlTFT80t@1uAIXQ3qe%>3Gjk;%uRbIZl}eX|Qx!X$z%!a~!+;rGhDfK@xaND&ISuoS1Xh*{54* zfC$&`F=|=a`&S3)H$twB?roz74^dmfyXblq-XqXtBSQGa2<%b#SQGpr28(Nk?`_7_ z$rh@kt+2ywz}gPzCAbT?6jpZ`cI0xLnZJj_?hbg`74%D3;JvWDhhcS(!RDUC&OL{{ zdjWg)GF<~tyO!R@jQ6qf7xW`G=xTV=johDp4DY!qY3V#RZs}+}Mvr6HUGT9dVAUG9 z`;ywhxq)@o!keAo|0Ve9lL<>7OqpjW-{?1s&){}(h|vT8&d@mY9PDxAe@VUalvVw{ zt5rd24gaCglpTLcG^I%#LcJ-_Td>j|#bvIA73UHwKSoc3Cdu6k-mp;PNyISm+9R^f zhvl(ZpQcZ@wzi{#hS{w)dUhZ6m6W(S32`(`+o`eDKiWZe*sXT!U3rN9JVP%*_;s`( z=_+G6n&TMTLH&{)MK{eTfw`MI9h~DRIB@ngv5Tc@+9)xn!;(Y}C+BofPXA;N6dVmT z)8do=gptGh$SI6ru5f!8(`~L_@1Ow+EF?GER9!Pot-JCOyqdsDoG0Gh9c1;0Qg-QF@*)r5E^0 zI?6ZFi+nTvnfKwicqc9^?xR=uakT%4D~hA^Iv>ZC#3?$)AJX5DkdA4+=nbUNaY?ta) zJ7R|hQ+osw%PF9jc91?tOM_OfM)*UjhHkDzFq2Sr5sDQFU=9S+308|UQOF2P=lzM+ z2f$V^Z3~@ax7tt)e5L3~ny;V%-$;7@mWDC&VC^e%oS}3sL?h!%GKZd_UfdU*frADm zn$pMu=nP608FP6A0%Euk1;d>(FMSKfb&UC?$ghO?jM+=Y#k!}Wy4aA1!{e(icIwr| zE|0sq*o}rqdLEZvjbH`DN)*L9(oU{y;#%dIEHJq;Y z%WDaf2<3h|;W$vL5mwMt@svi|8Jgf?9wt>ONOslA;~?V%lO?{Q{5CdxMFr}aW3;h5 z$1wI&-vZ};%JLQEIMjfSXA7lUeRopS;W#7`>P#mL-eeDD!3+j)Z)|23z9D4eOH2;L z%T4A=RI_$fh?bQr!3E3FQgT)ao60n#CC5uc!9X9%6mv018!3F_j=QT&+WR=PVvLX4 z^?M1#!hX+E0%YQ1%YYOazM@Q|X9EL>M>RHpzNWoKn9Jl$mjU%Fo^sU88FnvM^}wNG2$+2o zhYc2Cz;TAT7?jXb$C!ZCRwtR2LOIWkiqR(4U)mnG2)onp7^jIv!3x<8yE!Jpy;{J_O_;e1|7#5pG}4&t~aPBxrfZf2~A;~*`wRAMl`2X*7?eW1B97O;(| zI#OY`E{IGvvU$K{XJKFG#%%WPxt%il8g_dhWdc;#*Eqz3BiCOw8D*}ZT@S1974>!6 zZ^2FlC8hEq;}b7MI;o-@u7+gGXbdlh!K}c+_yQb&S0ZYz!Xb9Gl61Ui0iQ490V-fi znvMuO40%vPTwlV&(K2v1xe#Wx0)k0cLcIN!#^}1<6CHdm>!yKu61gyzArWbfE|2y$ zE)-rH?dEvbV*F%XcXPq3S@NS_=#a5ZhMLEW} z>f%g|3gjkZ;=?@B0vHLv7y<0mwVehlkAxvDIB@oiJ zX#Ut|$n+HVyR*eA2|DxHHPB^lBM02Q$%8AoFRq_ZKMxt_*ni} zY`xni0lLRfXTM{O;gsNsRNZ>ND1u5IBbv`e{N+s(hzZbAD1|6cnIKc(HzPiyUXzraVd z*SJGF#?NS{@cx3I)ir)j@5z7E`|$JnV17X_=A-%;eo>!+cP0NxU%`LYFXWf?4R~+k zSM=mw%b`p|4Kc`W{WIP|BN>mKsjt-s(-aunY&Y;@K?u~PsI?-jKqRGvb z1Dv$voul4{^v~(jX8`^mAX9R`q5^sM({epFW(MBb*)m63^0r0{oJL6gipjd$qDPP( zr8cQo-oW!ZNL_V4%=2sj;<{1S#ua;Mus~K~1h&RA2h|l2V zt+WOm^=mikPBD=^3--^#n@I{JxS=uH3vIM4Pk%Fm>;m$&h%BHV^tYPVacZ)!JzljLuJ zs#M>_ZRLAd=oIDi`!tY0z{%}2{&ey~#hZ2DoDjDnRH^Y(+XTKyS&{II>SkazXy7A# zH)db4nUymNm>rj3q%(IOPn>bU`^MWXG&;45b}Cvz)&$L@a3iS)l>ae=`xhAd1j7BB z3i(qA_8F>_&vDZJ0-r*@L~ZvKD!?--!gj9!Q5AL;c49GT;!A5+VfhkNA#!Myiz)~! zU#P};Twc$l%XZ&btvXJxXLP*d_Ie8AZ6JB$ZQ%5cw;f)OC*C%^9(TO0dqGhm{W#s@ zMlIPwn^Y%dEubfs0-3~8AfH$Y%O;irJBg({KuW9y2|6#9RI|G5z;%=hLbJ7@v``yH7ihz29r_!!5lZT%>U0)LNF7Q@9YRT81!*0$9)-OW u=Nb0po*aStD5r4HtORUf0wz0y5C7lbsLq4FBDZ|az}tZ@b{jVr6{)A`Wt-$-N?qBE2NZ=RA^x8mP~O%N{}`PKAa05z=ty4F=@l* zX1;SL=bZcV`}Gar5!WUT2$OYTzss!N*i*G?^IX*?`h;;IKc!vDDz~Fmp|XZB3L}wD zz2~Lx2cb`(fHikheKfLaUS-*;9{YmOPlUJ7Zz32Xc3e*bXIV2R5$Rs(aeBNAQxT4t>cyZOF!P&KOF){5s-aL#Ns z=yL|#8QN=r8yGwZrY?p<=A#Z<%*P!bGLJfZ!hG7{G4rIuXCOY|92e|Pxctw!`UB#l BKOO)8 literal 0 HcmV?d00001 diff --git a/bin/ij/macro/ExtensionDescriptor.class b/bin/ij/macro/ExtensionDescriptor.class new file mode 100644 index 0000000000000000000000000000000000000000..2e1a37e9dac0bcf01e687bd24800e8a0bc037d2a GIT binary patch literal 7413 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 H1m*h+!48z9 literal 0 HcmV?d00001 diff --git a/bin/ij/macro/FunctionFinder.class b/bin/ij/macro/FunctionFinder.class new file mode 100644 index 0000000000000000000000000000000000000000..c269bc785d463937aaabf71c23cbd9550113eebd GIT binary patch literal 8727 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 Ju99aP1yQ6q1qX6~dGP5~vpPS0@{(BT_EhTYW1 zJ=D+JX^?v%XLisT?xQQblcuDquE|tqeD$4ID=G|%u?}5A-R?B&>YT)c*A}Gm@!86{2?{*N0|D8t0RAnorRrpQiKtHC^Oy=n{WRv;3V3@flUb zlPbyIXDI@HS(#=!;lY5%=!gdc(o`$Xiy)1m#He*%I0#Lt%4+RXhPc;iSHMADmoJ{B zyCw4tHnKz0W3% z>4ea(4S=vFl>ej&{tIulDLTu4(>b1|%lr>r;~606f9WRQ01MCJ z&30484XLD2s>%dZtqH0+Ms?+4FZKgo=ncLJ` zCZ=|oBDK%NWf^P$tOHwmf_J@o(pv_be7_#``kxefT|3LM3<3zLEO%CjJ{(61=qkH3 zRMPY2g;X6q>ez(hfNDTzpM5f}35CHfz!vJ~GZmEAq5Bx1kTJ1XJYG9PPsd=a(cAs} zJYZ@!pQ+VdGRKjhFLw4)@Q_!C7x4!!U#zoY$0~WjiOf3NX`*Xf)k!N~z2wX_+abN>fhvm^-M>Fx4AF51Bh@ ztyxMfrh*0HzhC*lnU;1foOyqj(Cjpl$gRgo&$Io}+DATPV-bLBW@Z xJVzI{Na3_fR1@VqGzZh@>4v`}I*tFR+fqEP59mV_i8kv|^cjj2pTd9a{|6NqXsZAK literal 0 HcmV?d00001 diff --git a/bin/ij/macro/Interpreter.class b/bin/ij/macro/Interpreter.class new file mode 100644 index 0000000000000000000000000000000000000000..0984591f2abb590638f2b9e8de7051bc5a5e6fd3 GIT binary patch literal 51798 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 literal 0 HcmV?d00001 diff --git a/bin/ij/macro/MacroConstants.class b/bin/ij/macro/MacroConstants.class new file mode 100644 index 0000000000000000000000000000000000000000..ad4a52c17ff1b2074a0534ad26f896ba8db73176 GIT binary patch literal 15951 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 HpFjOSoim_H literal 0 HcmV?d00001 diff --git a/bin/ij/macro/MacroException.class b/bin/ij/macro/MacroException.class new file mode 100644 index 0000000000000000000000000000000000000000..f9ce8122d939d3d09704229c2b87fd2f92aa493c GIT binary patch literal 431 zcmZ`#O;5r=6r9(V)?xt#g{T*gz`X9|VZ?Ki>O|dLfIZ#Hl^>?Nt zGY$^fhs!@eaEuwQ9Nqyo7zw!=aBq-m|DE^28)`3>)8JUQH~DXv9rD^@ti57m8%^%` mo>e8TTG&}@#+(gO>A#}(%!LCZ=UXtcGSP~#mLcp~R{IBA3rvIn literal 0 HcmV?d00001 diff --git a/bin/ij/macro/MacroExtension.class b/bin/ij/macro/MacroExtension.class new file mode 100644 index 0000000000000000000000000000000000000000..e8ce470d374c619960677ead8197699fd3d9ed4c GIT binary patch literal 463 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 Mlpem#LyEBXKUntF-2eap literal 0 HcmV?d00001 diff --git a/bin/ij/macro/Program.class b/bin/ij/macro/Program.class new file mode 100644 index 0000000000000000000000000000000000000000..6ab6073c198d8101859f44e79f8a82f1ee028b9f GIT binary patch literal 7616 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 literal 0 HcmV?d00001 diff --git a/bin/ij/macro/StartupRunner.class b/bin/ij/macro/StartupRunner.class new file mode 100644 index 0000000000000000000000000000000000000000..50d77047471179da20dcca56fa3691e2e72cb7da GIT binary patch literal 1386 zcmZuxOH;tF@z9>EbMT;#cMG(|Q+TLk{A*mtJ-{8_EduLqh zqA)V!(uKdtaXdFE0~Hpz$vrvueBb%bIrsP9A3p)i;;jT9LsZ*M?Wsk}OyzCWvYqO> zV;J0$5MXHERX?aHT{X(7wZbkh+7dzxkyESiRSP<22+eDTX5VM<$Ksm|fhDuVW%$vo zKthYr{Jc$(lt~-&8G4?CZvWUD5U#db1a^N>hBxF(`g)C%)N)PIOD? zVhECZW<^01Jq%4HUU14eBEZ0QB=j-#oKnu)mS&U}9ZfHBD~ti6){ywzh>Ttg3EIO9 z-LdTdN7M05;W47%GOkeKYPHIZ62l--I_F%+NsAL>3dS+P5VlQ^1VdNs{1Ni)L`*?k z;QMv6EQ2AbpdDceQw>Ggs90Prg^^-tX-GoGB(94TZZO2prf^cQ^f{((T9cSna8uMP zxG%ba#4MjL>J0fz>nt(b?zrvT5G+P2J*Qw5Yt&vJRgIDl#4@7Top_;O z9eD~x&F9i&m^d5CBbdIhm@lyLAqEF(xKzXF5v~p#Ku*+<_=?Ce zCbtuj=|f!0P1Z0Yu>3AoKcNYfK0+HJlrcg`5ylLOEbvD>j5@x!rEZ~z7^DdOHtrDP zyW|ywzfW#;>7C^EiGKO$*Yq*wwhu91!@X?s7lxBHJe0m5nv5*h@bm!T_RN>&v0>5)bbmAxT#p!UqBxFzBru36qAJKmJ%l>BW{hGRJH$dzYJ%?%#w9ES$ncsB HKD_w{MHnv~ literal 0 HcmV?d00001 diff --git a/bin/ij/macro/Symbol.class b/bin/ij/macro/Symbol.class new file mode 100644 index 0000000000000000000000000000000000000000..c80cb48b577b158e12d13a179ee47255f20e9e6c GIT binary patch literal 1262 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 literal 0 HcmV?d00001 diff --git a/bin/ij/measure/Minimizer$1.class b/bin/ij/measure/Minimizer$1.class new file mode 100644 index 0000000000000000000000000000000000000000..060832f3142335f0a5baf4604d94c89efbf4154c GIT binary patch literal 751 zcmZuvU2oGc6g_UTW(%fGyY7Py##ewg+bENG1M$ELQl*X|Kzr$vn_8Qz#F6r`2mTdO zD-aL7ArLUrwXg5-Imh|+`|CFVPjTR&!mz2Py;zE@NM-L>X%(xFGW8D~ z*bLrOT!>yI^jYs@Je6VYpvus^+I?4OEyj^#sOIM?^Pdpja2en;hQ@`6e7Yc4B6=%Q z5oZjQQJqDjGn<5^|^uwjX<}e9GRB|K5i%r|e zX4qaz#y=$4R$6Em_uxCYPZXA!!|?!hk}ASbW~GVl`=m%i`C1ukYk!~Rv2mWbz0zTn zWJ;eM%ltf&EMIy^&hC8I)qOk(o^NcNk*EFV-kt``A z8A3lug+>i^I_*)`r}YZOcCa@`BiNr~Jvf+SBiOL#*a|-5#;3C9Ce2mq0lwoF&AiOp z*u`yHHHg8^0`Vp6vepV_1UMtS8Nto4^#ymYu-uZz53J+oKX|PLo`r{mTEQc9DB5Im N7yFdCglss__XkVongjp< literal 0 HcmV?d00001 diff --git a/bin/ij/measure/Minimizer.class b/bin/ij/measure/Minimizer.class new file mode 100644 index 0000000000000000000000000000000000000000..902b2043eb0cd177a897c3ff42cadd7cf75fafc2 GIT binary patch literal 16208 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%0D?LZb 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 OK5j`FWlYiOL6N_HXho3# literal 0 HcmV?d00001 diff --git a/bin/ij/measure/ResultsTableMacros$1.class b/bin/ij/measure/ResultsTableMacros$1.class new file mode 100644 index 0000000000000000000000000000000000000000..846a731e29f77c3f0164db8936d0f22a910e8e53 GIT binary patch literal 1127 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 literal 0 HcmV?d00001 diff --git a/bin/ij/measure/UserFunction.class b/bin/ij/measure/UserFunction.class new file mode 100644 index 0000000000000000000000000000000000000000..cbf28238798234e13f5b81384da3967d1caf5d70 GIT binary patch literal 148 zcmX^0Z`VEs1_l!bPId++Mh5ZBEdAWn#NyJTRQ=H6)FQXiyyTM1{5*CB7Dfhvti-ZJ z{hY+SbbbG%tkmQZMh2czBt?u2Y#PxnE}AZk3|ztarA5i9Zkai$j0}RvCh36;VPj-q ZWMBr`%fP@0G?m_Bqm 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/AnimatedGifEncoder2.class b/bin/ij/plugin/AnimatedGifEncoder2.class new file mode 100644 index 0000000000000000000000000000000000000000..88f74a68cf903437e30cfb6aa497cc133bd894b4 GIT binary patch literal 13914 zcmb_jd0-r6*?*py-JQ*5l5CQ050bXqJK6L|4?=01LX$KlAx&#i+NOYXlWdZOO*Y-# z^g;?s0g@AtejdnBcP{_&xi_nr5A z?rXNs{QH~7h-eZ2!XS-lV)){Dy^+4oaL>FIJ>l+PBGkSr+_AE!E!rN6EilNz{yV&Xa`S63+9*@eOEL@d}7?+wO6J$U0yOdd5-*V7)_ zj&-q6Zzw36+e4Ay4kja>2*z+9lOFBE_}B(@Q8+qpO<$t7FVUQcg@WBnu>o*(hM002 z0BoJ~*GBr{m~i!mw}&EeChhzxrkt=`6lz~<4|>|7k!Y+Y)GLs>)yukIEAEZ7C7Q!K zaiGy2j`v36=w-J>&{{VF?v8LQo~VrlyJb-vYnwv=&=n7M_eL5TONPjp%_lZCcTA;Tm) zEM`6tXP~_koX;!b< zr0G`gJd8a7P$B`DVEU9vm&t@zO*DoQ@wQ-Z z2vRJQmz#8jOuCbo3l5(#=}N)D01oSR^r};?GU;l%1{%CJ7Htc~YXdY+BrG(jEaPCkO@G zu`^^D0)sx!G&OC*>LQU)XE3rN*4ft$OSN))8!SXP+T*5MnNCyY#oyf*PxxCy{>5_u zs6QI>H?6Aj&@FVEXvqIC`ASaKD~phOO}brRC{!d_6$|!uh1;qVv4~BwJ59Pv=5y7& zx~(P@ZxamnnY3S~ELJ9g{}Rqh+QqsxDQIEyqL%>o1Skw+i$7n=d}b_zgyu9SK*iO; zUM1RYx{qm`G7^3@;O~hh{FZH+D^MIX=>d8W0FJggj*?BK>qWF4GRZ?OS^G7UzD^GV z;tp7=YO$bk*hM#_st)p5XV4JtN>ko4KvjL=2z-m19sz@uAo@!@G)T9}E^+$rS&XB(Yyt0sMh(<5z>-8r@se#xaJt0Rw z$u#HVcz4PxWam>ReVe`m#1c^p1*S12Cqo5V>yZ%j6mH3Kc^=R(s)P%+1UkOSpzn|H zr`B<9`T=}SO;oW09>q=1gKiD*7ygb|wA)|Z7ZacASJL99A2Cf&PX@uiu29UH6gd@f zUF@$4C*1TCrrGDkL;mC%nN39fZSboJjQV>*+x*HeT4eIO=_N3;G3u{TelZwv(@!A@ zHe}Wyp!*qwtqH7-#zJyj6~wDE7VYb4chhT-){Lp%SZHfF+81}z8_--FoVG!2JKXeh zrm_`qod`R^u)&cX{^n4^AMWt4*cuE+gmeCq#eSuNJ&_%yZu$i*lq|umN^;!vE2b&- zh$6awTR73>kN1Y!!X4pIyM>;c-i9Z$mw{S-YiXh@h}+_R<@=%VICa!U5%PCT`mGqA ztavEVq(T@s{hsNJI>boeOIyMwp>?LR99_Y!A^+B3q%Q;`lGqjs_4v!dgm(Xe(@*!% zAL-8q{YkQ8H&ZCP>T1M>PDR$7miwNQg_Vu1BD%yjY{7XKl!S;Zu%?J zlF`Tg_xqus2sJDdQ+WPKIs$<=laeBqeuR%{+hGm4LAGST2j7$)RH6X2P%)6M0wAq5 z0`0LvAA>%Cv0=?5AGr*6FpU>qZ26CQRXY+P+m}RR@H*@=*YG%!zTmPk;cS|v6mVuHsbLnjiEv}p9X*xUL$>zhrL{E@}v=-rKGN^a+0vOTiU>`CEe}=zW z;`h)!yvXF!#o#K45iWq%9zKK5GT(c0&?$*=W?p3STCqw_6-UV0CX<^5VP^-Dqb?aa&*b%L zL>V|ad80{Q%9q|IlM1L1)?Qpclw2Y*H(!X5R{>?m2BRcSOfeM0PHeCXv6-7ULpM&n zNkvX=+-h4>e|7D27_x8PPxe7u;hPrIt>$Y7(5|);qG4H z<0&_RL3(d^J0#3TABcbjFjYVbn$Qsm;!-hPHIl13R``iL+{fDt-U^4P_@Q#D)xkuU z$=exWs8hK>QKQ;Yq{Uh{e7*z#w%f#n*xr< z%4bTi!FyrQlPhY&kx=HWhx-u@@@7R28m@?LWZxnS4JVRE9+5DZ-x8^Q&a)L6Z;h zL*Nk<8`MDp25YG^N-0>>17i!8_iTZ(DybWJ9mLH6yNG>Ug}vS39#kFhaxwlOcOu{n zQ|V~O3_wQ^DfI=d9b;NMCbf211?};q;{UiH{*=kz79-$MMxePX8pAeMLiqqW4pE$& zzb7Hx%5}}vE7q=DFt=O`!?Q3Rf;#sY`~xsCO`wKN8DLe0^>SX)R$1e%B1)CNg(V{=7zP$B?Bzyd!xMC8$Ph`_5B1q{_9S5Ym> z7^+1PL$xSjs1~MHwJ?mTg;`WB+^%X7eyA4dv}#fCP%X+GsztFwwJ3F{7KIMgqRgRM z6ggCj5{GJ0;7~2f8>&TdL$xSvs1}6{)uODSS`;-@i;{+FQP5B=${A>hN-2Qn6#Suh z1A5Fs?_Z>$N7NTMLURN9Ae9HSL0S;-E*PXm@?H_}8iTZ0x=ZA-EZ`WV<na6CV8xr$9j2el*gvSxLBhL@N7j*i0CCSyEO8Ns;Ax2(Bf1(EZGkpT_@p}3Tolo!4 zW_q7O^jFkK{zg&yfD$u6>j(x9lFEizXWTKs~&CHgzv9zD~~R2cvM=cc~P| z-E``Ui!goZ0F9AV1C(==ZfPm_!V&u7;~6maWAY9QIKgH+{_Y+iSEF88d=S*hV;5)9 zJ)n

OK)7)+vV_GDBn8poy3->JF~j9G9M`FR+6DKLAl}D&;T&~Dljdjylg%}MeLd3bDRLe#=ofEm37*%tThOD!$;zIkA}r4 zoLPdzb(n@A(F369IeV2Xt=~(I!)4#K$$zhqzwB6Lz%fMkW6`F)RETEdUNVI#>jvr2 zVR`ST@qs`YdX4y7i@&BJIxI?rt}Jp%+qHaRv8(+6O%u)@rNNehBS+{Pm^m~=-z+#b zM8}8dTk67zyLHBRV^xgNQgq+X5CLlC(#;NK7N5Vif4U3o>cc zNwxH}&{I)7mt47&bAl#u(Fr;o#HH`iGZtC(hjCUmAbZbvjGk@L3!ZCkaTNTpxy4!V zLUW6&;K$7k_QZ=a@v=<3A``DRuXbBIUYChCW#TQF_+@j8QSj?#A?_ltr>nxDS2)p! z9=n{XSKm)L_JB(bKQ^5t!Pz~_Ou4bYZ?-#tQq7@)A4o9i>M zqQqK(Wt*&J0Uz8%W5DMYgYV0N#UG%>1%FW+%hi^tsVy5nZgFP*`4PuK zKxoP4d$bG^a31-Pt&B%Ifj7_s-bkys1$Jg5t>sPB!spWkd?9u4CsE73h;HJ|bPEUR zFt^h8xQ*W8cKQd0Xc!rT6Y*3wqTPIC24fLp6(IxjbA)FgbqjD0lG7-!<6hptTlfOR zT^Dg2_0t4a_F;4@-nJp4-_DmJs=tPJ^7ZKL;ZGqPy^QbVUHm1!obTl;5GQ_`5A$dE zVZM^T$-DV+zKWmXtN9u1dY(VaFY&ef3V)8@;OqD;#HVlb&HQ_u^e*3`^r+X8d|Qtu z@GJ0YA3!e_@NxP(dKxglhdxA42mbx^4`qTb;4jiY(Q^X-&2aQC{BIb3@dR3fo$#C= zq34F({tf*by)0Pjx3I#6qOI7!D)J8F`-4kN9RgkTe@3j}9e0AJ@VIf~hG`RJkDO)s z4^G`EG|G+X22MdW-`3#SkUcl}w=3Xv4>Bw1Jj!}Y*$_LAaMtSCLu@wAd4j$DkUzW> z9OtovJZ^w;#kLjl1TkZCj1}Ka~Y0IEJ_grV#IkAe267QC+zQE>w)`0J%k& z4#~1fNfqwLBKXZ)0zRj!>rsenjb334P^Hgsbtxr_`CJnmK4XZd>{*Akcy(;H24*A& zoc2J;o^`to_8C6s{^2LFwqY-VBspJ#fRyu}g)V%se~IxmE)yGF$n#;OC*X>OmVqAP z3haV?$xZGafSpeQ82f($P{^?KFu3tD z%>6nlst?oe5GDSFhv;wo2>p}4fx6bCT+EN5dh|HY=i?~ZJi(RxBscK4P+E8jx_2CU z^mCuH+(gEu-Bu~|lw(W=nY9P-}x&v{^O7vVP z_1Hc7Jk`LLK^LGUMau6RyeKs$8nBbhk7&NZy1}bFCup9*UdJ$EQMU*^wOJDw6i-EG z4ug(LSw_wvKq$^1M!a#VU4ZJO^S}`JSKIfoANc8jCk7$0v@c!Rx%^X5_*FWSU!&#xI%0)4=p25NR`bsxJ#Wzl z{srvwFA)p=3O4%JbP2zWSnwUiWxs``&E;%F2NI#d&H;ak%QR(|3P{fqT5F&hD6axA zhSk3YDjS9KbQP5Onsoo_g z|Ah+pJw!t9(^UQ|F#H?pWFJ5;{+Fka1)i0_@#M2rp6)s=z-9X8lN5RT83-0SW(we`(<7DtGY_nfz}k^Dq_TmMI$3 zOiiOQO{a3rL6w@5R%kA&1r;S!8*71*LS&VVh^T>sG}D6?Us^2^^Hhw9aZdn53zAA$ z@nGYeAr2kqE*R)yWm~xgLDc}&i6s-;$}K*ZvYpPkg?#ZYuXm?dR2fw!Rz@$`<#ne< zVRoH!^J#b&?5vZ0&f@*UM@HePdB~$>Qy$9y%nMboUFU`^Nsah+b7Ojvj##mhQ zDLl`y;7;M0u<`UDRkT9VJftWQ?gn$lLr{A-3|yxpybx0%Y^}5mi4w)rNXyWAxXBJf zif3@PE5Yh$ie4Am^xAg{R;S5W3~UdOf#=6lRJ>ItMX5%4s}b~+v6Ja3qc-hFQB)+$ zQiU{a;r!voCz7d6qH)?}(A7_~wJ9`Tn@UTxX`t;4s?%oD8f_M}X(hBnE2Zl(cB@uK zd$rkgw>Bq(-uqJY-e=Q$3SDl~dp<>NdM|-Nu;^WzqW78+^!Aa{gdrXS!7jFmZIQZ{ z9<;T8IRsg>$`wFhP%8*o6$Z)eNJBQLot`1^t`uM{-lA*`bBu1Z&#JXGG&fLmbs@)l z9^nM4BBRe-PC43H8E2+)O{n}%2n}o$Uz$2|jSZit=ypUWfN@Eh=(%FIv^sKV^^~QZ zn{lQm1*eBUrOp(^)Cq|TsSTJ)_zng3ih8jDUcv@VEGIOF`B)cHAlWhNG4 z$#?&kEbIjfw`3fe$-=8F7RqN>n<6KXd%WLR?Iv2L-K^lVTx4a6kd=I$f)7CO_2^j+$4PqL_*_7VA86RH zj?jy8xinKR?gml!We^~fs5jVpfy%jk*1f=5^2nL+saBCpMGR#UPZaXaLwpND50sAr z#hVbMNDNVcN?vg)hOiL59};+w+}Z=sp9dj{4*}uBG+Fzag2SpE%uFFVlW#=~g#t5v z0cO>0CIW+WwZ=!fYZ$SK4p}l#xlRQJHzEzP0)q!Z9|5}@RlTEpTgy@IZz(&%w;$m< z1adG3G1T3t%OLz2jev%vd1eZM({K`LkCCQ5PCo58P12sAY1)%is(mX1`0^C+qkWdV=yB?I^|;zwrMRi$TBu*>h5Y}jMAX3r`rD%Eq^$cLZ$mc4 zBb+Pl`q;7}R|MQe_Joj8B`ychAgj9bC?9B%B~lHkI>KLR3?L@BH(3)sX6IXu0lEMS zVm`-0m6oAny*f89v~n(obFY5=rQR;W+g?v~QiekqqJbFd;0B!b3 z^(f}c-koh7(q3Wuq^vYzD90ydEexr9+ADH>rqAs1<@j>CHv4jYxm}wp@_c!|ta~WO zmsgSH%XUR795~UJ?epv(z8i3W7(PcDL6vE=WFnY0d>^2ePuh=R4}OAR?nTPiUVz%J)-@ZzM;KC z$F<+m3)=7K73~l7ruIjANBa|fsQsCB?GNnL-s3{;eV(BGl_zU|<3_Y=@V-&|fG^bk z&TZO<9M=BHz1j)hu6@KiwPC&jj`k*<`7T}K2Xvhu(j7dgJNZ%F#ZMwRd{KAvJ9-v> zs3R2Bv$X=<)W+<Ctv|P1ar7Og&#K*M_w-^+IjAK1Qq2eOkRfPU+!$^5p}*-ufcp z(pIXl2w$_b7092(pqJCN{B`8eI%=gIDhzc{6Y{A+U13eeA|6BN*=o!fCujHfX zxsfSd%8#O#g=c~vL(jvHQ>QvP8(CmIAH$f5eC`a4yHGU$5INIv^l}lTkKrfK%TqSy zR>~cwI&zBZhrvFeqUMeNps^p(40)Y-0*Tk+4{7W$aub(Y1-mF;?N^X#y{cl9sy7W& z3wcKl%A~B;K5lj4$P>pZ%PZ?aJp4}4vUz+PNS=eSgt$5FAoNd^$yIi&!q6++19V<- zna{o3oz$YNi6dS;J_p`y^~v^S`Leov9-jy5lkLlf`aqw2E>Rz|!sT;AeVjPT=k^)< zhwt+_(?w5|;t}cjQA)=5KYc3Y>eHw|pHAcT88l0uNfr7ms@6+ryH!MsWz?z9 zp^LF*i#|^Y*=`Gul(A3Pg?Qkm7%KsDLNQyynS>Gm2i*Q8N<5tS9M_@P?t&w4RRU_j z)n1_3?nWwJt=OJL6%avTyN9Z6wrAs$oW*uiu^nX@xaoY#0=EnlU9JCwQH_){Vl2(T z(QCmZ{BjomCgDlNFsb(;?}w~gd4C6tPrY}FT@}fvf7m^g+fXN=a(t7~m+6%QWH^>N zC3IVIy9(Wwc#U0^#m?;*;`-vED`Yul7Y*^ZWCYzq_DMS0wDoGjmrBwrz??IvP+v?% z`VyL|FQw`FGMcGZQmMY2%Jj2nj=q8x=~c8$ucov0T4ZvoXr*39wdmLD=Tej200yq6 zklv`6roLQJht0I<(9Q3FwD=(;zQb5OL=0>tt#eRsv3laavq&%f2y>hZQsGC$qe(^k zK`N2p#;5~p3nCRxy6(GoBAyS$Ws_>wn2TH7m zm~H?yHi8;0G)+IB0{R7Xy8cOE-%2ac*68gDOVa|x#!{e1x#4sH9KTjS3Gl5p;N=3{ zNGJuScaty)hyrc0O~<>cvZG%@ZaqR?y@$r>Tc{ZAWIYabO3*yLF9WV*2R*7txitgIniQ1yrvWEHxd*%yvoc>m0E1$>JdO?VGx<8HZo5*)+$529Yg}Bk zfB5~hU|Gbvo$~cNAW(ObU%!h=(3b0WD=@6AXub_b5k5+PU#SXyQK4*9vJ9tvM1IC+ zUI#SbWI%)1BbIQ=QGQN>Se34!MTpCMgn!t8kFk;>{3uN;6A z?fVGT@Rt<<9P)dtlkV%YE#)M!g825#^<~9F{L?+SRcMb=-fR1Zf1f75g(ad5{Rmvo m5KYt{$sqfr)UA{FO*_pg${;)b_1Fm*FC=d$0VCGoxBeG!fqb$6 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Animator.class b/bin/ij/plugin/Animator.class new file mode 100644 index 0000000000000000000000000000000000000000..0d9fbf2f34117a800dcef70f063efc268e20f910 GIT binary patch literal 9456 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/AppearanceOptions.class b/bin/ij/plugin/AppearanceOptions.class new file mode 100644 index 0000000000000000000000000000000000000000..4acbf8c61dacbbc619b1369eaab7d2c00cd27fe2 GIT binary patch literal 6814 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ArrowToolOptions.class b/bin/ij/plugin/ArrowToolOptions.class new file mode 100644 index 0000000000000000000000000000000000000000..4d8116be27527acad7041115781bd0113f8be8ea GIT binary patch literal 4426 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 J1i~^k?Z2owV^aVC literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/BMPDecoder.class b/bin/ij/plugin/BMPDecoder.class new file mode 100644 index 0000000000000000000000000000000000000000..83051801ebbefe4904e153db45b5c8a99fd3d809 GIT binary patch literal 5964 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/BMP_Writer.class b/bin/ij/plugin/BMP_Writer.class new file mode 100644 index 0000000000000000000000000000000000000000..fbac2256d6bca21f30c49008b6a5d46ce0a045e1 GIT binary patch literal 6635 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/BatchProcessor.class b/bin/ij/plugin/BatchProcessor.class new file mode 100644 index 0000000000000000000000000000000000000000..58d524b579712fb177e700761227f83aa576930a GIT binary patch literal 17755 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 RmW9(=N~iI>qiT;>KLnK`YVQC5 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Binner.class b/bin/ij/plugin/Binner.class new file mode 100644 index 0000000000000000000000000000000000000000..9b8f2f52ffe39c5c2dae2a29832bdedb9bf29385 GIT binary patch literal 10338 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 gJKWPpxwDZ7823i+E7@sO+mX}MtfgaVozx@sjH z%graoXXj-$(9q7^b)|FE9d!lnF8cFlY)4K6wo^XoSmiSJlIz;Ftq}puD@k|ad{UsN zN$SfvhsF~r85F!6k42>Kd%l2}7HEIScJ1JpK&Ur9DG)yCt2=3YbYRYdMpaZ>x1GI(7*liaoej!`=-n)lVCUVIPspD;3M3(+vjeM2^I#b=)VgqYwn8;Y7kNT6sA? zJ*cwi=hVmlz#Cy^xiq!1jo=Jt)W;o91T30$bC8mA#y@ zN~;to@&)#{y545}DE4E>KmrE^qInslWVuX?0^57zn@^LwitjLDJkV7aZv;zrqie{^ zQ9OVLH5_HA8IRG&4SW*A0=ivJS=sUQg<7=9&_j%vT#(t`0}A_rff#`t_w$vabc5b_ zT$wp$;1N7ZUB-^ez9;ZtiyLl++~O9>-U$OI6_yUWJY~B%7KMh>0^RG$To3YzitXg2 zuj4TWioSD>bNEnhT9QkV%)$v6&^kC>X>w#S2b21a;+kZy?!^}%4xFt^r-C>Nw6q((+s%}+Co zGv_PrsbrE&SjcLaWs_K^m9oGtN&`7CiB!rpc9h3vA{yJIz(U?Y0XEx$$`XMC|ARx` zHp@Jw>SYu)FkzbjG=v6RFp0ElmK~f?U5JfmR4H6B;44XoT(E;l%c)2mfxytH8?awh zN`cI+Wq9mbff*2UYTc9#BsN=bufx3iRSapkCa`z??yXFy0vy%xEaSV7%MYpo(D7-m zTp5(JR*Bv}Lzr_m+o!jn<3%F7?73GelD$*M=O`3E5SrsB-$7imG3>{7vU z8T2ocI%m(>>n-w^2Sz4+e?XO&PLFS8Zj(NT50$_{tZt2nus>jkkn z)>F({r9u_kw^T#@cI&vca6GBwy8`$9Uo2l&EHABg&e|KcrpHXLy)bJ%EtD;?0<}^- zJ*d>=eD#U~Z8j4#)+A~bHa0rF5p1^pZ7uj?;WZA7u}n#O)?@0kcaHMMByRN2jgUt) zyuqqSdlf$`Rd-<{zJu-|l~35mha<#agkuyMbUyb{z9Z4vm(Lf!VaoY|eqC%Lu1#@q9OSVK1{fRI}eh55U8Oc#L)*q4ZHokKvCP`6> zbc{E7?Zn6F3zS4~2!|V9`7hIBv=E_7zRPrUq-KQRm@Lu zC`~Qn&%@!ju|HEgTjtrF;VLS1YqzoRhfe+X*pmsFi|I_*d@h}dn9rv(?dA*VjAni| zozczDr!$@H<`+0>=9khbF&26tyo~-xUF|DW8|VrjN)Jcqd}pMJuhIAG(ADPHOJUI! z9(dS;#iB6pr<>@VRcp21nnP&11OJ{HrA`V8jq9Ey00z551A z_${vBU3TlgqKv~cv+0lNApHccQE&rU-2tDm)2K|D2DsPWr z%lp`-VbA+G!tp~UcbMPRzsn%HtN+5D4{%h&GPlM3ZU07e>x72u+-4sBl2+RAtN#F~ Ce(7!i literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/CalibrationBar$LiveDialog.class b/bin/ij/plugin/CalibrationBar$LiveDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..6c6ce206976b145719a419ee45062f3a299e8ee4 GIT binary patch literal 3215 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 Ldl@TZrX=MzNhZm(o1L(` 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 LL6uR!r-E+*VZDxD literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ChannelArranger.class b/bin/ij/plugin/ChannelArranger.class new file mode 100644 index 0000000000000000000000000000000000000000..e2363e9bb3128722fcd4095add83b495f2423a57 GIT binary patch literal 5932 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 OS5?s22dI68I{3fg!nzFr literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ChannelSplitter.class b/bin/ij/plugin/ChannelSplitter.class new file mode 100644 index 0000000000000000000000000000000000000000..ca63545fa7693a5a72d3a598a979208ee04bf864 GIT binary patch literal 6108 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Clipboard.class b/bin/ij/plugin/Clipboard.class new file mode 100644 index 0000000000000000000000000000000000000000..c001413f98d4d73b884739c839e71f1eecde044d GIT binary patch literal 7020 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 Gvi}Dx43ozI literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ColorPanel.class b/bin/ij/plugin/ColorPanel.class new file mode 100644 index 0000000000000000000000000000000000000000..7effee3f049e2c27f14835265e7a3e50f5ce7d11 GIT binary patch literal 12780 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Colors.class b/bin/ij/plugin/Colors.class new file mode 100644 index 0000000000000000000000000000000000000000..1430c364ae7e826f91e03d276dca7a7be00082e7 GIT binary patch literal 8465 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/CommandFinder$1.class b/bin/ij/plugin/CommandFinder$1.class new file mode 100644 index 0000000000000000000000000000000000000000..a8d41b4a05ae5cee6c5c5c3fc1db0daaa55b8d6e GIT binary patch literal 1143 zcmZ`&TT|0e5dOBUq?FLs0!647)T*UG3y4<`FBL|eqR!xy8Qu=b0fK4LNm9U{;?MBG zf$_l~;Dh63#@ml^+>;iRk>=rKchBzkWxw5g`2FrZfEhg05Frd%Tjhq`+_0Q-#jV$c zV=h{bDZTuZh8Q8UCAP(G+2668jq;O4Pt+x$E7-LB{3I7wJ59zRvJ9Qm|aUz6Sov1W5anW}3DyCytK`R!m#bxFu|`uhA7ciEE{j_bUt zyG@_<$ox;p?f5dNS-urA#R{vVwFGiFqv13m&HAmd-tNwlaKwi6bezRGLbou@R-Dk+ zZbJzW4uC~cZJyV03PS{K`v9C)3BAjKFkUW+M!O5mwETwabCc0Ri@Bg4e%$;FgZtU~vx+r?(2oIrD=UjZj**%pp9*Y+ zeU+8Wy@poY$8a&XhY^Jr4e>He4En^s=4Zs|3wr76KiJ7b>?AJWB4g&mN{X||8nsED zg`~g$6I{vgF}{y!+Q-e}9_IdjdzKpqePgcg(CG)#^b`H`E9BD|iUHbuQOvU!VJYtM RJH{Jx9~HI|oTc*k`2(*X_yqs} literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/CommandFinder$2.class b/bin/ij/plugin/CommandFinder$2.class new file mode 100644 index 0000000000000000000000000000000000000000..61980d51bce189c53001874beefcb83a2f3cf9ac GIT binary patch literal 1866 zcmaJ>T~ixX7=CuMVd=7bG!iIZZN*dr)J73wg+iqPG$j-zm?|oENseJ**^Su^$av|U zGyVWOI@7y)+Zm0F7ybZ$l1|m<>}Ejhq?yU?+4t+b&+~r#@%L}P1DL^=I#dNi_Fksu zG^)0n$$19{mRnh|-HNEErgdmk?^%ad#yScz;!wCjW>p+7R;*ee>Iyo7UE5E+Mx#QT zAagXjWxIB;pkOrJ)@q`pK+Aa*aRCatOvKS)Ack%eI(ihu($b)}V7ua8<6uYBH?18< zC`c5%vgMSlx-Gw3LQN*6V5Hq;sc9zFxi5}4k87e5ru8huWr=leqGVtImvjs&7;Ix? zVhFkD3^hdc*eDL|{p!fh%}X$5qB`!7}a#{F( z>J0_ApTk{Spdle^WYw87a1F2Mcv-=Cn>$ly)+W-JU>&L=*zk^WUc(JSKzRvh%EWcN zNo&AX)X&~g;SWxDLai>+Lnn= z@F@%Hm)YQ<%cfVdeLI}O!F0sKc|SR^F>xqkpQ%q*NHkx}%O%O2Rwb`ME${M>BQBpp zde`Ms=X1#lEII9Gl(xuX0IGBJ>n;XXd4J8SwYGp!^cwZDSh2%Bac;5QkYU(s%Wm26 zd{%;kx$9L-96%0ky}@U)g(+;})@2frnRi`wuVeYXVA{sn(BLXpSB!gA>FbmGizk(T z9i5b42=}cTN{q>=UtmoB3iC;5)5~WEZJ>6ck7rY!Vc;SXe3NwaM-h|?<91T^<5UyL zhcNgW`5xMn$)91I;BphGd5R`aaARTY1Tzm$RX3ZsmDQ4(_F%M@jX%cymPaxkYhtm9 z<=Kv*4jxzOn$^dW`XgMAOl~IQ$#@eVw7yP-F|COYoA~%K63KWJmX_M1e}0mnVvwNj zNH}UYdek2Ds=XLi`!KFv#Dto_q}q?Hn#7_yfTB8xZS@i=>JaMcFuqks@EvtOsFy=R zHX_h1LI@w9VVjf;;~RXA2PAbIU&F!<@6Do$GG!VTXj9=S&e52Kz!xNWn=7hHI~}{s uFA2Q|Pr00xrx;L_zp$$RNuwBSDpl-ZpKpz;$AQOFf-+ed`MXAG4eG!D&c15^ literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/CommandFinder$CommandAction.class b/bin/ij/plugin/CommandFinder$CommandAction.class new file mode 100644 index 0000000000000000000000000000000000000000..b976ab109087b569517fca5877680f1fb6ec1ddd GIT binary patch literal 1126 zcmZ`(ZEq4m5Pp_Yptx-j5DUJcQVUn;)psq%#F8|r()s}<@#6s-TyiYrIP`C6A~ey& zAK;HN&fZ;9+bbV#cjo4qnP*<^{`~#<8^9Cn#SvlHlml~YPy14tjnQyusosH9J>irB z?M>H}BNa!KAvLhRSf*{MzWKg05M7sncWrAj37`z=<{6~rI#TuP42hvo(?eGb88W`z z`s$iT!Ak;g4QP(Kmd?Zwb3e;TX_q12T)>TdVy~o98Z54_3z(x-(Rt z-L@R5_26YxSIV%psDkq|(5-Ywep?x`VeoEgvLCN@m>>OL^Z8}KK%~4D5`tk-{b3FP+_y|P&iB97S zKY$;~cy3{t7(0`_xV?XJ&hMV{+^0W(eg6UAIV>F-p(V%G#G4LfV0A*@w*$8;16M?~ z_fhCil2@TK^+k~MWSodVL^|?>(%7EamS+b;>)l{1oJ2=~u%}MXtoWnkb1~y2>+ftr zAsNZIc0}0fWnA3mpcgWb$xFg^Ju|A&C*(V!D~cejn$WRgAcr**MU)7+x(Zn9$w0iF z`U4T2+5=Az%DvFBy}lhuwO<_ORZ)batjjXA;7Uyy#3D*QOk6u*dad5e7U8v$q}}Kn zsN$}UEy8Ao1Salbo3LU#j)+sWo?Rit(v`=j;w?fBL=()*pnU4j$^?OSoybSvBMV0@wfph zm6TK1|9Kuo7<_s&{TnoBJR8l!FEE>5aqAQBG_3Qtz%if!%Dgt!nzL?WgJ)I%l?CCL zqZOgt{LBCz->`X(`{#IcNitLfxmYKW(kk>r)$56#w0u$A+*#2#-ROV5GE70wfi+s33}|D4Gx~Z4mLbBwJVvo6cq<>L+LP zqdKF`^n*Go9cOepRpZ#EV_Q4@?Em7Q;N#r8X$Vl7ne5%Yd(Sz)$2t4gzc=pycnohV z;0(jYX6j<8x^9@Mba`t_GmCSES=6nObJ|)-&z6gNNr9VTWK(-rdoNYlHq7;uE#9SO z*DAK973|%U4DOOqu^IX@;&`fR8>Q5&WohqcXnTr5N#`=>vkOZMY@VSbT{bJWX4-kJ zRMlOCiVAt{4Wlx0ieWHw2sFy$oi=XahsHigu`exX`1b1HBBc zSUj(wpP}bIQ2ppbkBA#o;enT-D`S}YLUn6Rwjx6f#{#xM}OuVC@KpD;YEVpt&Vypv}JX(8eP=v4IjoBJn52FsrZm2 zG3SgDCGr+zF{-d+YlU~R+QkO>cB*)`ye&}(0UuSV%SI@!VthbxetIp9o+XU^vH3l4 za%-DybxuZ{AL*-Bu%S$9H$jL z&k&IIw`f9UlP`{<%SO@Opd=?9fA;pjr0txOb21grw__eJD|m_K>fpTWKd&N#ED0_F z3jZkTo=JyRB8hT%MZqFN6948u)AJHn%Btuu| z3XICDkP3tt^vwW{(?1qGSqsK%!O>c9yvAy@7hA*t_z{6@{BUH!aT}&;EhbMMP1VseDiU9u^LH-Md_^+}`!pYPGKa8g(cp;0R z!O2!PW-v`+2tsb~fI$CDQ{FXtBjgQ-zDDOgL_6>XpRFZAt~#E(i!-}8oJRT`UHls1 zsJ93hZej>GFf{6Q3H9cvQyuo-5eC72Qn~G4adqQLZpGb=OSu(~y!Qr%a#`2RkLVeV z{DBua#v;i&7Cv^d$k?5o|Na)8==j=CT?T#=A%2Uzy^V2NPxC)pWSMM2n#4JrCjcSZ q$zz!aM`*xaC4n4S{NVo}s$hk6(Yx2E2FEI1r&WAK8Q#EK;Qs^YzObDD literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/CommandFinder.class b/bin/ij/plugin/CommandFinder.class new file mode 100644 index 0000000000000000000000000000000000000000..4b4667b9c15781c351ecf861a97ade5fae9dc9c1 GIT binary patch literal 18916 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 R81fRH-$dJbJikTi{{V*bs!IR> literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/CommandLister.class b/bin/ij/plugin/CommandLister.class new file mode 100644 index 0000000000000000000000000000000000000000..dcf8ad54fa806ce667c5c0bb8483433b7f8b1170 GIT binary patch literal 3859 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Compiler.class b/bin/ij/plugin/Compiler.class new file mode 100644 index 0000000000000000000000000000000000000000..fd73f8384e2291fe95ed95ae93ff08794b99e377 GIT binary patch literal 11135 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/CompilerTool$LegacyCompilerTool.class b/bin/ij/plugin/CompilerTool$LegacyCompilerTool.class new file mode 100644 index 0000000000000000000000000000000000000000..448c9674d4cbcd6d86eaa833405a95303c31537b GIT binary patch literal 2445 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Converter.class b/bin/ij/plugin/Converter.class new file mode 100644 index 0000000000000000000000000000000000000000..a126d689116bd0f1225d78334af5452aba816551 GIT binary patch literal 6522 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/DICOM.class b/bin/ij/plugin/DICOM.class new file mode 100644 index 0000000000000000000000000000000000000000..dde5e387de111b8afca043d8325fcf794087c7d0 GIT binary patch literal 6796 zcmai23wTuJnSQ@BGdYvV34sZT41x|qB?*Lh0hENR1Zb2bn8YYxmC4LWI53%sa{+>i zTD99M+NxCxDphNx-Q9Kz3lp}8b-S%?wHLc}*~hk4*WK-Ix4V6|YxlzLLiYX68InN4 zmgiw|&i{Yk_kWl7yZq;kGhcrdz(Tp#gdwPKwlxeS^L51k1QEW^)--{!njwm)JZwHZU~RXFiv<_|KSqQw9Wzy>QXh~v1;2K3n7%z z_#UP6W%6`=RZ;3#KdFkfUZCjZ_5nN9?8K7kz98la%Jn_k?8MXk3l=O0p$7G;-~z$i z2}F$#LT$J5Tw&o#Tt#>=(9KT9j_1;uL3+QYc07mDFSM{o=@l4jouu6s>$go@Bd8wZ zc-5)djx(m25Ei4pls|Lysma1pEF0zaU=`+JVx1sBc`eslXu;efsylPBTs|Ab4RrXL_O&6j zqTNIrz3Uz@#_bk1;6{Pzq!RY7_TCGGrX`3@y3GYyw8M_kRueY~E;?V_b$KW0I^t$* zG;zy02e$WYqsJ|5!e&8`=B!S}vV=tCIqr8C>5`A(b`!TTB}R{Mw=8T?cbMeYaHwfL z7UD>7ZAUDbx7CQt$AZE;w`sx{ccE8tG&hK?bX6!C3ZVL|E=thi!Y zj~7?8ow|2{QA?|u!Ccy-*J#~gA)}BD*jjWHBJ@TcI|QW(C*Hp{o!7%yBsK-HOOWVd zmPZq5I~z@U$hAti>N& zxEx;*EOPB1HEDdE8oyH89mJoo?6l^SIcFd_$buQfA&Pg?W0}J9PZi-u1T$)yFO1_L z3S^3AbFuh#b>%S&f2OXSTy$kiim=im6~u88u1X}_((J)9lPyexnzr;^ibEsLflO0IL z20PfvsbHX{xw*DmA^cYsPAY_hnsi-*19r)4?{ggCKb7!P3qQlpnIBr-2#%f%4M?-& zP4~3#w`19S#%>t1*A&DrShm+?VuR6cEnp$Mj}J`zjn`%-;IQzw_&WxeTCUBf;@$z% zYFaioPXq?V-!CovgV!AFToG{^(O+5kC$+z9C(D0&=gpeEy8h1={zcaB!3$VA!y#&d5RPoKZl1#bnD}o& z04oQi9a z$(Br!u&4NlF@h^Dz&|h+`?W3+imsoPC7HAiQ#X*L>SY}yrnapN6uA{b1M#%o%K^vO+{`1` zeu{QY6m(KKRN>-r5=Q~W!P-S>Yi!``)k5{t=&aQa#5C~_#I&XByyXqB=gd|Y9EPat zlz2L|gUISi-;~O-r`im{tn>Ss7ew`(I|?hpPf4pKZPLzg^3V*KsozOs% zg*Y3J*i-Fwxbw6nl|SWXdZ;s<=S-q_BBxtD7N~ug#Ua+m{{i@6=w+MD%+Rsd{mTog z`xYCU{5kG;AYl`xxOBBf=~25&A%dGwVI!ftOLD9`IEkN6p4?9TlG6 zI=_I1HvU}Q=#Tin1OK7pSiG@5;vd4bje$s@?g(Zd#S&>Oi3AF0Y%Dp5o<=ib7O=cA z7+!r0Ya+q$^#!bd77e7W+=rTQ$8mIRRFdujZas$B5bSVY0Zt^yp91=iVc?KT%kD?D z$5RpBNogICpl)$#a36iDBzGRgo{U|+*0~B`{FYJfuI|#pjap^I<)EKHN-G@i1{g>5@ zKN{hh&84rZ1>vtzad)|Z!{MW6b$2O;CQ-gpe*GNEq4Owd@GJh8@Xbh2ZGR<@EZ{YL z`c`8Zm&%UdyZZBk0^T}^=hULN{m-LK&vXxXXhbQ`h~0r#Omdd_-1VvW>+LRSydzTP zcbDkF0^adO%EIp!@ZLeRxcjQ>3U^<ny5hOYeAVVtHj z)sd9~`dsM`SBgQ8>;7SdjQEvHojIj0qR(O$r0gIjd2je#K`QFp6AMyVS1A{{=qX5~ zE>cpEne)P{3liOjNL@kZ97NcIU0JxJQsxqKE|3o~OBSMuS-*l+fYpH0Obc_ZgL%}+ zyy;?|-GqzLjTyL^ACqq3x2KKd*i4>Vv63G+*5Nj+$L)ODg8LA|$I*kw_$lK_^x|3c z@iw{*FY%)G6(sRhr0^!cDV)Rr-bEH4BFAx=B^r4N@DoA^JEf9e4Q66cmSMN7!=3C6 zcgX&= zMgu-=EW~Gw<#^0!$K%EpJYm@QtdYc%MjlTYd+<5qLF_Xg#eU-mo;6P3Ipa-y-Z+Wp zjrZ_^@oT(j{1#s@&fq0q5MT67#>>7c9Hd`>H!xKz%h~$ltCm_8IL=4DGO3e!ENYW| zB{H9DK2)=C*YnAb8sj&zKpI%Z7aAYP6>=pj6g??d$(DI=Nf*RPqed3VVy?Yw z%#v&5TD=>{8Ck-;QVGZ@X(U~UUa62KS<26oC;07gnJh;cJ#cC9sU5c77rC~D(d11L z<+MF4D`X`qXnBLIlItj8HSO7rsxz2jqQ(S2LAMUGR+}coeJie+SZp%RTw8G(tNF{# zQdJ*kTs*tz-w0px^IYs(J?m=UY9958wk2$9bH3j$w#HdYXmgj6FSus${slAN@iN@ z`5KKk6#np9%`23Cg3@1Q1O3LB6Q+)wFtvCBP8Rrc{{`|En(yf8dsd8D3fFVp*sP zR7!WEAUAvUzMIDy=B7JMl#KsF+{z>`heS9qkX3r;1w{=Le4BZFQrY=WNem zK7Wgbew*HZ9hLYld;Iq>hp3*%EN$R>BYwba{~@#bN9e;_*ulT|;m3H8Ul<<7PnhFx z^Q+NG*Fn8>7fl~=&@(P#ncDn1r5HDO(o-(hd_3R(4`eV`z@wt7XO7o1CIJn&Px^k)py&*{{6dFC(Zr1zLa@4F7K^W@7e>Z!P$E`ze0 n_FRN2xs&D6AcrM)bJyoOjVX2pb*A*0l4f=?H};UvkbC|QkfU?A literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/DicomDecoder.class b/bin/ij/plugin/DicomDecoder.class new file mode 100644 index 0000000000000000000000000000000000000000..b53558b2a25d819a39f35447aee9d95586bf8151 GIT binary patch literal 17974 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 OfW+#GUy@P$^8Wy*%v+NH literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/DicomDictionary.class b/bin/ij/plugin/DicomDictionary.class new file mode 100644 index 0000000000000000000000000000000000000000..ad48b3c85c1a7707089e1b44f97d16d7640581f3 GIT binary patch literal 37022 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 kK#sAWd`Z3xi;+9XnVk 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/DragAndDrop.class b/bin/ij/plugin/DragAndDrop.class new file mode 100644 index 0000000000000000000000000000000000000000..856d110eab9e1ce66977a9264c2ff0bd3c14ae86 GIT binary patch literal 9430 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 G@&5<6mQfA> literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/EventListener.class b/bin/ij/plugin/EventListener.class new file mode 100644 index 0000000000000000000000000000000000000000..a839e080cf38be2a0871d7cfc1da4bc9a4ef23c1 GIT binary patch literal 3727 zcmb7H>wg?o8GdGybf%l3km)sPg`okuNplel3JI3B*$v&2-EEUJZ9&n=?sPNl?#?

oc1cATXF*(of@t?=`1;;1*eoB%`BXBv!RZ4 zoNXOB#FNRA4nu>*+2p}BHlk{GvgqX9mSt1z&R4zB8Q*KoZ&34-ezEA3a*cTn>+bSO zUbsg?M{HnTL+7NQbED|MMibZKIt|xOdnGqrEiSmhoU>4HX=d8bI)!;B@Wi-2*%>Z+ z6%D=9t@u}kJ3$9QwWMKlY`S&v*)Z@*`H6w~D7ND*I(BI2l2EH9XmC2b3Jj|%~nG?cn`4?|Ndfv@*rVf}jHe#qobMMF9HMqN z_R><`4X6CT%?Ez9MEnYVz{%Kva^Nh7qjGK{iv5V|n4}$<+UAOh1g1za+L^g$zob|) ziUT;PBdMXM#e-xibn_xx_hOm}4E+P{X~hkJZnExCYNbvY9ciN0vY@8-SG_{c4GbL8 zp!G+Q!i=DCZxnNw*KvgDTYJRBQQ>oarMgg&Y!s)Co4616GjgX~c1y%-_`k9C5}@nu z2}la>Ht_)7qrnLM#!WUgpliQ~0Y}4*$wk%yH)o${!fj^--gfBQB28HnxkgI%JK0lh zDKT)O&A>u)vE9ISESm5{INi*J$a2~#x&}@WklAG~%r4S5U4@ITZJMh+X!wr`*8K7W|4L?t>=YF9g0ZaEtzu zyNcjZ+=vgF_>d6pX0^UXq*3g`M@)QF=&^2Ll_aAW#zQ7PE~M6@EVQZ3z$Z1trv1FV z?3Hrp#zDYu;NUCZ| zZi>oHv(A#(@*O%-TPk8g#`-;cU&mP{qp5$1<>9JZbxr&L&(W+`iF=i@L$1v&2ChRW z_{Je^engqsu|#}sz`#$4u^lVS=^)CaE-*_lkzF4kih zrxyRDAu?pk4D<34kAZcD50*)@=&WB2vhI{8-^r`EJ0j9R74aCl_+Q6*bmJO+nF#9e zwa&hWqf!1+wCEZ<2kQksM6ikdIyq@#GoMYh0b8(@BMsZo%U&FQfWHy`ZW*khkIiPg zg>A^Xt%k9dwLaPdop#c#!ILH|K5eA+P13^_Wu@+c_V5neLE9Sc#JKXMt?(k9wDRWm z=hy@PkUivFKcA1Woo9QQ?eQ8WR!!PoN)XxT}Vnae>9+CB*Ow zKT%%8Zu}WTc!M?Y@3inw+$v!nBM%5*6XrMZX*^1JN4Wnn0v@6D#?u)L;4_@-z~ek~ zCyblM#xBx-rw)yTy@htS_)51D|f9)gn(+9=RPm`JE%^sL0aayhT60gfG*9hOb=4(O0j=(KsDVT*1-T zFXQMNSL5hzI@)^$N8h@Pqijec#+g|l#oyY8*2EjVDS$Hi+}t)fmh^I zmEnN2rz*pSdMX4__%~1YKFJRJ6wlX9&eYT1C)56div5`N^JcPl3-hY*sBBNO$fVjr z?rUbd55KIJh83Js_O%|`7o}mzs-<_R>VPE2qOOU;6Jr};8)F-1+sig(oll>|_13T1 zZ?j%xZ&@#~H?5c1ud{xyRQ;+5_Ekg$PX!QxAnPC538}M`)XSWLW{|ZNILv5fsJ>a; zMwQ%wBiKXr#`%qTkVPuRugWTgAecO;qHf-839m~g^Cr~qqeP?w75t^C!U3%R7lw## cm-rW%Z4;o5UDV^8(4CMVul`p$h~RJk0ct2rfB*mh literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/FFT.class b/bin/ij/plugin/FFT.class new file mode 100644 index 0000000000000000000000000000000000000000..30786c2b28dcda8489a95ae0620690f1feccbb24 GIT binary patch literal 16815 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 Cx}V4Z literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/FFTMath.class b/bin/ij/plugin/FFTMath.class new file mode 100644 index 0000000000000000000000000000000000000000..0007d6afcb4143c0426fcd1cff15c1a8f6bff06a GIT binary patch literal 5460 zcmai233yc175;B#c`uVk5(a_{1Q-xCAt3`%P$r6EVj!9XG$CoAS{^em$-~LKapnz+ z)}<|N)!J%T3+{GF7rF>_OkJp23*Gmcro?M~9t zu;4ha6cy94kiy%YY}QJduBGE-?x*3~oz!-YPEiPTSc%fOk`6e{Kn-f8*Jxk{bm=u2 zn299*5WZ+>j6mF+jT7L9TeW@^C9+A=Op2G90%4#9^r z478%2db^{x_*3jW{)lO;FPo>|>KaaTP~$*|;^PL|a5nYNjXFCzY%}GIc>dTVx~(=a z8xiT9XJ8H@vf+FK?dYHhNk>qmu%O|12OmEY$NJ1HnNI6-&BWzBX2!E5gmt)30|6vp z8|0}wJ9^ACPbq8QB6QQ;BbM9M(QW5kn%K~|xhsTTY|yZt$1W#&p@ECVd@b#Grz;%$ z4D@3o&qnsjkrVh0U5&@96v7~eG`vQ^n2Zbqo3OdKbQ-e)9hRK_u7ZtDRAp2FG zhPmiiKIcZaT2V8akv&moINBP`*;iW8hSn7;ckmXyu`MUd#5;im%^ec zB%k2&b-2pF>v1)4Kre1g+gIeRUUQ5Gbn2AbPe=_Ku2q;xZ;#~d*jg)XW$i=}uOVE6 zH%Kg;rQ=OB%5&@TcvR-!Y~U?;E04}hCfi3HJ3)c*DFjZDi%xV$&}bNhgtujalh@ zt_;$aQFknL^6;>`bc{F@d3z1qhO?+R<*l<)8JW97vO>M|?lN$<Yw%0m4*oGZ>k*nRPrO2363@{7iclHb)gB47yno<|m+|O~c1E)arPE zJQ$%8(4RH%AReL=>aZaz_lrv$zZwvW7bN*Tq7a!B_MVnxBRRmyN^1C=pz%?K`Ce)) zr-$R|+T`#aXWEu)gbMKbi@Hv9IOUj3FTHrez!&6Lyu{4Z>E?5y`e6fK#Fv=5imDf7 zD$HZ}{!f+KM5nJ9_^SBb@RAG@Qy(*oj;|}!7u|d`54odOG{?~Na*&R1GDY{70UEUh zW6Oo%w+(zp@^9rN^lriT418bot4!uIDLdg^mOng^L#vJ-E6izk#xf~uXOvgxw)_Z* zsYJ@PGpSu7;YkBO#m|_+k1qF=R=uP%$zGmo+i?z%2_3&sIH}XgXKiNHq%&sP>Erl6 zZQvQN@)-8D6z+Ys;v>cZ%F0bhL8>c$#Tqb4iD!N{X|6$;tLR_7- za+cd;r`M#DJtpIB5HASwUQ}2%MY{Mu01+%6Aw%!dismvb1haW&qb+#Zz$MxfMXH( zDtA~Z0HtslR8qeVPB9e5EHjtSTCquMZ6Q^orfaH}xq{%Cg2GT? ziIHHAg%=ei`AkFAsac$|Xh@3rX08|0Q|xVP+$4Ik%+WDTWV>3CHGPU1CeXY$)}T`PB$5oTYQk71dG_EEE z>u%D9q+an_Rx4i1D%@*XgL^HjZnhOt;d(9WTebmG%|$&W&f{nU{QLrogO6bTkS`qV z8}f%2^$i8Wi~A1XRNnzK_zz%7xcLBD?jv0Vmhl_*c2%>kpHBV4n9mo$a&J}MMy%OO z++%E6Um>vM0i1qpd(fM#^0pX70<6GF5-OaDv-lO&hUlUSwlkL$uxbx#mMr-U&f)Le z0@esGpQlPSThLIAX3n62qQItmvZaNHo z0zJNczH@x5$FZVmUdse7iTfk|Lzo-!7cd~7*WQh4Z!m$^?M0|*o^Sm)g1al!Zl9Xh zbnu>6zEZyzM%W6EOu+W-tA9AuTTl1K{l1j}-^$=P&S|O(wCn|Y%a+y!Ww4&DCrXQG zhfo{w*99Y50b_mR2+1IPMc?kA+8t1J0TJ-{e&m)OyayT&D7PQ^azDQx|6A2V>yL|54gMe?J^zNZ1rM98e=B520b&z(PFj}&dqQ%Vnd6Mksi~V!``;MOH zLB!avC&1N3m55K(5%vMDe}Y^-i#SV=Hhi66_yNwr(^!q?aV}H-d1^Y= zsJS>_oq~4N%;(^8T%cB=Q?+5OYR5Wt5npzFxKM4zMJmD9oP!>96{{M-Uxr!}P$m5N z)h>JtA1B;uR2uteRRzPQjT-30ZawYzBysD$o&|`T8}OO8y&ch zJA$;P-rJ#3-`lVs2MAz6_Bu>If@vB)^Ai7uLmk0l4THfKQBm`^y2eF)V@x5WKR3;6P0RF%T# zKC&Dj;lN$q)YlkM_eBDS8D8I@JkowE5UGBgqKkMB(9Ak8vc2C{FIDq!@n$#e;wCUTxTroL2LL~Vb3W2 zefW>1-k-yNE%pBX5C(@BJrVlhUvYgLvGBi3OE1MM%Zrr-1#dl~`R`Z0h#oGGp$aw? zR488AEZ*khf49sg&Lf<`cz5JyKOrCq;DGTP6Lu(w|>5 zp%xy?nlIyA+I35xK8rgk8$Kc^UdxBn`o2Z!1xlB)To^ zAToDi6mR0pjo5~_G2U*$7~geiMp*`T;tEDtme_PLfjk}|^`qE@Cvg>?;w$-CT#e^& zEu-x^ynr|02;a2|yHzEhvIeeak#d7N2{)>GK0g=YCUq(wo=td%T85j|YTTmM;GL?I zkIWvtTV00ts3hL2#&E03@wK=Md(@5Ct3HU^)HrTepTr&Ne%z@Z#9iuhxLbW5_jply zuUw<*iSjMMo?Mg+3-;uytY8N6sZ%K1hZXAPQqQLzQ>Ssxk7`v=4XTmRcb?jU8$1 zJhYk;zEK3d@Ev5llKU174}*2<%t_PUY;oZT=8>STIy^1H7@yCTni}H>&gAB%moQsX VHT+w{pDd~x{s-n6ghPc|@gEjVAZGvo literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/FITS_Reader.class b/bin/ij/plugin/FITS_Reader.class new file mode 100644 index 0000000000000000000000000000000000000000..d39270407244e1d2bf1af762b7c1e3df8d2d005e GIT binary patch literal 3088 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 WlDFf5e1FZ^hJVGDd+q3V-~R(l6)Cj< literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/FileInfoVirtualStack.class b/bin/ij/plugin/FileInfoVirtualStack.class new file mode 100644 index 0000000000000000000000000000000000000000..ebb75a5ae7783bd6f7e7599da7c3ebbc2cf6cdab GIT binary patch literal 9294 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 j!V={Y{y&8GcWj6$%B4%6`(H^oKO5Nm{7) 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 EAE#&VUjP6A literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/FolderOpener.class b/bin/ij/plugin/FolderOpener.class new file mode 100644 index 0000000000000000000000000000000000000000..36bf4e0b6c8e36fed96bd4599e09dd694e4073a2 GIT binary patch literal 23605 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 U8Wjv&LERN-Ax8cQEmYvle*&;cZ2$lO literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/GaussianBlur3D.class b/bin/ij/plugin/GaussianBlur3D.class new file mode 100644 index 0000000000000000000000000000000000000000..35562d2d212346389230e0e86da81e6de2f94388 GIT binary patch literal 4549 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 Obxd>hZ>lcBvDX1uQ~GHD literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/GelAnalyzer.class b/bin/ij/plugin/GelAnalyzer.class new file mode 100644 index 0000000000000000000000000000000000000000..5aa2e72fe5d7a6a51d88c61a2240096de39709d3 GIT binary patch literal 13659 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 MHnPTB%mlW703VH7xc~qF literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/GifWriter.class b/bin/ij/plugin/GifWriter.class new file mode 100644 index 0000000000000000000000000000000000000000..5a9aac8c49e5584a6a7a32059db9ba31ca9863d7 GIT binary patch literal 6351 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 eIw(mt@JF#uQk}WB!WSu43kSTWHJ+HCT!wH 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/GroupedZProjector.class b/bin/ij/plugin/GroupedZProjector.class new file mode 100644 index 0000000000000000000000000000000000000000..abc8457128227012d47a81f453fb297e11dbea9d GIT binary patch literal 3512 zcmZ`*340UQ6+L5(G$VOT9AViAWKbFw$$Lz2QyH9qjX{YQ8dJB}O_Q-S$YW%UGLp>F zbj1x_N!zrvCN#t(EnR2|1Y{ao+O$pDEMLF;gZ`3~o;M?7W4r#WnYZ2h?mg!&^TB`L zz60Pbfzo>gvi z^Ip3|3q!GiC1*y8wwcRx$43JQW0Qi761-HBd{RRMjkG#r6qD0>hQ`#!;zGYOqql|b z0Jh*E1yKpw@^uZL!oysnAh{;&A04lGJz5a5!m7B2CN$G7!?rD3!j3iUU)!TCXw}e$c41)Q;K{sUI|exfj460TLfsNz zON0l|f%PgLl>jm9(om0v0Ev1`!)K6?5GYJq$NNk@Yh@%n7`q>p_?X~yw+25{A=IPc zaXcZR#)*Z+TO>qR$R(3&z`IXFFZviF1hZGqniIBOG%b?2(IsQr&Y{2|ro-qK0Si zB?-#l{=)}{`$ip1B73&h9Y{Sxht3sbH{D4&+!x)Y6a_n?gIiu!|#EGi3;+wuXY((VQJC ze{Z9tVFrY?K5Z>UaV3eIXpm!Lu3aZJoDyj(d|Q6NlQyZiYMeDGn0E`Ck#;b2v$N_H zNvRijSE}G*oLV-~u02)JW!b}}V!l+AP`$WBfEWa(f)^Bg%T1_E$!tGhOqK=J1Mw^Y=9kx6W#~fL}01XY_2z7-ooVUw*6GSi0k@y$RqP&Zu~eNJqu^l7`pu z2AhqZ&l@>f-@07SVohJAr(z&0#5w#*sJJX4!npju2UYxrSGI`rzteC9zh^@zTCRR_ z%^I{41L~f|A2eJQ2_)h_nd76N;ySast*uRL-_URq4Xh(1VbCZP*j>c(f~c<5V)RE1 zZ;Ps3eOxc}>baDWHPT}JPa6I#T1$}8J!G6Hx>1tMrHvC}`!5>)ig(=L7u#+t<-M_Z zpNhZJ_Qj}H@velJZ{V z+KC1}d7l*J{XVWu-NJ^!mNGW?G|I1`I=D5s{T5=^xY9C*+S}+jvVhLu&N6mgYvSUk z&qLYQ%Hcj5JHRiKydYkLyLnL;8cK zF+v|@9K~UL&OuwpD6#fXPaL1;noLb$d;v#@!5A&6gZv5V6>O2#tDj&O2MX_#M0c;C zb%7@XJb5Yd3aTU5F@9$rUg>Ic8P6tUJ~uaJyS&YEW5`=ZN`Th9fK0*{_C?B=3j64O zF6?V8<9TOXY!1ubwy^BuPidn>*jL7hgc4SSKGoahzl<$mIplAXFQO)_hWsJ#@MZYL zNFJW`OCf()zB7OKKVc=TQjtnUs;fx3dJpBN=@;-Y+1^Tow{fzmS`d zKr?-8K|flFQ9B!L2hn|mD0Z@A?Zj8G3#U0hi^p)0Ni4$fsEfos#4kBGi$J{OM0k}m z5;5-KnX~vZULfKTyhv>-acn~oU&YtR+cVgYuaggjclO{LJf|W<3%|*?-(hAee42C@ z;|M5_1VMb9=C8xy=rk`4dc}SGc&jk=CT~!<^08oL&uz>e32t4$i$kr!Z<99XzoxT{ z@3scdM$UN3_+juT30dy)db)g<(GdxG^XIXB)+gP@xg%lu7Jj~fSA!SJm>1OkGWctb z%J^+Up7ly0Z{&3ZLcZF-{N49kYXcstd$aXE!E5gEGTv$}qs-r}IcE434sJ8eoA|zS z_9nVTvdHtO6L0F&B4UuAgBJW-@E=S^&lUL!eR8vBjM)L~p`Sg>pU0UCPms~QWN{x^ z>m_4-*iB@g<%i+~=N6JU&Uk(aPvK<@;5@(eE?|gFVwi>h5Z+}re8jBzH+dBN9(3`$ z$M44#e!ck6$sf3o4Eh`fH$%Bglava73T73| UNhB^_rM1LVvi1S*tip%?14V2tMgRZ+ literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Histogram.class b/bin/ij/plugin/Histogram.class new file mode 100644 index 0000000000000000000000000000000000000000..fc5d21a4e4a0e9a4e16e10d14f6a467d8795816c GIT binary patch literal 7082 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Hotkeys.class b/bin/ij/plugin/Hotkeys.class new file mode 100644 index 0000000000000000000000000000000000000000..6a2b119bc2828d9765e447d9279992a81f0a3090 GIT binary patch literal 6996 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/HyperStackConverter.class b/bin/ij/plugin/HyperStackConverter.class new file mode 100644 index 0000000000000000000000000000000000000000..cc00a79ecfc2ac6cfffecb47b249af16ab8c37e6 GIT binary patch literal 9929 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/HyperStackReducer.class b/bin/ij/plugin/HyperStackReducer.class new file mode 100644 index 0000000000000000000000000000000000000000..8e865b9d61175b3b86d7ef267e448723ce1cc5b0 GIT binary patch literal 8265 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/JavaScriptEvaluator.class b/bin/ij/plugin/JavaScriptEvaluator.class new file mode 100644 index 0000000000000000000000000000000000000000..ceffd21de2e59365dd93d34431d6add3e8635696 GIT binary patch literal 3532 zcma)9TXz%J75+w+Em@uzA_6!jwg(bGl4S`uiEHAJ7K|ZwB*#I-6nbMU5B4~t8D%a; zkfv#yCcRU7O`&aiFTEGHv8=#aU8^riSD*UO{Exo2tL^G{X2!N0lvN%yGw1Ac_T}5( z-skAA|Go7m0FU5S3=s)^`cl4Xcni9jpHi3AQrXt4?!>Zcc&cmJF+?S7U!ovysOCcc z>A5AX?8eZ++0BhJ+&^i?5SNhLTzkqhO?A%DBy_lowx-TgTG$ky_UFbZb$pv!r6t$a z%>^N~HODjDtEfE6xtuxP2GJ$ zLR%_5Dp%sl+oC3fBz>+E5>hg{ zAjgo|q9s5)jx6!obT5H4aw6QkgmklG>-igN<*cwWETa<%vHxM2e0o$u*MjEG*t%uw z?j;}7$pqey!!e9X=-y0yQFRw(JO%+sMXfQ7smaMS*QRkq#uNAeH95_iqN&OBtPmfS zaZKc;-Emc$q`0T#s^#a3YBfy6XmbD|vMt=db1K1jPRQsJ%G*?%EQ$(BNjT6#WBShh z0*|L;Y{$Lgt0-d|WbyU1jC;@>!!r_gZvqf_cFfa_dCg8>20h|&TE?>=3p>gJEKgm~j%dzE0yfyW zST{}mtaQn7wTcW6$JaS$RW;kaByx9AMiM(>Ae9x7yOvF)sG2muB-YRq$0t}?6XV6B zL7NeipOo9O}xA~xl8D|bRSv{O6_G}~Xh ztlCPsrW|2dJ{xj^PXQgnnUue-T+4+WKHQxW9XG3(+C^nkWe`bL5dmeDVTwJ&;Qkl8 z@FjdDhA)$!@0my$PvXk|ulKMF6S#`6$M7`?JA}sIbY!hq7sat4;~Sz16O5*)ISyfr z<6F%5-9R}}uBa2@RC`PjXTfVSzJqJj<##;ijP8p1X+@oLV)=Jvd{1=Z4uf=X7sb@; zGJb#`O6YXFImbU%CF~Ln_l{0Niv3u^$X$84enXh3m9?tiQXD^JbMQSEo#^M{NFfJj z@C(t_e)+C$wuSEjdco9sH;d!fEa9SBwk@Tsnq|$<=0!BWmGL|Lo;{#U#;CfQ9qViXZK7mN`Cu{gwcKb*utm6ieDT>kS7|14n zRmX3xV;42SL+rZ5g!oC+LP+bM?bW z@rHQ_kMh%S`qaOyM;=g%uR=XRiJW zrtvp+rN5)(CnrOwfp@~>9Ae7;h&PxF6Bn?GI=>vKSo7&4XgYw-z5mAk7;68*y`>ng z{zqK2uQG!>_VxdhS(C&Kj89Qs&-$DL`U%6`FiBK4FzY)O6m5U#m@v)%YkNYfG&qdl G&Hn+8Xl*qB literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/JpegWriter.class b/bin/ij/plugin/JpegWriter.class new file mode 100644 index 0000000000000000000000000000000000000000..af0287b6726e72039f67fe01881b170319afa637 GIT binary patch literal 7391 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/LUT_Editor.class b/bin/ij/plugin/LUT_Editor.class new file mode 100644 index 0000000000000000000000000000000000000000..d8b2fd4c5424b9df59846cad618e9bcb5f6f6443 GIT binary patch literal 3678 zcma)9Yg`o975`s$VU}S;STwRph-)GWNPw7#3ehN!APS105TJPsyW=`!nc2+Fg3{JB zeI;p9`+g*8V_IA3D{V>QLZwaLNt>qcXZlXR_G>@%Lz?z~XBKu*KNS6OXU@6z%>SJ8 zKj$7^dGpeX0B*xS6ch=pGbfu<$xOnun){9)ySF=Ty0)VrC{Qt}PwUM|-AXhcnV2+U zu7VOu^ZN&Qtk+Ty5~%U^>N9S$F>P3G^ZuA?+E$;Lb`8sL1Onz%N}#-thkK{=gu$cf zb^+B+8CFNeb!|(avTwzRFOnP5`n0jUL350>Ih%hqt!cw?m$holPTJ0ZZW&2|Yvh-5 zZgsB6X)rWlx?M)fJt-i11xju=Ez{jgfOYl50>Mr@Zm0;LTEjZ57g*b8TE^kb)P&(2 z(B(W;G39$s7<5mX>D6D&2a{h7I2k&)zRvg13+a&Sm{y{_epp2mH!9fV zhjzrY;`U6xZZSxXhMTaNAQFb#nQC2;LvPscL8xE-}@H=0QsQ96-U(SkM&?YNz`X=lK(V?<|@ zNj1LfDPNLf)3Ul<#a`@FaECxd65y(vH0+nDEhn1{x8Jn(Tk(GVl>B6;hAydElS##O z*Px=y(PtFgxvEU^d{WVk1Cl9ZQf1xhjMNWHcMoZ(K&9N*ui>x^i%NI;_0$lF9Krx` z%A`$Y(r)x5sUF?FqczGrJEoB?#UPF;7!s(?llg?BPZ`aFM$C5Nk|4)H+Lk!pJ1IS; z;4U9*B4al9$l^0&U8bJ26Dme9Cc)mI;4O52rP#cZQE-ny)rzdgG2?yuX*=VpxEuG$ zW4eG+*UKMC*>S|AEG}?ejx4hF{S!IzlG+MRki9EL{YnjiwWk~*8Jn7#R2UeOfx2Hq z88rDirNM&Dfa>x1EV@d176ZlBm2sa*wyo14rVOZoHDvk#dzUz z9AG`@-dr$XI45jpO2Xe*!1}_9?v`VO$M;w9GX+0gHhlSr;qBA#bNqtAAwP%gj1wd5 z)$z`yT{8Yx8h$MqAM~$7f!}KQ9@b0M?=}2Es@P8DoFPSj)bJ-y|E8CB^ zvfFqoFBxy;-O4pcD;!_=`x?GAa}`{R8y2u(o)1Nc@OOzPT!U-*toa*o9j@maFIn8c z-#lFOA3`Xj=~DM-W#QWJ)SsK~PER2Pp>rtbhTU20 z;nLBtgznMC1@v6RK^{1mMPH@BIq%RBdFVMD-5YowM+2?FKx=WJwPXn+qv6^GjJ7FD z7$052-J?n{5L&>!3z%pN(V#IJPAtH@h)G(UY{()R3CX7{QX{_F_0-c8POqq^^XgNc zdY0Xh{Wed1hUzmb>N8R;@hspSZKaV?Di2Cgq%;!pRwYtep)TQ}(F!#X2rS@T zS-d-79TVJff@7hLZJ0$P&T=GtigwSFIM1>{UBWFaknMPtW8G`~v*C5@ zz(3K7e`6@&+TB@s}|HZ$cDi8{ura8N?2D6{mZ$8lye0RQPM{T`U{Fn%{eIn zu(A&*Jj3HZ#w$7VOEfMIAJ~?~Px>24j>!vH<8$PfbGTJXrY}Gx8Gkc}&GL12WyA00 z5SEfNa)U?8*YYfB@S4Z~fga^FGf0jMkq^h1|KrT_Fm`bu*@wH(gHaD~)W@-$73S*C i_zO`MqZ5C{--uH>QHJ0f*q|U#URh4%-zh4>>;D5dIf_sK literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/LZWEncoder.class b/bin/ij/plugin/LZWEncoder.class new file mode 100644 index 0000000000000000000000000000000000000000..5f923c3bb9edb66905c9830c75859851b21d9a22 GIT binary patch literal 4123 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 bp5_!(H<>kaYiG@>4$Q6Q^m7ze;cNc^7xfgl literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/LZWEncoder2.class b/bin/ij/plugin/LZWEncoder2.class new file mode 100644 index 0000000000000000000000000000000000000000..b14ee193504c58f93dc0af069874725de149104f GIT binary patch literal 4125 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 bl%+~2pvF+K8k=UOBVdpO^(6pS>fiql0TG(4 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Macro_Runner.class b/bin/ij/plugin/Macro_Runner.class new file mode 100644 index 0000000000000000000000000000000000000000..482fd9abc8af7dd291af869a5e8254f49513ec24 GIT binary patch literal 10586 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 VB4E9lmcM7ydkl710Uc>o{vT(|@-qMc literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/MeasurementsWriter.class b/bin/ij/plugin/MeasurementsWriter.class new file mode 100644 index 0000000000000000000000000000000000000000..e0eea1fedee5eb1dd556356e6f2280c8c28399bc GIT binary patch literal 2076 zcmZuyT~`}b6x|mRCQJt?p_C$pLW`8}6_siW)FLgQ)Bv#{2-e~dE@5bxq>~BIT0b6q z^sP_u8+cLI3R!e5m%h}s{71T~%YA3USHlC7nRCy_KKtx@`0Kxe-vHde4;sn@E?eu- zOe&YO?C7L4vpGj@NZZZMJC-XQ4FQ4k>*l5zO__EwI=#Fu6Rw5|T94!644<$CDsEf0 z<=zn}?~Kd|1n#9*q>gf&GoT?PP!qFkIhEU3md>oXoRR|JSUO>*=1j*@-$i46c5)~(r$eC5zy;J3N7meAtuq}Ld7LrbH3g+4jgCe% zX?V+TcHXjA(p!_JZ6>8-;4+#8s*}k>WuF%e2T)FK= zXZi4*Lug|Uv2;?$HN35%o#{LXN1+Bfa9tq8BD0pO__}bI-kV0TTOAQ}Yv?*PDgVqs z4|+Km`6Oqi$muB#{9z777Im~BYTzBbD`2Q%#27binNk4}HHIVTo_R<|Ki=1HL!h?g z(7*?Z^a@4#NLI%!3~Cq<2-0)nzJVd!rV^~|1DVaGs56gMPcp=D3sD`z7}4;NKm#K- zd`U)&-kw4X+{HcSAkvd&fhHg9Suun>j4=b__*eZH)L1DP z9ZRrOL$&Jdv4IpeC>t}Ak@ku}@5wBO*SaK25h16$gtUQ-cU|1H@2^H)N8zoiYsj(q z(MPw78>GzL;+;Ara9@w!S%xd*)R`}ShB>Z4qdGcMYIMl5gRpWqr2}RStdenosu|Zz zJf1W&#g(Veq;pO}s&Q}*rQ5DgjRtL8YUTV3a($~`E>%7&`4#0a<%eL`KJ=gYQU-(1 z3eTvcitmQ+fNIq6i*%rtPZc~&?=t>sUEO(v=a0j6&s@bc)n1B=zFRScQu{?TFbO$C zy`N0{N^})Wc2K~j$(~ne@A(5QJ7^BK_T+IjkIr4x_WX{Psop&LSns{P0&ec0zJL#> zdSBtkUS-?G^K#nnD2q|o#};tSyi zDq?mY3%<+4!C+(XPZjnOOA9KO5&kn6Lo=^Q3+8YICOs3l3X6+9%N_n4*YOiN@q*F2=)y1D z$_4a!oIdnMaB#%wL6Oq{@^Bbk#$1(fkt3#ldTnBhZ(}^!##6po(7n?DD|Fx)z9HHd u#PFQ>g7}vB+fex$5e?TGUZYyW@Oe@5FFG{DHAMbK!#{Kr_>PWc`2K%$IPh)& literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Memory.class b/bin/ij/plugin/Memory.class new file mode 100644 index 0000000000000000000000000000000000000000..cea6090e65487210e6e6939008f1e16a3aee3202 GIT binary patch literal 7008 zcma)B3wRvWb^gyjw6m+RkR{7zEdyhlha_7PeqvT&Y)Q6}v1Eb##(+nwku>(c*qyay zk`w}=kQYgyA(5LBAQVv^Z4Fpf<2*}4D19Yu(ictB(xfzL+O$buv?lI&V0m43Co zcV_NA=iGD8J?B5?Ucdgc&wmNPYPl? zBkkJ<4?1ZthN|;u`^nOi6G%x=x9uGDfLC;LBOQV&H#g*rtsq6u^v6nm3=3)kayQ?; z+08g4tsdsH*DaOENs@g6v*eC>M>pro#ZK2F`*m*4_0|i*P0hOmk*@rZlRy{^Cg!3} zaM3n5=k%4cgHCaWJ(zJ+rF`1X?6Qllepfmp-l*%V`|c%Ehj6jX)ig9+xRmBy2?#DR zaj8xfa7^IK$Lp})B;LHVXcA%?>28|+06_&MFZ`ESio&1upBGY9*wHSDif=* zhNEt2?P}kKG~GS}*9eTJwX0hPU9Z`|wfePcrT?Y;9VV{B^|ZHLC^)$xLF;VxxWGge zbvjhU4JI~VBjtMez!e1Znwl?Q>4>3=Bec>;*=^tAy71xn&leyL);rRH1p7fj+_9e!FOA zGETv$(2j(&XDW1je6P4(qNlZU=B^*=Ztwlc>7lG)(tvp;x4>L#m(4~NE6Mi-0w5- z3o5my!rhtAXB<1HL-&|?zYfiDbBFDWtKLK0FmNCDD~Kfn_X{rDG3r=3f6Q`no>O#& zEUIW#xY{(z9x(ABK1khlNxe>~L^Cn8XW5w9yiWuC!zMnWMv1G0O~qoqsB#}N@iA4= za7(>*di%g0?fis^hqW{AmUa_c^G8Yseo1gmU*2<47OkM@K_7N1;bLXntm|1`-m)z& zwCp5C=-yliY3!Vj7msSZe@d{3X1hSc>8Lg^PG{@wRPV8^04=Q2(URw669#@+aCO!` zYNhRR$+3FWQMT%w!?w$S%B#N>y*5jY>A4;qKR-;aU!jYtrkm(H>$evaK}+i^Kz1dG z{W!FS0z5GASt1dA-U_g{q;i*3sHeefiFIVu$yF5K3{P+$+>%99K_-2>dS4m0F9 zAvLkKGRyfp`8g9$;a8aeX6)o{*BcEy^8dZ3`{LZw2@^3G`u1xks*y-97!`Jtl?abL%&h+V^BIuZfIfmaBTt95K&kKZw2Vom~I#jBdEzE1qDh@P$ons>$UyMp>@Dg7)cyxm2Yo}0jH_&sI& zeL?aZ+srvSH2z=1ADZ|h1)yj_xwYq;CjM9>Et<~cOROHgg+Gnq+cW0r*}iEk?fAZE z;?FdPDcV<}TCMUICjJs{5G9;D%XWtFeqOaOZIpeQ1sk;aYmKh&zAK4NlU-o=bK&;4 zoVh1AoNp^++>&SD@ATUbthV&*hI!7!6|H|A!#0U`Rh?^l4*oPd;VY*9umOZQ}RFSPg>iANep*AYcT8V&K z_Sd%~Kj5(Zi~7jHwTv*m&)+s%(a^!Fken+?>`zMHC2Xn{$yQ=_sI-d0hx6RFg@Ns7 zaZpML6_|4x&Qi>9J`M`e{oL3Lhv8V%QS_!7IXg?TaJIx+HZn@@W2zbO?DU~tyWkh1 z1amP=iA#c!PZ!%*U_n5t`O|e0t1ou62mKv}m@Er48rsqsyHsi`*xqPDYNR$MOxiOl z^#LE0O_?k7^J+@&FXjtQ(L4IC_zkdUw)iz<9#_&v1={KHGT)R1a za(WC|$Uy4KGfn2aQu{gQWr~wUtaRm)pToM9M&&QY8T+{`PVP@-2y5s^&v{7T4(jef z3b-K6wON~4-z0c`ap=3l`C`^D%a_x)SYaunrW-d$N6TdVAuGqcS+Z;|VI?g7`M*n@ z3THcsiKcS7i5gsF$d!UM>h%>(b>rONKwvcmeQL{Qzi{Au)^MJ6UmJxO(nO%FkT9xM zo0+36-14q+u}FIcQIxU_Sx#edU;3rZly(IulY1P<=yr>7y44C(R%#^|A94oEBfUH( z8M2zEAs@)}aL0JkG9ktEzgX6qa*cvym{oU7co;CJF;@>}30a2~Y~(squGhk&${8ab zm9*;yQ#R=7r7Gj(M!eCObWWdADpA#wJB#`W*@#!!$R<-Z%N7QOW_Rv+)2*G%Ky)4+ z5aT@q+t;mIbL|}~)bDO0#>+OvcqO?2Diz}?^qYokr}C}YF+*6ZM_Tt}$CP`(lpV5@ z%F$Pf8yQ$y6?W;!ZdH3nLiWn7F+FAmImSQbc9GUj6PHZcFSn`7m%7~%J$y!*x|;VT zCLu-fVtQhq z>uO3DGA&eM)g&-Dk@SeuH)VwM|W{-UVj zNtUIMf1+je;Xhdr`A=2>{*!U!KWqJGjC?%Ov#n=uJD)sWA=+{h^N#Z+g!yc%{LVNQ z@ZAgua50knBDkZGtukNFXNb>e%On<^V@~*;bGZ%kd;#+VqKW{L<1%Wjv+}$jh>A9F z6((nYWD3?Eio0?inzIjH*LV!E$OIzc>-jc^{mX*>Q&_&YuI(hQp2Es;7LbMlBkROe3OP?GOCZSEXLD zuDiGH15^0L<1S?cPF{qC_cB=T#}a%BOYs;lt3HP- z@I0ws##Q(tFQ~qTW_%kh_zsrgdn{?ck2d_6Vfk;kT0&SM39OX4SS1a-PF%zs(TcUQ zf|pF|!5qQsr5llwThJl9ah>ev&617vGR%IL;~Bo^af1}lDevQL&;#g}$FNCW;nmMK zutnbBZO@zNk+-l_euSIkr`Q&%M{j5ewujcBKa|4Fp&q_(Ltm&5w}f_LAhZiRL;J8R zl*8^&5qm-p^G-%l;(3A!!?zOTHKF_Q6T%QPaOgewDS;=1u24T$WzdGbq0RVr{0Hs# zvV0f+NxG=CQtSWXTa~PkLA*`679~f~66RZini~?~TQ&8*R-$|}L}BwuG@izM*&wG;BLgukKSOoHghxsW z@iT_!&R`d!eh)ulyxCxXr{7N$o7qn`?Ux0oaXBT{)iflksypQ!8lBx#xYg&n3C0XA z@yFPF>m4j$OOo2eMz(Kb&1p0$qv#c80Fn3s?WGCswke6difH_W_y#{Rw$w}Yl+5XC z{knFx)=T}QG^~v@L_&}I9pQ$Eae%Sf*P?#j8iZ`UBo#u7?Yu9&gQh<~_qOT2gLKn0)?o-Y!@&*=<5rCLj?h9isbs|w61>&> ziCoGhETE$*?uW^}Of1Tb_}fs2sPPWwMY!D4sFk;Ig>DY7Y%0q>O?hGVyq8c}LH~ML z+}rZ9EFH&2-vW)gu;tSWyCPUv?*uOOL;m9PMz5-jwuDc}(#FTI_`K0(jy5WAGGVB7 zdh70`TtdLKRo41B^$@ks@-4&gzY`4%^hRE>t%8SZQAQuH)dnzz-8jlGUZL@?hKLoc za2+cv4>I}NpKd4FP z3r4Eylxz&ZT)I-RWGq<~CfZB)ZaUL*d;PC^x$&fIJ%Oi_u}SG|p@66BrH?*%0#p2$ zq8rYw^!A_EOK(+TrHLGvlv`@|JdWCyz5I+FAnybdzJ65chYFZFfoJ&8-_w(_D>B3g z)_0|PCURgOhbd)B_8dF&hTl&XrKj&}uBn-l+dno>hFtyWI;RCaiIo*{<*yxi97`7a zG!sasqH11EN6c}Wj%dO73yWVMp6$nLa)_w)x{UhIAZ&`v0pQK(Qryk@=pOX&Ds%w% zVh`?PUbvrJ4=`9Br0aZ;?(rdf03T*<_y|6OkK$Q8gs<>k@^yTinE44J-ovsKkH~7? zLU!Ym#KlKtl(&h8@M+2N&hTy=llPNTooj#K8WrcFlaNx2tfj1|UA|FYxyld4Foq>0 z2kF95YH^ynXISqY<^4idazx$L{C|xAb+h1El^G=JJLU$MHH1k<=|Z-P&M@v{L^Hng zUqoV3h{;AEerY^*)fsF7BYJv}FEZRfZTO!xfiq(TH4^F_o02;x<%r_W%+m7-e^Ei{ O8LKQxWK5~#=>G!z7q*T7 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/MontageMaker.class b/bin/ij/plugin/MontageMaker.class new file mode 100644 index 0000000000000000000000000000000000000000..ae304719f7b9daadf46bc0a520bc736e7b72adc7 GIT binary patch literal 9902 zcma)C34EMYwf~>VWM(qC+507#ED)fzZQ2q*7%-)!G^Hijl-jVh2y~K6lW8Y2VP--L z$U`bnWK$I6rL3iux&e}wNwI<=i!34_n+pi2JP{R9L3mKf`=9&GB_f8)F=m+-$INT$Ccm&n4)hz?@!QNP+r6ZZhM0%qgku}kjA6`NE>c}~fmUtx5 z+j3&p>S%Yy42AAj>i(Q%v{PiZTcw`xGmWeErMV_gIWZ*z~WdUx@54wE1Ehf(xn1dE>2SNQzEID zzULZ!M_V>Q6yP9(15i&^nZ6iRQM-6tXA`wNn@gq!sX%Ai;&EMfW>T?4@9gQP1Th^m z{AhM{erhbylU&;oNzg_qgPE8`$$F!iw!u^?n#i>Gvz;TSP1nWj8_koR9YhNb_2UqM znb>Y{7(UH5GMJ!BDxf`pBLt3gDzW= zSFTlpuJ?I^Gn7Ms9IU@<<5>nPwXuke$46tmeVHJZqf6a|(NQ?9o&0oM)L>P9vQ?Ni z_8G+TjaFUSxW*uker}gSE!EA&l5{8^IptU-4F+(wD@V>rD(NKF-y&->uB*)UBP|GV z69cIv&6B1|7o$K5E(MWnQ6>XW&XTAK31{YzyyU~+k8I99P z`Hfl-%LSHd?PA@_C4$KxBcK(`nldNU27}A+WeP<#Pgqz`KTS8ZFPT&;f-4NJ#8tFU zKXEk|{B$}y%iX?4scZGG7<^S>qj+$jCz6TUZk6_H247dh7g8hb3pBQ_GpIr!i0d)r z$A7a&-R}g0jktjdAacginJ7VX(!~9>5s7svhG-wOnc0ra9ngb> z`tbv*VVq}2B*XaoA@1Ye(r2eMu}xbr-5TRZ21V|y2Mm57DeRHDL<7id^+sdq5evM8E} zrefU-Vv%^VH-KLfjWvNc_i!K+Lnj8|qiWwS6A zjrRy@CVEXSY8DVq0{9(Gx{T?)HGn^;6=|8Vt%n)RE~3i51ugU-B| z`G2h8O=`bA(VdF+GiPzYU&v*?tqGdeYEi(Zw>U&|c~hF{o$qgx>tZbqTqXqh=kX7N zf8rf7r|fNg(e5=}$#pDd_96EQ75iUQ*vV;1I#yIPbt}FptM?85SA#B??n|z9^{CAs z8tk%ZWNjqf7D;qR0A+oq$7jTjxRfT&B(PbTBh zNP_4hrAEr6oCSoUi%r5d;Kp!<_?8bmTMe|gFKBOHu~&&8QfZ`0CQ;K9VCpczr}i~} zUmRMb+DMJ9c~p-?iG-Wz=`j5odej;7lBbvMRjo4Z<-7_S#2zjh`nW}jTs~VSYND3| zj2tKjQDmxREKvy>-7JyMV|%2SpiGgee)-hy$!_;SMyAPh#s$5UC_I7n{+gdv_>W;8 zkQvC+VpDVFi}`v+s7JZHz2g#vvuMTM#6SbSDfW) zP>zyTzkG(QaymST&d?P}8JR6~kxJ@fugZD1bnkLjK68y6lbl0*&0H@#C|n`JN&Uzs1O{C}U0?h^>pp?deCGZ!@x4n~QC; zd;ST?H@QxGVpY;FTj^DM?sooQEY2H)fP9;qU6;ewIYGG{zvh>G&&VBeCl`+l3`7%5 zE)DxqS&qrfY6c2kVf1J}j47H$&Fk)nfJ$Gc1u7T8Ze_&*% z{E%CvmrDRo$NvXtr(Zqt}+61p{SRe)E8F|((zsTig_g1RSJ#2Ii#A6u-tC8pA zc@;q41^e$&Doj6JC34fbo~!)wB7I5|k;`q_qWP4tdre0))f)}U3pj{h@@pf%k(UVN zEP{I{lu_Ja)3iM}?p-{!$Zw6jqE{n|?75R?*&fr_nbR?uO3W{>^6)0F^G-!rHUB?; zk&AnKa_dj`2INlyo*3?sKO6ar{FMskJxwG|&bwVC<=x3u9#y%q3N$YWJdPi;I- z1@-i0pFBj_Cy!9}$>WuM@*riOJc`*Tk5l%^!<2pUC~coS`|()^hfnT{vO#5Q+(<_U%AEGNy1K3vo>4Hy89Q~nnN zUKA?i+sQkzd^v}l&d-@6h_w2>;lNf*pXmhH(!)=07OVM4v=)W@vJ53i>`BF$UlNFb<7L}vbaAzrWEf?2qe?<2f`c>sstQ5=dFcx(C+K8-hVINm1LcX1>Sk4Fjc z87V}om0*A;cv^=msO6Np5jW9xUh&{&a`jMxhj1%hs(>0{TTH0{%yFQt)%Yn z>j`@u@6D*@tK;3kmJP`r`0>>%Ik0RS9weL7+0|E3(6j|blko7wHs3tk_EWZbx8X5< zo@m&HCmXrE4ZwnJc#5BA_<5Ec*`0WPdF6{E_*JVnB2es_rrLQ z?)|bG5cTYReHc&Vd;d6$C-d~5hVfXQ{_`-N%F}-xMq{4-+c2KV;_u}0?l7LUR2J`r zyjgrO1n=ES{9Cn>C+@cdcNFq^vm&k#PQLevJn>|ncq~sml_wf0$&K*q<|()LHrj@Y zq1X*u;8*>ym2X>6oL3mPksGGmq;Lqu%~cXmXwdf{RhWA|E3Jhtmh9g=!?hr_#ra&5 z8y%67*21kgaJN2PY-~hUDili|L4ctz4oy|Nm#HZE_OVcwW}HB$oWo$4%PX>Dh`RF_ z&c`yK+vvIr7(mD2vsj4FVG%x$;~Bc`SjRb+vh_;d{%=GF18fPy=tKs~QU=H}-UW8z z9^UES%izBcr!b^X!^2pCrx}DVk;@yrt^X&^#0NNwA-hs4(IxxwQob2I5=KphkDlILJt;_1Mpg!>JiQ*oK+bbQ&f z5|?{=aD}H2S9;>O$}>RFXriW$Cqyt{^N>SIfreSq6Bo^09!&L|Aey}jxYMOl$2KpX zmQ7MG4e)Z!Rno{Ag%(N=46N~1FK6c?ysj2z^#glensUOdOh~ti$91NBR%XZ-i z;>&(TwF)29m75RmCVUvmRNp+Kp|RHM_#D3zaEhFuxw{m@k%KWc(?7~S|av! zb~6?~Y3dw)l5#ggmNwy2Q=~NI>)d+1U*%?$()3T{BBccYRYFQj08QTYdYj5wq-1|8 zBFHs;cGJbYM&s3&E52_Q$23xzn-zt^#r!@qZ1{a}xWp;mQ0rZDb-2_qE>Y%`HuO0~ z4c?xtLir9kvenrkb6TsnqOTxa6RLbb=50hVpGXa1VW_geG3BPmDK0lz znIEbw56BK#*jgK^%*x_W?RGh_0*`LWL3Fr=;(TrhWlqfyL4v654c91LqDyk!gIlY2 z$VofoR5pdGLe*J0gV`ojm6eEo_3V(|u}){ab9Ja{r}T4M$pWI^h@733OsIj8{ocgR>*jiWXExUdQQ$lqdxZWwHoC;T(O2Q4HI;Yera~iU8 zNoyS?sdH+bP*%R|PQI$O!3hqbhFWy$Lk-$zhZkk#8orb|MJrc^Ye|)FLLC=pcbRPq zk*e4P-&P#p)H}YO4fIKVCwItod=IP)*Yn}uGJ2*{bTdwNO2f5Vu^$(yEuiDle)Fvs z_E=){HK!~bvI7gqzuci1xq-W{JNjA~7ow$cerrRBdm9=8#b3T%Zm#tv?~$7=y~)y> zLX)aw%TBqSTK(3De21gITP1g8ME1{M^Tv!u9+ zIB*m0;P;)#;x6nYdOnG}@f?QnDz@WwBI%p#`5V#neg013!4JjH-#rZO7Y9FLfIT3S z@e?@`56T=oByD(D7UB_U$D`7L$7CrUm*sduR^n&8!g^8$@pD;EJh}?c$T#o{xdG40 zU3gCJ<>mK}`D@36cu}6@ZyV3ySMnl$BY)&?8E=u#yZEhqfLA<%S3M?oAhG5;aQ455kuefoP|Go*5EImwRp>OE2VE@RNc{+Q#r#^Q4c>OGgq zkJwg<$({?j=Kw+NP)~|kBfw&It|ux#Ayt6|p0ngZd59K1(Q}eKOsWcJcote9n}n3- zPsd${F$mIoI+JH&!q|1rVZT zu9GLpxlTHG{rPjY)zd;H@)Wr=&_1V8qMOh_jV`tYXoJlf@1Sm!&ch;;uE46ZKqmb2 z%M|^O7Qg6!@TZkZW|oy0-S>R(X}Pis^bGB%eis_n%$?w=^D3_b3ODI*1JWQA}){*vuXOx1Qi6-^S??perR0bsf3?)}=9K$!Q z2mF;UQt5vfFD!!EOkDCwf!%1eJ8W#Deqx9o%AmKXIs46J8Nq`!*3WtbN6e~{H?s2P zM$DKoD=Tj~zD+n_RsqR>czd$)?h;FFK}oLnw$ax)t0XsT2$c%qBl5qB-^A-Bc38N` zVLenBcZv$*Ju7(#Ds%#*!i6OKE5n5|X0e|o@&{v;Fprz8rC5*FN&u6kh_^9*gryh@ zh_#(k!oNHzWj$KPzwan#ZCQbfShqHD53AgrjO};5#R~417nK^(sJCjWD z^q)Pu95V0od!GC8+~+>O``q_^@sr1&0!CpIY6~~(?Xy>F~mu8Sy7;9AxzYj zN(>D2rM4ABM^Am3?rXc#rVo19B$fHYCN*qQ(s}73{|9>${r!pYF@hG9(QrVU=0I1n z;94!%BFjW8N_x}KjF>9q6Fp*@LH{YH1_%DYHO+}8)C8%F>B4sG<(G+Eb5F8chH11= zok-{U662$=h4e8>3x4xtW3G`M$6HBXKRqhx%A*JzUt%5mnC_)UUM`dl1Pgu1QrInc zI#yW-rIVT7LSJ`wFvA@W3?%w<>EyNq=ak*woz3sC;O*$({`-kjnV#g3^dz>$AwIU{ zlRIhUM7iwlWS%>{R2H$EBh2Yo)z&t56C170_9TPw;^Ht?;}Q-zXYQr|9lA7(<@ghk z)`hXcl`acor7K+?#wu63B8&^LMA}^$Mhmn^o5Hvd+UGX7DvYIA#*JB*$|Nrv?C(nE zuS|5MnY(rCvfYXFrbIp^xTLfTeJMJB#=1|l;X z>k~OMnh#63{_bo#o2QD4oikS=>+KhyN+GitM{7IVr7}ZZgQ+wV*FVsoNT-?WWpP&` z%|;AnD3?#}N@WKJIH(v4iM()vqotFHd`1>O5XL#sK}v&R?80tFyE~g9lemj|P8-AC zCdO;S7{+xRF_A7L^8=|&uY|-e4P?gODIm{JN zpbw?=vzXr6BQ3^)nznDRIkUE7e60IS6ScDqk+2?M@C1 zh#$9xu@jewbbAM>JJH@5#tzs1^)Qmy=EFS}CQqDN7Y%hMbA?ov8|F!8 z`O_!6#p$h#cW<%C!^AJ6xxV;?=5FTt#1#|WkVw+TAf5^?qPxM3*};5wa!pF2SAB76 z+va?V@r=prm_g@+ld}+of+q42dC2%E0u?w&9IC)!;;ITT75EHd zrvgtQ{!9g)N?c!orx8agaFn>A0yh#*ufQ{i&#b^=pIL!FOFXLr%Xnv1;Mv4ySKv9s zb1Sg)Z>qrai04<}1;numEb}j{z%zKJkpql%6z5jxn~4`yVA+So75F^jcm+P6i#KkMVpY>)*y7l}lfjLy)ZC)# zmA78m^-4K*O?Zgy(;e*{?d`O$cWgqD);hMgeZOrP#YpkwBlO6NtNFWzSp}7ZtvXPp zwqlCk0_tB)eV@V_9CWk#A3A% zt?DLRslJFF>ayx)6QfJad9bq-Bh6E+*ThIpu|7t@gKA7Qk?~><+K@8Qn#X){qqUe> zzMVQZT6M5LK`jI2!)B}MV^mugFibod-C%j{XRrdyLXS7@^Tz$&rK5O3mLFl>M3ysc zVsSfScP<;nbWul}TXem)V~%|Uq3CiE~{`_o_v)qTeLbk2=%Sw0v(O<8R=(26YF#>TBG=I}uWM zVY2!<9CbG$>JVnCd(os0W0ATK=c)T~A$1q25v*57afN!2yZI1ysbl!E8pR>?EsUsd zQT#5VSX_EyJjG>>BO_n@a;!G3HK!$mvu^wM6NnR9@rN+mOvKyC>s=>Z=_OmQ{j8ndXRn_8)pire{oJ60Fa`!gYsyC0o-(<@XSkp+&=jw&rfXw@R zeb}k;b}nLG-ryTrF32vUxGhkPL)FwkUZ@i&ENgQ7(GnPPeD`2dljAEZ_Bd5epfg^@ zNA(Ceg3R9&uQIC$<_=*am+7Z6pYDnK9SKJxrB8>22Y?Anv=o+T^t7-D10^gGCp1!A zGC`*2SZtx5Mo>M2N$OcNs=vW3^&HE`^H`u>K#Te@R;m|SI(~w+>Ln8BXV{|t79Hy6 z=v2SJ9zJhYFXN#474B5O#y8X}_@??T9#wyjC)8^s)9d(|dIP^y|Ad#-n|MXNMMAxe z57axBt^V1nR_|KV)bFh6>R+sR>R+v9^`5mvow8P`_pOW6zgg?l2i6w#?^c)k&`PQQ zu(Ikut-Sik+O7V}x`WTde9=6jiiVUwB(0c!B~os+*0`j!s#(5VD!zpi*vmBa;scE0 zMoPA2<8JJuq_Ec7f}1F5Tw!e>8HIprtgYBj$&cIF>t;#;JTxx-<+${dap{*}L`=Jt zRu}%1QV=ax3SXiW!YpeqiN6G4oNevFm#M450&5rkj8e5B`9ai7DzdmvBFjr8=dIT* zrowjHSd2-k>$s)SsjI6y-E7VTR$<z5hJA^<#5GIf7vtc+qKY3DgJdSk&aK;~ydU zEXYOZYN^cnFqqVZxULU)W04-=xoCU^O4{*tN}@adBUn%$P_c$TZm)85++QEiv3Sto z;Xh>NR-8L3laJ3G@Q8<3?^nhDmzm|RHd6FB>hLH3?F1j@-nqApr+CkW4ngTK{JIJ? zx*Ah-4eE6*&eU~i(v#7w&%hGxV5R;HTJ==4>3UqIr(u(hphHK|sT?s|z@A_?Hdo$E7Yb`Qa!!M zGX3chW=`n-S0m7m1LX@ZE_VGwZPN)HXb$pprrbUwQk!J5TG2gJfEl{>AWK|(+-p0zeub)no zfky5E^k(Mh7S!si(4eozT;0LE+>E7qt8r$EJ7LKg4?ow)jn*=I!H{Go_60<3}T2e*)Z2%mgMd5I{1pr33IV5 zbCy@!TC2#bW4O#@W1*u(`d(G3G_(~;-~AETJjye2`muD;Db8xP7-9 z`nWHSt>fF#YVH-X9qy~4Y)9+(cC?OfM{8+2{0#Jc=0(AlPlP3o?c=ACogXX1qR2C;ixT?O&6j;UL!~&}daaCVcF3aU2>?lWfiow)cmfq70w1@aw=_pQQ za~8$X`CTGb6CO6D*$Io)7Z1}a_X2*sh}8s#<8ptcOfLF+O5ZX>t_coxR(#UfY+41b ziPfy=bX!rbi}tivmQwTTr;qZSF;8d{vONeBiNw7&vN=G%jsjJ(il?u1p4(4uv7mPOX=e*rBC3i`Xs6K6zV!hFkFSn(l1M}Cgn>yYlrx5@PLbac$NQKYX313K2fs2MC#GN`K1k%jL;nL@ CqI;eI literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/NewPlugin.class b/bin/ij/plugin/NewPlugin.class new file mode 100644 index 0000000000000000000000000000000000000000..eadfd18d57670c91601e7809de5d4ebeeafddfd0 GIT binary patch literal 6274 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 Mv@4~yh?HvoKg`9(A^-pY literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/NextImageOpener.class b/bin/ij/plugin/NextImageOpener.class new file mode 100644 index 0000000000000000000000000000000000000000..da2df14b8f6e4a61818078200590100f36857e40 GIT binary patch literal 4983 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

hLU`U=HdgXRgv1nJn6On3u2)i7yETSn&_R71K> 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Orthogonal_Views.class b/bin/ij/plugin/Orthogonal_Views.class new file mode 100644 index 0000000000000000000000000000000000000000..696f4d7a6c0b11a3842a88d7f7029ee32a455a2f GIT binary patch literal 25900 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/OverlayCommands.class b/bin/ij/plugin/OverlayCommands.class new file mode 100644 index 0000000000000000000000000000000000000000..5c3bc141e894c9b195ce17664b54f62a91ecf32b GIT binary patch literal 14766 zcmb_@d3;k<`uFpkq`66YTH2HX0V-j&r7gQviwn?-R-qsu3b=(fw2?HaNh*uDFNpg# zI^YJlRL5l$G*nSUMR0V~amLY^(Q#oMb#z>2R|NTepL1{0BJlaVf4m~O_nzfB&%WKu zt3N)og@`7&N-R=L!RR@o+hZLq(fH^Y=S7mS@Upr@TU$8ZoU+KxRCG@GyzuB)INmaP z#=>(VO=*if813FP6MGxtOje>j+!ReOW8wxTe{*C}xFeRHm54IwX;|LU5gm=EiA=qg zN0Ny-$#6W?9!^H$Y3!KK7L7;K$1u6dE9Nq}>k`coA2AhcvdC*u3BYy(@>76H zHT9vsOrE2homP1R{;QZP+k%>W@LSbU^6Wmk2H3) zEsP}RgcrslOo3^Mrf_U-I2n~^Gw3H54VJ?Su`k^kO)(7+q7KYB(CA8b;E2-lY2ECb zolZvMEfXu|dg)Ls2sbzTse&pk8o3Xb?PZ#(s2YSt&g}@taN0hQY(Y${nC}Ofqcx2Y zv@`(Dj>I4#Q0Jv_OnK3^a7#qiOwe=~)quxX(;$OF{d~dX2qvFdR-H^ly;KW1E{cWI z=}27m9i^$33cUoGxLc#m2G%4^$H>?ejHOx=OJrTWrYSNu6&NQYZHe>D$`dqAmC+Ni za#1qTCM&0Dnl9KD$^g`1dN>Yrl3tp@q|ymlaY($$Xut%2z{+lTcm^f`TB>Oo zEytQ@YH~6XHWmeCSwR%cG^o5M_}p399fp(FYPycD2mUZv zLu0RfH$ic4gp8-#qW99xOeEa8Rnsu~8Pj2AdAD?WVCz`n{v#E0g}b+F`Z=wJ6v2#{ z$wX5ml}aR;%4{CD+ar$Lv(rHN4!X;tJDK`}{(TlfuhLN3Xj4kl-E4`nPwSO z_t}#>cVdIE%2Dvqb>UcaVG`gH@rj0Q`)ullInf7n)@fQ#_rkcB*1^Lj6HzEL*UQZP znjSDS%d#^f;s-T7M8ANJHznffa5N5=lLuc~G2cgz&|?-o3av3B4=+iNu1mxcNlm|` z$3?2k!f`0C&CuMeQNs38jG2IOeKz2>|QLqUqKm7o2UyQQ_5ltDD1S1$m=E5ZGSX8(@(rk3HI13`Dy5Ly~^2U~dGIucf zz|%#6ogF|9rcuA8w=Mde;Bn4qL$;>h(>snUX*a&@r-taLw(#c zLXU)-_cXmve->uOn%;JZI?4=f)0zSiHN&Nu&lz4MO^O<~>xa3H>1MW6ZZlY;-IFRJu z_K!2N{#3OfO!mpjXCF_7oZizeSq(glN&;SP**^M!W$rlS2K9l^96p1tWFlH;Cr{pm z!X1P0Vs5iIhF}DYPlZEHEOF9d&2dIXC>(C6p9a?jsO1$48oYcia29PdWQ-a$%FDoK z)STAH0h~!X1b>rChtf+DA?Ry76qVFJ1%KMa9mp&BJf=erq@UfnT{a%f_48szR=J2# z-xWJ4VmB*ABOO zlrOjVGDs@tOLDWCuON6kUMj0rYQD-mEt7Nm$tHrmfwEL$>C|W>2F2`qfWzx0 z(lHXf{CVAAynH{TG{dgPL*-+t$Br2@yaLB{f&ueUe23EA)whuPkmg?qqXJ-*gsQq$ zA-6$N1etzB^P~Kjox94kT`il?QE&0%!0e|QGo>RMYeree8<;9`G%t(@*^%Z@Q#c+^ zq(gX4LvvHGmryw6AL8d<(r5VLr!;4H6QmJFwiO5I)dwKGXANTOsdMT?FFy?<9WqKl zZPxs(QOk6~UgYI1Od}7BlY>+Yg<>QL+`@4|s*#ah_Td+pLP#ydJM5F$AsEJd-d=>WZkQqF9_~Ktl7J$Rwv({IL zic;5+lt!6>2#16s!{#UC3Vi@8B(i5I7m`3+lTCzxD%L6>$|EV(325VF!@ugS`dF&e zDO>Fx;3C0F8gffSM?VxktK)XXc+i$&Ooio)FEIa z`eKlX^cN3Odd!#jid4lep$>9_fE=RL&@9*L5L?nRI9#g{k{DR&#MC58S+6RGN*h$d zu~=wHG~F7CnT!Y#O^wv5QaU?5cx-D4m{zUTC^gy+T}bQC3p=69NnFf2dwil_jaB0< z1)B)$KdaRQDWXhz3yw^V#T>JYHi@zAzd<@6>Imt)wFr*2T1`|(+3w_&c=X(kh`0`> z5&usv(EvL}t7FCQcp-l$G)Jjku%4n-o#cg5qs;EcPB1u5tEs|XF}5kubbX{f-Rf1y z2Cb=fgV3wc*c<2QQYew^uGq2#jY*1~c4BDO@l)&s1nGyGrqy)S2;jh|qus8hz`dWO z^9>aZUNsX*N{>U@&{U3AJIZYX%pHf)K+V!>wn8SUopX(Mk(@<1ZG>j}b!ZW*x#~1a zor<~%GfvLzXn&*$6+tAa)jUZ;(bAM>uUY{22}e1qC+PADh<~m+L#s1QeQK{ouO{YI zXG_u0)*d<v6V)OnFsEn;(m!^qrvA&Jat9426>$TEBX1X84}J-tl8oU2t* zSXnF$U4b(vkwaFRCE|7V0cR<(K^lyFUM|!X} zJ)0FtL32`ee^;%f!G5(;U8B{twqpd6vl2_}#3toe*Q*;XbpzmbtA;7GQd-@lZZ_T) zEwDLGO2IVDF~%M(t^IRAQT$uA`kA^7L^qE!{sj5k0Tv<;8Z41xwN`7gF7EhbqN5!d zjk*)bkwW4)P}1;&Bwmm#dW}5BtJXH)1fvlH`sPCTTQ zNepGjoa}^GJpygDvqLcPF|B^7AV={obHlL?_@)D8stw3wylMl`5&tFJe^NF+#Z-O( zPu8#>W%A-gAt!X3+7#40t<^Jj{1Y8-i5N}P;=(A`Xd8`wbU+LW#xNXy>G^r*<)Vy0kAR67?f7(BdtD`(kgF> zG>5zjeLas|Ps!|GwfaIy$#q^-UFyHMo8wjAVSOmEDCAYUKuXpX92bkUA@hMru2j3_Efd0+N~ZC4 zPyE<{j9YTmCTUOu-XfeYLB_Pf3a4Ho^ zS*~J`nUm>Hj|5`2j?!QPQ}H(szkYdDDiwvi`!rWnwzw8!w15h+su!M)Cl7v!1{5`K zq@v9fnCEi2-J7WQM(VeL`nfaI{|QVhIt0G~a^trc;QLSs^`-vQj|Q1p0yw-9b%2h> z74KmvZ{#Klb#M2W!6LIo+Y4w24aGvZO&X40V8pFB45>41tqhFFQ2A=|kIc}hPTEiz z7;B#MN4hdJzLVBf1`c-yfF=fRtPC8P+cB||F0Kq5oqImEliDi-lXK5?opg3(;JDoL z@triYGSHBFKB1m8^6cylufuXIM!!+71hfyUyU$T)hQ#v8u&v3ck46 z2;9^~w{+6<%Akb-uJvM13C_5!))({@g@DGIT5r(1o%&P*e>p>oe53n)=Cj ze$d)L4`=9!EU?QxTY38B7qVSlx0lXT}=@V?u7)Lod=x5N9F%i?#uR0(PI! zc8Jvl96zQVxN`%;x9An@%!9(*OuqtU9$?!+uVU5$u7x=F*BD(2ie9HTaCa$Y-lX4v z{=YzV?Hys9Kb{J9lh2~)ZYs2B#UAQyQ2_t%L15tBbOdg9(?E;v=&QdcOI7Gy#{b|G z1zYD_7dSfirs!N3y*0gRI|aMw-PKf5C6@Uobr1O}A*2r+?b|^oLPV|GsctjvoEP|H z6Mb6i4!Q%MXXuNdyQo)DIXuHx82AUS{}LVepZx4v4Rw~AJ*&xU!>a}}N|;2kp_rnz zjRG0!ona(eQ*p`LK?T)tff@E?xBvsi(9kX}b>ByQ%rKsv1;rWej|FA99c56gk(Je( zI8<${AW*%Phh}&<_FC>va>H)AxO@%uGSnUo+PBl;o4Imc30Gs+=vq(Eli_h|fH#hk zvvGobC&<`$atNj-$l~o32zpA`_J__9mO~kA3jjcHbr&BAI6?Pz@|IfQMHe3>{KNEQ zV=*7#uCB_|zh+ zvWw?;ThOa;Oaxcv#(PLFY+xWo;O-VdV_V_gqL@90j)t47hix=c8_lISbUcBT?HD;1 zf0Fo(wDJmk&u|TN|7N(w)fl}OqtDQV(D;j>xfkQr=%w@?K2rEMK2z|} z6}7+S@(@HUg^T0WJo9El%y|G6(aC(oM z#P)#Iq1?<7d_Xk`=P$xu4=`E^Z19B>U?mU+{jj44sDh&y^BD&|jr#4OA+R`$D)!K1 zV{SWPR^;DJ!(oDbd_@B$!SqV-x76awT~xN4f>=BWEvkNu^*;A-S}ZT-g+u1;r0?vc5(WN>MhSvMXe}rCk?GG%^TIlOQ3x2 z(mYYXd{Nouos<{2Y4+uLT$)$CW!>KQ?Fa$4oT{~g)^-Yszh5Fg+{*9;;^Lp=OAsP( zxw6LNs`28`wKTrkWq|vt-R9~K`ZK&L2;E<`o`%Z9)$(xldg|k@@gnk8W%xSmSxX)~ z4}fNt@Qs}Wi)gR)Aq1>OY(01dNZwrx(|VRJ;L<^a6p|1v=72HJp>o@ z3%Hd>=n{IAR?=g11HN*(ogPPseuADv-t_{$XxT#K$c`^N()ATxCoxOp& z@lC#le#5uWTYLxomT?L)qPOw>;$41`{=hrwkLant$3M{fY5@IN4Wtj%Ncu?C(#PsV z`inZ9K0!wJR~4sE)rIt#T1B6$>*#OlHu_TCMSoZK(pTz1`dU3f->5G7R&AkwsBK6a z#L2x%y~Pn^Ge}grzl8%r38h{z?oN?cJ;%S}w~m%wYP6#{`v?9bMlVtI{3l$!;PG&N4|{#!bQ!;oJ3lD@ zC;u4^(nnumy|`3AUjetd4YT>+P=(o{!KcAyM**lC%pV{D(#C0CAx;x{3Enn;=-Wfu z;*WSQ5*f=v@?;Oo5bj9XbEuQ|pL5xZDt3`;4^0qXiWG;nZwlP&ZYnarA&Yj77~X$3 z%>=X&M^umR3s(!jybFq_5cHpBl!=+34YrFk~!|CFsYi@!5) zJXrq&pcEqy#_f-;jN(ojT$xcWj0Q34!oA0g=2zj?=hT6AK{FLKAklvz*#Dsp{0<89 zJu=K)i0wb19NCThcn|X39})TY(lREx1X=10tPn|Ew4U8`Kj+~yGRvsj2{tFP>IOrz zvsiG)D2l=<58=Iu7wR^aE>Hy!s~eCfDU>sqTSgVC7iK+1*dfVDd$ZOO<$_!)mqPZ) zr7wSvpa{J2&8jU2>{WnD)ZY|6?u;txQYDgw=2w-dU`7o%wL6#TgR_W{L32L&SW^KP z;DACyL&%1b%?nTo+cXqVff}SjI3C;}uH$pK zj&^YEO^5WrwUE>v8foLY%SZ^{?>bz&(m{7%m#Uani3$z+Q6*gSR-qj^#!j$7psg#1 z$ZjqJUWXu`83ewCsF(-iE$R>&z{7grG%$x#za0Mx)nTf};h({&kHKl|F6aQE7ISb? z`B)>(j&BSLk!?(eT7L|l4Fz6-EHXJJ*hdR__xBt8^uE{FIl;A}?13szDUSJ4=*rm;L4?d>siB#)(I zc^uXAcshwE(7AjV-j~+U`FsRj%17esTuG28(p`KsO!OGa@UgUsCnKIsK@6)yysD?y z@O98zJQW`g9*<7~PN2`ZfxhOGj3`fspN1H6qWsbkrH?*!%y}?WQ{saQ$>-B1F?OM_ z&Zsavwj1U2<3{9pNaNeoLSxu(Ar{CE%Q1299vUlZK!G2{o>s{JYV7FU)QgLQB|{Ls z@ZQH!)}wJ%z$g#gY!Y_aoh-i$A_TknWP`TdHpz3)#&QR3l5f- zsSXU{J9>wV-#cU+=a@>*CJ(4;gp%fOR=wwKQs*^FJH=W@fhv!5ckDp4Q7x$kxyyfI z=QU_55;udo2o!J;?7RikwSu~H;CdIs)?=Wq4N4ftzb!~W3EQEB=k}oRxEzI(b4qiR zQ4ocG%P2#uO<_P{u%VDvKD0`c)ALOL$RV7x7`gbbz3p{i=JpB>f`@U-GPJk1x$qZS8q^YcYnM6 z@C9P8=?$n0O>Y2B%IOVMqecJ%%vC%KOt$$5|t+dU{Ry>ATPg}56)n(T!(Z-hINI@nF`AUaDO zoM$w57!~~J)VDC|fK@n!OZuXyHDSB}F?a>#QK3ymhoOaL)kl z#c(T!iLPx{D`7XQrdJ`CxY~70Cx1S&Y}^j=bn^SSk%HH4uEkyIy4pN*TPC+2bL+eA zNUI{j5@7m%63aFGbxZb-S!oKhLN;P|ZonSEBAVJ!fx;T3efM z;CxY|&8qb^1?GBxO`*9ySkntCJ_IWMANN`l#(uyx08b@ZpTD%QrkAUx5UIt(Wo0Wf z>QUT1QDz`c$f%8Y%9tnBWj}2$Lpi55Vu1;-xf8M6KzE~S(7gapS2|CBE(^M!R3AD| zJI&K4&eLZ$`hRmCzbKPHFLeFA4803;_jMVHadY==S*dmw{U&JVi%2b=Q9pER=o>U1#*0do z?}X9b1*5$iMvF?7*U}(fhj!6=q;U7r(Tr-9?}x!YKxZRo??A)pVtxpXu3w;`^)Q-N zk05qGiWK`XdI2wwU*^XVvp1lI+(;kslZew#(RcWyj`2~QkGpsPK9H;CXZT3o%=P>% zzPx=72Dyb>_<8Q&t@sG@MZSq&;(K@-zWaL_JGNtF2R=}Hh2Q00@lJk~zv9>UJHA^n zzpe)H8)^c-X`Dj_1`oq9p@~pGj&&?cs|vM8{fIbJ!e5)J(FOY6GS#3PRBl&$Ng7f| z^JdsKT4SJigNsqCTcCTLOHw@_sQ6{FXY?4lzr`71_z=O7`xh3HWD+s>1}; zm2B~)sQ2){i)a%6NTX59$m3ol^@v9MFLUO>btcqEEj0u7qOIxfUJ5?#qR?c(Amu_J U#c!cY-p|Qb4%=KsE+qB;2d70R2LJ#7 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/OverlayLabels.class b/bin/ij/plugin/OverlayLabels.class new file mode 100644 index 0000000000000000000000000000000000000000..7e8828f4095c7e2193fbf1088f27eb108dac49a2 GIT binary patch literal 5023 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/PGM_Reader.class b/bin/ij/plugin/PGM_Reader.class new file mode 100644 index 0000000000000000000000000000000000000000..7eec95b72a18c1c217e04e05506d09bf12838d5d GIT binary patch literal 8732 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 Ye0m-S#aRxD`*Bdn_<1%@a;W-$0bO_7I{*Lx literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/PNG_Writer.class b/bin/ij/plugin/PNG_Writer.class new file mode 100644 index 0000000000000000000000000000000000000000..69b134a2163bb9172bf7b466110a694070ace8f4 GIT binary patch literal 5963 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 S)oM`f;J!*-N_EQA&i@0Bx#U6s literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/PNM_Writer.class b/bin/ij/plugin/PNM_Writer.class new file mode 100644 index 0000000000000000000000000000000000000000..a50f0b692ce6343712dc7e5ddbcc616cf8080928 GIT binary patch literal 4104 zcmai1dw3jG760AcnVs3mB-w1cZKn?iErFzggjWM>Akef>7Lq2C(30@VB$;NXY<8F3 z*``IP6e^;{Y88PhN{iK?2wESTLJ{i&P()C~H{b&hU*E?+{(%UMzdM_Rq{)Zhmz{g( z%(>^@^ERq44FbxV%%Ec+h%yrzbb-n?H|?x14EH;^ zZo5C_2t?a519obIopa^;gi!I4ZeCzP+q4~~PSi~6xdJzf)waENt}ZX zIQe`gC$OxvI$M%VSeg9nn>yY^HBJyHo5K#|nbdkWCL8sKLn^P7<)B zk8>I`QHwf(ayOqyZ*g*-GuT$}d_%o*dA*5K1?lustGOg9a0g66uiZuaTnD zO`L%saL4APW>W1>aE5=uGgAulNvFEp_ltrQI;?c8emrqm|t*2phwO0G|hpZ7yc zI@Bf-bL^<&5&_6XjP+$`qdDh>f}J9PW-z40+~&Go4eLwfbu*1!_7-QYYo{_pIywam z62H1cd;{&cT++5n;M_TwHrw^t63NDPTw!8^giaD^t(#-|WOCc&hbv9=$Pbbz7rQBE zy*=z`xJqE*v9Pul+|;0xlTKb^;#yoMplz{J1*c<^z)90jH|gc zU=TxgHtVDZ1x}rAWip#)S(!KpdESd56G@*EUZ$k4V>7*!@G%vuC6qc+By2-NgUrgb zi43X;az2?E>GEu^kT)Q3gNYm%tcpBiae5OoW+G2dtM&7L&fI98buy;DhJwH%A3KIt zBHB^#vIUQ)b8NEMgRNofG?lfsXU|j_!Hp844>B(&@SvsXDt?8raI=Z+lFYKKY;*@D zoh{sAVuuulDZIpShmxL-+XMogt8{!=j!il~B2aCt@Hyr=>FWM%)xNiv`fyD!PlEeV z8J~{{EM-Q_@^vx_I_?w@M0q!>opHvhRfd5PeA2`oALkKS!DX%!=Nf$4#9jDI>2#g$ zR!1ht%oLvBj)r@9*sEHlt-U5bi_g)zZ;Py}y{eU2NT@$QvvjS=q%ygwGHKvG+;3tZ z_Lm^?_oEe=!HFQo!8?IZxx<%Cd|4JObs&|=vxLpVgC@R$ukx};+UdcRbJ5lTR!}#? zioa+&N4t}`%!s@+{K$VzVBTcp6CKmmC|O&U-{>q5kD(}5X zPi8QW2g~?;7Fax`aV&*tSd8#TmqXc!C@ZqUA9L`T@sg{UyMkJRr-@COp9Up$%9*tT z*SFhQ|DM+HEEzP{Q6=4;Gx5BPYemkQmb5jX1it-$imRsTPSYH+jM>=qp_(JN*~4PG(vNBoKVapTfu zlaLhri-}k98iU}IyepZ>O?+kfuEAeTydkr?oQr%OugMhB@*Wo1Mh1`H6iwzn3 zUfq<6${$j2o8m zG?asf(c5E%58>)D^jSs`{Y7kQlftM=Nj(M8>r2u@7%rWAAIer*YW*0pTE)k6~LpOs$)m3_d^9RAz;xtz~|N zO%clsac;yal6}cwUn~-RV0$ET4|UjtmAv)pv5Sr8Zoc1x z1{@%hzs1#$;dFeTedbd*6HoJBikHxYf79Z-I7cY#5>&Q?l{jB4MvFKJ7l`w5p=iZw z(Zl)G>;e0*Mr^@FVh7fV+u8Zu!LDvM65=jgD(=T+;$b#+MUG==7f-T*dyeDt=nyZn zZF`O5>(qUdYyN}F0}8qWVO$ZYz=ps)Tp3tI4`f2Uf(0^ZCK5`{9>ya~DJ2lY*YORe zV2Jt$@l9eD4n*kJBgCsRVBp*M4pXj296^z?5c+mRX(D_@A z!_*5CktKMXGlRarjqlZ}46W_xTghe&}P7UTi5qT9q;H-@P--aI&S(6CdgdgEa zl#5+R6W1|Rh&}B6@nigi`#wV-pT&Z4hBBlY1$_20QAODxdQh7io8x1qrm}iKw>DxJ#XJ`1?b~Sl(4cnV*`Lz$H z!p3#zheNhxIo`z9ZU^6PXGZN}4=uTQ3B99llicJ?a!%e5cls<1(Aur|8zUa1y>6dD v3T-X+`JmEPi3N(!f)!BTK}@`h4jH)eao!z_2QjUY@tgoxkbd&s`N#hNBLdf# literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Plots.class b/bin/ij/plugin/Plots.class new file mode 100644 index 0000000000000000000000000000000000000000..572111035a3100a54649d55eef274f65142fcf82 GIT binary patch literal 1571 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 QP?=ZGN8oH4B*j_(2M{-`r~m)} literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/PlugIn.class b/bin/ij/plugin/PlugIn.class new file mode 100644 index 0000000000000000000000000000000000000000..76dc09caee641968f727de7f13b23968f0562510 GIT binary patch literal 141 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/PointToolOptions.class b/bin/ij/plugin/PointToolOptions.class new file mode 100644 index 0000000000000000000000000000000000000000..765b9a3994694ed5bde11b1d550f3a7482b1d608 GIT binary patch literal 8440 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 kn|VjN)f>3#O+M#Ud>> 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/RGBStackConverter.class b/bin/ij/plugin/RGBStackConverter.class new file mode 100644 index 0000000000000000000000000000000000000000..b974fd4f72ba8097ecee8d0d7b956891d8e53410 GIT binary patch literal 10506 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 U3k8oDDP15fAY>|*8pgE$1J2^BZU6uP literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/RGBStackMerge.class b/bin/ij/plugin/RGBStackMerge.class new file mode 100644 index 0000000000000000000000000000000000000000..e5a32846fbdc7d829d8c33f814efa273918b0f70 GIT binary patch literal 14745 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/RandomOvals.txt b/bin/ij/plugin/RandomOvals.txt new file mode 100644 index 0000000..88c8548 --- /dev/null +++ b/bin/ij/plugin/RandomOvals.txt @@ -0,0 +1,15 @@ + + newImage("Untitled", "RGB", 400, 400, 1); + width = getWidth(); + height = getHeight(); + for (i=0; i<1000; i++) { + if (nSlices!=1) exit; + w = random()*width/2+1; + h = random()*width/2+1; + x = random()*width-w/2; + y = random()*height-h/2; + setForegroundColor(random()*255, random()*255, random()*255); + makeOval(x, y, w, h); + run("Fill"); + } + diff --git a/bin/ij/plugin/Raw.class b/bin/ij/plugin/Raw.class new file mode 100644 index 0000000000000000000000000000000000000000..fc9279fe396ba41d917c2ddb8466ef380b05b219 GIT binary patch literal 2773 zcmZ`*=~okX6um#POcF+xC}5CMTz~`uxG#jd*A`@}SZuW0K!!LNk~Ab*yV_zGyW7_8 z?K%C_5B;L2NASq0r=Rqk{ssMG)b_r~2w^FZ$;@xwyYIgH?)-+oAKtzLU?+ajP^PfP zn(WG^3P~&7b;7uy!L3j=X`DB@Qbs!2H8eJ9j^{Oaxw;rQ%=q3kmlEcLQAp*FS~*5# zau*eX{fo&X`J9zb#uR)#<0&g`<@YPN!jVx0_mND(^n=xPXz(di_FHLlurM`d=1v-8 zDN}$l<3?)K$XRmjEP8uvm;hV|>Zn41Q2Ddgv<*<&2aCFL1yZaDuOt*1)v&(Ukd^5g z%9`n;mXXRNeW+I`m+K}<(b?GgICQs$sBWol@l2-KVud#AFQ9Fc@%0L^HSLqtcL2;k353f1A$%k(F;2|^7U`Li_6gvA2O zAt(nmtSIW`42jP0X6U!Qib)q%@r4s7t4>V+yp5 zKT*i0tZ^ex2UqbKDGgyEmo=4@`b(BYsho~!q^dfDx?>_UtYl+rG^e_YjYO(8InDdfgY(Vdo-?OPsiY@{b${CM~Y$g75L zJ}-}cUU9$WRWrZk$D{s$euF1vsNm6SFPEdzUIDN8wsBIr_$^7EwmDQUPx0F+dNBZN zv5tZCCTe+FFtfH|8}-^wj0&iSXwXonVF&-H+Ai#N`uLsEZmBbmJ#h-LPr8T=cHBjM`#ko? zJ8t3N9F7K`oW)bKIDQj@?F=|^6VJ}$RGe+cU9))p`a)mzHs26y)U$4}qX-@&mqxpv zwqiBBWcM%qy$pj~gwq(t?Xhh|!tO`7mqR^3M8lpUadUyV#ZVUms@vLcVPYV97d{fR zuB$~ZLAzEn8MScet*jrhIW!d$ZPkcC<%=A=td>ERz*3ni-$$j!$Hy<5VNowNLQIW! zXCCP_i&qAs_vpLp^C-mUaAB~6JYF4;;~UPMwp)1nc=XPn80vPrc6)w9Ysg*WX`jV= ze_?1u&bPNwS^Ff)3#xnBqiQ_SJHP(>&pfWsM05DK+cS^Jcw5LbhtImbA@5?NeKZa@ zdYwbBqSQe)P|ra(@!!K13fo5dQSP+cu$RC4~#4v&$4q_iHuH7J*8rLXGjPc*#TbeFUl<)98&n_`SB=y>o*1~-s aK@C6LM@U2A0jf3ps5VpDpUA%qKmP}1pJKEC literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/RectToolOptions.class b/bin/ij/plugin/RectToolOptions.class new file mode 100644 index 0000000000000000000000000000000000000000..212c5b0281dfc3b0559d01c18f440bb39b7a9696 GIT binary patch literal 3712 zcmZ`+3qusw75)a;o!#LIxrKU|E zwuw#J*tAJ&LYvsuw)TzwbNeUf=!T z>NNm6@oy7l2I9^{%T%_QadIs~cB(Mqy4izM1;@?hO;i|IJ&`<-Y{@2bnU;e`COE-F zn7xZehiKcIGZ8UR$KFiQY3X*7SvS+~ZlM{I8-c_gdjc2mjh zXwq}!JLnAW)ov`5W4%HgHFQ-Nck%|-OYkd$%K*x~Vve9S-i$?JRKmDJL1C4J25d61 z(Ln7=NGN<5n+-&r{HT+6bdcQ5Lt>RsXp&&eg4^S{xq?NXTNJiRU$wWKYetjwGUQ;m zkW3vLNKWa!NfdV}ti~D(J8-v&od&ERiXqog*oAutWX3K8Fvl7bHv%2O9>QOstd1nT zZAmY+E$=*ThdXvsgZ!~@FYY%%5+Zbb$g_{;6*{nw0nshFSOK2yM7M=5>^DKWMT%oi zF6~YaBy-7(?J4x&0Cy-+6+O?+6?!Mx&zTI4ll2!>n{T(!i;tM-GoY6CEA(T)K$s4= zNdr~BohX~%wAz7#3WxAfLd!WLC#UF~p!dqiu)>IdQpFKpni?7z8dVsRA;qDdd8y05 zg9;Df;iWc#oawYjCOoR}aanE+ExTNgiNG|Nv~QX#nZXgSv<& z)9K-?la~6jb_Ms{-Q9`N2u=_fAF=v$02%!`zP|__CxsfVwtC5!Pb)lu&yWaluFF!7 zV`mBC<`q_0DQzS89QhS;>ox4Y+fxc(z!ym$-TII$*2Z;>^964CNPA*bB6>>UwB)Om zA9ttc;jGLr)>9&}P&N$&BY}LXQp>c$^4LqEI+Ih};qpviw=k_%6Q3G)~(o)^!6r|1XCNduhL^ zdP(+uU*QM%Avd0%Pw4>PipCycX$O9!@MHXhNN~pT{Nz#4*y~SvRpB*UT)NMjEeH z6MrKy3!0sm9fKhf-AnHf0UbB>2W(rzZqZBGWP;Jw3cP}2a97$U3*Zi(m*pr!2qyoP zA1;0!iNJJjEN9VT7&%h%xnd4BBKK4~7=6>6TvV{MTw@{GppS!6Dw zo?DGgR@cH!oH<>>arrD()nd9XH2--r9x35dGVs}W)PEx^pVt^Yxd=`U>$#~qgb9Zt zT&oF9j6|*D?`<5rlkjXMG_CyVBm)OgkHb{XgB*F3g)W7Sn7}3+$7W2Ukq1%}o3L(9Q?RXn^Y2Y8B(g-97 z_1TK&@hr|T(oUXFU&V7&;xq>FHPRB{x)0#%e2Z%OZbw+Xk2NNq=idjYFE>o|c9_`l zA8g=LIQ<5`88Ca1%P0nPuDOJ>eexi2Y`)bN)=v`C*%paMghXQMlNgPMN&YO(wOR4# z96AJJ(z!bxO`LhEg!A!eU8sZ$@vsaG`SVP9KP>N+@o3yC;iqgWOZZuW!iihdQym3! zsV?*iYG-jJcD00G%^(`PI*Z?pRlLd&eL;_sFZmx)2zN8^UBvAk;<}qG?IB)m{Ax!x z_VQkMFNSd+9>e{-?{vV!KAhxD^a+lh!hRmrJw)~Z&Jw$q(WkkYV!{%Ng`_yAtAM2H zIs5^CBsU?<;9bp)A|rp&+{DPtdqh7>AE)tWzL|{Xl;$hKm^{r_l#z|&FJ#zaJXMG+?{wssf{kqDr-Ed>&epF!ei<3m k8TeeNP(mYNJLm>Bq{9{bn-09}P>*#H0l literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Resizer.class b/bin/ij/plugin/Resizer.class new file mode 100644 index 0000000000000000000000000000000000000000..cf7ed35da5cf9323692f2b8eefdbe9d986796464 GIT binary patch literal 14246 zcmdU0d0*0!H*ZPhN;T5A_=wJwO}`MH8Yk>MLPtU|44wwRJ1({ zz~pP3b>WF~%6pbdTckWco_+OAXl{#j#8Q)(JXNC?Fd0+hEzvw;8fjB0 zl}YOun;f@QWz!(CnTlq{I-;|>+LuNX^CL^!qC!=?InuTul8DKBrqeq?qxO@BYHg~a z5q_$p6D>M{DJZ&3#G9kZWL;xBq%_BUi6?9tLnko}ULH*~9w{iW=TwazKte+3$uy+x>J^S#p>{`VdmL1pU!5= z2h5aMsv+7bRt-yN6jhyL(?VM0VrUYx#z2D%oNLp0xq--PA^ZZHE~F*cK=Bpx;|aWs*LCzy^MK&^eWli5w97YM~)wkaVL7;W+96&7I!4?cX~ zP3elYwL}v>TFF!~VM4};>O(QnU?`Rh$<$~|sLD@CS}i6%PFibi`U+hPE+Uo=GOLaE(WMae=t+|V*X4riio;N3`S@HEGV>~%*3s24xm4VRXDWjQ9L^)` zU@EP*=~^0xRomjrMZwpL?cBgLC_6#?>s%-RF1al_K96psn=QHt`#$G=bE7uhB0j=g zB`#97<*hbVP^HYd-KMX~n738?Oc}h>reQM3w8^I7R3^4^w@qIYtF&O1^Vf7nrSD#w z?h}gg#8Ts39W66@}Pua>uO+4PWpTVv59 zaA+C3oYxxZ6ub4)!}O@=^D&H7$u`aI8nKo)_`$`jd2KNS6X|}{rq^=a(-V>QXwpZ&11;LSg{rg^L8p)Y(A$~R)1~u| zaFp|7?a@%xGCf!N{)C;N69b9vp-8e5J0X;i_$|!-*`~MXFL0t@c4{UL%how^t1T8j z|0*ucO$|b;kQ;;>@{*KlAg*`3KcWio>{t1iLE5yMmQ~dz=2Z)PD5+T-lfZns| zefl>#;(SB^xL9LT^G1<9Y9UaJ0{B(#uv*^=o%q-=?87#Nq-Zq&a_<1@W`ZMK%Yx*j>4Q`gr8ZeaAB^m)cxL zBVee>*7&O2_TfP`4;CTjuZkq6Mmm}y90avS4%$3Kz`b1^5?`P#uCTdM0zY zFButbb4ZXB#5z_++G4WXkd^uPC`2rGW5yF9*WHF93HT5f?09IAk>Yho#5%yo*nF&D zDa_!X5|6hX3`| z`wYsWpObJ2)#G`Z&C?lKo;RTnGqRN$8yEMcdD%4@`AmzE=N0tBZ}UtMv_w7e$9`_4 ztwQ*0o9Bo<<2uksEo1&28N7`u*8(q-~Kk zViURWn(31Smti`cS;L-z7@8ULbAnSA!`T%=5E7F!12#?oT{!aeaVWXOrZ<51U162Y zs~H6N!AYhcM~__e&w`49B={>fU(A_5?a98f~a&AS`uA!Vrot3$;hvkl+xe@othMu8&fz$-#*f2%ay%I8-yzz&)8@N`OS2h^A`NWh z%{Jey`4(3y9rxILFW(2(3_&%-lASPyxw5JV3$oP)v(@JNWl}*`XGZ)ZXgRj{9stj6gpuh>ak$-FR?RX`QP#Cn$ifE+LeNcXkCRH?1XlmpL=DYH?6nB3>&B?xfg>=CI zqw`~_HaP5w>L^S<@y~cfpE?Gs*TRhVs0J(q z+SPP5!&1mf`&Kg7pg`Vkt44(^G%q5iCcB!cy03^cns4mu0d+ZEqP1*wH4_Q1YGNF~ zvU%?3Fz!>c!NlS|@&&0aQf`Fvbkj}bvOdWe#HOTApUBa0kKwQ&w+HZ++ofkp6hh$f zYS)S$vl*cVKF7B@1(WWOIF@Zghel7k`nq;)im>g@cKn^H3L6&uU z(ekZc+l$<7YUFGB%*(XVZ)!ee@wK_}KnkZgea9f%Xn7Q2v9eFj*?W~SO)fR$9LcOY zY!#Ocrt4yu@7=R-53VE#I!>xCOMwG9QRs8`e4OBtwpytW)$=?l0JDu7YxTRq&JH=oH*FnDyj8lDm;Dijok4a=AQ2!LN$=3VAAD! z=k`CcYi^}@Su6amv(?qI{VcdKK_!_Q%vLsH^goDAPT;*_v(0c;=QQPdw~2>lM^jVMNU6)F{7WjmeEgK%HXNw_Cr5WrO{7R zHS`nZSN%k#RXjH(o}i=gHwu5pZbyfr(fBO{!307K5aGkk zP_-T*Ov1emeg(xOzzPz(dM6!My^D_DL1PccPyh^NV7NeI5ViqNVLAm*rc-I0iz|O9 zRtXZBAJvr9r)k2(T{LA@ZJK7(8~B}7Z{l}u*c&v`v|yYikHzEi(0Bf3GPVcJy_6p` z(-hfES2RxgoFw?wkGY#EEyEajN$!>7dO$EE@nMEbLcha-Z zI`$^Ys};aKVEVamf&MM@go`}kfG1r1I2{`<0ed4{E=q%?K>Y;mmFI0w(0&iBqMP4% zoc03G4X%EYHr`~m0SK)-bZh_Pbo3^20JUEF1R}oTCQNq`n z3pg@#Kjcwb^83t?Q-pr$c-<+=A_o^or|C^}zWq3ja$A1`ZDZvRf*6yge^duc)AX+$ z^uaFrXfqw<372)#AdG+Dl%?sDJiz^|VvK>|L8^ zs0^dI5r22YSM1`NYcZ#r%KPis?cX0XAigx8C>t-}#ojP$ck#(VbD!YRHnH)}%~X`Z zdB>fb`10L6ei4|M7&LbBWU&JPVq6r%4fSQgvNX>KJMw6{mx~(RwgO_L&&6%zTrK`D?g`8!;R416KLB>V(gM)+j(tI>KHd9k@u%3|SFV~mX z2FugDvYSrr1*>wwM$`reXT=iF$|RoVwHn@6%XkW}L0JWU$Ky%!rP)2qSL!`A#&N{t zYs96M^40KZPJ!dJqq|>ockzZ=V+Y?X>Tyb7AY5PS1n#Av1H-$I zN~&v}02CMqEJ^dN7%IM(j&X{eqWfq#EPOBdf~C-Dts~w7K!K182X4w`cu26c_5muZ z^@#SwUq8cj2METITZ1u0QY=~&!US5XQW zx&U!89Y@ztEp8}}$4z7%JwhkY<8&fDg_(OWb03YR59wr96y|a|g-6h-dfIp#gltl|wAuUp6v{((H^VA4BU)9kCY64xTzC=q@ z6GhZQS_&C7t2Sy;mr+z*OUu+vv|Qazt!f*dkI+RbO)J!Qs7?Kd+SN~~L%mFK^(u9$ z*C?Uhq@;R>QXUU=dGcwcrXrt#ty4A4gHlvVkH-^$z zjS$^ojHEk_YP!o9Lz|3gwAollUo*Ps9^+cN*SMRu828gw;~~1=*iK(Jo}q2V^Yno6 zB6S--r*9fB(}Ttv^et1-!=^>wHjC*|vz#6?htYQPD0#ZsGS8!@ z%_X$kjM8__R(jS<(H`?E`kr|+ecybDo--e%ADBAK`DqE{n}S^P~J2a+<;B zA9y=Ij-;;Ae1)GtPUEGK=1cq}?9-we^9A04Rvuk!UX1O$6Bhi4c_!}yJ{#7zm7hXi z0c>zHKaHM3SmJHG8?7Q%w2;5U&)`F(a#-DW(JJO8#$Eg@MhkeEaVzhU85}ci=I`kl zZN>)vK3V~e8`tr3XccqHSjRs=s{}S$#y`YrrTh+G%X@hrm8y+Y!TT{<=J^n1ljr#W zl^PcAGJeEArZS@t7bh=Z)PXKP;-Bz~Gzc0S#y{ns(O~Gz%Rfh}92%>0`=Aq({|A;{ z3QgR?zksYt`InIGG#b}~ZI)*dTQm`WjTY5ew3K0$WZRR`X%0U`V=cPzbnc-usc-;D z3}WVujMVW{`AFws{8y0IgM2psbD)WOpt|DE8D;$sQGV%Q=MbH2(SfqTfkU_mC^<-G zW$`GhproqwAQe=V1y1MCXEfeo{MX2b=p>65RE!EA0uP)xNX9`L(gW}EUlwAEAM_R+ zqDrv805tW$t(E*o+5kxK5hAam;l=qVAflM=Cd%prn3rV#2f5y;ag(I#nu^Mu{HuDS zo4!;T2wa-WPtD3e(WR3B^<3DC-??GS1^rFE5j6HeXOGpS`L%IA{QiENKWLQu%Kh-p znX#Zz^8k&-*dLJ|?&dcap<@rfndY~4@?Ql5UNHKPZluLR%8WGsE6pEBo6kz~L1e-R z(~_@Bm_{%|nEniKFRzyz`DGoJKdUKKoK~KCZ_v9J5!t|N(3@6B@oUm5KdlNe0g0fk zSNKI*l?08)z`YI;v=-rj=%;x58LZ*wG>Be8AovBG|1aU}e}yy7%Q&R`8W-5Vp=tCA z?CVuJ3ttN?rr+V~rQcJ8{y@v=b=->nkuIk<=sNlnZJ;+1c;2F|^cQ?U@;2?IztW5J zH~KC8o&HS!z`ySxWW1}7GizNI}tzVn5(y;2}){Ixe8(nVtWBo{vjF!EAgMspV3jU z>yN0+TaC_ISn44fY>}s&loY02>#^Q}n+hSg?(1hKMr9y$q}8Y)+G<`7ThPHPVV`4# z^BiB;kGHwue8)1jknQ-4ZR9n=reorKP!aZbleZ$AS7EfQcdYgCaklgq=vSY-!Zaq< z7X}NF)!~d(m{yhH02Dn;(ikUyml}aYrc@y}my?-Og#5(o!aV6h-90ZB9JOOdS0a_^O@x?SqTU@7MN(rRqI6Lcz(oCck=I<#YdC(vrN zgv*U^u$!JT!b7_0K~H#SH*IptB)Ko&r6w5Rif%fg#uJZftFu;4If zs8iG$9PEHIG@PPcYTCLAb}Ag-ww@P*j#E@2iL5$PXQ^1V!U$J((}|#5&;iS7;bFlN zXPBTH9;|RGHJ;#bjn_q2$xfx?-`4Y!bwe55fvr8u)(v6I_ipQ1urA2JV{Pl1-c9e8 zs##fntpWJ;{a^X5aDrc$Uy$(M_^ou&eIb7H{v*E-?(}}j&$$8)Qxi7C`-r6fMy~h) za>EblSo#Rw`C|m2gQz!rf{<{CCL@M4(P#MFtq1uS(Yc7X7qEv~*`QTy(k1Mrb#S~l zu#a|d9zM?U(>~6}hfg-XSSp}jaUs3RMf5rc@bOMDy~m~4_GS1^#leRogSdnTb2e@oEWA zP|J9tO7bLiF`uS3;{9&SdjRtv#Y3F57g?dPxrQO)EYKxi+I4{+z&1xQ67M-MSSM$;G(JAT{bv9agG+u2`3()e@ zWOa>pclpR=FXW4nDcN+1Izyd<{G*W8sj0}=kqglc$cXU;Aq8j$xRt!37@GK4Ed*Q% z^zoj~#!8`;chw@mmBC5Bsum+db2vuR)VXL4g7#lj=fP?%&=M6|K-)BE1GL~4?nSj+)AB)%mXAS;tQJISkfBAHSz43>T7=RHH8M+! zs?E}ZdE-^9(4v|_i@;T?WR@0nagG+r#{Naa^G`<_W3wb%C(Gx4l47{|ko-zz`4E+R zdT^j}lbSw*9?5|OUb3s%6g+%#ZX9sP@&H)}JMakBLr68;zal=aheDDH^!Tu&ZLf?BIPKB9k7nV zNZ=$2n|L4!OKBAa7YX%q!$=4-A=!6$NH!gBg~Z~#V4fR`ah#u6FFC=C`ut$NV>&tn z*+E-}Tw6kJp&Ou3bL^%|5tO|~3*sy4kKHsfXgC35iG)E!cZ`{>h_V4Z98+Ryfbn8& z?Rh1LOd=ao3xWlLR?3%x!S58i7)pWxR4ss^L?d!B6th!Yv9;&bbww;oZ|%7vn3oG1 z$oz&vAZH+-IunPhnK&vnVf)Xb5jgrc;4`c<`7GpKb8#e_hws_v<6HFw)XC@2N?wRB zz8B$kW-&s=xpW7_$*4w=r7@auYeM= z?iL1~$r^D5lF1q|>bhGmeWIPBfy3haBu>%PPEMSn7qq{rVrW@3f_|?Zst-H&8SNt6h?fqvt8wB^6L7ZB!S*g%r{nx>`HHB77ZQqn$~B?!a+T z++Hz`sI}VF1n@a&wH_@-eqE}aU5U;|zebiE;{4goraeZ^XPiKRBL`e(k@XegDhfFy zF2X$_iN_JQqmkrbGp@n)3xM{05zM&nT7hDUzPjK#h8dE3D<}cHH zPT*^hB%P1iQ!95-JFlb!uYw`2)kZbRWhZM?v`T#i-XB*{s1RMEF2zx#9JQs((DI;8 zHdmXHfhyQE9bZkYz9D4v(COqAEvd^b5k39^sl#Bfa#QFE4+ki(KH79aU?>%j&y+Cx zmx}uccBZaU?o?MoMkr7svrnt5P#DeBKr`0`nNo5eCG?rMKR2&ebjQH26JJ6WUy4wC z8CCJ+DA`>>r}32#*HyRzT!;V5;~FiV5ZES^1l=M}6_shBFfBn#gu+??2C37=!cMMH z>oct00!9tY9Mz|^-vryZc2Xru@s&{iePoG7D^Z5;C2fdkgRg^DuSYHB29URbLVP2Q z#20kp0;*l4Ic<%CwreJ(@6lwfrfCEM%CV?q;L}?giXLJmrjO m4n^_5_vXzno%klt;9K=-gIu88YNq`EU6{)17ED%ZBAb94ljI{AnasqQ2@uh` zVR1*XE`Xro617%AVHlw*Zs1aDdv6zO)v9f+U95Jq_Ff?OdCxa9nJD!4yT9H)F38OJ z&Uemx-sO3p^PV%leq`@+L^PG(Ferz~8@#xpJJiz=3|GvL1RKJkK(xb*8f0fGxHzyX zP!S4*J1XY2UTn6-4RWG)U_uM#HHHmxSzKhj(j|RgXHB5PRk#H;?2*(!(LOrI7@|cF2DMco?LsTH#W-<*~w4mXl zmc}Iwa~Fs(7Zot&XU260*O;LgdfLqufu2yjJ~NN&p>(QuQOs;f4di7^wzfcssdz3- z>M{edo~T(-7YGGgqk(uZ5{7D?o^UW8n;U2~F|NQ;KCn7op?mP&d1ep;ra@9k>0%~( zU8LRgkc~zw^3h0V?PPcwe>^mfPB7?rCKr}!iJB{7icX}HFt5XmGYv2Kx?k!`>)pgO zLD5hu6al3&rR?Z~mz6G)!R3mEQ4yRlHyAdXd%9Z9=)yp22+k{<8-cBh1JR&7r#kKN z&LCViLhS!K24J2o+7m`>kD^`aVh>HFN`p>ga$tJnS&B}lGhiSLYU~PhV9kP(Qjx5m z0F_vchpOpJgCJOC_bd7a!G%uD1OyRiYo5Rf&zMz9GYy)7Xkb>VS5X}yuFhagOx6Bh z=Av0lMhe0%ngi!8gn|CHNLN=N-0qK>m-GaqX3QT5`vXxk;E$Ohpc;`DTVTN1G#A#G z(P#w7op2PZzaAy=Lp3!knkVYyMOH41)I)0AgytSZ3cH`(G8Mypd zz`>AKYB#72jvq+Qra-(?kx45cKhO%0$g=g$+U_)o61ipV4*HA%d8iJ~ZB2`3EE0LULCL|j};J&IPzpdlE91X>l0^u)A}TWO7= zUb++>ibi_E?Ut%%8FU$tJz$W{LsqHi+!Fg*p9=;V|-CBd%la=(Yx(v_0I zQ>1mZqHAa!)C+WXnji6J*qIlLko=rSwjn{AOOY*n<8rXWgD{S118_iZ3~ ze8N~bbTAgt9hD?h(w&YR6N3lrcmYYyG}Uq=5WgTEXj|D7=uY#@SD@Wky4aw5!AL1- zEs|7o?NUs_6 z>L6ON5T)pKdIO2LN;chj$n){pf~^g|<|vSJ(VLj%xBj^3Ej(C&K-whfM~eP~Mgl(2 zdtqb&fH*(0TD17FqMy)DF*s^=i_e6P>gtXnU!=UP=;s==kkVwvV%Sck`xlDdrT5?~ zn3w7nF)1OkFV{u7x+7sT9Ir9xmuW)FY}kU84;77|Vv+VMMZc!sFuB1fjbVHU1(h^5 z!j~@kEu!Bdm4aVa05bk}iheINnhT}sI?c9~t&ue@`XkfWhH%`B`om@i%7#^@Kdd)i ze>~!kb)v*s>83x?=LUV2txsJf6hUhKnf`(#Zx2LQ&W;9p5hW|dEmJT837(|Ci8ubv zG&MUwsp@H9{HzG99(dFX)QBnnKuY^Pe$nEeivCMhw9Rk8j+3NAiVo8e023yCwQ?Jo zA>qMqAkzar$T66$a;7QJ7L6#j3GgH`^SWhA1@xF5ik)nLC3?Es199`*Ks1b!57e8H zJt#>;G$q+Bd*(=K7CPo%p0Dj#KB4< z`87u1#&%u(Hpb1ax=xhMCMK01!_P5ycBPPHmg)#8(ac)^>(7MmV&c~%edU&iP`Vc>M5SY zli?+JazUggDjA$nDeH_Nh~<+NpCUF0QD(DO%-~Z|{$$o&h>&PnLjlI8DX!$x5xe#& zFn4$fNX>GRmY%}9ho^9r;%Z?B134=aT(sFk?{k%`JWcU*U5N$5Ahw`zCyHR;73vN< ziq8Srwv_SWV{ZMLHy5j%37Gj3fsi{209?c6CFOnkAfm7WnCp)Jf zS)u=cDJ^?Zn(;El=kfW72NYk}+YL`6{ji}kU#R#Z24$v?bErw;^P=+*nXgugh;_;_ zC=P%j=2pdRGS_L5s4QeEUcsOb!Qf1(^)ybh=wIX`??Pscn}fX4;ES`-mU*i8=i_~v79h6DSB@Zl<6WAJJa z8;s~qO|s5C=~;?J^i9QA@Rh&;`ZbGN6_Q@% zYQ@(`6>MJ-425LbYZZTs*CW7^3!OOJbVj5|aH1TM(u+}jo+LV1Cw$DpPF0Z^h#fjp ziSTta+QZlJO^R>kTM*4QjV&lxswwQ~W}!mJiko=-R>il8FL{kT-LClC`l;98JFVJi zur3YtO&-2OmcD~GDc;POs=5Lz%~_#HAda_8)&FY!#${TAw03${CRE_Zt$eM)+XhEw z@F0oBeTwhbr;D~ockhfiZYElL;wJX6T7E$BgZxm6_~OyXO5nRVyXKe~hy~j~Pk001 z)^eZXNBB`pKuD}^z2Rj@FMGVcG>Ge*aki!qoy8p)#Ma*O1z!6buhp%#IQsN zHLi)QSb_D?>P@$7W_#CHBn)Gyz?I3?VSBtfBy&i za)bF<#m_O4)omp!j)HC)$NLq(psyoZQSq&uiGLmXh$gXqN%7143Nk1j(Lo;buYxRY z%4uzN)<$GOBCxi#2h9q~LkZG8jLkiDBvYzwN zhbtES4!0-z8^;O#jZzWc3UhItgU?ZTI*06N5sfO_L&udBsv;VbB>!$2UsjlJOH#>h znz&O>o`lar3?|CMX94~Wg^)awOCb>n5mZ4ZF#*Xf*Du#=y$4xE=D-83@b_ zI-btLn-07Q(MdS^kH^V=0$oO>w2sPXD^1iMnuE~63n>pt>vkP9J8h6^(@{sL_i|Bg!1eM6bHlPVvz-twhxbV;CGWr$SwfOp1d zD<%Vo74Xl=@Q*}#3RTjnRD)Y5!9tm}p0)~)bf(rDsR3KWz8sAAXsc}6mLhT-q7kBZ zp=EpVf*!53OJf&3QRVR3H<2&(WPRr=LZB3pt!!U`XIT?zWcrwTl_RL>vyDf%=)fAt!hXt+ zcN;a(12l_v(rkK`=Fp4ONN*r7eoS-eJ!+y)FzZuXD*l~XKos-YiA%(xw2(*8B0dh+ zgeTIuTtZ8@0#}1mX&Kkhd3-jV&n>tST#S_jSKUH*yd<4LXi45c99o-^EucM~jXRMd z;BO>PldBNm<_z}paP%3tlO2imieTFed`o`0QJ)rZF|sNbmVCxO9tkGfMtgY_5Spja zIhE95+zQ)jxgS{#Xfm?S#x}-(M283`^W+SJ4j!V@@i}pLL7}_ehI;Qf9-Tt(@z5s^ z`hPev!DE^y9VCPMD0h-A!N>3C3DtIV=T|u_U_FX};dvNfYp*nrt=r2JTmm^z@+) z75tA@Ezdyc9rP~Gmy3msH@XMKvH73l?7J9c>PN(t>Ztq)^fX;(KR5&uIo6&Pw~c(mqKt z98qi%2Kpkw^9GoDI9x*13p`&6%&)~Rbs0GNa_!Ek`rXuwJEx}Hsi;J|E(bIG+`{L; zoyX(mQ1(Q-cF8odAEvP|)!;dj5qacD`#`!Frt_SB81j{FT(8W5#mFk$IHv5}0GXoW zl#-HL)`P6wvPW;5AxpWOs}Vlgawbj~ zB1)C*C`<6tX%loAVIK`D9c{l4nd7x5`GOsyWG>qABnM<*d$q&o*u|ZdPWkGpG`vo1 zP@RtD%PXB;Bi%IiaraU$I`(k1+U0X4xCb)q%jI8!FZH<)`^$YU$h*qt8ZyL@;C1`> zx}|&g1{r%}wcF>Gr93{*F5V!M+ADLVz0qo`GFg0sd~AfceS8O|-UX=;dp9<#ydhru zdI-HIEi^AB^q`pQb7S3meQqtoYj5}FF4vNzsYE@z-hP0T9=AQg5BuCZ;A3kSxdEsl zx*jgP0c?9CP;wIxbTbn27DQ+RDx+JGlDE<6xXYi0I;a6m-U2Q^5A{JODugbiZ7(W> zE5Pg5f}L*$_O_s+*$!@gg6^fgbRWG+_tV?x`3U>Y=ky?UmWSvc^e`>}`w-nncq~1N z9py2NgmuEdJW*F4Pz){EFK{QtWB4&hwo#GZ5w-x4r_IvY>>*#4IQrMCo#hV-7e=Hc#{haFX5eNDd@O>pW>&Xfz)ji$>~Dj z#_Ux6V4FNtI@n1JoGkIN3Qwu#Rzh**k=q6fk6Z{fq>Ni9@M)08XN(5ZtFxBdSLgubVCIahGO>W{hiJO^=aBsPTss2(!Hr7FKbzqnH+0!) z`A1x_e41F~SYBzDL>uk3OP)E(>~={m$585*^m1VQRb?2c-H??h4l7S^o)~3czP^a} zW~Yi%QpNBYR;n11hDcy6x)pmPzAaDt3~`sw5b4spVY$5>?Tymjh_>iq3^=nHKXwy6 z4+rcAZgA2>73HUwaGrP>4toXF^#N2}2azeS(gpMyw$9hFUA}>>^83h^H<52YK(4)o zy!s(R@+0l!^DU=joV+vR_isAA(^&0*Czy4Et-u@i$=DkHN6N1;c)Vl8yOEP}MG3X0E9zV&H(wC@v{+Y$Lr=>B+r)Ai-B*V7jG!BJr zk7U^P9~rj&Jj1qcX4rOfhHY=puSsIrq-vuRnAMKm-1(LJ8KGSxIx#V1KunZZoujkG1_IiknzvoZq5HBIk4%iKn8kshQa zdUeyZ(XDpcO7=F^wEHk03si&z7EG(yo;LbT>36e{G0;CVKKr2UJ7nV4JKtIs%v6ioInR>+3CenfykDGX=KdsKx$x+!8XHo zpBZl191G_R2@hFzU)Zq2lFbg=Ln*nM@!=#?t5_~jNGD?FYpB5rPRw*Vl@?e&hv!F& zMXiQas1qoo$+R$YcINDK+8Cz2)iv9szrBN&j7(#}Rg;sGDoz%taKbuhTZwvyx5>)b zKCDK-M>IuLoGRd(7SwPW-X@?@|0YIRpptQXLt(;!|v12j7{X}w+OYu>R7BMCV97LxX`5=yJR?8HMHR(fnvJ9o`{HZ z=c=gy7(;e_3u}p0l`tEN(Sc4C7Zdr;R(Y!$w!+6s+9kOz(Qv6V8Ik_=4Y?sLNWULX>3l?NV0x4{y&9IS3gi}cedKqRYoRP7w4JT8q zwPbJDPKB)_?Z8yAL!eZmuAi0CY8yQ}eXx*_5%14MqFqHwU_O}!(y1OZlc`@nY@1u$ zaf%sAA|W5Fvy4oWHzUg>Ay*nOsNo8!sf0w?0ckL)m>?N-9s_8LhTQL#s0-gHvk4zYgFO2j3Ic1t+##&sIr zhwHhkF)&~z;{po_sW-qkC&eDH8#ie9fCRkPPPr;&cs{7%L-;U>gp>nqfz@*r>5Py$ z@jVhVb}khk5m+>XW4)C$(!(=bwGVrx*?V!b1|1(GlAIV#r^-=ttAI9@K(_6S7yBe- zI>$u)E%><1=qCi09>vaCNMbU?Q@i`Uu z<<7etI`f${d>;2xF%3FmKLJp^aih~r_Tvj0zK912GB%Sd%ru*PjT7c6u;;65bnKbx zqF!W)p^K98NSjqzZrz0?hQ?*=tGswfAY2!2PNhwslb(JvZpl-^Dcz)L#?5%0B9o6<_O-Kp|t^D)$lc0g0kASq%xM9q4{`3!#A92BVbu=UOXy$M=H%`G9QOEd<)-Z zm*+XVA=Te+$im3$r&Gu6Rxh4llXmRQ2iQsYu7)R3MJ+_g5F3d-$lh3BqhKJ^vtg%ydq&*Zh%+m*&He#- z8atILj|$hE{1eZ)XUIOJ&1^#w$g+LX-0obvuXLsJ+PQRx_llmK9aDqzh^OS#hK0_W zb19nHXelLVj?&FKSuhjjdF_>4N;mCVoX7EoULj5BcBnktB&!QsSz%OQbD!mof@2mm z+1=~doYlnDVtc9$@a)UK)Uie9E_N&Cit*YRIFNmg$9v-$)L81AJI%&`Bdp@zc^6a{ zd@7zLwpvqz=^pbOOI|1|31q2d>S9|bL53wD>iA#CC!oU1Cs54hB!4B&Tj4ryseG1^ zR?j!P7YbL5!T$gU3d;E`a)<#0Io4bTRG^YGA_!G{Qdn&w-#m&uJC1qHwU42sHj71t z2T>AS5zKSQXx^oO`qP|s~(XvC4(t?OKikm}a zy0#`-QX4AC;*=~-kN88r39ReX{bM*Yi}m{u%p;uBq5JotXcQOge!X}cu}Coo-f?V> zc%_b(Xh1Iv1s+7ZTr1R7Io>A6s_t><@^M+06!7Rp&Z>ME4y9<1CUi|NJutZ=2TrhD|lxTh;pq8I6^9B8^%FP4MS30R$aiLQ-dXSA#!;vdES2_$o*foLEUD92DsU303OBHPQ0gP`*|i z4#~$m_n{;d$l~3j@VVrx_XLF=)GH25-We*Jz-~tTy}Cb(YqNO2#E-d1QRTvAgl%wT1 z9xM61R7c7hmSH1Kz-FAti|$hF#4?QVBj+k|U4v@ezz>|8umZPJVidJJe^=pAO8$^C zzvPwuk2o3spq7_#DqcfGcyO91 zHP(yku|e$Ro&Q$y9K?BIoN|xw*8c>W#M5XNFJOy!5f_S=&>~(%tHN8o;>ShG0<rq&xpF1Cl-+1iZbY|o06ogxh$|0sd>Gr6C()}s zgFe|aL=$7czL4t~a`Zhs#jfE|p2GL>G`q|qWem^YSvIP$@*sY|*5tub<$gTJQ6ZKq z_c0zm_Lr5)J@^qvD(aNG@H|IetWgf)#~c--QMnC2!B08f%v;0H@N+h~t-LnBK#HFc zUW;EarUASrp2x39D-j;?6n@Q-CQ8L&{Dz}au|Pb8-{N;D6N`9(`@NK9lyAU`_yYor z^JYhLIitPN(X2Dxr#PCc#1o>J8T%6@Uck%vGyXz}7y14x{)Te#GH!MB1sPq7`u>gz z#yHOW`cTOzyP9>z_BdA`qq@-9Ey&pFE{#ZVyRhr+KPme!#`sC-|3g5YN%)t{sYq_J>L{Dg92Gpq$+86c zoZ>kal^;1rY~F-<$KX9;y~nmJioGgsQL&$%RlSaLRoqKI a8(t-7Ckg$kw<0>_?J`!!tIV^4|NI|ln4w_+ literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/RoiReader.class b/bin/ij/plugin/RoiReader.class new file mode 100644 index 0000000000000000000000000000000000000000..699f84ad273b4aa33a2a86bda1b1aa7d1b1cb3b7 GIT binary patch literal 2812 zcma)7`Fj&p6g|@>nKV2gmQsjR*`Y~kgP;Nh7Z#OZ7bse+pfaRGI+)Ic$%ImIK~dbr z4Ylr|xNoRUkt*sh`hEUK{@`-%Oq;gA=jYev<(-@R?z{Kg^WOX8@ADS`T#H|IRB4!H z9Y`1QrJQA_yBw>_G`Jo-!;v)G0gq-qL~|q zVGUv1VSciXS@^zvhTSVz*AVesBWwC7O0>bUE$>DR)s4w+ZnD|wHDjp8e1&;vs9`<} z6?8;pwpd|WFk7lHJ(w+5m>JBH3ReWPCWTqStXW}pFiR`U31-(QB*2|#O2Qqb{(Yu< zhp{hjYN%~&>sS{w z!E(}_1=HSQ8F?oc1(!_|yr8Qy)6&us!&=-R<&uTvmkD&a(6Z)cg^k#xLFG(uizQui z+(8X>jmb+_i2oLaTf~1F{cpAMW{1&Fsx{oEuvPZf9p=Q^V%UZ@9k)|&eo@Rm(|OC` ztXc3Z$5v>UATb7MH;Y9hXGYPf0Ws{rP90rS#28Saa0l*WtIdNYBQIP|E;7*T`sCd@ zc4?S%F^!u_R-P)4p&RpMxLaX2_Ar=HV79#)nlBgZ5(37;td|h?D%>X}L_H_SFA9SN zu^9`3BNamyrVa)Vv+0>_3j2khu<5#v%a$8%PQ-F?s%&eO!$q|sl_@P3o%C8<8nVeb z&mKRsgY{F%703P}qfg)I8QJ^Wjl!4;V-^03LUd8VLy0Zp@hq90`!y{4Uo`mj4~W** z2or-s<^vaTI4<8y>Ko@!$AcAFQ!ZP~EYB=EbOZx3Jfd(EkFrBl2!GZvgMuU@xsoNf z7E?HeCv-ec$|tM}$MK|w8m`&ol3ZiwtrCFX?!ZhetXrUSd~x8LvS3*i*TTLt1=ZW9@ z3a9*4gMvO#_)t)!&v(%Ak%qZUtKd3Wp0M<$LC@SC%pKBv2A@a+Kjk)!nM|@~q+`6F95zExuzy#jy^~Y@FOc|DG-w zLQj=DN$(R{5E;ADdNp|+S>h3>3v@MwMDe%!Df=1XUug%;RaQa6y#FYHD%$Ck+^i`# z8Urwi;1IV+udtB!TQxsn)F3YJ2ikNRo1k>5@GzF7 zBEwjg(udKQiVh={iW9F$#c60s#c5f2)-P!l#~BPOV~ELL%kR}hDhNz`9g&9Xu_oA5 ztEZn%+pbZp?Wt{T8o`G46=mGi!C`Z2C=vP{p|h#xM5v5gKgR0Ta3WkrrZtj?{D$d? zNNqa+?M$#xlMr)sXa7F&Uti%+Depb*Lw2 z^EftOF8vZ{#Fa?Xaw9FB`2mdoVw*7@U90}ro(n*sSkwFjAIYhKgTBK!t z83995Z__!rBRE8&4zuKrQ5@-M9>GIGS{V-qOg+|07M^O=6Z&sRCiF61NJPHJt1=Aj zJ%xHPy)EcH8BXs#jdi0q(=*8_9wU>L--$->acguGCwf*Sq9gcBVwdq{LKp785g7zL zw~!fN5m{c$)R*vYDVo@amE>VJc{z$CFUu5O!U|qs%|6F_{F5OnUU*-Gs(-kBREHKY X?~g}x9Mf@9=gmWbYWSI+Rruv!FAQV) literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/RoiRotator.class b/bin/ij/plugin/RoiRotator.class new file mode 100644 index 0000000000000000000000000000000000000000..9c1760c3eeeccfe72c19ac7e6bcf1da012b66c31 GIT binary patch literal 5176 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 Ko@*9}hW`V{_KVm6 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/RoiScaler.class b/bin/ij/plugin/RoiScaler.class new file mode 100644 index 0000000000000000000000000000000000000000..c1c66c5eb17eca7a29a666da21b5e22168370a39 GIT binary patch literal 6666 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 NsFBSQ#;}DQ;0r?nlm`F+ literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ScaleBar$ScaleBarConfiguration.class b/bin/ij/plugin/ScaleBar$ScaleBarConfiguration.class new file mode 100644 index 0000000000000000000000000000000000000000..827fc160bef6f751f5fd3caff5c1131e5698d8d1 GIT binary patch literal 1673 zcma)6S#uOs7(IP6naxWGAwd*Slz?Fp5=2onF_6VE!GQ!45*D$lXQngLvD1w`JrQ^_ z|Hx&q$_IV%$#UP97q!ZL!}Zbfe77frRD7@>`h5M}d%t_WbH16^|9SZp0Qci!3d%x{ zyEt&kt5;lqV5Vey&Zr%BuLfg*zu;Etp&hw_pF+Yy`=Y&U4|uj;8JH?AI;DuIva?{< zy@&@V9Jf+s%9^y$R;vcjOa!5OHSi7kP6`ffCAp`~a4oQ1?#PT;DlaUn)A`tX=eYyrV#--kcq}^4&}( zhfJ9)TChjsF6@vRdo@x>OLUJ$2I=Ih*XbG`N%d-Eaf?I;G;|ydXlzC@gL`pb3Ws>r zMq@QyKTO$I<)U<3FKiS{>OQE^jxAE{A&rOSRMKcylf@y8j(Blc~e*TP5k$C(&Ym^|9|)X zxh!uxtucdXG5b>4j+}fLEOA(t=%OcN<}^0Nw9aa@iJ{qy%gAxK zCw<=u$2_}MbI84Gb$Q*d2)kLhg*I-XgB%iLDUCKrn+?)igLKv)Sr{Y-gY?-T9WqEW z3{rN36x|>tH%P$^Qf`A3+aRSTCiuP`JIHG%zxER4X-&O=UC%L4xRdXsq}4UjmyA4| z)8>)a*pM80GWQbh&LtY?&b2hKFV~5l268d<<(|i`2KosH6NegjfWeU#27?S9k>Hq| z9F>#f^2*ESM6SK9f#dRBSmQFv(S&*f3H2s2>MgXXx6!5E!A|uq_Ne#Jr{2e5^#O*} zhZs{IVM2Y36Y3M3R-fXm`V1G<=cW%Gv&KnGH9MMac9dt2(q-0k)O!u5)^2YT`_vb_ z{Y&2dm8rBX-hHD|7PG7=nsC0FHUH*0skxHteG%Ov5^_2iWnk3TNULwquD-=~^&NJr z@6n@vz(MsRj;Nn7qJGAt`UR)duQ;c!!&bk+RlnnD^#`t~Kk=;k>n06WZfLO5)Ii}J TyH+@l3!*61u=ua?1k(Qko9|Lo literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ScaleBar.class b/bin/ij/plugin/ScaleBar.class new file mode 100644 index 0000000000000000000000000000000000000000..46ebbc4baaebaee84e5642c03bf910dfe4314b01 GIT binary patch literal 17486 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ScreenGrabber.class b/bin/ij/plugin/ScreenGrabber.class new file mode 100644 index 0000000000000000000000000000000000000000..6530e9a666f23a2fb817a26ddb59ac99bdeb7c2c GIT binary patch literal 3592 zcmZ`*`+F2;89lSN*=!~)S<=AT04WM2B+DvdDw{ytgj+BPDS=?(jmhq?nRGL=Y-S0x z)mBAYTdh`Wg<5NC!6;Nsfo@V#>m6#n-!FfGe}F&v1ZBS$@bg)?K=cPBP&ZA zHMGc5jlblT7()^%6`L5L(w~NRyU0w8+LNqaFXjZ!n~EoP=6GQ<<1NjGQ3f16^FXg& zaA8+*w}Ltb$d%00Zo@K)W@d+}=j~h+TS=XtW@DFEWDIR+7jo*O*v4Yr<;JE7BV$|H ziRQGd-=pDP+(%nIn~&}OzNh?uO`M- zaeztPh+SvN%x8^a6x4FBbla&@PW54a4{lb)y#whQ*{B=<+5ad}ma_N7ybY zG5nYq9#)XN1sea~fmn`c$bgZej_pa8)KP9X7QpoSa7056^*)}Y+$l}Sw5egt)z64w z6chwOs}hl|f!#j1v{yRIF3SJAeJ(J6dG!wdMPrv*9Oy@KNl>J)(bZIfim=9e^l z3omB(xZO^+4wz1! zzhPJLOBHWX2IRyes#mx8v%^k&8^7|-!Go50v}E+@u< zm+4*-ye{-FD4T>8+{K^EAU|P#Vsg9S)h5Dc6-N{wN9*}5f01Rei_l)>TL8Do*qzsK zyE_Apc*F2*Qv4RH@WvU`-e^+onrPkzEJqzT@{)jhJ|*CJ1_<)IUFmI!uP7t&9JH4B z8or!H^~N&p>YYNOCBDv|n8A8ZM7hx1+j<3RYZlif2 zfSVbz4J(kwN;joOFRY&u6To}8hpb>L-pdPL0PmwO4dHpzsYt5JLc9UWCB#cysU?ab z?s#|(ZA0<*&mi5?67RSG?Xh4P+gr-$cW99k7RT*e3EA1>pg4SXp>pM}w&oSk4%F}NBcaoC^*18G_tVSc&U@f0bamVR-*H~{%#I%J1=4$Jh)7UVLDDJJ#(;z zW>Dyz#nD-m`dX7^Jd_S4LRYYS4#}bTBZ<%qKG`#cP;bjL%>!sgD^B{Hu4a5N|A5@w z$Wbj>T1keQ$x)KINcLO2P=59S&VQD3qVY9&43E>cmNuWm6ZE-~?@!|MOi%b-j=(hr z3#d>?vOG?TK6y3u>QLq>ebMO~v-m7gW-`Ee)9vhw;2-bhE3w5p_|i4dLq zsI96N|8X_qf|!maVu^?V7@b0xqtG2y+;u1sy}~vr<7czDl8y@Hp!+DIP70W9%LgCL!Y1!&wY=eIL_hM_zel# zz~9#2;&&t{$)Drj;}6Vj3;q6)q=gytewV%oBlo%Osk-rn$SAYu!k_SGHvi-N2;nhC zufbm^m6%I&o3wEa8J#i?s}NWO(~j#QjiaaP5&`_Gxb@;svul(od|@60=ZfBX-T CFg
Mo$Zm<#;Q@vJyb$vE-JMlz9Q1v5ZzE4 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 jT)SU_lE@8njqN|F5Ai$tPx@Wl@g0VvWA zgFnC@W%><}cng!v?c~gv^PQRdhkthfwox*XA2&d6FGwSi2hEM@|%Gm;G@iD0 zM#xsfwy;2$bYLQH!$84-1(#ryG@($JN*tyArif054!nBU@`E!!l6pS^v)UG6aqMly zO}1r3smkL6VM_MO!$8WX;yTG=jsP1ASTeClnBdZC2M(6;kznzm6ArJY2s@?vVgfWXAiliI1O%w^U<6?I3O~+)# z-SBEZR0)^w)RYoYHSptD#Q4Oq8r&y9uB+4KACcj1aixSega45kAolvk4V?8`O#K;f z-tw4Z4zLH0M`y6MF^w7C5#GTaDQxk1&9l7s-t1q@pT89N`9#1P2^efmTltI?p0oIZ NuUO@kwxz|m{Rib>oWuYC literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/SimpleCommands.class b/bin/ij/plugin/SimpleCommands.class new file mode 100644 index 0000000000000000000000000000000000000000..33d30874fa12ed0e70c3052d7522fa60f76d9bc1 GIT binary patch literal 10205 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 G`Tif`V*KR* literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/SpecifyROI.class b/bin/ij/plugin/SpecifyROI.class new file mode 100644 index 0000000000000000000000000000000000000000..216079317bef06fd2ecd3d8162585eb2850ea35f GIT binary patch literal 6388 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/StackEditor.class b/bin/ij/plugin/StackEditor.class new file mode 100644 index 0000000000000000000000000000000000000000..6371ab0564f242ddee534e801c055eb809eb463e GIT binary patch literal 10833 zcmb7K34B!5)&HNDNoF#6nVBREFeE?%h#?7!2vNhPfJl@@4YH^dhh!ilVJ1!{M%=a0 z+G@47E(JG0+EnXa86qfdEmo`5YU|pnt+g(#wr=gG1@isReQ!d5__ZJW^4@*--Fxm? z|L5HM=9#_QcLJDX1-!5X;rN;3TU**!#}nfl($U5>$2P^&$&?o!LFmlr+0pSW(ZuTU z3s;^QYfO8Q$KHJtPUO7$gdiurwp9?A$2awBqpM@=Z<`|UCK_7ejj=X?)C+Pu;!Wvh zL0)q#zPdTh#?*Kso}SL>m17nQJhPHbF+XxpWTOxPL2zC?5nIr{c4aKJD7vyGCMcel zY>c)nj;7-J>~?z6&2cUvig( zHuB-usbg$Z=|Ej9W#qF(#g0QyiG?PYhw)di2U$i? zXmmF}+G@>JSsI#1yN-|j5k}1JZXNgv) z#|^6NYc5xTxQ#QFRFD&AwWm_CMB2!r4&7p7tqS3fwxmypt(%?fNcfS&Sza(oBN`_C z(zTHS?G$WG#iHq0A8v+M*4LXL=zpFOzg>OkZ2D(EVP+z;-zBHiYY0~092@I!F16g| z#*ARp*AO!9UD5u6(70rXsg z6Wu3*Z`rs)$=Gdl`$h(2qAA%Su;NX$SDU(wK_nO990v3?Ra*Lg2uSh?AHGM(7%|(# zQ@JtIi1HNUGM-pvHhzFXTrZYNB~z-H>uubiW>)`L;QFiywjbJ9uab>) zLh?U2_Hx{$A$*}a>Mb^I#cgy|k~F-`i>{2dC_d202L+-Jn;24N+zK;Lxw_rP7W{}h zq`BxPRKcPBS?eHT8uGa?vn(ljS+?(xo6``Z= zxAA~>D~K8z+WDZ3t=UdP!G~?G#ToNHJftR7SMgy7o2xYXy?BJs#o$=o9v^>PED=k^ z8)wI(Ey>kBJW7-~M#1)BJg&)UkhY$*@pJrw_)J`x)toerSJ#gkPCHns2ltFw?88%J z@x)ZDHI^_dJ#FI|{EEsq^ka={RwmC84D4rxQ#BAC#&b4)tuEnjYfg4JtZVlRHhyDp zu_M|xE1GDe7aIC6+4wDf=lH>b*g5H}3el(fCN>Jsa<<%$`;8mKHBQWSSxEUi>#;;l~H~n~jg~F%?YS=e4I9^(B>K zPV39Zyi*n#VEu0ceqOhwGlj#_(PbGc9PZyUR@hdIzeUGg?3m0WA43r4uc-Zy1G)j2GGfF7GDBh)^_ z(G3mKp*Ne1n+0th?ZBes*y2&G6}GoFQM<<^nr5e>9WC=!emG!i7P7lW&V( z3h0g+^O{`a$wbz`3~OJblTfMIJQG^lFoDH})p!^$=jtWR3!zuTg)d6_2@cM4-gES7 zg6}}J(m_F~2V7K1*S##H0ZH^x&|X4rDHW+gu@ z?=J3>nfnGhNGh}G|L#=R7kn~@#++@^o=@iTz0)plbP5(V95u*Tb0TAZ#j1sqj;u5T zi-T-k=@gWUk|qaxWg%;S4M=AXv)@ao56dS@8+WgW~cwLPCmgQQl!{qqZ4JAfW`32MCwlUVXiA zw82@nq=XI`)^s(Wg)jFJOXBHfCrAC)C56`h(r(My(m@Afe9Vfr#8;9}gHJ=8LV9g1 zs!AE(=beJRth4D@=ecpkJxfb;9VK<p|uORk^+IcHHkt$Wy!2W*4-E4rAmLJ~saFu8_D@> z?YqI28=b!BIZU*&-j)q=lPQnW^WzDojQLT*YHsE1*`^E^mX(Eat6)N3FLxcGZ=N-Q z)4RDaTks0mXv-$qO#5jD>uW!v6=SPXtp#cUXJnJ9j@!;8%PO6%hi!R8c2dEOEy)Dk zX8)?p*u!oLUy_Eo%QjO-qBeXcdYnJTb?%clm?~F z{*^7y%5%;sM0QU!a9@BJYdZZD^m+M>S6*OpDu6X{rr8J$;KUNHr0ylrMQ-Po+Isxw}*<#ltukczEMo*nCbXI5dckmL?6hv!3r z!C&Vzv?h~1vC>NdPPB)zYRf8uR%wa0ImbWsOlrAlCvbCEE@%-)j{BBWB+2qY%ZNBV zk)iHVW{(1EPAbXD!_F?EwqYj`l(J_#`>1J(S3YLRqgpu_vV8In(x2e|Q+)ZRg{uUk`$lgzIu3&IDL(m%;*Hf3 zW4A4P)RpsEVu{u1X6@>+EuJjWXWH6VwwaSQ-M8o&!huPay2xvaua2j6i)VR=miQeh zSb4VPwR~jERBy_dZt1y?m~8oNt3Y2lM$+O=U;7)9?WxAtoVY@4$Yg2fAak59$sG!n z&c9rEc=XAmJ6@sZaOTbfH*@E~8t>GroRn>zSmO=SejCPxqTT$&c$8N=h>f(tROj-|r zRR(ok7*$<7t@n9G7Y0-p&os{k?wr}mQI6qg9M27bA_`Z`|0Nhoek#e!VdUmW4Cd}X zg1H#NGqO@F<0+?dKG5OL<(zzSL_K*wp6_yzb7U~Y29E|EapX95=CbETM~o9TpwMY8 zTn`^PKKXv<@sthl+@CYmd00BXCWF&G8&TjqN8SFW1*T>3VoeyK(tGT^w&TGMsXiQ}PN*K9Xm$ zqc9qWU>vWtI26;VojDkTg*=2^ifXJu4bm8ki#XzPn*C}_;L*!rMo*I*v0Xil###8b zag%X~<2$<AV|@UWM<{77MAvtNGSrv^W5H13dmcEO)$E&YPF1bQ)E7Os`U>a*jIX z%@k*J?q=i{U(=0iT}@KOrH-^z@jxBnJYBcZDE!903U}Tto#)(}r&X6_u%X(~{h(^< z^5z9&cR3a--Z(ac%{ovxprfqQ>hWwwu4kHdaoT|zK2Woh7(kOLK`nK31SZkHCZi5V zV+v)TiW4ynr%=DkF@wjQM^jrfaV}=@*ljj-bS#g!=HMnAXEa;o$nI)(E>CJzpF$e* zj210Y2;j%`${eH50P+GAs?Z74r)VVdY}awm+-GT}A_nMfjs~uC9j(hqmBF3Uo&LLP zJx=4k+FYmcv)a78M=*I=PI0DTnWwnBVOehR_J%rd#G5m*&NtMv1;H{pu)lbFov#aa z89Thi-F3blcz9Xy&TV+CEq&EiDV@W$;>UVc5^`$f+&NsSR{tbfkc`{6hwB*99X% zF7!?$n89Bn1riB{3mGw;0SO06%bGX!yxN%~U3iyNyA9Q*p|yowIN8t+=j!0sY6D8V zFkI+qDs3)nt_|eW2KQSd6bYF%Lhc%&a6s1xN;p`$sps|1!XD1u+;dN-#ih>WQW>*U zt0NY@HmI$~@IeNDt+TZq_LL1ZVtJg!r;yj7&81C7zMNW57smW25yOQ#JX{zKZ0>ov zQ$)!|3QCpk-y`-ev_|Zmo--c7r**0`BT{8J7^V%2B8B1L1NgifUo~_gq)!n$9H7o7 zHW;BIg*!L(yzKH^7xdJI2)mIW2ZlUN6T?))-p-JOL#11K{-&ccVwsx*M;XageGbwt zWk-D4@_0Pi7U8Nln7BD@paUPzU^)RII{aXM->AR>0@6YrC7j4$;s*_!On7R*3Oafd z!>SpJ(Z+8S7qa(q0{N8$?H}MYT!+&cRLij$-{A4-3f#?44;e)95LV(z2GI+g{X4{X zg0~89Vl|J%n(;Z|YcEzwKGukh7Kvc33`au75~L>bTftPEB{Pwd`TQ)f6lqz>=QSLY zMu&9Z9O=Y5xthoJ8*m4_@+FIPF8f6$scjKyvM$e zafN(>@33yT(z0-sW#hY65La7aTw|5tdsY>`ZymwsnYhk60oPk6;Rb63@6EW;N@Bfr zHa1un;3n%*+-zNiTdW&!n{_KTDxk;^rZz%=3n)J8MoOj`J!D-Yg=|?EY<)`tY~`@x zxKM%ys;jJa39*%n!>tx6Vk-}GttC<{B}}LbtdnE_A;^a|t46{ykd?z`*&~B+8AoR1 z0|QzGJXZdT45k)sYI`a<@w1M21LLHWtpGJ1HYp+~Wmw8TzNMz8n{fe|ff_R|NNqdg zLe%N;4i#$lI2o!`sLN7^3bocK!?;=@b$5nC#VW=GhYIyJN`^~0g4P0l-K^l;kO9yx z4C$eJZU@$ha8mc6JVx_V`#>_)DP_!5P?5oEh8`|SG)1;h7X zw8$Y}B7ZMGf^w`3u&o}J6h#M+CQ(g(9HOJV+}Z-69xAEuYX^ck8v1?1MfRgzWWU4- zwf`5g?~qZ;w#n!^Zz*BBAmSxJ$)QUW!}XzC9~9yBVY@y=ycrq8Y(T75JeR5^5g*e) z{(fm7zfT&-@0M|O{%$#ZJ!&HUT{tx2FOk}e9Jv8y5kCt!IdVPh5}Dd9)4PygBFC^{ zhL4H(pTa0dCL-k!r&aC>xlzZ?;#f^HC339hnSw|`i5wR(Y2=AWfhHJUoCL$hi*@CmRZ9}!vQD31alPSLIlEKIMl?d!9lo9J^$zo^n?S1_m?_z zdb)5msW_Qr#?U@gay2EqR{P&ju}EbusW?f(O~?l%2~$&MlLUEHzPP{!CbcGJjKD2D zpL9x(3J|ef$S>T5lUW<`br*^W)dBtUoIyy}Tu4awG;QknP^T~v)df77;|4@@R8T>k z`E#$6(Y_AUb*h${`>=Z$3mD*bV!{?i){ltYKPGl}5xajv?7o9#(49n#pAxO_BKqFV z53={5onNrd!+k`J`x(Oz;9C3)*W*EK;Mc1Ah}PS&jadB*uV^WessXN>RSW%(4Z$QO8(QT`fn`4!8HH!MHivZR1Q#A*|!^e-%=JA zV=IV*2GOYl2V(X){Z6U=2q{RO_WEO5DUggL-p zRjp($tMe;4{DJtKtK@RE_uH)JS&K#@1ra+V-__fLa;;vj&&UsVpki6XA1T--H|w|v zE5zFjk(&wD#Ir#WVzybSFv8?mdc45&DxtmtJx6Y|^ zYa)5XGr6yhg?mOG4EyN9TEfdi#5?xvcuclt|~Dj^p4 zf50b%$xrEjpK)LQInN;ei4pi0YWaEe2zQKgocqaP$o)5X2=kJoGRru}2}ldJ-nYy}C_ zdeZU?qX$;VjUjF6A(UV?3p(cnptG$r?7k{1D2f%a5YZ<9~D8vVdTRq^g^f1@uY9pWZSg58tW97`R36*E8-04HD zE%4Xm5DWLRYh1?4pFWgvo9$69oWsBoG!yvMHdl_JiD#mSb;4kojp1@EMlugp@hnf> p(&4VAm%AF5yIdS8@S5rJLarNMf2vEuoIQ-M=@s^~Y}M&LGdXad_WIZr!-RmZOkK0bTN zEd@H_q^?~#!<$84Any5PclnTjD0Yw`BFp?;CgCB^_k!aB(QIx;KpU-<-6W#eZox~z;1!9N{QVG zi~(C<=VZ-ZQWBcB({Sxt-Sr2O*o$5r_X)&kVev5&pT<6cRCAt++-8%S1o}3yZ8K{P zWCiw~2<)osGy^+WT2r{NNWyGesgC{Y@)Wu&ea)Nb$AEw#y;@MpyRyZcjGjW7yXHs| z2Qj3BnKSRCo46kjgjc7%KxSlTHn-7C14jgqfWT1`It&RFOcD$o5AhB~Ru;X%)2{C} zywaHGRBM$aMsQpvzT3dVR54XsY?R!BE$NS#7{wS9;grjx^EIzT#f41*c7CHN8!5RN z1E&PGjJeIg^PRw}`32j+qjdF2Th>=dk$Rb`&tjZ~NWH8>Y3wP_t(Msyn`j>^O3{;) zbM|9$&Y1X|Ea;?6`I@^^CZ_QPR>G3g9CiGXTXiX#&2UaCf5OC*ILn%6o+jPpAauMa z8%i-1CDVBmU&NQ_O`9p~7WI8hM*Yhsl41NE6Db%nuO$;@DOl&7>C1K3z?{IL>3P>~ zs+il3U$)C@(Xl&)6-X9m!LTuZo~Bhc7nhq6HlS4rh%L&0(-LS zOK&|^WKijj)2PVq-_XA7sim+nBj@Fa31n4r80^@j6gYG|it-lfY~J-+1R@oNzFSf^ zR3_&~D3{$30-RwQ#SoVVu`OyAOgMEll62H{G-Rm1Y2sV>cGxhAMWBlf(`hM1Vy~^> zdpf>LG3!2<_vEt7L4>G9q&!W&Og$FY&8zE}rq0-6{jVeh!3NL3#EY6Tijp$lRpK zZRYMfyQj^oWFN!}CSJr#j2PM55$nB^yFIo)7D8Dg@uu8U%n8>1q0?0w`Wqerys*q6^}1GjLX5VK-`!M6U4X2n_9D6-{Kc{OxP2f4_+b`)^-^9(@ZB-o$VR9)1raXVX1z<5)qnwDf2C`^h-e!U-ZL zB{JMbPH&C2P$YGt4YcsMrM2++=b_z@zzhYq&tZ96Y=PLMsy8FY%kWa1$3>@ML_WD!#8FOSp^aN;4@tu#K_Z z&bW4C7kaoScjIpK@*jZxxR+b62S-U6gN?^Xo90Tti2G0_#}e213ijbS^x*}{d>uLb zfw%sM1Gs@a-r>6a69(`B6a6>Ngn!@={)r*{i&Fp1Z5qL0k>J8i;;87tu(%5aVR23N z;h4&OopYP{ZRcJ>3-FnJP4uFMI;*8uX!r^m^sA4$n)F7a>|F@3$m&?46;Jc6QS&60 zc{fJ=r*WB3oK_se74qq{<$ipXkb$o;E~kAzk{30^YZSp9Qe zNMX;frFuwdeVFc^AoK`BIm+aoWHL|TFkL;4NBLJ|T2JBvSH6pJ75XcowVlvoPK8SL z>YNHyL_xB*qY9GUtx?NGyo^^EW)tUdl~9}#rtm7i>eM@qYlIAH-iy~*E(z+l@CKnI v%j|?oU`i$7B(zl;X{r#*fC`3sg^!>Od?pC468I#Bh-BSIhTqe?2;Tf3eowKH literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/StackMaker.class b/bin/ij/plugin/StackMaker.class new file mode 100644 index 0000000000000000000000000000000000000000..bab4cc9540af8a217fef39f676f1ee3b1c63f27b GIT binary patch literal 3347 zcma)8ZFC$}8Gdf|V`e9l7M5&bY0@sCwE1jWsHtq|hfN6GkfgP#sljTU>`r&5-Rvy0 zyG@INh{XzMRn(ROMXF#$L5R{#B#Nl#;5q*B=s*AX+y9=U9!`$+xpy{S606yhnfJc; z>$%VSyzkuK|LdK10qnqUG}H++Iw!h|`AXI)bdQ#;^vPlCr0r=43fy$UI%Rd|twOf@ zz{CkVUDgn$cWL}VPD>R8f}T575)df?E$!wjlLfZI6RwxBJsS5p1*g1QAkf-2#=(7V z#x@YZS`+m!1=bEZ1$(42IbnN;t%U>j=A%F26f*AAuvM_KwrApYtS5-9UGA%Ro?R%XCh6zM z);777YVn|@UIWe8s9}RZNcsm&BqV&T;HohOTChpOodV|aX(sN%X8O3kf;;Lw!o^8N zN=K`}#=|^SBJEC2T7^u)vmdTFo?S{D;p+T1+95yk6 z0|NCV^PuOFr1BYo&8uv%{49QcP$D14sD?uVH!pFYDwJ&{w8QuWw=G&;$)@kWXf>tb zh@X_%iqrjoU9deT-S1d=H>+cuxRi<#DoYKF;Zrh;Vmdx85b5)CFPV@X$4q<{7Q=02 zGR!Wy%z$I(GXl4+a+v;9+nA0tv-J=ePbPFQzH9cW6i!SznQ|_fkVIuo7?P+lI3`X= z{Trp6JLLnE?!1XfFcIsgtWuvvE#z%(NxeTLdy6I>25UkC*&VS@m;JO?{r!^tvWW^# z`RPLT({`!ka(J_!i$&j4%gKdqvMzAi#2Gw74klT?6h#8vt*Tb6J}H$-E#aY)J_+vCcc4hQlP3J$@=*J zt9%LnNt;O#I%VZ6lu5Xx37Q4_ zON72qMb!}uT*k{XAXm6=D`Q8-FW~zoUcsvpfn7f2I%JSt3!PU@{7`Bs$O>7M^M7RG z$9SE)Gb2Xxj;vtYR!M~$h^B2!!%tS8PoZ0ss)0A~mV`Mi(52+3z72jjZn&_V`7??5 zZCM!9DHi47Pp1kTs%HHSmLZ8HDFy!{6H<9gc5*cp2?kA^<{-&Pt<#Y9!hpKce{Qhy zc+{FP>Q=n8J>bX>^;*@fYSC6HbKJpH4Ddh1XPqpmylB+PlB?dV9DE1a&ksMJ4QzGu zE%*+#&tT288g>((VdZSV&9voE4nP#Q@Xg|g7@wRh{z!@!?vqIF!Za0!N7^?5m%$0$jkbnyRp6;sLZ99qVspNofQ@Msl} zOS%G(bUlWReBMHC8_98u{H`Nyx3LxH?{@y0uz^>J7Ct*Tb_XN1m#rb(i6hvAqr9cq z*o-2!;8C>VX|&-Y+L@Rg%&<3j%31=CoApV9Y@nz~@1OAM2^aZ%*%lHbdAa^{6r?_8;YpumI_$ovHHLlhSZ62nE zmWFwGb=x_QZR)v4!&rwD6~F(!3h+2vAx>%R?5N_rAGL@I+V1Ap>Gvs$ax?I9!@CWs zxtB1L3ib|v?jr0)JgZ=Hy&7z3eT%dPIJW`cRR9bz6)q9P7{d}^9}jq920z1Y;8 z>j@FjcbfA(VZNVl%J*nn4DB3H3J&|_RhOGnIzg>$h;Ov}msD!v!j z1!w)Bir3B>B4#vSx&D{45fO_tUAg{;SnwtEa9l7G3x@KU9!^`djXAtA9@jf&@Mf|; zULOnc7l|2F{4}Y@^|uxY*{77Vn{fB=SbM3OeN;prquop^`u2$k51Ia&r$Y{4&-?+n)Cm&*4f)$=ZXMdi!TY(nTdhZ`D% dh6_UdX}C;K^9YFzA3*;AyNK~Uj;+J5{|*1P7*7BI literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/StackPlotter.class b/bin/ij/plugin/StackPlotter.class new file mode 100644 index 0000000000000000000000000000000000000000..a4959e2e57b86a7e268ad3a9545a82a83725eafe GIT binary patch literal 2875 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 GU;YP9VQf7B literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/StackReverser.class b/bin/ij/plugin/StackReverser.class new file mode 100644 index 0000000000000000000000000000000000000000..8aca69241dc40904a993dcd498806cf968412b84 GIT binary patch literal 2221 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 MeptouRBG-20l!_VApigX literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Stack_Statistics.class b/bin/ij/plugin/Stack_Statistics.class new file mode 100644 index 0000000000000000000000000000000000000000..8a3cbaf9389470d4ecb87c2d303eac800517f26a GIT binary patch literal 2725 zcmZuz`Fj-Q9sj)9V|F)hAc+ZN0|B`=7_uUGBvIrDNHF0F24XEZ*&R2-p3crfz_Xqv+>5vziSidG?*}eDC|6|6aTD0f3G8+(3gu zr+qY8%2hIUAvx-rsq7J+u3dKRRM|jKq4}tJ%uMFYLMFLq;;5B!4TOoEk=je)p@KsA zPP<^cn-l_x?lFbnwqn|fA%I2=0}+M!!*;>iUCB>a&H-~GXDPG}7gJ_#%yevduOWl( zq+M26JUnCJdcOu}!>Q0@dt!LTyHVG%3z>oLu^4VZyMZ=^5Ooag(pZ3n3XzQE4&}`Z z4L2vcMZNb%_sat@#IeXgr$T(j>_c|WwVdRZf|)yU(sDEwql;ls&4^`|D~^@73NBq` zyhEa3sm6_1Mvn6J9Kpm;_n3hdwQM`ZlvOVK>7L1U49l@fa)qOCB`H=e23NxswG?dUUbheC`}HLONIShlcT-r`uMm$Umt{aqTH z#C9}o`}Rn(&H0YM*2;t;aTvK_N&j587#WQjj||?h#~^$eos*nRLa4 zA&p&vHHbZMqD1lAF|09y-9G15#+YXH7SMaIiOl0J6PI$?={aj4ioMuxU?02m`&5ll z9AJAL7Z()NkZ_M_929JL((@sTas0r*{j+QCwcJe2H6FkZnZaDKklAZbSvfEB)VNCm zJEHL*n5=mWVVjw=Cmc>@k$vAeyWDj&W5&44%L zwK0lPkF9YOS=O;6u6g;UYlA|{YZQdk=#l)a3%;cB@QlcyRdOdIppEcYkt)j4ixrJy zI8G1Dbb8FpRXC3e=9uF_nV1u7+UqBLYsJo`EhmCUSlqEkvQ`k9H~Y1&=o8xC(C=PKo2 z1kZEjaW534?q@W9CRqq_+6oc8NLNPP^q_T2h`-c$Nr>UHhl=18S{t!F!E+ktg%-%$ zlzUYO=9CD%rt!K@FsCB;mDu8X70Pckek+tvI!{alZ&CAzm9~9P-qCp1GnV&I5xhs| zYwJOZ@du4RqJ`?plSS4fU^?uL*6(jslI(nmtvGuxaYSnirM3%0W!@gFx4*t?i|)Fn z=N@QqT&`NtHP=2OL%@;EqJ`tzy~1Xr@>JIEBt~h=GN&!&u!rZGVK2vYhYKvgi$oF*Fg@J_F`qKHumAT6j{p;s4|#PCYn{ z){$v+Z0ceepPa^$t5`nXa~Ui9gI#=^#+v?6JXFO^JyqP?AC8AVLu))-#cdZ5BkE4o zAD-7|5W4$xUo;;69PNa+^0$5RbVoGXXS{3#e<$m)1g%)XZ;k}o(2I5q@O$HK zEW}n`+1tr6j78YTkB1bxkR`W+r8vb;g{RmWXL#wKqx40r#GBOr9@gLzHGRsP{(o4D zFK`pSM2~9b{k<5yDuJZhh?~`BtXIRlnGa!uI)aT}ubiP@?1x&f2=piXncWgp58yA< z+Q5tU5dMk-jHgrWz~AsbV_Bi@!rxJ4d-keZa2eC=tL{@$rsO{qYQSgT06{jCEdT%j literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Startup.class b/bin/ij/plugin/Startup.class new file mode 100644 index 0000000000000000000000000000000000000000..70796d9ddea4d20029ca284f0fedf9c10b397a07 GIT binary patch literal 4749 zcmb7H33waT5&qYfy&Gjogq+yekgzxbWIH|*5(st@a1u-vY$uI^g9u00(#BpRYt8P; zCT-fLr5B|Ky$MiygqF6@meY0#SFg0CH!bu&p!a>>e0=?9SCVZazOT)(lIFdc_s{$@ z^JX7=_JM~1tjB*;R4F*o9_yUQ6-MlQXUaESzc8U9q98VAPMV!LGe6R~b9l_k_$q2C zo!h*NI?22WO~J{*UUSOtv?i^*-L`GikXwlNdS(x-Vx>7=6h-DZ?H&M=Vc5 z(52%9EK#tu-_Bcuh4Ep_-D3{t82@;`lQDBcrfbXZqOf*T5Z@@Ou|daKsMBzc0!C56 zxjL32%9#73wnzGe-h~-PZmZowo-QVir?zD%N+aj#I3K-)Gh&s<8VCtrmS{O%NmRD! z*bZh|)EjlC!Xyglzch+QT&Uv(U}n@>R~O72VJ$d1O!1x;ZdGuRj(!X~Yl0-bT$Udk*5XH*Jgr7$((N*ynPsX(1Ha|LVX{^ux{LQzS1 z4>H2_<|y{S(y<@npE)sMY8|dn6!oNKd3$YtRCIkE1x$)KAVZprf{hh$9q(L)oKkU(f@PI@lZF?E zd`i1C0=lD^LPoOxr8*Wv*YI)$eZh($U5t;sZ?dwohMhO0Fbvf<-Aa@1@u=_eXIEuNG!r!Bf3Bnv1sNs#59Qt<6u=TOkEnWDGiy(->Qx)bKD>3ARBPgv}=Gfq~0g-G9nIOsxN%6j(=QO)^ zZMe+aW==EosYk^p!fFZnU1u`yTkeEw@fpP|Hpt^a9h)#I-yYWSh=9`wE~sjnwgZjg zIB=X19v-tfSFnlt*0xc(B@ zuH;ua7v>E=%!MQPqK+?>ucY9XwF2rie1&DgYT7yB^WIG~2Q$Ie@<`CU#_-*O)zTBy z@HG}hpPjQhxP5y#Ah3%k!e*Xm&N@^1oMUFa=9Xal;|f;JGaSqg11ygDCcmkmllOBj z(QJ=(1kHn^nK$o%wP$m{*ewyG+S;zb@RR?a(z-zD+X{MkCnlPEhn<37Y*ZZ`9nD;w zGHpL`R#%sp&*U7>TDVKLX!tJO+3nc#u(AA|tE|0t)*nrThkRebs(CI;JD3|^=KKA_ zg+Z5@rqA}hAOapY?&mcxL6>6qv5u$kba-o)ded5|*<#5`t(D~D`ltB0ik~sd%V|?u z)bR`alBp=ogT33|<#@J`9t;{u(fqZJ-{7}FK6{qSBCD^^uY!J@#_x6fAt(de<5jUU zb%_}Gla4>5j#yoeDwgN>Ihg|MQDJH}%~joV@>r&2DG#PHFAjcT1#rCfdC%9)DV4n7 z?r50ql8ZcwRV7zhhxm)rc#Bn|3N=vqKPrsXPv*%6&pw@Jw_!Q=*5e#B zU^7nQ6uAW3I5MmYtbq79tQJvjrd~%J&)U2*ly{K{>QK z@bC;q8(7I$p2|2I^yf7C)68$H=-Fzlp^xivW&p+r4=jS=(Rd+&a#TP9yZNmODca(M z{tT9@XslPc#+kYZ_#m~bxZZvgd(!ctSzI!UD`qf!4;rSCoyO=auIg_)2E8qwpT@*t z)F^kg-Bkt}4+w6>DTLKRSZ(MCK-Gsvi=a*+Ig%zy0A&%Xo|u?c!sixIa9;EH@6dvGs9Xz_&ma6ixVP>X5IkOoPZ6H)yv4v-}khg95p>iNPJUx{9 zOaY@oc?Kt`92pdR3ZE{r`3Moza`ox9X?%t;9^m1R4JzG{b}|tm6Mcsfjeog2ayxDj zQB6toRS{h$qS}(^>mr&Iky;XcLqr)7X(iDUBHAUQSXuL1BHI+m%BtTHSt5{?b-yRF zWr1u_Xits*K+@tER)yK{qwbo8jbF{;cZZ?LuRqRphB3Awu&y=6O`$Tz-gVd4f1bDqxNHe_)J6JVCF za1UaH(H_NPOr#n<7ce}f8Tek|b-??`t2@>4~a_@it z=lRe1&UeoL=f|IZ=LG;$<*zK{2+E@CCw9kryQ1-lOH#>jv};`|5|1P;xCKS)!<)ks zW8rw$#7oz#k94Fgcv(9*Vi52=He2w!M^NDFilkaMhP$|EQEi=0ZGUk$ zJ=6WD#3&0@+|eG^Kzr2SES$~hqCKhxTS?1(sNqiX^X)nSU_uV(M!8kcf{8SnsacK41QGRg$9>m5ht}pB)XSGuXgO!nhU{V0}DRw zxYXcsomR26G@6PL>{p=O!j+E0ToH|TCbqPN<3wH3U^!L@^4azBcyv>5WMOzCCp~Kz zGYun_g{vrV21^&Jvqn4SM#HfLH@gz6)xS!82oqhl93idkFz5uAC{iYJv7+kB}y}fYRV#75 z!40@^u*Kmr8@qG;xCz@W+{`61c2dO|+=5$)ke)~?gL8K+HLHT&Zg7W6q@JM5%t<6- zk#Jm{amj2`X*P46}wE-70a|;M)pWw<4V23n%HOC8xU}bd8q=qx2 z)!AT1#>#{m4PkbqA^f&D)GV!pd?FcT!Vr#S_GU$UY-SbY_Gkv+aHKCD*|xr$7JlvO|X;KUWNr`5t-oaV2rachsfj$!)cAe+dbRD z-FAjy;qSQJ&r-jIzZV2QR}i(>&kf#IZ_A^%&55SwM!Hk$6yLuz_!a(v%M9Xs@r82) zpY<%qn?7fwy1+jgyknD8w5KJpu{+TdO+_sHMo>(-25RiY#gBLKp20t5y}!-QwFK`Q z{ENy`>X>t2kdl$g`fmomv)cz{9q6O&|8DRf+U|G8XNUc#!GCFW!Qe2bsS+O={Etde zNUgN=CX~MBK^^ANlc#c#T-f5Y zsof;TNWK&}d@w(j2&Wb$qVW{PtaSJwYp_gPY-?8{KHZWc$1LhWixPGvGrwFZkr9@Z z3Puir($JAc0>b3f>g&#|lCxsIjw&}2v>UcM4Sp$L^H!bwvjm+adjh$?EJ=(GHsY=WWHQTy|SS-o{A*96EV&7mxW`! z#FM9X?%X=Y{$tW=GdW+%ItfyPtw`Z~zVjORY-6)=%>4!AX%4h=awMf$oK=$Hda604Pa zKUD*WC0Cz{7hA0ZaPw1+YmIzazC!tE6PxDgopbabQ(3uAqidwok{cM1hA_aKt*OY6 zS+19D>e@GPBZS9_IbUE*J7okPY9u^zB3=`8C3g?;yC=5T7cp z4ez^++#~lA4IPR2=14NNG@$|ss%w|q8D5s+PV=7osPYwabe{W-JYa8ICD7<5%sqtW zarfBABcU$}6A@X%Asw{tDN+4YH7XQ3x+kl~ALsd8o?!km6w$UA(|z)F&aRl#^_jKh z$^jy42!Ne_de^{AhiexN>d|Rp99xp;O?E`)NA9s3!QOL4!I-m%}{NiR%$|ncAVw{q_<$=Q( z(bkYg#li_Mp1OE|`XrNhOmH zW9lIep0*qAL+lwkjG0ZIfTziO7;|?a|1d6mDLb3Rer-*@fX}@bmg_lO#_uJEu`HAu z@VNJ(AYkoAE?f7ZmlaRIDjLz`If9k#CAIxn74irDC0`2W78RzkhQ+l3&vuu$WL+BT z?J@bRi$9CY(&!HQ(@5!K%P#o1$km7R`{4Ib(1!&2#=;7&X3OLco*8gQ5H*sB91B2J`#zaLDXOMW$s> zC@<5J*N-PcMr8<^m#HF_o($y&^Pk5z^zqCCyWtO-V163U1x*_L0gECBsJv}zPM2-Y z*D;0Ve7O0gMy;eV>uJu(H0BhHz7v%kaOiuynydh2bFRyjuQQDES7nc`UmugWilH- zWXa28eo1N^0@^S zrSXdpy>B7ge!U9~^+)k;8o#B>jJ7-N&2;)_?Nn#?|5q(mpVIn)R^{2PzYn>C?q>$T zG@j(4+bZTwR2AXUh>Ow0E2!CMfNcm$Wl}JgmhGI$0qolLmHcyUS zQlq#CJ(sOmH)al$S_eui2TEN7B~`J3r)r$P(ggL zaSU81DUQ#AICvE11UyOr$1#Db``8W(JPJA5^OVpL#xNulQ*Y$Jg#$TaSBN|_jze*^46!NIpjYg=eGyERsC{7C@+|o zmRU!oC1kWqv1^k3ayu{Aq_t~zG#6{tNT+3_)50%pDXn6}C=KShTtR<%ad~N4=BDK$ zS5sg=-Xv@9MoA!0a31SVxCw~L%@FGdFQ0)bPe+m9PK zcYarMS$Sc3X;*W3d0BbchI0OF=r}A3?%vT{!J3K$+spM;&feu!?#}HSM#!Q>S94W) zX?fX@?`l_5O;yuqT_;eptwtz%Re7}|I!omp98q0f*3n$OZS={&=toYzy)7Ur|6?Z` z?M?XuUVAeEj~%R3!)NV>Mcpo@)yXY;P!iM_Czq$CT@`#)FmDj>AM(N*SWEbIF(p_> z=tUVf))V9#5F$6uz((F~$4M6nl0!FXVH4dWNf+or2U6_W%bLyT!xns*&zl)*Zo$>O zA-o3n^YZdx)*irhykxqOJi86=^CI#C_WA_dMQ{r_`BtgMZBmEZWh!<^Gwzg&ahJ5= z9-cPtl@-`28*!hccz1XW?+b6ogYpn|$)ngUkKtiS^RDlE*dsr}UU`LAh_7+pw|JfR z9-ffjVV``6C*@DrFDG#zCl^oU6!6YVNsXkfj*)d%mNKNql2Ta?JUK?bt(+kOFY;ldu<2yqEB{7Cl1nW?_!1>;fP#H*vn}6DjoBHM4gy}XL*8& za_5F&;9Jtg5&6`?Az8;#AzqVt?0Jy%{Jzx7dX`G@Dc-gTAwZqLMlg~oi! zKMDycCW#VT#lQv%`-sAx{%OjlxGH*}NsDOK&=C9Hoo zX}NiPFpnzuJ=vL=Ea}`Cbz63n^~Ix@^9~txM|PC&i$@g%3!G81wO#Y$6MSeuzcW-A zG_JJVMQQTwcYd(IeisDUV#kr4%&Ik>>KV@* zYkAaK&f&Jgp5`s?|L0`!x0&Mnf{D^E=|jJ=wK~&TCaYCSpY^wCS*`L6^OSspS`G4K zr1`&_GZ>qnH7(~Qbw=pj8KE2hQlUQxd4fY^b$H<&%K9$X`VD3MEmwMPh@6cBayAag Z**G9)-GH2d0XZFp;GKXxtvZo!{vU{5(WU?Z literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/SubHyperstackMaker.class b/bin/ij/plugin/SubHyperstackMaker.class new file mode 100644 index 0000000000000000000000000000000000000000..e096854dc495aa84ea9b641902ac8818040c3e9a GIT binary patch literal 7969 zcmbtZ33y!9b^gz2MsJp<#Unl0gS?DQY)e9mZR`P$z+faB3nbY_UNBpDlAfh!k7krv zybH+O&ZNl60Z8bfrn0v}xMlp#1l}nbAm= z{n4+IX5P8yo_p@u@3~K}y!ZSI09MPm29IFAy{BV5ksGm-9Rs<+O$W!VbS7&K?d>!7 zT4@blLG2!MpV^TxlOr9Q2lrS*Sq+uMPOaO*CoU8)b!d(t3Nk3Xr;HAg9%Fz?oADui5+IzR^tNFn;o??f`z?PJDe+7CwEEbxT(?9 zJ0;&hHf<+II-7R{Q3pdqy({NVJ2{-%-)APx5i6~u0dpzLh?VWirPEe2+cQQyDVv(r zmd=POWjg(c2&(&2QCks2Q{z^$J%~mu&@f-%Q?%FVSco}-P-c`HSeXn7gNR~@hQ$KC zoSlw~@d4tv?|?G!fPILY(o{WuETbOIzEL|9O{TI@GwL{j+eOi&qq(@UziZS?Capw9 zOBxiSAYbC9;J=ATRiPVT6mkR1= z-_eriqmJu?Sb;UladZ5*OmOptWY$Vkt~BGoiZ)s8Bkj>uZ7Wwdx0aCm?d)jOP7bB5 zF_-i_h&o0Qi&8@ zbp@^z_=nB(-fPq5LHfvbc8Q`>iFuWdtFfMTFo%Z=J`k*)B`&5GZ%ie#ol`?+H-?QL zU4lT-#&HcB1aoJG*EKmiF>Iy%xK`j<*%m}M)~GMW!u9B78kpmZlwqb;Q&00b!C6>M z!Mo9~V>7l;!EDNnTsnzCICDcs$s2SGU>i+DC0*i;ww2&6P3KkAN!db5^eE++l$skS zaX)V4z>SW$GU81-ZdOSeRFUk)hC<(}<2HPd0lweNbeYK^D`9a@M(s^%*3>bm6sS@W zXdPf+tNinWB(gFH!EPNR7!}MJV>y`?WrA%@)6?x-z26UJM$W3d*rQ{wQpKxOp|ECp zM3tIz%&KxYIiAbXsM95(4~M!^xg=+#hl4|fL1nf8ct>dsPTs^#2o@gnj%S3bHz;oB zWEv}$HdJ)8cBljujK*zJxvV2il~W)3%yFk6YWNk#{Y>GNaX+f#ZX992j+^O>)yvcp zw9FC>#VE{W?L^1=G!seDGO7gkDF56m=%3XW&H{(lHQxEhGrT~{4!QCeWhx8-9H5f<(L3DWX#A zw{$#;<8Hz`2bL46%Zm_$R>zm{Wtu3-3Ny1px*Z8mkEbX^ndRJ|2;xb6MaR?1!hSoM-C-tjRQ9V(-7XdW zU(@ls$g5}_wTAYx*3XtbES*ZcQ#zhgl~7~Ct6Jwr&X3GS(6!d~r2V0s)lWMLmd&!wtYtrnmv#KHBR%=v#+Qz`Zi;vC8&pn8n`oY7lSZ zpEdlGAaY*5I{rn)p0e~7`+(DUy^ep=@$YztSnlIuRbZFZq(W3WdqnXA9Y4g67=cPG z)%r2Yr;3wg;(2|EuHYc-J+1p`YnhCtckPEB0>RM$?_aFLb2crBXCXOAWg1%y_~)c!O#r`)s!G>?XPuQ9zgA ztVHGkHLdGXCDrWQxnnsGrf*Ysw{mAxYIT_-VHR2L(`6>?L6#i~Cb-BgP-Co-JRLg9 z=BR#D>UA-sf$PY-Y);VV=s(*sb;T*PqorcbBPi&!6a_c)##6XZxL#(p7~7+5`D(O~`$*f=czK%-2@Z#a`N$#UBhNJ9xM7if_y`6w|3PmOp9g4aZ;%Tnf+wfTWNgWf5=n|JeslYU(`E4<(71KAJxWh-N|X!WFXbME zHvori(eTsj`(gPY$?<)^n2f-pjLxkx9@SKOESfsT-IoO$Q|U2ts{5eGs>uQo~tOmb%0D4mw91Ut_*Q+CToprGR6>?#c53KfmAL%WNoxn z(#}2iokY759E;gkRPf{DOO{GL`Raq;0ggKO6h3_|ClP#xfCqYk2&hFBVcjJ_HEKAb z6ETP1To*Vx6T0Y`);tz@ALVFiVYGZ5Q7thqzuRKI1>O_Tyc6(N z#3~K%)Ic+QQv?5Ltk@M^brP%Nfk+@6ldt0PJk}YN$1tDcYfhtkS9s$|Y+8^ zO?hlJv}0Jn+3lrS!&^S>cc*twP5a8GBY`}2sjpj(!TXHbDWpcXPrw%*iU$?*azw}= zyK;oy8VN>15&aabcvYk-4_n!@!m(!>^9k3(3xlR_pk3$EauGrou^VdS>;gJzA(mqi z+Su2uQnuh($cp*%bt;(olqyD^BDD6dAY&6l*~vHTS7 zihElLwJ6BfMyO3el`Ro(#22YNg?^16=c)uKZGsiCP(l0}-pwL4<>FT# z2}wmvFGxIR0*51W%I9m3=$x-A2w69Q?UB0j`S6h{X)&seS|gmtw`28@fDtlugL(Yz zm=Wvh>tISB24xF|WDnoajlq(G*e&;9MDFK1x`$!Q zaqN+&IDeA#FLC`hk&;)DmhT}WZz3l@KvsT)eex6R_vkp_slh?dJRI^Y#vPs(eAsg- z?)1d*5zl%Y_FPArZMe&GBkuN?IHIy$+L^D+vtqUrx{u|P*-q$VgqZDwK2E3tS9zAo zJc-a+8$64gvgyP1o`_R6E3ws6=afy2b_vKuETDWTLa5P!UM4i(f!>n4WdW;)pZAa3 zoWdC3En=rEB&5=Xnobi-NW;wg3tCw?{WAW=bykbbzG=Er%qO| zOJc6Cs*^SRZge{ybwW(FCksxKci8 zm$igE&Y|vwCrCf5m%qd~Cz-^RY!z|xR(aNUS~_=~lq>sM>SSGBuHF@|d?Dtmhl1Ku<>yU81g<=F5o#UV)_K?TMfM+CC{JG?J=E?;EBn1 zj(ElJ8ouN2z1I3PjeI%Q@jpz1cz~VRcldNmxZ7#o&(iY%7V~{)3qFZU@hMV2MEZwW z)Q{m7e3t#wBe)Bn!%=*ml#g-rIDfyu_mWTGS>{k4Ut}5o5_vy~H}DjL^D7d-)3OB5 zNHgD7UdrcswkFpz3{{xC$OD5;mSHzc2&v%U1(5eji{ZphQ=+n)vlVEOxiZ38jsNJY zmQg}}%2we7R)Es2EI?iC4{esWAf;O?t)vT3j;#ggY5vQ%owy+7tSvxl*2O(betS6^ zq6S4s&APZZ3Y>M8!$xN}-6?G9uH#C4loG3M^$yfbvO(8ajrbm@6D%5%04CX(2MRd# yP*@fj=IjYZMsZ8>8IS<`?q!XDp?=1DGon#+4a}N(UOHu@nq<#rNCX@UC literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/SubstackMaker.class b/bin/ij/plugin/SubstackMaker.class new file mode 100644 index 0000000000000000000000000000000000000000..14df6735caedbc46b182ad451b221c5a40809abf GIT binary patch literal 7517 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Text.class b/bin/ij/plugin/Text.class new file mode 100644 index 0000000000000000000000000000000000000000..898e5c9e495e5334771c5f70d3f58a587a817a77 GIT binary patch literal 6265 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/TextFileReader.class b/bin/ij/plugin/TextFileReader.class new file mode 100644 index 0000000000000000000000000000000000000000..5cf55045d09d4707e618745e9aa07d03a88f4c25 GIT binary patch literal 512 zcmZut%Syvg5IxhTjmFrgeOOoSS_QLlS8*d03PHt|y1s2M@s^lKOw`ZtS6meoT)6b3 z#JOo96kXhzJCAeb%zXd6e*!qgfr%WUtrpIMpLi;8#_}Z|C|}-4F_)1EgHTzBC*k-a z@SLmZLe63nMb2iJYlaR3Lh(!mDn2LVyS)j)xCrOcLLNIdOl%R#t_tL3azB;PSWJCM zsJY=x_!AK+9j}u{e5W1>9XFe>sjANtqJ#%Fx^AXz6h|uX`n`#XIx{mk=3UZ-ZdEW3 zpDonT&{CR&)<4MMP^-?1$kW395EHCXm_#$F&qJthYC6&VI0O^$EAVl1?0Idr+Z;LU ziGAT{3F|E_+iZ(z(#9_T?NtLxD09RYMwP7w4`47IQ2IdaxJs{8`dVTyW3kd|nOQ0+ VQ*+h5W?~##XfqdKpILI~`~rNBX~h5l literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/TextReader.class b/bin/ij/plugin/TextReader.class new file mode 100644 index 0000000000000000000000000000000000000000..56baf1362aea1ccf438ccc99fdbeb1e148f045f1 GIT binary patch literal 5047 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/TextWriter.class b/bin/ij/plugin/TextWriter.class new file mode 100644 index 0000000000000000000000000000000000000000..c4d50bd7c8d96e884090baba73d50e0406978bfc GIT binary patch literal 1265 zcmZux*-{fh6g?dhCXOR$SX2~gaDk9W+_xx;3sqP_tpcISwyo=Fi)c$i-9dhYFW=hvSvUjbahLkB5=L4T{X-DubSptP#rM(f6p zv~iFYIJ%|Ys8T}(_0rRgEnSTq^itZje@3^ZK%n=oANbL%K+jZuO(6XstZ5fLIPAee zR-nJ)2l`37xuMOf+GuEjTqUfk#+ov|y?2D^Xwz>A3|G1tx`@h*ZrV&XG*#*1T8WGw z)XVub7Xkww25q`*OTE$7lP=2CZMJ2*?!O5habw8+uJIg9ib!|MH!fAoNx{el2 z7(|JZQKs@q9JLdb7L97^vV${}6K=$8N&DhRE=D0ejAKH;V;l?SnF@5n!q40K_8eky zIg4`waB%@s4kiVT?KAO^$241QYg=HK4s62<2Sqlp+rO6sWg1 z;U;eJqKyfSz~lkXzyEnH4Ks1^eBl3IO&}F*lP(9m168^vaN-{d{@X&5@W@J}s;`#S zc1&6a3uH>nu02)=xGQ1XRJE;}~uo?piK`Y^Ph3NQuv9}h3^=BkDH<xkdJsnk nZJN5WM2T5Ekj=%OkZ3+@y*q98;C6gB| z#9|BTz#ZnEIdjgvcRoJf-T~}m(}Kp(5CiWa%6me3H~oYMU0N^eUG!=tP{s%=#j2?vbMZx8wnF4uuri5yMjJKhoAMgK->pc^M2- z4lI;x=$LU(hRdM06k)~}l3(Qa9iH5LyI1`<45Hg05h@>ohAN3+@gJ{dn@ZA?Tq=Wk z2MsJRl(P6bON8t()LL8qsMBwhUf9=ho`n2VsIDvHL)%qss_sOFQJj*-InVlW*TO18 z{qLbTn8P~5TZOo!Z8TrHgQDDWL$zD*u8@;v*-Bt2l8&fcc3OW`^N5c~7 Q0|r)zwLvFUmvW``1SY)EICf>Iu2@agXNdnrgd98xVTQ5Dt;u59UD7=Fp;PBIO7>1 zjckXffwsKqgI7Z#5GYM~mH?GSO!!3TT72X?{T+N@)$BVnvc||=pk>e8IrrRq&e?mP zJM;JVZvO?q!?>fMPT-)il+KyO1;a{DE#`G?e#|I1dR{?Dpm9lC(bA@7Eu_y}UedFU zf(WhE6=%6HV+ll#8|*)PQk;hV0WfE9*p4_`endJ1=>2_k5C!naS;CjfqNvn zYw5x&*-TH$>v_j84ad+6DvsmB0u6?>Vqek6G_pdzf@Pd5T9y=&i`^3ys$2w4u4VO{ zW7slnEx#b8S0f%yWwlj-R&T0S1e9J~jt{j|9C z94;!jumd7*e-$5-YF5~?6WX#K#U%kXnIzUWd-19aKL<_4Wn?J_dR1mzNoHhnx=fCB z9`4N4mK984QH3Fe9W@FkG=15&qPW7)$;!jB0Ax5@MNaxDMq$`AR+tpU7fVjqPSSto z!6ITZb}EK8tg2W;oQIH0HWRap(t-a$;rXNzL^if=aW*DThf522yOv%9X_lV4HsReX&{s%{mR z^*sAfw&8^U(|%cVDRQxnZ%F@dN-k=Byv*qs#fv-|y@g5FDrsKF#VB539?9gmwzfk7 z>-dg}@5&_iQ30H-h(NAdloY(G;+mvDaVap#>}z}}CHad%;72MlI4ws%R`FSUj^yxE zI8;-Gmmzh=V*Q@wnJqG;N?H~q^fV}r+s+&{)mF)KBT{i}shzHb5Qc&`6uc?*_8S$y z#ald5vc-I!%J7i+?{nP;S}wSy;`jK2hlXc%x8-TJ<4q8lVhgV0Z51WCOuiq_YBCaI z_%SO-l$*LhJa60E$Chu4Q5Q+wI27Pc;66xQh03OjAiNh5mPG^^aAcB z?)Cf+^BW}0JHHQj-NUCe@9a5*yEYKJ#X%iZUL&q?FZOb*dJfo!{d}T25a(4cewUlc zEl-w__!~mEHqkP>fqTX`(Kg$&f%b{sGLoC9@eMIonM5BNcx_}dab}!g_$GQZ<1oK5`f!MzEqvj%@nv=xCvd-; zY!^Z@AwO9;`Ur-(^M3T>NsO?(7WC5lBx5{9&o+dVEu?sbP;g2?i$WWRL!yx<5jE@8 zDh_btdM=pheutHIZDMq`r;PCpJd>C#;e2-qv%Dof?u|cD!aPk2*APuCvEGE~*)3@> zrQws(yTD5c#S&H|^j_6RSS!#?V0+NcTvM!dfILm|c^VJ6RVKYuaFun)-wZy5Pcub8FKO4sUT~Jxfx+Z--N;bBCi_^bJ_O;V2!p2wY&|f4LJO_Og%AKbUfqDcp4t zV)dUN;^n#iQ2QOs4TPIRcd$MX>23|zm+ zNLGRSeto}k6ZdtshMSd#f~5SwgSLzxKBtIgrTxa%yA{{S`?+3sU7JHUw*Fb=<202A zJi_AoS?;6M<73p{S@*~>gmr2&U&DBxHuS@vd zSob))`i~Pg5$hfAm80Hww*J-ClQ7G;UWMT8Pae4ed5%~qUZ>&`L4 zg+TLm_|PaUb;E^b4_mU#Q5{zw#Gl+uWTE>}_a5R3Sfc!k(BU@Cb;R$_yvw)EG2Z$6 E58b6USO5S3 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/Thresholder.class b/bin/ij/plugin/Thresholder.class new file mode 100644 index 0000000000000000000000000000000000000000..9e1105243494724e51ad12a64a53d318fa96abe7 GIT binary patch literal 14805 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/TreePanel$1.class b/bin/ij/plugin/TreePanel$1.class new file mode 100644 index 0000000000000000000000000000000000000000..14d64673be71fc2535589878914e8e7700be798d GIT binary patch literal 1373 zcmaJ>T~pIg5Iwi4q>WcVMEpbwqO^Qf{FW*x6{OlK3Rc0V&|GUUWVKM}`>H4mkVmO}M z7m+G5M6=QnaWEX@IDj?_1`ctUXlF2z8qkqN0Ta|^yHXK82|BhH`e=2HsAd`p?D1|SCmDutiPI!rVTdWY%Z#D> zw`hA@2s?ml9M?4=!;|GWCUMgQb+sj7QyGq1pqXQD>B^b7O|5V0ydYg)Y3~`1Y>0Yi zVH&f#*f~-%naib$am?esi5!Wu&s=jnK%U0Z5ijkUr}Qi;SNaT{t!9U5i1j=kay-(r z+#ZOM44ivHt_u~sBWl#2zLv*dJGMK3grYU;ZlRhrQssvhm0fyWuq%xPuoh)4D2X{& zugKx7^i?3eaG}R_`NVkEFL|;`{VxdhN;(|Nni;npC+}93@I^q=#&bRyS7f5M!oY~4lWeYSSU8L>x|6Wz zZbbMlm-&;ul;U-NcO#NDObn+QIa)ex&Qp zHtufWo<6OkptBp$*-vV=MjK*=iGCx7q!GuIVPV$bA)~}@)&?WWj|Ds?PBBd32^OiU kC5jS9?NSp-6g`~|49qr|ASeb!T%&{!8D)3H75u;eYcxR=q}Kh{P8u8?rc*N=*vHuN z-~RF$tWvagt9$^T$+EmX3CM0mQB&9gsa+Krucn)cTCB^4Q87cec@`wa%M_v7A;df#SO}uP z++-Lobv!Y#$&fAUy6OeP4+352cEQmn;Gmy@m+XOK)gV%<&7G$z-jGj2#V}CSz6>{I z9N2Z+nYCmXPIdw+&Jr21W677pz<;6Y45OuLr{tDxp7Rr%y*Pnk2PYYhcgRiP6bcM( zkSxV=o0QvTV1KwcjZp_@7*2gNw7^-6G4yRKlTs<`W@OS-D=tC)C~zJ{h92qrDoMqi z{DyWZ{Xe~AkI=n)wsP~9z(s*^i;yLR->qXpVDiB6#Kkmb99&|^TZ;d=F5rQRnWu}7 z^xle&t;{CS5kpt0Qkh5z&Ir3nMNQFu!s_uYF)qg18nhql4;yP~Fo}f-jnQXeajXMq&845?!Shrd{HbEGeKqUfY zEHMoFI@(pSNkeVub)S@?l*W(LKi!)DnnGFcG<4Kz1Y4CAf!}bK!7)n454N@#7FxFt z^fMIGTT)xH8Jb$tq-{yARch@cVOub^r+LE`J@*x{wM8AZuA8y1Rs!4S`Ld2otiyC7 zXKbQ04a<=q>V)#FDf3L%1s>RP2wAUJgT$yv#WV$?5|L33Ws;}_V>Dfy2A<_|cErfC zD+fK~?@d1h8D!|~PENh0$2*AEw94QZJ-g`)ct3Jv3v0tg9s~5I$-rPc!e128#=J85 z1}=Y#p{ae0biG3MwROD5Ia*(!e`z06H%@0>5eoVDam51h6sTZ{@f>=19yvaMJU@;i zAH-!ol!A}70NUULMc=Pq2)>9uT*VyGy+*OkO~M>x(Ma-3^(ED)6n g?zK5`uo>cWpXg>#MUCEBYO+wI|t1|t*0Or_4ZU6uP literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/TreePanel$3.class b/bin/ij/plugin/TreePanel$3.class new file mode 100644 index 0000000000000000000000000000000000000000..787b43acf9a59beca641ebf92ba88bc22932e433 GIT binary patch literal 1308 zcma)6TTc@~6#k}LSy%@J1O-GzEz%3J$i-Vx#BwXPh+>HFHnc-oTz1LsRQxMG`RKEW z7Nd#r+23T0XSP)^DT$lR?(Eq!=bZ05-`O9(zI+2PiRUI_3_Z@Sz3)~lj%U9Jq+AuA zbQ2RM;tW@J#cN@U17*wC(o=TPuZFVdE64X{%VJ;2fT2b0IALOp;uM+$%96vh<2mXf zLvOOFM{1oRp7YDH4GgC_PNBtufp!iPXBdp6b~sybJh@Wc+mgWxvE@pJu7Y0@?z#vZ z{oXLfbukPVnk7n1P%(T@LzoLhr6S7=my?BN5a%`8Y--)YdGwmNz;Lb!HjX~@Gqj3Q zNrn*x=@Sq}N#5Auf#X%|WzBNd!T>ItxWv$RVg(#mFi7N7qtw~pwlq|0b zwUhJr_I;12VCYV!>P)$UAUxgLC8w+jn_>8PUPaLCIk_#Wt}0el)EP3ZPx#k(#*OVx|DvJGvdaX6T8kFRMqxIdH^F0;#ZZwahx?qAh@0DCX zqP|oGkP*LT%>ii zhQxz`*gK3j>@}pnW3+~`w}^kJ+a|x_?nX!ZA@0}kK%3&9kkk1M==}Y3_%or&w8kwl3!Ft|CO~4WsY3bUgkjFCNdO}g?FZUB8te>0<^cq7EEA)(0C!gXO L?JPQ?!O`^_)h#wz literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/TreePanel.class b/bin/ij/plugin/TreePanel.class new file mode 100644 index 0000000000000000000000000000000000000000..a61c60e427bc4e7aec7ae70c0316c09d7357f6c1 GIT binary patch literal 16195 zcmb_@34B!L)%Q8iy~#`_7f8Yy1qEcyLIC%GCq^lo zZYXHo6D?^c*c`?_W6O6cFAK6SLtAcBTHQ``nWzFp6 ztK6o9j|!Ov>9@gki5hpU8%fkmZc2ork(r@*!i~5wANh}7vM>~Bj;_lr2{4TmEbD9H z>q5Y85Xt6K*S7{E@f>^()2RH57ly)N4bYrI&1AJUwK5HunK@HkG?Iu#!}?egn2JO3 zhF~b7ADMVDQ$Zq>2)j&WGc)I%n}~rhEDwa@Q)0oDV9rXWUd`@`U|TpbGujlCLxV*I zMz}F28j2(UQP3&}0bBJ{>WDMsC1(At4Q{0E{9uf!U+x0$J=WNe0P`wOPBw814 zOC(&d$S<6j6HK%)otU$e=7x|jlK}0xw5>H5i$&LsTiP5<1ew&5dPpbpwn=jgl!uz4 zk-7$^!I?vbRtHzQHT98ZcYR$n9F3(|QrOm7kJU{5bM~&6#@fkv$`fuoM?3oA3ogbN+y$5*P=y>tge?| ze%b08I&`SjRaseSb&VM_#_GE7zWc1ME3UXgP@V5k1&#L66sD8X#+R4q0M*e24o#!! zIO#->u;Nkm^-JpO7uVO1ULfdZI8;ehKAOqYH`}n%=L*m)n&YF{DGwluFfSSnUm8j{ zbP-()`L1*mo-TS5P=!%OVYS|PZzuR@9$4Lt=mvLnG`2x+H#gcAYjOiLmlg=w3z>$2 z+U}ysk_rxjV+e(f4lSl7FzBY38%($w9n;uR-GTMQoLIEEt;vl|$f8kCObF10bcyW! zRhVoRu$eU+Tjo$Tje&yB3`N{oZL62Nv3X*E;xph1!V7}2kTyv3a;ia-QgyAX$FSUE z`KiTnMvGh_Y^(@vr9-``MD9Wk4WttTxVuzlVc_%sKkS#`n(<>=mlG|aIFuxdNMmvc z5EF!yR)alSYkRl4GFh4YYF%&^o$8IQ1>K+)-v_c2JvOWPFuFBWR?MbB#mS(sfKd zu*3Yg8#A#lOTn`uA{6}FDKpWpP2dnlT<=gRm5H3bYp&>L>*52dLy>uS}YCLW5+MUmCbz@!#v`4`pee?j+knH`kB`8PMPBlRH(?hcT z!*FGJv{n{sp&6NMcW4lucwu^tj-90vzpy8F;e=v( zfnM^_iwN`C9MarBdbvX{i!@7c@Ekls&8rR_6t48d#sz4wf{O~|)&!WE!5==&n0x3I?Jj6{>V=<3#J1dM~iiG>_? z=pC^TU$D7Z5&}`ZcOCj6{Rs5H3?+vXHX}6^BhGT-GJ|vup50GBft2d7%#8);r}T3l z{miog9g@;39r^{mk9du}kYwehD!;^-{>!0X37rmLQ#of`i2aR2zm*BQHQ01%5&e$- z;G^Fo7ldKgBi{azN|=h8-Q{g7MZP}zZ;xlj5gDg(zIX>W=BJOq>AE>%a=k_E-~fF{ zTjlashd!oHaM)mLs~f=~s`DtF$D=Ic_V6R(eMp}=^mqCP97ZB)Ht+7 zFfGTvy-Fp(6(p%Hk8x-KmHQaX>y_3D@XAfZ16;%7;Q;tl7&oM?<;@j*y2EGincx=| zH3VZQuW}`0pm7!-pWQ7@osp^-{0#c->2A0+z-PfS_$;2}@VR`R;d*Xenvd(bD}-@C zalXTKqV3`%%>p>9)$nyVE>DH;;0u^4vLu-0%QBjkdFf~5;>F@M2hRz{poFpsT*5TI zuv@h7OfE-Qy37z0*}@GD&tl}{616lBbP-u@C`sj-sRTM#zL_V<=Fsj%kOQ>4d#7O7xBxHY#p2{|la*g__!34g-3MBm=0Zky zhi7rl=>AcQwA7a!W4G}EUdoFRl(@;^W`-%2V55d;TinfTGUTXDau(`p{%#bT_$P;Y9kIuxfO{NkV#xVs?otzl`G2s&=m1- z#vG1w0`~3U%-0X`LPJVFGDweJ46FzUaI*7EKScbk1bUNY4?0#tlB&Q%@p&z7R%-?I z@M43{>lLKpc8ZpCw?I;P;Rz(tZIe{jBt?qpZd7(#rIVdw6j;arfZdzSM316SJniGR z@Kp|fi;=vEH#$Gs7HN+A_}c8;;bP=wXoDde;%j)ksQI@YzM41XW;N+mT0Tek2C;Gk zJ%$fvv@o4D+fhaD+AQWi@Bzi<~9qHLD~+!&*A$;XHSfwU|*}- zO-HFM3MPKg;fMHP@M1YC@#aL!1)*3xF*VY*I$a}_dlIW@ON7ETSyK~=c{@Mq;~kK_ z4jtVWIQ*E@V#N`6ePU|ZT`dk2TR!3NPB}Tf1K9IIvMV`%$Y04Zz77=mR38;@>_whSmOlvIKcdPF={B8J?PQaZ21gPXM3{mZ ziZ& zpSz|l7?uW+cfgzq7uw7GH-|r!oZ26UCFx8Jw)+o<|A+qxHGqdQDG_{KK9??fBhCmF z%Vz$};UkQ?qnCz42kL@oOeUGm%Z-}}Kpm@5u9Noe8H`5%g~MM8dcRgLp!X}(rR5j9 z;b^dV@ErYlBtH0pSadbgI81{ir5t665B4`nhkJ4uFa=20b74?HDW9YKVyi{XQIC%> zg{IX|vqE&OwcZxMn@F?12OJK*l}M9Ce{%3T8(F!&sTH)8pwE7Va;FT{Vh)@x7F8+g zM1|UCAUYdHo%2?Mn^hl2>C&gCH0(WZ7m^sj3gypQUMHt5chm`Th;r?4^7+R+R+CLE zWM_yJ6UJ=k{sMCkIJg z9D`l=r@-1$^Ekx~rnbv^7ef0!v?3&y?hz%Kw)e(EgdSH!7W5E}LJL~EUp#v)gH&WT zwj;W#ov?hoN(m>Rs#T3op>sS;Ozn6O=xp*jzZwT0yCgsx)v1m;O`Q&x2bFNw&tB2Z z(!6?MbiH4plHyAQO6M5an?-1GL^7$+CptREQ5UI;VUev^IUZFpw#ar)$D|LZ-l*m| zYQBOZ`PK%*ZLaV;2iWW8}Pr~-lAKM69Ve1 zYMD}m>{cDghR5{s(oPizKy?n*I7w?o6vCT`U*A{iPqpndXE2PRu zt<0`Q3<~@`0tU1x?R7)LfF* z)2Br^(Or(ZTV3i|w-+ztVk`GJ>Rw4ais7oI=^KZEBb!n8qs~$f;AaBartVop{ZXz+ z7iX*8KG}>kUq@5^I8}Y8;1HN9 z|5tFSp8*8S=iLiJ@~N~`8t|zfVvG@Mf{5!QXx-N#SjF8q4MxUgkxd2UqarHC2Tzi( zinx+|Q`93qsp%0PL-mL+j2M-C+R-B#k$S{;Nj>6IsUGniNsmbU^*Drv>Jb&U9*27) zz5(L-1o?KaM|^n47?3aK`WfGWF;XocE9o)2v1EEl zPh1!1`-L`c7s;hjKd+G6m3l!-0d7O`ys8kFHo04?XV&>}yWWq>h7y^&Or|a`!P`>0 z8uz7iO$la8>015jx{@M!*eI~2^lhLmrA-1@O4sYvx9V578JOEO%oYQ4hk?1%3|q~x zO)tO8V7Oajc)-9uXoiP0iiZt~M-0$*Gwd+Kqh@%_43BGsPZ)$d^}{m;)2@<$kh$Be z+hbm~o0rLwVwvkO!`>1{p7xpHSu=E+VZRxkGaDSx8@ympzNpE6Nnc;q*H`rQRee3E zuZQ&YHGO?uU*FKzH}&;<`ucr+eM?{82I}23zN8mKZj}s~-DsDcHoLK)?DW}UYt2tE8lZ<|k=U2YqEXEqz85 zl7hI-5Q|Y;$*=CEKI%TIQ1{am^#EO}9;AEKL-dwq&p0Bp^6KV&4qPFti)l+;# zJ*}pzXViu2DK$&&Q7ctDE!S8HdOwYtjP}zS*sG2Aq*ZeLN^PSJ|0c3M+0sGZ^!<#s!@Rdmw2#>ymZ=%8=5)0Gvtz4`^(*iM@&l5|5A zE;nU}9ZwF3?aU%}KlN76f#3r)NIg%ds26CwdXdgiFVST6GF_lv(FZwA4hjCG4l*r$ zkZI|IOw<0%((7)A2eWY){6xSUiTB|dbF4t`md1)bw5fycY^S?$cUM7@zKhrQ%j;+d zJ=ji<;BkjMKGH#twbM@A?X;5gl#FG&%i5FFA-75DwD-~TioE$(WVDlXsFU6h=$#$( zy_w*^+ql0gNk6D8`*D)q!;4><7r(~Pc9728MIUUX@=p4&vFuMB^k;dSq`xHTZ+q!~ zWTm{=L!X(UE6Hj*72{z|MUo3;KFLLx-bMj=*vmcezV}}4*8uMG3G%Kg$%8TO#5hc( zVQDRnh#yyPQZMyA>aV^}Bh*`Tx_TSjKTOlrJ2YSYfG$z*lB<41Yt)aaP5p$fRPWKX z>ZkM_^)tFn{hXdwzo1vu`}CUnB^_43qW9IW=>zo}`lI?SeWX61Pt@<|Gxd9B^#?7< z!A1(6B-h}V8pC-6Wb_gBcHznzrWP)zpc^t6xO=kWCiyg+Holccbn;n^y}7o7Cu(M#Q-y~lPnK2S#gv8H zQ%k1pqNN>NpI$XBdzG+IK$1K&!@Wt+H&Xvi#p+Mc-;Zdp`ZJAGe}Vq~mCi)`nxsBK z%JEN{p+2KU>IgXbZ)#Sb(`xl4tyf)H2D-uH#sCWP9KHxg9Z!AvVx9{FU4*-Na;N!H zL_jdZ$2AgY@zvaLg>Kf$PIO@f1U_F=o8$#DcJji;iVki}@)A7m?Bu2L7)sFAISrJWq05+^8%yyp9eVpM7N#K&I^pjNgm+4 zy6-$8FEG;AJit4<1D}%@7+G*0;AnT?bMpdU!NUJ>IAS)p+qwf!&I3&Hp~eFqk5NSB zhp6CIuI$dU=~?i8Pg;bpHm7>Snt`-tWWP*!dnp-S=$}LBaEH5<>M3I zlH|L(^J!jQKA{RS zW;gAp=lKxC8iepxAdMiX*tX1@+6{g)*EA6+|(2G`_Ua=DNM{6y8Vy&Yi))Ka?%emP4nwHlJ zBNI;rPNqN82!0(@jl@TXH~38`$Z2$tzlX4CQ9V7w-^Z*?^XL(N3u_8!3Ej(YBQ-3f z7P^HG^E{1<02oidGgU^azpEU zw9&^ufiMpOyQ?{ z;In4Av5Kpx)Vey$D$3GUQ3gi5pRtNk+~U_E-Ud^FM2xNnWD3-K`IqhdUdGZ5?(4|0 zHfG`Ok;dJFe@labiur?Mqx@YSl-C304aY%=Z&HRzo&y@iN8Nxt;g1bfVs<7i_%jlE zgu_qd5%C|7XFxY$e}C6jcaTPi^1}N5Rcn{qhlGLp@pE}RNU$vAT>^|04ZwY5+DasBhaZl_bMEi}fu z1BJ<*G|}2h7h2nBzI8V(uO!Cz=N!441ilpiXx0O@_GQ7~WjkSvg zS-WYNwFhZOJDrU@d^x0N+;wgGEVcZGLT#g|wMh*EJ!)W@D$!8UOh2|6v{#+nT`BjG z&w4fsZ#uTYS~Xr^8=l5?Ixq<~&Y&^L%>FDCoF3sb7y$se6 zrGRR{9STwu{obLu0W(Of#S5vb2Jhsvge1^Adsek~T6-BD<+QWB)27K*vnA+%h@ih$ z&BZZ$tA#k2999k|x%L`K1Q2|O%6(EbA|)uqD|N{W#I?S1pSUbvQZ;!?)rySeoq@8N ztheBW4kPJ*hx%DRpi$PlG|u`VJkO75q4g7LvfiVx^;7WVXLP0Y3wW9LHE%99=Sr*p z4IXdA%Y+dB6mWl2bIC%&x$7voBg%zAs>$+q3 zE9Coe7KXw!75F@ay~knqb3K3VtEdEu%etfZlnSlCXYs_BLII&kqJ#s;CnJMQl|Z~m zd+D(0X`x2gtG=F8S5+P&35h$C>NQl zpuC`OVRc{0bkq54A>+9qmCsg7I$NkS+XA_T)=O@z6I|@_uiB#WqC-?!b&yI$OqK8u znu0EHj?hWgziEW^ zIl|W$NL#-||P3+GH2dO?DyOYWwJZyNDjQ1GL93rViUd zv#AHYX!oRpb}xF{F4bIb)qV2AX|5kmaUI>5%}6B_y6g|Dn~|tmNM7$&x9F60GTo$Z zMS5C5Q@r%F5N5U<1~?LCw*miW>@e^kHK~sr6xXhq^DR9?VEZyLG!S72Ex(w>^Lc*l2~? z_#$DSNN3suA&o(ro2Fqp-Q#94I$;lM9Ueeq)g$18LP0V{r%IOQL#gy*dV>L9&?%Cu ziz~v%Ezbq;ePFN;2YL>K-gS^3lmSKcHrfh*upLim{)yh}247M=Dz|o0J%-WCI%ifM zq%a=Wh_W_7{g&eBPi&<{dI_upk7z^wUs^}GwuwUEcuJ~0U7LB9;6f{UZ*VtX_#wkxQ~uB2AGiq_iIv=QU2b`9NQkEQMQ zX|&%qO&=2|p7XekR8FU!(g$Cr_CVJZ{xf2O)>sRj@hPfZ>uxK$YeskRbxW64g>(<} zQZ%+uOJy__jO@byW=t!mDXTKd3AY60L{$Z8;ajY6PH0`3@+bhF`=!^dn-p1Pk$n~w z*`{k`vd=);>?BV0&6Gt&&};l)z(c$MYJlBnn8>$NC8afdS1G;Tq4p+Kr${%=ZJqp@ z_L%OA*b~9&bLbR%QkI(@=54FwhhW|w*^L{-`sXu#S_d2ZyexPxP?Y%93+S~QH$NPs z&?tFB_kNYSQ3E661;|Ti%O4HWy*~!8AxXL<$qGGuxxT6nVmyH?UWgHYywn?4nDPGr DTot$X literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/URLOpener.class b/bin/ij/plugin/URLOpener.class new file mode 100644 index 0000000000000000000000000000000000000000..f11225c7ee8af5b360d3c9a8ab8b28ecd0800757 GIT binary patch literal 8560 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/WandToolOptions.class b/bin/ij/plugin/WandToolOptions.class new file mode 100644 index 0000000000000000000000000000000000000000..a40ad4605b60a1e4f0f54191afae34669e5cbe0e GIT binary patch literal 3628 zcmZuz>0eaW8Ga74+?nBsI3fZ@nGm9Y3W-4@pvK6cjsdkoppaje%glASI^2tQE)v_c zO_#LkzNf`D(WZ-CY!-+k4K>|Oo9=6xr2VyD`VaJTezniJci431!#(G`=iScpZu83j zUVI5aJN~JmOrX|0)i#lv%$io)kZxrLY&&;s!ZmFxuOTQ-U1l6rT zywqLPwR47}TWKZgo;7GZPZcshW>0mG8R^rb_QT{H5eVm9-Eq|+ zwQx8mP?n;S$ewi0v`lv&^))pQ(t~b#pkc#`*ZTfNu#w`uMpjRsi=bYhynTDxwk!f+ zWFokogxyPoC-J+0?ueU-gz<601=e!)|nH=pf?DBOuX*J%ov>PS_^n<7-fcTK};Bml(LkRs795tbWq}u;t7!_l~i{riFK$_y9XrJ zqAI-K{x?$@Q0&Jfjw|+ZvL7(rq%q-+Df$VCehkoM(vLfK+Q{c^Cgn!o$qAoQOs9ow z`aFoYY8WckM@c4$VW>=_-aeg~#up^Rd*68EU$4D-Z9amx3q%LT9D});%cwxSQ$oim z^N#`t9etdSZY&iwKal#A+vai6m)*!n7&yttnfXPXp2ZP`#+bw!#5J4}s47ByNR^(M z_DeB>9Gy|+olx~1g9%HeSZf3rb4>}C zlgVWBU*#5cR3sV} zVQ+|;`GlFZY=>7ILO@v!T+VI^@{;`?}J>1H8EtJoKA znNmvNIf>_$iHf4C1FR%Nw^Vh`O1z)|jAm%pHHcCK1;VzN1+A?N-5g%j@B`KeW05){ zu^p`%ekid1W^)H7%^YhjhB~|?@gw}0ee$fHn>3Em{O09Z?T_THk{-{)ir{AyP58MR z!7qgBl=FC5;+OapGghCNFf78oeYwu!V7^sn(Jl6)dHh=9HwwLoYy14_dB2nRJ^nzw z8Cw;K>J_O}@>VpPIg$n2!P*kx)bJPPu!o51fD2*vCxvP*ej{J39lF5sV@f$Lk)=H~ z&;>h@;JAF2HN zvmTw($6Gm%yp=PHtJ1L^HPl(l(;)=;1?t1sP&XWi-_}1IjBn~64#gAwvuKz>Ltw`& z8lNXi88-7l4#f?N>4=k4A_coWMp{J z$}c@CJ52u~Bz3mTq3sHGw!DCy*Re`11>AdyfB5>gUB~)u1?<%>pj@q?i_n6+3GDNk z>t@hBjY?mnhX>woJdg6g@GK6`;>Ze>JxXvE-BsxA2(Gv}s&00L*M%!9FCfMxaS@w* zrEe+V!H%F$Dz6Dv;h{8zedz?rH5X7Df7=}1@iNx<66y0rAFm)g4|6zv`YOgdwOTE1 zSJVV&aHfDf4^u>Z28{)rqgZwt<)pnQP{8}ResBhlapzyeYqd~-7a!xrrwaH?t=3Y& z=WDeBzN8Qjc)=QFNWf5OjCdXQ?L_zlcO%#U8@Iv5Mm)|iK7o2XjRc!V1761+_zxQK zU)+fs*et@>A}Y`%YS1j|(IU2Bt7ye`(ar|nfi^FihiQQkD@If8&NDX!>O=xx!B?3& z?bP!m^D;o2*YOmSCrHaPxWJqY)82!)h_6x0)AaWs;x+poI~RjN&m zcU&#%G8)7UGKaZps3W5Yu&wh%u3Z|;dfNXwzTsyTz1O_N&2KH>o6`sdE_3tkS)AeW zJtnvhYBf*KPGhwnvYGgmIb2nwSv)p_BTphagTqhZF7CNu%;qsa%xKNw+Ex67(V0dh z{?khc#eZJF^#Wd*!*7Rzm+7S+jvZdX?w~98u!!4Pt~(i+T@1jz1ba72p#yhg4|jW! z1U4BmP8q6>K{Zj&6zHU>E})CZ>izvs#E8=5G*&Wz>*Q zC2mnV@>e|Lqr|8yr2zA>@j0%3$@_KN?YUA$Y7~EC2KoJ?kv!_BGWc1p%OwsiO4Q<2 y{JkjA!`0JL$4cD5f?R44es6hzsnN?8(C3L)`^)fgS@`h*bAO8m>UQrqV literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/WindowOrganizer.class b/bin/ij/plugin/WindowOrganizer.class new file mode 100644 index 0000000000000000000000000000000000000000..7b072c101c8249c5ebe47f536579cc91081964c5 GIT binary patch literal 5384 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 PRe0Iuq5nqP4*dN;*dclg literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/XYCoordinates.class b/bin/ij/plugin/XYCoordinates.class new file mode 100644 index 0000000000000000000000000000000000000000..1141dc6ab8897929dece17055021227425f10e7a GIT binary patch literal 7855 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/XY_Reader.class b/bin/ij/plugin/XY_Reader.class new file mode 100644 index 0000000000000000000000000000000000000000..d7dd803e7bdb99811fe477fe62741487458ff143 GIT binary patch literal 2397 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 U@6IObzaT;4yReq9cb2dGFY`}^PXGV_ literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ZProjector$AverageIntensity.class b/bin/ij/plugin/ZProjector$AverageIntensity.class new file mode 100644 index 0000000000000000000000000000000000000000..84b6a0e4c9bc917656d2982ed1646024152478f0 GIT binary patch literal 1471 zcmaKsOHUI~7>3_-dV@0DDprLSS}58Uwe^BG)I>B*NUEYWU8tL(4s~#xX=bKI7P>P0 z0&j_$fC-w2E=;T@E{tpch%Su3K&{^yhEk!rnRA)%eD5>wd)hC*KYjvm5XS@H45R8? zrlM8Ls-C$#Z5ne*$u`WysauLEmz62qR&-0X=L7IE#9EeKl;?9*y=1F~&JdWbsJ9i( zV&GSD3?99DlYvdqOjGE!-@dL|i7Z1n->TOHRUB7!)jr8Eo@`y>?3zYNv8+r^Gh~~f z#lkQrrcy-)?`h+j(hi0m2|;uO;Xz1(ge?p^{?Rg33}P#`1rVkh!PT<8gzebD&|X&T zY4;oZlBxWhd`r$~vR=-dpAqIw1mG9)5edH4*`S1Ww28f#gaFzYy7Q{8oU7iPQOry7 zjHWPz@HUD~S zlN6ZB&{?qM(v7on#f72DLaOKi>U9xr!8w}~XFDo}wUz<}ope%|T}__uK(Js`&61K+ z#dmhE-?6>IIRcu}b;Ufb$(E&90UTxMYjrg!&y9DIl$-7zVq`Gk!3!>&rx_P+J0tlI z^z?`eL2DmXO6a6#7p+XH^U|0|kJZqf9{Yga^zQfQ`;7h?2GZ$w82D5}G##yB=&jQ& zPR}rP0l$wRe}Jw0AqM#)jPS=8rgt%iBY_blXhoedik+k)W`f8=%Da$s_2;RR;2!pz zc!_O7E^;+H7CF$1`rU<@3->sS&}MY0{#B@ZAk;&Gm_LD!KSh|vIDgipJls$oMha=t z;TXagDRbhUY?mS`C_)X2Y_kvFHxeup!3*^AWkmSPCW2^#AW8&&hoFBmK^MK) zhy5w50s8A@ kl*RTY5=!P59c2U+RMZ6~laK&{V_3+lj0KCeB{RjqMmNTd zx^&~h#1(53QG5|JCiqCuxKclbAHX=CTPln&y0|@$dwS3B{?GmJ>-j2xKAe)l8CuO* zbUqYSk9R>vaXYhjpz$QwrkjV(_NGiU}z~>c3xjh&)ZqobZiEBdd^%htULps zOfyK9K}j~mQ0`tf^D&JfGF`-JVg;O$o&YWR#2c2NjfUdW{NX>fe3~ePP_-iC28K0h`e*e*hv^N>5GOnJzGNR9UJ!Hfg!lj15ppXLME*Z-hOyJ;%{OZvTY-G*3$ENgA`GUpCdzM(X&Gf$xb}9xX_+FE|dpq zYQ$5ZI6xJZQ0QAl(Gpb#$o3~w%h;PtJw;8j>k0O)pbY_WmDQFBe zjo*}5Bl*#Xg&&XTR|(DZZK5{dD+uyusN>Jk#9yF|zeI}cG5(6cKue(YqZNn93aIgR)`p0&N6jFd3aP!VCn?kU-LhzDvmBq+aRt5;{`RBef`8za{XZ+Pw#; z^}Ijn@td4QH*vpi<3>mw@HLe2*Qn=j&`!R_-xgu^ZejK!g)VB%(c@uq50h|zqKq|j z53SoryX~W?0xgz`9u4&rk=6KQUbYDA`%bWR0(*xDe@_~&qlbSi0#moZR02~yu&&); zjhkSa4|c}~(*#(1DyprZ_dXiP+0sq%>9iuUZ#&60i0l&%^3RCz4XFG}5t+6{rV&}W fN7lWYtcjD9{WL$0<2XS!L>@yYPLeN?9SGnzTGcDL literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ZProjector$MinIntensity.class b/bin/ij/plugin/ZProjector$MinIntensity.class new file mode 100644 index 0000000000000000000000000000000000000000..d8ca9efa98f5435857d305b8498d3923c1a21287 GIT binary patch literal 1376 zcmaKs&rcIk5XZmowXjkafm(_{`6Y$Y7TH#Q9Z(LCG$APp)^L#1QWsoYcS*Z7a*>0? zi}C8oizm(|qKOCM*}uRTe@Bf88Vwpx#yW3Vp+fbr`(|dRGoSg+OP~CDv1qiot)btP3Wv$k%T4AR1~epNH_ z416xd;4w4`vPp(Y`=Xu?Cm4e1a#cnt(OC~5#^Q4sCz29*zKm7KWwn$p#;e`CO8r7Bsy=C&T5iHn z^LdSCluIudLv_hTLI(*+9q8dgO|rPq5~!;a%O&4S8P$;KT|?eFWqRoxiN!Z?I2M11 zx>(->9C?hU4K&AM_tCt$fskixoxYOO>TjD_^RQO3LwAc`@6k^^Xs34@wE=$)AAf-c z{t|8c6}tFq#OWO7ZwL(Z2(%G&;uxL6>Ozr@n1YRg{`wr?I>rZ%QjkC}W?$bToAgKfX4i)@88ucLdM+s157brqNl@3t&AW#D*1(UQkjuRN5(@z#dg#Jy%C5m9ki{Cj( BA|e0) literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ZProjector$RayFunction.class b/bin/ij/plugin/ZProjector$RayFunction.class new file mode 100644 index 0000000000000000000000000000000000000000..7ec894f58438b3f192d12bff02e9244c6cef10e7 GIT binary patch literal 589 zcmZuuO-sW-5Ph4bjZLGCwN~pFMS7?fj0bOu2MYzEs95PiPusZEEiozi5IlMG_jnM& zgFnC@CC;`1i{y}fGn2=gnK$1*pI-ouuwy`DXvncM@sg4Bo!hH07z;NJ!sa!9?k2t) z%fL6FGuUJP#2t_OBj<9cI1GjOK}O9(hFUL6;+PmG(wFfmL%o$v*&8tEo#0*+!BDc0 zN5Mo6ixvzlG2~i`Q0_@zTqKV}5#I2jCm1Tdz~$b6hf?KfQCH(I>}GbF--iaXCbLER zo^(ks-?}?foe29Wb`_Q;K@^iGu82t4><3Bcimp^6l>fXsP}Y#X; yVi^^vd##|F;%bUD`p>Y2rBt~+&8hZZ@O4g*%>=38I?*-Mu`zAKNMsY+I(`8xS9+cR literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/ZProjector$StandardDeviation.class b/bin/ij/plugin/ZProjector$StandardDeviation.class new file mode 100644 index 0000000000000000000000000000000000000000..b5153debcd295fdc17545d11e7c54df912a1f0b3 GIT binary patch literal 1901 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%0Aouqf+*1n4?1CGKUIs1qmSF$uEp z_$hsd6ccFz)S+J-j*6(sCgb8D^+^~P7gLjQQoqrt;CFG?>SX?CKH!V@0~PfcDOJ*N-FChiR8B-B(K zDc%7)>KGQwxu=NaO(ptss3}xKDA>MJ#vPRMl@(HPKqAeh(b5VTqhc3SO4T~cu8@;* zGEQf!Bi?>1P$8%1-zP?V!|y7lR*B9q5{Bn>s>VngF_n|LXlYIwckuW+m{uugrCZEs zTT@y7w>;vFl zMbEh)Cl~KPSci)^+&QFlS(I6|xVgNhbo>376tVkj)q6ThKmIBCBPS7O^BVXue8PGWDGIA z!WdvWW#{DP=CM^3(XspGw*Kw+;?b&zjz6s9UG8j6dO&W!h2d-F^KT(n8AI_9{5O_} ztCu0tiIC)vWYp0^F@AW+RT2AP1S58ou3DiY9%KqO=+M!SC4IKF~O_MkChHQY)(flf{QF+JfYMmK@70q-YD+((qUpRL>j?0a_ep7S8f=|ij- zpCsx$j2dF#6cE1gD0`<*vsL^I3)5rlsy@rY`#1~f6D*jYV}X2<2=)~JC1oG6cRw-q zY2s*(91rk0fWvqO-{lwfAL1b1z;i^ZL*mB^Qi>NPikDSjdeAlxauX$GD zd!AMJzGn-5;JFt+^gM$fd0xPeJ+I*>o}b~To}c6Ao;UCd&pY^~W#NCUBK*oK!>_He z_>FZcUbmX?TWdakXD!9=tyTDg)qy`+SK|#Ui$7V{;?LI2_=|NH{%UQUszu8oF@;ljVYBkJ>~LA&XvjAo}iIY?wKS%m4}s%p146Ckx!w5 z{^*iDsWSc;jTrj(8vxrbo@3*SgP6pp zcKOqkqwYRUBJZHzaqOeG%CcK(w#3xGVn z1Ca(RCr@g(RU!KaB)13Q2CtcsXDZ}5&J;Qry#+#edl%m5nI$DuBgB3p!cO8OM#rh_ z9wxGHn5^A_gs5gI+s3fzQrxd1=MKdcZ(I0n@g6RJWG(f?P(BQN|f DOiR13 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/AVI_Writer$RaOutputStream.class b/bin/ij/plugin/filter/AVI_Writer$RaOutputStream.class new file mode 100644 index 0000000000000000000000000000000000000000..ed95f82ddc0dfd045eead9da2197f409a7da5ceb GIT binary patch literal 1065 zcmaJsl&hFFG8p}(d4%0oagj8&pGG*{`2z}faiFoLJ_dMQ@igEj=jL{dVV4!yWVm> z97Gjxes<=$+*kTC+h7$xb+V*sT$*iJN5cV~$EFM7fGQVYexkCU0$2qD~Q(p;_OtSQujcl3Zs% z`G^I@ze21{h&9~D10rJLjoHN2AxhK4ArbelC4O{4y!jvT0jcLPWms`_y*Na9nmUHA pBqM6;JawWuLzw&=?{5U}wS_zPL9$+!Rj literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/AVI_Writer.class b/bin/ij/plugin/filter/AVI_Writer.class new file mode 100644 index 0000000000000000000000000000000000000000..f7881e3446a9ac3a8944f5b218ad96ed26fadf99 GIT binary patch literal 14845 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/Analyzer.class b/bin/ij/plugin/filter/Analyzer.class new file mode 100644 index 0000000000000000000000000000000000000000..1a3df07424587aaa98a595b5182811bf888de672 GIT binary patch literal 32621 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/BackgroundSubtracter.class b/bin/ij/plugin/filter/BackgroundSubtracter.class new file mode 100644 index 0000000000000000000000000000000000000000..ef30254de639b8028e2cf9ef59fbf9023e76e7df GIT binary patch literal 18803 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/Benchmark.class b/bin/ij/plugin/filter/Benchmark.class new file mode 100644 index 0000000000000000000000000000000000000000..261cefff2f6693e8df41195cd05d17e136a4b88b GIT binary patch literal 2408 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 Sk`tXTV^Jy(Quh*h=zjq~?ok*JGVk$T*vNlrH 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 NMlR?%4i(Cn`hTe~ag6`~ literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/Convolver.class b/bin/ij/plugin/filter/Convolver.class new file mode 100644 index 0000000000000000000000000000000000000000..1b959a573dc5fd165a390c2686a4ba2b56429032 GIT binary patch literal 16200 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/Duplicater.class b/bin/ij/plugin/filter/Duplicater.class new file mode 100644 index 0000000000000000000000000000000000000000..cf2f44bc7419b368de9d2574ecf260fa0b327264 GIT binary patch literal 1301 zcmah|+fEZv6kVsCY3W#QmfHxeAYR&XDI#bhh8KxRV}m9T5+A3-PzQ%;GZ%gGTl@hZ z^g&28@dNxQ<2t8PDy^4?*=L`gz1Ld%(jPy+eh09HM_Hr<=H1g;$BSF8UpsNV$O&qD zamRCQi*6R0z{IKb)~b1y->SW6o;r4vMMj`Fl=hM!b^n>d5JN%i(Ld zBanIG`fjw#_j2V(KzrIgb_}F3nnw&&X({lu_H`=!49a(`Z@5zr^ z^x6$6Lc;$|JlXWniDJrFE;sr`97KWZw|1^7tJHH4n9d`M5sI^dmSi4E;!_a&0;RHI z4%)U8hDm|lOFP(+g{555&i^Bby*cjn`5?0FHv$_9wBM~^`pQ+&S=bIabxOQNm zgi&d3XGzyMXvcx=a51TWyw@Wq zSeMh~^PbZQ9J%htSmF+(`RV*1SAkagTus_*yw!NhH@$j+{AW5TjL~M)j|Rr+=eq%z zz$9;6BNS+5@^eO~dFHCCUobU^4Vvckq$sitB}Kd2O}?ZxFKNw$me9D5O(ogH19}>xb4rR%SzD^CZ9L?OU+Ovo)c^nh literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/EDM.class b/bin/ij/plugin/filter/EDM.class new file mode 100644 index 0000000000000000000000000000000000000000..84a12238b61a4195e32f619048cb570edcd678da GIT binary patch literal 10652 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%0DQye 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 C2Chf| literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/Filler.class b/bin/ij/plugin/filter/Filler.class new file mode 100644 index 0000000000000000000000000000000000000000..8e6fd4755fdb76a48125149326d7d30329ce4e0e GIT binary patch literal 7990 zcmZ`;3w%`7ng5^6Axsz`KwyX>l0x*PZC;h$ri|#}ipQUAHoxNZ4ruUWL*f)-J0q zVI@22*0t@h+p`Az3Kb_!SJ@L8?skn5;ySf!hq_erp>ae+0hTWk|BHnH{rFxQC1=XzJ z?XogE6{g9+?sTf%&ScyP&O<8AS;2T_qrE%Jl{smPf`3Uo8P6``E_16kagC-_%nreW zauX3uQW(D`p0wBYbhX*(jaFO27Qs{PR$`Ntj?1&#>CJY=sam<-`y;UA&c2MD?V%#4 z%spB2vAR_?2ca;_L=YiqonoRGB_Yhl90R8jpF$)|%taOT=&&87O6FF{Y9sK;{2CME zU>cZ56x~-Y2J*JfL>VRqa5}Y*rLEo&7U4{T>?pLoV<8ju_yqT|FYmDuR5wznibIdZ zRa*i$i%XG8i4bbB*u)YnRR~bP#$<;eNT^qYa5fqZoI_khoAXU9M-xZIGc8v8y4H*J zC@ys_P1c_3>avotnzjJWR*bfTus08KvmL=G+Qi6>%qI)K#*GgeTWXgZa(vUV(* zrPZTtebMeTc|o>CLs*3tvGUpz6mi*DwMj;-H*o4ep{tT7ggw%Q4LQ#_TdnlJloGI0^8bs(E^7FC!?OP;9TV!>^T ziAzLkpPf#p($e!O6PMyL&Q9AM@eEy1W*1CuN?4i9;;KynSe(|Luq{q&hi!lwg+}RO zVms)Y25q&muiG(Ia{%-eUz1DPDX%!vHaH0cme+c6QOOGHa5%qiU6MX5lZ z5!?u|Lbr*_#rEUl8F98eFtT38L>4^??W(*YKtf0*|^QF;?}IyzO%*ZcB6wQ-9t=fXsJIYG6~xh zZZ%=yW@+7S;&i9A$HZAq>+>dhoz`6@?#9iOQk!y}BbY)_yj#rrMHBZ5pneBX@%juA zx8fb0SwT320X$&feixvR43#FnB;jugaX&ASYO@l{sZmFo(KgnUN(k!|zHH(?G|7$+ znb?PixeuG;~x-r(~7KOdORyQ)Js@C3;9$CdJ2{uqkzUCt+K&G; z@g$y7n9!a|((^U;8&l#v3R5*1MxykXkS)7BW8ztSnGOcvfFf_+c$B7M9}w+ltl|2FYIqEyIAWLMjLD^k5lG4Kye{GW^q3a)F! zw*vSf-OO$NSYhYJR8%`%v>+nXM(w2J#%QGkx@;<%a^hZPG@gvQ9=I$-*rJ7nL6jlh ziO$hEU2~#yM+UlMBuw_D2p4a3*MUev!vHj zF4JOlB;(uTq%YHo82c|K{vUrOGbxTZzU!3Uxy@4R6vf-=gCoU4!ICUiJS?;r-d0e) z#K^=Dj^jfE{}@}_ZLl*viEKtkI+Ifr136_W?524f#k$;ju<#|dJFYyY@`_o#nfMh9 zPsf$tRE7#jfJ-HMx{~MaW>zCWBKekp3NgV{HnEx{e`A@-d@e<5id?f-0kvaU{pmc1lI) z7mD$wQ9GqZP8X$u<;VD5vy=VCEYE~YE2LSz>bmhG(UElRl(;^ftB8SQCIZc*takI) zM~_xfQ_WJ94x5%skapR0vQEVNWp%2lW{+_D#K5lRu>4bV=??kHn#w^nM^&4uM%7ZL zwgl4w5gpaaaRM1M-&A#aP;X~EYX^zp>84sJN!8aYSwVWvFx8pTk9*k6_^s$DVfUx*&kEwQv z9Dy!tr|o3Sh1!Kb#%V`|9Mw@rZ`Eh2D+ElFVCrJ8RR^vCb)~7U(kNzhs4*F3|a87WC!De;`4?jn&2!qxJ=ti68ZoE%bjsxOGM-QyCH z9y6fsHPwBxzCW(38R;4@)%{X|1Y*ZlWwwzVH#Vn20?n zf)D(Nzz+~S-tr1Loa#Fdx%y5E>N~Bb?>wIJUc%WtlJh#5Pj$R2-hI_aFy#OnMVQK~ zUv~yEjcwBzfa$2<6YFoxwjYLf5MIw=%xBO1eypttFUVox0L+^3C)w!75*hj=IKQFf-hRv*!m@q@!Yc;h z3$I*z6stCe&p(1S^Kw`hZsjG13x}|ILMir#)eyE$@XlOT6W*qmYH&86Dz_ZkN<({5 z67CG|7(znEiv$A*@l~oHzKZ>|6{Y8F8$x=&BU3+uwH40eF1_5ugV=o#SM5WvVuBa3 z{S_Q|jeg?}bhbOCV7tj9T9t&a%kP`thfsPn)K?Ht@Y8{h)(O!vCA7^X!n*)da52hx zlAMaGX^86x|4kfw8{z)~q8KC(zJW>{#HlQgXX9zi!FTx)<3&`lwyVags9|MP%d%!Z z{z5w+#{%Wy*NK@}s7}KoRgE*XdC%a^G@)x=X$ham3AThuMU-GVZeX8>dw&%-l2smj zmNL&r>7)w{s1HRK-VH1=C`b{TYFc?CAEytajx|G|WU?wbjceY7n;q(4nCljC-YoY8j^bxjVp45C+z(+|Ni>M~@!ZaXB=!)_bI`V;%^7}NxE=JbT!-4b>HU( zF;Vw91`No!JB9m>LF%bCc-S$~K$9%yXPhOxF2#9RhIK6RFTpvi{Tq?ur7JQ0Ukyz#6e`y~7OAGM# z!&@7iL8Rn60QES`@z6o=6+U^`wug}GM~EVPYd}kL&{1)7ZplMn? zgeO{R7`C34%ds3@a1nTV5Mdd~CBN$`II5gOv^LVMotZU8f4AwA+ex(!QmYf^BTkif zP|2NCFoA98;;5unH0lVPSJZYDUCIdZU!-o4OF3V$=L3FQ5Uu$gWQ^7}pTq^H2;?I| z4&nQZxFYZWjtV+SLIiqfgBQpi{T$J9v>LOPn@Thc(a8mW+qYoA25jew24aKD=97=#(V>BzfT%K4%jYSYsmf}dv*_DxTQ9yik1fl z1CpF8Ya)K{wxeqN=7s&No@W{$j zG%#V9|JSmR4Ss}ikflsM?Lf0gy`|G1bVAZGrUi}4bfHjA%_JD35QHfV_&v>wuMv#> zh~n#7EyojQ<<**%SF1uR<9H&&_b{Ro>3QOHgeY1{RRq>y^1Hz|q)ut@m;1_Ts?+*W zd{9+2_ztRh4gS(#y+p=08v^BlW0+bVAotG-P6-P8&4_W+lpwipM2ynlUQ8?Z9#RWR z7e0as(pn@fqtvA3kE;63W$Kfjh;c+U7uW3v&guJnN!v%zD`_wl!iG#+8?A_ev{SDA%^@MtDGaO zY=%g)Z_za$V>&*{r29B7!xQp-kl(hSr1L&SCw-bEe1@Z*W$(9fjDO$5bF78FgV*sq z-ogubhgH%C?3Hw}kJKUldD{)^ibpsztgR^FaoDA#v|`Gp{e5^(ZCACthwx|B!IqyA z{*0eqgpyK$iOUBlX{mPkAZ6Q4%84`Ij&9Y-{t)Hds!2SK(l%)lo0NGezent#6vt4n zc5+;aCUzM-$1%fDbNKH#${lfX2+P$p8OR{jtr`h=^6PQZp)YW{xw%*I@4UDfI zPc7NOD66*WG4do`3;0`I*eTEaE!rk8Bfk8jP$554-fiq;!S4urvva#mZsIX}MtO8K>tk_sgLWv!4~gK9nOT3r>iMS(RZ&Ao%{77@Ch`T?oK1cg{xaU3wCkb^I z;Q{%YLteeaWFeBPSMqn)D=R+7BHUwM6x0qSog_uqM_c%PNmFg}5Q junzvPAoQJ`SMzLFO^V{GDYdB=MYZzP-~qOa)R+DV-7{B% literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/Filters.class b/bin/ij/plugin/filter/Filters.class new file mode 100644 index 0000000000000000000000000000000000000000..d8584f4544c3ff0991a8fa1a36c4fb6d3ad5ed14 GIT binary patch literal 2527 zcmZuyNpKTK6n!nr9$9L@$k+z6IV{1F41%y@He&%1*v7$NzyZP_jpZ@&NX$rp2?;S- zAp1r_Og0B{N~MZ&0)|Sea!C~jQk7edIp>^1E`&%cYw!uoH2dTEY%yb6@m@1qFzomacW^ZL1*-b>Q+hnBTbcNt?T0kTO%5*y;5N>;9rK@0@R;HOMb09A;wT-5d1A4}w1&88tCu^n*o=yq` zQo5BgvW7C&LzR`=n=>iin97=#S=dY?jgf8ve`_vn1W-$rl`|bc5M@{_u@Kb^uh3^Y0`)4@Ns)HbHVQ@Jv2c<=CPEZRs;FWT zl?bZYQi%{`5OJ)~uv|cn2UOx2tQ4qVsxEX>8zahErf9~wZdRvOORT|K0gZe14dw|3 zK}`m*p2tkAtE0qbY~e=ZWKqvD$C~lsdxU9@^e8ti65A9Q0o!og zV1h_shlcF}!AB8E?8LLmvD0E3x*JN5N^+OPbW{b=h7Jwwq=HJ4!ae8|2+?3MpD^`o zPLZu~B3{V|nN(&g;>hv2(@1Yt&oJmIT?IXLqFv9IN?2wWErEHioxGh(8IDt;sJF=3 z&5`aP4x&ZFA%R(pW%6xm&+`)NQLS<~A~6M16&5c@98-{o2>CX%kTCLvJ|#acky3I= zxf@fLdDc)0L!wuKH{AsOzFeU- zmmh5B4IvX7d%Vaki9S>ZK*niKpKj+3OP#hQ@=9FbRwJFE?}Iocaa#GV>@}_Qb_$&U z28pPiPAm0`66f%edpUXYl#4{9JAiX4<5wi6p-RIAff=P5?=&pKHd9`)0(gxbV5eSm z98BR*DIx6=&?aHYNT@;$ua}&}rRiEwv&g|3u9D0T|MA{iv%2GuLu`~LD==Hpa&iW-Gx#zLzV;z= zC*Y_0hN4P>k$2^acFJG_W({PigD+EU7f-PgW&D?;g58k4j9t;Ex`#Wmceo>|>5eRz zJMy}5Ea&bps(E?_S535|ellD$19f*W>qpEV<=RIdpmo7-2n$>ZxMKG!os4r-+Vbda zEPDJ%rF%qr8}KxiaHBvYBK#`1zte%AV{0^a3(;uPEi^UVMZ9SY%R2a3VfM9L)nO54 z()BDvF&nEf2OBUCEtv0yxQe?OMkz!!Qz)vuT2&8bnAHsaIqap67OuKzwG8{H55e~U zQ4NXvyb%KX(e35ttD#APD(a#)ksO8=8^Zxkqos5ci^ z!&%SL%dV`O4$lw6KRO}a!>msHXAB(*zj_<5w>O0^{eb!9V|a^+yvP5CteGm&`B5$| zlTb10X*n$J;ImOh6|Y|3u_9n~G%O~XOIWE!Rw%+%6uS^3>P^Hwj-y!4Cb|L{zI<7% z!~kEnL9E5gSceO2#TQx8OW1_Ve7T0P1z%z-uCZFz`5fKAc8p;MZX=0bY4sj<<2T;t zKhTap(Sg7C-u#VD{6lXKuvZ9l2_N>0N*oYXI4ElP9Ms~l%asPNtPi;&>hJ}93EEhI zukbb5p?Fb?Q0QNJt*@`Iu6}?u8iwwpLcQ%HxZ+WSIs3R1@DrFD9LL|bW3Dfy7x>22 GUHc!CnI^~p literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/FractalBoxCounter.class b/bin/ij/plugin/filter/FractalBoxCounter.class new file mode 100644 index 0000000000000000000000000000000000000000..aba81eeaf2b4f78006d1e31dc6bab0f6c778ac24 GIT binary patch literal 7143 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 Pr)y?n8=}AhvHSl3?eKc* literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/GaussianBlur$1.class b/bin/ij/plugin/filter/GaussianBlur$1.class new file mode 100644 index 0000000000000000000000000000000000000000..fd02334a8694979c28e49caf84eb69c7c018bb7c GIT binary patch literal 2874 zcma)7TXz&i5dJ#5WW%N*n~;r0j3KZ37$s1azlj%3SrrD!?Xrz|^AUUrvR@qNp52JMQM=}jOmkUC->Gqbko6tjD$j;MT) zK&?QV)>Z<`&gNYwU-3NU1lb|ew#_M93DgE9%g?N%HAZi`NS}Z_X4)A?9S=q=hdjGS z|2MQ5xYJfXJD^$42~<&evZ16I-{;roX?OK8S&g{fkqBmaEIL)$IqLjFVfNns=1V{NC`1K2=>GPnm^i_j(l`@?7m!%dvuf zfp*cTEb!hunf%2r9Hh0(C4I5(|-JsYjw7 zlBKm0iy~>A#9}nCZoNb*(%mc3h$hx;l30RfmbOTwBk6vLmPpzvaYy9QFL7ri?T~1V zq(Ou`IHCP@*G}_DL*{q=zI{Kqe8xghU1p z3&c8k%ME;k9;{4HDQ_aWhE1a`6?)S2EFM>talV@Z9XGF7rk6TDPTtg{O`Tm)Ni+;h zx>lhtiO2C|0#68Z-S&NzI1H20EUI9@DNql4YMNj}r=LqA50yaS2E-XZ;RkA3;wjJ_ zYplWy-`_FIws{k@@!d&SIGVr_fmOFoh5JUr=6Es6V*$Y-{~QidmUtQ-P56qEig)hr znoJ_Vu>>lFcsIo{i5VOhs5A3+1&J5& z(tl=j#Z!@4wf(DhuSmRleYg!toWhv|PTz2@6D7XE60hNP!kqbY)k=Zp8>$_dQoT>% zO^LVgHaS>6-&N16lmkTtqaqOe#Tg+95;OwR0mK81vtl=)W0}Yz$ML|-A00Ex)lR%| zrRaHKP~UkxugZbtI(`D55pwFX>fXCHlcf7{PT~L#lKr?_@$zcK;+LoK`ZsJXd(wgK zbn>?A6HK(B>m9Z{devw#f#p%nJeP}@9kE?AphtrTuGcp_e1OiF z@G$2%${Vsx-&a0eQ`$e#0<@%33^YKhiD5=_0vXK-Vl*d!(VXB#a{?F530gEKV9}gl zMRNib%?VO8CqU7h;6!r*6U_-qG$$a@oM1$A0ujv#LNq4;(VXCenm$I9!Ai=06-gIS zLm&p6Ho8NsHhMyI8EqlDjdVQ38l!bVh^&#W4bf|)6CpMjt#u(b8m$XMY&KexA?`EM z^&$Fblpi`{G7O@ID z#A-Yy3>+5SI4OE?UaY~-Vl93VS^Or};ZMqUoX7lzoRYt~N{q`FjVRxeuc zj;>>yh~r(7YS4*ZyodKGm)|gk4@eio@ASldNKzahQI_qfi(NvKxP(FR51h+r7NX@U zdLRL6C&>Q_ M=SgCWVR!od3(_*AHUIzs literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/GaussianBlur.class b/bin/ij/plugin/filter/GaussianBlur.class new file mode 100644 index 0000000000000000000000000000000000000000..333761808a45da495b770ef427e0d5b4e33d7093 GIT binary patch literal 14589 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/ImageMath.class b/bin/ij/plugin/filter/ImageMath.class new file mode 100644 index 0000000000000000000000000000000000000000..d7da5ff30fb1c26164f57440204223c0827f1546 GIT binary patch literal 16598 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 NF_gvgvcGuY{{isAKu7=p literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/ImageProperties.class b/bin/ij/plugin/filter/ImageProperties.class new file mode 100644 index 0000000000000000000000000000000000000000..7d0feb643a26c4e7c751f8b8d2ee50280c6161f1 GIT binary patch literal 13924 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 A!2kdN literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/Info.class b/bin/ij/plugin/filter/Info.class new file mode 100644 index 0000000000000000000000000000000000000000..73d53ecf9655aaa0e1d099923fb4e0d360ddac9a GIT binary patch literal 962 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 BM+^V} literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/LutApplier.class b/bin/ij/plugin/filter/LutApplier.class new file mode 100644 index 0000000000000000000000000000000000000000..c0f617d14313e01c10013ce0c4397689ee7ed532 GIT binary patch literal 4204 zcma)9Yj_-08GcW)n@J|qT(()7*an7cu5`JEl0e!fDK#Y_(4;M-fJ}BL$t0VZ?d)vZ z06ttS20^Q!f)s%ul?Z|ur6B@RKtx4RM3f?mmp}gXFV6$i_ng@@X{mk4kDW7TzVn^$ zd%yR4&&eB~JpU4a3-E6Z1p+Ibu|y`F8*$vku#*mKKhcv5)@L$l$M!Wu1S-a?omL`k zxg&|bp)ordXebh>{nWB6>7?7eO!*KfbS5$aWj!?Op0Gx2e$BRVdctu9M7JDSy9J7q zmYcNGc8ZgO0!5cNt`oHLYkk93fk>y9vUL<9W}pn^0_8o9YhRI@7_$8VYbb3C#Cp7> zmELOkj(pEgMuJf%%NXVE|1UQN6V2K|j!~Xo-?Nxwf8aarNZZkYH*}Xk;By8v6f-!> zACWtMYLP(ckd?LjGJ)f{44}}@G24^tRUE#Tw6obf8sUrQw>50l>G@Oxr{Q!)WD)pD zfo8R25x5gN5GZh%#mW;H>8L}4hI)Y-3CVFoG0=!6LOfyzy^cFD>f6~-FU`WR7F|WJ z)IVX8HPDg3xf;$9FqW3qzD@j|&|LDV9Jsj3m%X$|%o_&UBp@+;Boa;&r`Mevy7bvLjV61dmE zH)VcOOgeMEZ@WQ%+DR&ceA~c%xSu=Hk(!jqc1vUvc)-9unSmtFl7iIXy9OSTfEH)- z<&Y>pVqhANvPz0kdF_wZZ&%Lh@R+2~DUv!v()Z&AW+W%dx$javAcCo|lcTB^xjUOj zmPA#9-5bcGMben2YD~r5&Ld1X41GcY9nDIrs{<;}#t=EK7*1OwJPWjBhyxoyed@I` zDq;;!Yj}>c3p7?=b^Hu3Xn0=0?MkQR2Xf=_^m*-nU!S8nvP3hv(2o; zcCBtwtqF@UfXCH6J6Zm~gpz(^nfeo!D)t8_+S=04QDJABx5>QTf8QZty*-d3U zh_?;=3Ga||ffoizVA&eSBB;9KF9zPlA>u%(*wssQ=9Oog6O4AM>&<6Y)j@gS{MCSg zWwPh~X5jDmK*|`svL8k$C!|uOo+?KAQO5LsFXtz1-nvN6^2M9mY8fR?r8lWLQA8*w zyt^n_;jyQlJk`{boAFshJKnMQTfw&kpVTTu8=phvEC&Txo;L!ctK!%Q4G_mkd?U3` z&0o2Akk)cTy|MWKYARN=*FMF!&_fvm0%8nlIm4+^o*F~zyeFx9E3ry>*HzF3v@Ad^ z=VdY{@Ne}!)n1cRx( z*F1-_s?dD@1E`?a)v@zq7tZ6NZL^JhyL9gXtdoNd4mR+)v3(ww??ZiUMe!pjsjaA< z#wy0MZXe=&Y^cQHX_UtLVpq+hf0p*!v}vs2b1`cPDy>a9hEE!qq=#WlIbV=!up;#y(f3@%hN8y-cuoQX#wDNdSdtV~lX9Jwep z<;5dY<9qw|Xo8l}M-Lxh0(Z1(`Fw8U^DC{zO@;ILT1!d1q_Tnv9XmRu*?38XIgO1p zy{AQww3N=^tfqMB9KOYw@ATGauR~*|4>s2n&EetZ8f^~W+fypyrTjd&|M0s_ahqUeU+f zaF=IIa~>;5zXPt``Sz zgP4aUUc!)g4Jq*kZ1E27P=_$0T4FEzon&8V367*fun;F`k$bp`=9?5Rv!Qa6Ri2 zWp=<0$=XXL6We1ye*p)tiM{e1eo;Gz*JE$Y;dik=@MjKxoX4N{p`Rkvz{^qczc^I-Px0JLS#e{XG#8kJRoz{8rN{+R9)<%5GL6x(vi4Tp23W z4hHIzyjc=<2hQSL1INt>NX{Hn-bJ)_qaBkZ+l_?wCerz4Qh68Cy@l4dDv#}9Vn>qR zk*7fvX;q>E?lyvd@VAgeZ&AgH&|9@4NmP|>86qF!JRv?pR5TP2vwt$|0{rWL@8|6t literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/LutViewer.class b/bin/ij/plugin/filter/LutViewer.class new file mode 100644 index 0000000000000000000000000000000000000000..757f36318f2934c2958bc2559a29bafa57937cf2 GIT binary patch literal 3775 zcmbVOYj9Lo9sixXkGs3M*=&-%B)cRSpps~`Npj5B`G@q-_naU4hdox9m20%b-6 zd(Z!z|ND8))qlV7CV(FNPK6>6wvI;gnR3d?MvqvTl39o*%B2IAIb{}9I0XVnjpIf% zV`Nj&J>y5siINJpK>JNxdnqHH-DZmra9ERh0e^yI@kt|P5?kyOaBs4*R%r|Iosk0q z&cR&L^ud984PN*J8WL94++Cg=Hw*iX@r)_Zl*mmOnFB__lKa_1XDMwJ1=^(e_pzPI zTt%}~rWMOO6Z1X{mkL%k)%V|qMdDrv=o(abXq!<;NyRrY5mhK>1(tN$iVL|3vsj$< zue!+<`XUE>Sd4ZRZ2}=_-z~{BEWrl^w3J!eJ(xDKSu;b27?rpOOX=CZp@9xd2CmqV z%yndQrH-_5-0Vn<>@TkNVL4W)xPuCcwM|2+Dr(!LG zcmqD3O`6m5L@L&?erReUmpe9+w-fJU(XN-3vcb;54FcwQ?Hu%=S3{o+vX92?GV;UL z!>qL)Y}T*^_X?;y-e)Emc4ueg@PI_!r(r;%yhIHZOfy@Qr&~4LFHb!@9mteT`*29Z zcEknhtm06?I8kJgvRawIQJJ2d8WPw=ELq~&V#&x(5L;i3r`-@c`h3`f531Npb+wEz zmHRMEwuypiluVmYAlMm?N2=8*!)c$Adjtnnu!;P0aj-EO9>AfhW*U}HTNCt9r{+1b z+Hs#3hw&j54=$8pM^VFv@ex+kRN5-h?!Y{+jizz~6Dr098gG)SAqnz$*nBoIi)*$Z zrSVmMS&jx$8q%;x%DI!-$H~H+&{aBnKw5T8L&mmZJY!58lMGo6Ih!F_Fs3AfyFbrK z#y?44)KHT2j)Iw#)WQ*#!d*YE{=k%2AcY9<701+ajn&x0>9 zT;1!|Nz0$s@QfV6YSJp^b4AmGXIZhb`|cjecg*%*1Yec?_cehPxA1AcOsX|1J^cFn z+%N~NY%({c;ss7xmVc^jRSSF2$R0P?VdwA-sp*?6F?-tD675?=7SHqew#0r%p#KJ} z99TC9vxohy$1o>_fMW5ISbUpF$DmB_;-z45_6zpBdZM;-O1vRw~T`7SsHl zpv-AbMdgJ2Om|C&AD<-$`oStkoxRyU%jO+73N_eWohXzuD(|rBU=dp>WMWdFV?M~X zgy;=(llPt@Q>G;W>Cl4VNrXCCoq{ZO_6WyTchdz&t!=TN9=2C?4eP;&(vV?nuW>ec1a z-@8aIgS{X=@jW6|w&Ec*X-JhDJX%k6Zu{4Y9ioqn{%+%f{Lv6vcarqga+rGvV^f=6{#_Xirr zBxWw|O`p)+YEoA#I2{UI#@RzN_>3NOCM$Tng3poLITrBM@J&wz)OK~urmf)1mk~MC z^p#NS44&%^U&iwxe$E{Vg=g^Msu_Gsc7a27fmnhe!Yyo@R@AdN{Vd8xv>}Lg?w0W1 zQY^y?bn$h#7AyJY>Etse!gom*hOr8xeEAtz4U28|5MOa8aSu+T8&8n>8D4|uunsR_ zJuYGcE@2~n&d1cd*o6168P_Q7Pq-I2 zbROSj8#gLRe2?BK2r34?Pso9g@*rL&xAmDOc_RnkROH; z#0=x#fQ#a7{1|W0m$k|OF5@R0U|W>+_$l7x7`tCtgP)Njh=dZsTet!p!^$$eO=uBD zmBq~86;{hh@elk0zohol;%|5dzhbTY5&ifzdBbAc9Q0BRVmzDeAsEJQSZ`CP^IxaE zAr(T!V!=P2@=rnz72PU&h`3t&Kcce8pkAk%@In^Jb{iIAdsg@S6Y9Tkr;sa?_Aa$3 H`0alHK|pL4 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/LutWindow.class b/bin/ij/plugin/filter/LutWindow.class new file mode 100644 index 0000000000000000000000000000000000000000..89124be3e5933bdc1a134e34238498f245997469 GIT binary patch literal 2450 zcma)7`*Raj6#j0SWVd0V4@fc4N-3gA8XDzo3kWR~BPmo0sg?TJCQI0o?$*tw{$hVfmWX9OJ*`PrRR+Y@}NM+q&}l3^;tJ*%ovuN z+?RDt+e({7*RTvnAUIKSUE309N!K*{JyD;4&zu%mo0g#+J8Kk+75BrHjqOk+xXZLm zcaOl{sMPbq4;MZ_nz`Q(V}m{y)%B+XkjC{U&Db`3kQlWjwqAML5GMY$~1b#x|S z^lIqCE^9N-r{Hi%ST)?GNey? zO|9U#K=Y$&A&g>7!D|AMCl0uV*KvaGh;P6y*v=3KlmpfA0HeHDOnKuZ>&JZx-XOdh zX_fPol{3!Nl|mTDTN>VmE)b%3raEGtBVx{KN*-o4Rt8172!! z?`SB3rBOLo8J#X@zQib`_Qx_RW|%7zQ*nkayew3lV|8U{6&HA#K7LGky{O@moGO3u zls!wanPpm?^};U64~y$BdW47+>&%PVccPz*JQogAjL17?DO|EEOa*hKwbGG;ro8G_ z@N1DVjakF#k%d7g8?u4F0DS!8O!HIYlUFF;e*Tt0$^5^Iko)^a6dHgLsDyBhH} z?c~|+1*{vJ$NF^l0yc~V=CQGiXnX;QvDTh>Jbw>c;=k}$#^W9R$AsMq_5hd42?+uc8li+T7F*+;OHA>oW1jF`6jB7~0#{1f)ZrzGSv*8L8;@HwLR0zLSW-;TT3jjwope$CPS1~$HB zN$!%S@2LH~N7i@+ZjG!vUaxRGsdbq}^fAH*w7No4Te-i=Li!o)0;O{#s~xi>b&h?x vgE6k*I{Unrk{h&8J)$?jcb{oADi8`r6^!$B^T9vwIsAo#Wd9Z+`S9VtGKf5a literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/MaximumFinder.class b/bin/ij/plugin/filter/MaximumFinder.class new file mode 100644 index 0000000000000000000000000000000000000000..935854318a1eb3a588cfa11176daa6d6e55f6465 GIT binary patch literal 31278 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 O!Bt?`y>Xc8)BgvxtGTEE literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/ParticleAnalyzer.class b/bin/ij/plugin/filter/ParticleAnalyzer.class new file mode 100644 index 0000000000000000000000000000000000000000..eff6f0015a6bdd33402927ff6ee0eb149c2d1561 GIT binary patch literal 37002 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 BGvELK literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/PlugInFilter.class b/bin/ij/plugin/filter/PlugInFilter.class new file mode 100644 index 0000000000000000000000000000000000000000..6eb96cc1b6ca7db13ef2eff7b27bdd3113670b07 GIT binary patch literal 985 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/Printer.class b/bin/ij/plugin/filter/Printer.class new file mode 100644 index 0000000000000000000000000000000000000000..5b487397b4377e3c9a3fd2bc9690d949a66d94b8 GIT binary patch literal 6161 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 D!z#Vs literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/RGBStackSplitter.class b/bin/ij/plugin/filter/RGBStackSplitter.class new file mode 100644 index 0000000000000000000000000000000000000000..557147eaf03772936cacaa4299487b398e43acda GIT binary patch literal 1308 zcmaJ=YflqF6g|@?)CJlC1);oE`k+)1d=)_@(U1TdOEBW6Z5hkLcH3;X`ltNj2ZU(i z5Aa7B&&<}=fZ;>$%-p?m&bjAKfBpXc6Tm8-M-dR1vrdXlyIr%K;z!H&q+5LZ>P6Wz ztEc6rZFx*Y5fX@>n4ip|Z928$>&l6&dQn6KrmkFmL#S=%rA876TJ@&D;10{S>t;>z zYpcYxD~|+*x@k?ve7NRH=?H`>c3TQW)-A{KHdvp@9x%JrIFd00kuWfTK@H%@y>`7K z-F>rS^G0%~Q8n!Y)3wyOlMH#sR!d+((OuuElF*V~o0=CgJH3v}o@+U^Qdh5TBfH%X zftv=R=!;^K+aCVbvFSL{{_9{26G*5qZD0no%78Z89Q_Z6YBXrNHPy=%0Rqz*-G$qz z%2uo6sefp=C3QDoQT*gJMXE(LSb>Db>dhs45JMLEC~_P(dwFL$M~%Uyp+cO%Jp(JaFVLrX`MLz=uanfo86!Fw#W44VUw{#e z@+6SL7^Av;$m$^1k=)W5#(!Yq{W;S4ZV@7P&7AOlq(ciIgy3pxWKSZAO7RSY%XEcUhyV68Q^cI!coTO6@`V zI#aZ8j5^2vflhRx6Iz`Qt*lef9(z%?r}OCx+}+C+f-7fO%@smi9^?vPE{}RFDf+-# tIy^-`rina*N&2B&$@w^4wltn_WNJKVl(5bQB6R;LHpnQj$$S9M{s40w`%nM? literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/RankFilters$1.class b/bin/ij/plugin/filter/RankFilters$1.class new file mode 100644 index 0000000000000000000000000000000000000000..b756d551b0ac9e1b61381d9ed15ef9e224cca051 GIT binary patch literal 1785 zcmb7EaZ?*b6#s1q1j15M+CZ^TtEL)`wg-wLf>eyeo`ympjpO96<+3J=#~pKb>3|={ zPoOh*>iCBrzz^m4b}v$#VKQxIF7J1{@4esKcfYrP|MTZx0M^hdAjfb|o!DnyzpH%v z2j#^wv=4>!f%3fmKY*WN!qk#1bT7{j7j?Z?Wq-N1MIVJQ9Bt_jZ* z$DU*ui+d`ntPn+G7{?}gz7$?XoiW@so?+n1D6*YX(Uk|;OAwOVly>u!FAqgWDTZ9z zVVKa1u5f$Am}}P=Zd@*Wr#f+ufsxnr<>FJRy1jVdmYukHaYn(j+T~RFd*Xb*AA3rM z(UI!<6i-PFb=(VO)C;^0b;RTgydbRggzrmlkno}&giUf2@aT-u({M?$k&37oB+dpjsnItdY5AYMU5&Pli(V?&oPA?8dW)_QPRlbxS64c z95Weu%yA31_3jEsIYS?E%x1f59CNs%cR%5n&(=QWcnf!n7{O;83!ny$RCQs~4N_e8 zHJX{QXBvSkyp{-+ejhC6wc4rrp{=g0QrbKPlU`=3OIc6NT(tt#*({=l-2%R1SQs`j z9Ca)*j0;!KiJq|A|I3xlC@~dx&s}*ItH6&6_=e%ut2w^hDN+*}9A99E{Evcu=t>%X znupSrd*HDSBrWQJ5OF`G?b?;%2ZYy4q>w*?KV|7z3aPP5GlXAC*xkw;W$E* zVX_lsHxO+w>}UGD{#t0ip*oG&xI;4=);tkKGQxdo!3aI0m@1XD_<+)kmY%UBJYz|A z#**laCCM2}f-{!nW-N)#SdyBtBs615X2z1p==vR@X90`EdXLag$m#4ju$EK2Z9Po! zj`b+Td)9nD#RIE6n&JbiJeFd~Di=~LTXW+n9$9k}DQv4;OtEUsPp0_DD)ST{Tk}&X z)~(;M@hj!bVT=C9hzaaDJ}1mI#sRkRgzQcI?FV&4G+cUb!%C=(gsMoWh=fW=sDOm% z6CzKDIw9hOXcHn$h%&+YANZ2}Lctl~EVUEt2zOW$RrUgFgx6W?N&y>L0i_)(;A@I{ zO0jvOcT7tj5YnZLTEF4jS82^TXhWYPX9D&NdWL;t5AfWq)^LaygtWYM8v6YUCQ`7q literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/RankFilters.class b/bin/ij/plugin/filter/RankFilters.class new file mode 100644 index 0000000000000000000000000000000000000000..6fd8cd49d0d60c8bb11d850d5c5e0953cf8e3ea6 GIT binary patch literal 25548 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 E1&VWGi2wiq literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/ScaleDialog.class b/bin/ij/plugin/filter/ScaleDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..2b1d071d865db501891cd9a31bf8b8822810dfe2 GIT binary patch literal 5079 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 MCW3ydzV`lq0jX2~0ssI2 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/Shadows.class b/bin/ij/plugin/filter/Shadows.class new file mode 100644 index 0000000000000000000000000000000000000000..27303fadb064ca13a00a4b0c7400273fca376207 GIT binary patch literal 3402 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/UnsharpMask.class b/bin/ij/plugin/filter/UnsharpMask.class new file mode 100644 index 0000000000000000000000000000000000000000..5bc74ef4a0779c283ce758c5de08a4502a91f25c GIT binary patch literal 3929 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 UVkA5N>~D^WH3}1`W0xcT0~rfj9{>OV literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/filter/XYWriter.class b/bin/ij/plugin/filter/XYWriter.class new file mode 100644 index 0000000000000000000000000000000000000000..c02f58e0b4d6422880ae775de33928b6bb92bf96 GIT binary patch literal 3425 zcmZ`+{eKg89e;n@q?cTeK?`LI9T31lQ)t3)bS#+&rDMw|v{F`D=EJVZH9ffGvdg8k zQ>Vi@a1WwxPO3O$K2+y?sIs)Ky7@3gr~illfUlSOnhf9HTtd<{`yqGV`+lC@pU3Yv z@4o-$TLA9CKNK_xgv`^iyj9GYx!8nhxrP%P9XsimTqy7ftU0Yu=`l;sWnzQlr;U`W zphaNg3ZWsYNahZgH3&4D*}TAQ17u5P^^8GkAtBImznL@L{iJtAMg)9^?6jex8LKn| zp$e=XFmuMSVs_kcPUz#7A<#Zxr*vyXcTD+R+4Q-SW(iIGKuB5BFP{GIy5K<5Ss2}q}>%lBss;Lz^1OUUB^xtg+j$W@56Qykr9=;IyKyh zjRI{2eaaXeJ7n8V+RW*$LDX$!@mf@5Aeu}9_1tN!R>c-!6blOGz#UZ%X=yq2L4n&p=x|@rw9dkSb1Sc%+;?*Y@~T^h@P^ z7?l|s<3y?GWX$TFF89U(j0W&Y=Ds>)0{FDR=0|d}y~C~@mK_sL)fy(QhQnbMk6~Pa zUYCvJ;H~o2kdlB@rtTRFVGWaDd-x5WcEW7!;G8->vWp#*BW2L6ZkA=SfMyRWKJ8_aid^cMGrP7?hG+2{UC2wcC(X1wDIoe~7GKctRb1rgP4A(u^KidVf7yA`KfGfO|~#wxqa$9eof!w+TrTM9h7jC2rl_=$oaS2JJ7E`#-& z3=vPDIlL~hzb4!N=d#g%AxCc`cV!pO%SQechfpJqYP(9Mm!+p~Xei0^)t|EL0>^E$ zBToyh{;VL10_Q=y+*>x%d6fEcoRR{;bagP2Ie(V$yrx+`8_6%oT%*->DrwD(2`8g8 z)1VN@&}1>ug6CmK@-!+jX@T%7M|52{CE=97s$o}8ojI!KE7t{e*e*ILLk?jQtAi&h z$1hJKLgLTQALIpqPaoI3j_}#auNdEw=I>sBHqS*9ZsW71ytxLexz;=etU(*Uhzx7_ zl)^{I-OP8ed)FIS*XFzOIzK!GD&N35TJ9(-344qcB^1GW++J23EaPh8`VNFDiVsp+ z$_Kl&Jw~$cU{M-C2SvS z?^;0j+e_#fYmbp|cl+K2+Nm?&xf}4TlyK>3 zRdlG5>pNF(&d=cr4P1!_C6e#=s?EKvb2!(b&ej|IQAca^{D&I*X_eZ@*c|rMKubM~ z_!<4ag;WZc@XLC5t)inD67RZ{zorzaZN12mFz%i?sbG@&$-*6z|~A z^z#ZYAAi9s?9LlR{tm7qNR$^jaR15yr{MWAwH1FOx-7K+VXcC*H_)k|M?vx?dZ8#F z-xh9rd)!tyBDwci=S{}3wIN->`7+~E90l*;!T&;gAMFa}gm@1F(Wd_pn!vj>+JwLV E9|ZVXHUIzs literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/Channels.class b/bin/ij/plugin/frame/Channels.class new file mode 100644 index 0000000000000000000000000000000000000000..e2edc938b2a09e5976e8681de7053b69c4f82a8e GIT binary patch literal 8468 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/ColorCanvas.class b/bin/ij/plugin/frame/ColorCanvas.class new file mode 100644 index 0000000000000000000000000000000000000000..7c7c1270b0d1a70fa5bc576f748a34adf6eacdbd GIT binary patch literal 7339 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/ColorPicker.class b/bin/ij/plugin/frame/ColorPicker.class new file mode 100644 index 0000000000000000000000000000000000000000..a9ed0e05f6ff2532aeb0f9db8b2e205b70675efe GIT binary patch literal 3521 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 H`1}6=N9$(y literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/ColorThresholder$BandPlot.class b/bin/ij/plugin/frame/ColorThresholder$BandPlot.class new file mode 100644 index 0000000000000000000000000000000000000000..811fc9538d24c50c43661fe41da3ae291b4767af GIT binary patch literal 5607 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/ColorThresholder.class b/bin/ij/plugin/frame/ColorThresholder.class new file mode 100644 index 0000000000000000000000000000000000000000..a9c9e3ee56bfc1755f0eca467b4c5d6b11953988 GIT binary patch literal 34837 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/Commands.class b/bin/ij/plugin/frame/Commands.class new file mode 100644 index 0000000000000000000000000000000000000000..310f785c23526df5f09314dcf6817296ada94831 GIT binary patch literal 7118 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 C9nxq3 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/ContrastAdjuster.class b/bin/ij/plugin/frame/ContrastAdjuster.class new file mode 100644 index 0000000000000000000000000000000000000000..68bfba0cc63ac5a6d6a8ece53914b14de9e1bbf0 GIT binary patch literal 33889 zcmb7N2YggT{{KyRyPM5RvPoD-LJLI<3B5!TdM8M)22fd&B`gWqm_oBFiWSS*zyb;y zY$!;UD2g3VxwD<^)Vs5Lr{3v#cb=X9_c!nDW-0plUnF_+=FQBT`OUBYe)D|c=SL0@ z(GY8wm!zQl$jbh8HBD8K+WyPyL#x95$46@$>q8BVV=GrSH8h6ny`%+oI=a;?bSbNy z5DC>ptGwh9l(jOnI@G@=R9n@5PE&1dXjx6zOFluJO?_xhWB>5#aBXA%u@#MxXzjEJ zhOXto{erq3-Nd|k6Uh77%U_l)dEPQTqmP$T&QYVXkE04QB{DbIv?{G z_}l>25~*!$;5O?3MfdgmS|-R4_y=!?Z&kE5Itj>zt!x{HE6~x~5UO>a`CN(uC3z}V zM9oen;mLSYg&t|kFm5$A$6cp;pMZkZP!X!ZYdw{bsz@V7vq0bz=Yh0|00<^}E(@T7 zTMfo(jh!`XS~=G%Y*2zHaD_(0>18vxXmN4uDO?1T^pBfUHhIdt858HuW2m0vdq}xG0x;5!`u#dJT-45~5?Mb&oeY8F4wwT{>r%gGO&823Waw(6I zFj5Hs--m;LL(heT47bO3w(hSw6ZC6CT%Vyq^^9vgsarKv1y2L1ZH# zU9r@AftPk4N4SDej0k4N@23apAs(ZVC)8q7N9x3-eKvKYAeSDtsk>9!Z&MGa^q5UO zozmkro#2$7v}veQdfKKr6!g-wz>tJcWs>sIb3oGRAijpWP(`@XPtVZv{PGtBbvVuf z%F22#;3@phrul@@i}bRWUcxRjBb{o~D+HYNEXT+L2XVVU*tC$_33}b8Qz+=8H-Y4R z-5;+1qfLt`olEc7w1m?A^e6g@m;Q`q7;{#AczJ_O@6r3<9aZ7R$xV^D4(WnCCUCD0 zZ90`ofKrVsA|PZ{-vq?^6byb%jh|N2-)#DfKF6HGXEcRaP9L!?GncIlS2Paqy@*Hp zyG?WHL?6xLo3Ct|$2VVN$c4S8`BC+6oBqQa>x1l>7ip}4Qt@A`e0;PfTF+g+v#BfP zbITuW`jLJ@OU&4`1fMVHeJna~+rv5rzv2T8QQKZ=-U1VVX z_2DYk+v<7#Nz)%+6KZG}juAzs$o7gXumkMXLL*tGhiXC5^|ojyf}pcdWu;T!HUuxp zspBE=x(>F;5xEWs$`VRV8bHt{o&jLd(WYWL$tyYwYM;R3BP5YubfSm2X}(P}Xr@tegMYiZ8`T~a7 z@41on;1^qS7deAkz!c|lU!ur0l98?is&W=+hPa-W)=(FwOmtd zi<1&H>$s-G7NuOHmq)-JQ8B_6Be}u@Il3ZR?-irLyb{0xG{cPzelbdn1yKtqO5TDp z{D6qjcY-Y@ib?1TO%lzHP`R9*!vRn+#TI2^D)bOEN`jrBAxU5vA6-=k^#Mz1L%d_* zZQv!uG+Rs;GXN!EhoHja4wD3-wz0t*#VlLQ7IQEibUrz}uFW9mnlz4MdL%u@o4sNl zdL>|E)J?yb>ma~Fn~JE9SCoT8B$|wi)??qC9rue<=pi&0Cwj%H$>SIm!!MSI(;37| zft&6;k{Q3lHpMcVCesvdTxpB2SdRSwP~%w9204Qh%xPZ~D{K*=6M>JF;bl!#)1eKd zh-$IQE1*BMEyge*ThxjuwsW14q9Dq)0nKyl<`Qv+E$RjKIt?!#7pkbPs*g6+Vi&TL zpy#yiy}&1$fNBfF4UN3;HN0!ka7P?xeaUZ%Bf@caFolFzZ&Mjf@rkoA_(T|H&@djW9IhhkS&TTvh8A+h!XEP&hGV2c~YP0ZC97b=<>9F0R4EaENL0wjSZ8(Gz~ ziuuzmwzyT?1`uF^u6FzD?B6uPlo;~cY??shec}!tGqje+DmO z7SzX8AFtS#%()y@-Y;Sfc|KxOIW6M#KWdA|SYBmf{o@?-!D$ON7ZZ=$;tBC22Ex-L z-39r_Y+Ukl-ZBjSv@MdB<6D(+MnAKD9SDOZM)n{mD%zUo=!lpr(9!j(L&zH9Nig!#y z!>nd4=4)1Y{w1jAaTrMw({S__9^pT>_(nk2N(F(mX+6mtE1~0n3w&pb?^*X~R{=5$ z*UzjqX7^N3It&pW?nhhv6sI>VF0!8KjUODgC7GGR^j+hX5@c)k%W!hX@#2%P^#XCE zjk{O;Qb-SYlk^JeoxG~JXUvs~$s)l}q)f4?Gj-wqskXHFopalXqZ{3}?SApj|#W0MKFygIhVsmV@OGR+{SS!YusyChu9?Ry$@WcQyb* zfh@M=Nk+wQj5_VT5=KZHc~62J>yoz|CWkX_jKB+7ZZXqeB1hSBGz$i1`eSP%Rkf~A zXbi8##(HYltvCoET^5D!Yn4&QfE%WoH({bJC&g7x_Hc3C6kC?Lj0aRw&DNZdCj*J( zG(klLM*`g_*LC&L3V8b*AJ{DCCRz_uL&zDnoXN=NZ>p;dHHIC_RLI%3oFnIA+D^3* zF3=`2j~;yiPiVd^7w{NqPUEq)l@sbiYoH;85;D^`7TK<^3S%F79Lt>&Ehn-!nc0(E zB$s&QVmMF}%=BnUY}(rLRCyX$H7n)Orh0ZW9chKRI0F$Zm)bJK;AD%K5)n8RyFLy%GvsTnR(1EiYhr`9Nv2pzRq{-C)a$ z$s(6SuN3d>Ok5C7VW33BUUlTfQp)z*Oy0LWV(_I06`T zQoe4>H{_eZ4KxBI!2h6h8&eB!HOOY<--HRQ3p^-)$A4b(jDFd`gFK_Zzo+u4Z%=Ar zT>XnJ-;)4vz+t71*)lFvkG19`G0iq}2DSsw^Fv#H#1QoZMy~smCG#h?{FJ2+re6aO z7+3wxmY+#@%2@Yh)tFsnWv)7QggiytzuWR3WW$+J-9(0t5JxQ-(q+$+OY0hI}E$(sxa1l0 zZR9Ic7+aC`(t9XD=~E@PDrM~T)P!rRzzR?`!d4^IC@ip{X<35__VTo$RUJERyc@n^ zj$0icg20v(1X$tJhenB)tT7HuY_5rlLFVziL%>IKVahd;JNKn2z)(l^W6;9)JsKD; z&Q;TFHJv?e>_9Xl!!SKp&9v35INqCD?li}yF-8=rdA6FbAWKqAU}{2i4Zjm0D}y9y zSOz*^8et^~Yg!e7c92mEH38_d9@aQC4y;nFM)%6-^iV@JBH)a6r`hUs7P3syTn#?1 zLXhAJ1GWd|CDevFU;!AsCJX_7tE#kBn1Rl^(85S%MsC$JhGA;*4o07wY_*v=S*8I-GE3yA zmHej$8iTskR=26G&e8xf>_Nfear_@#t_m7U4mKgar{mVal!)6Iezi?)hh?O8uwSCA zZ1K3ZJ&kZ2AE_ybt+3Ct#wh;JtGjH~tl*hS0~p)bzMwh3hB;-wME}RyKy72PsjKd> z)xC_%?eMw@PQ-s;F+`FfMP_{W+iI7Bg{$hKuxpG|n80j!x2^UtcTS5m%!O?it{hkl zea5Huf~@C-SJm<0F<$yUK?9CseEtfmGE^TvQgOkoE2`V3?pLj9KM(#WTRc2e8AH?P z8f7fE`0WP(N%3&+lxQ8r_8X@291aK0HP-;hB= z{u+c-{HzdEh4UlJ^O4z*k6?U7b$<7mCwK4XQ|~}?Gd`~TY0+qPQ(eAss*L7W{mE8; zR)2xU#Ezrsu&4p5ljC&jw@y5Jws}Gp$JWaJVu;RJuBU^pU zE79?aPi^&A#!EJzoXv+IV3zcmtv)v@n1KSQ(LCiRf49{?46}+q;dy^$tAFyl^s;cc z&Zqt*sGz%9V)y*YXt*ICCT{+kXni$c)3_pku#+=jNV9>v|HoF}uufow>+7TS3Ot2; z>N{YF8KX3R72X9|#6B1G1AH#(N2qVdQ!3-06`%SU8{F5YnLXts#e1~0wc_`{ z&&7gGTekLSXkjd&%mUc~(H1QoFq{eV*jWq1I}7DwI8ww3cyRLnr1EU4uF#RZPP-Nkx3k22%` z%}c;G)v5;OmC-sUr@(e|)?7U@rIFpC^Epv+;2jRmx04LpvR;p`CVu>U#_v5==s4 zPVMF!w`BfNgbKHCKL_@0Idb`-+DgVylVOt&C9V-Xslo43u}?#=P;;NM+|1!}SV1j0_u3D&k`chk8X82a5q0F!im@rN3WYSl#rmdML zAlfz7cz)q}(O22}Y6_y-i0knHX(Lx(YwO=|u2u@Rz;!jC_Ig|2!1+L4u7#$}EpM{* zW>dEeDX@ks-z;d*5i_1x3&CFz&X4QWOzjN-IrcSNIdVY1!9%eEpS~4Z`q52&8p?0_ zk&UsjHIY>jh}M+Gwp2=Mxf5%Lx`H)8M67`2U@oDx&>D3!8}EI2FYdPWJ#hg)!}Z%V zc=dfie3up{b2fFP!jWb^y$ccw#Fp=>>G_U^s_#YULnHi}3rKLZH^0*He}pR?tp{YM z@6`|4darI_lB}#m(i@LEDH7)7&v$ZM_=`3|ksKHTvSUb2^zh&z`vfarx)C_oXoHR$qzjti?F1u{l{R)Tb z0=8+n$DeKem-yar4`&0My}{PKZ|e{AhZqoyqiySe*WgfP-2Y=+f5J0i{}=Zk7me10 zL+lyWf3@}Bcxa=_u_K%-KezQ4@k*l=apga3{UyiHa`2W}QG^(gD91WB5|ABP02{>^ zuKH_R|BG94xH#UDQS3jq{)Uy~6tGNZ-+lU91X>)VR|t2LwzlT zN2#Wudt_z5U+)F};nPrN6?6sY02bMnU}kQ+a?y1nvFpHq*>M-RMOg*XYaf@oM^HMr zXewq9X9=u*u)$*Xf^{6e85l3H9~E4v)?7kseO4N{M4ZFrI}A2l@3S%lRmW)oP-P5* zTz2u?7y%7Ub#>8tu)==%V;k~A#wh3;GO0s-{BiBiU~2;_fvb=jAL$#%<_gXYIk zy%(49!;ZGq$wFozBa+z~wh0^wnb;||>8-A|m2Y)(Mv12;vNz1-tERD=o(PPxde~M^ z3tBQ$iwkvglp*^l@e2)cc#3r*!U|RaFg-~y;|V{w8^{n@1%*u4n3;Q>yad((Dml)< zF(YhyN^RTTV)Lr7hyFwrZd45)qzEKSettC%ZY*K=Fu^gg&Xa6w7;D|_j)&ARWwD;Q z$DD!*0-VS4OBQeJ3p6JO*xTr}MjjbQLbyBB7Rd8iqalFjLL4D=)@O|c`*J$xk4Fp_ z9JavtEPEU2V~vOOu_mC+ao9Qg-F?<1Ou!w$I3!E+eHOIrPH~(yHi+ThZS0x(o|&*q zQ_fKbbK|q7u^e4hXXrgCAW-15W?&mYy7CPs+SzP6yns-!HP^Q08AEwZD8jiJ)&kpF z$Q8cIsKb5W&$Y^Jnnovjt;O-!v=N(SyyW_(I#AEVwH4TEkeJsxHJ&YC+Q6?EUBmjg z&w^^~n-O)0kM}WTTg$8pU;#F4Caf%k)*O$Ei?D9paK9C{s=O97U`9Sx*WD+!wSt*% z0Ld9`@dbpAd=_-u>?xs!eDJh*Od=oWPkdGlc7xR;_JTrlZ~|9s5P=I?U?=HEK4DnZ zF{~b7(A3-dN_hj98f|O1Q(DcDFjKeIRp>8{5Vf-An@p-zBznDK~28G;)c{Z2(5; z#NCm&FJQG~I4Hw`_SLNT9WM&+AYvj4Ps+N=wyv;uS~t#nw7I4$5)T zKPE-^97ixN|Kj4W9}hv@;Biu<$s#YMP%82vb199|QJVp0xugK@akd5J9BN0wM16bQ z<9v###~BiH#R(Ezi4Sg=D-yhL_4DZfbKed9`3HwM%oRsC%oPVX%oWEs%oVvB=8EhM zb4A96xguY~T#>MBu6^ArastgglJv|KnR(`lWIS_40))9D{lQ#uIK*6$RBx_GA23&> z445mD2+S3U1LlfIxVa((ZmtM;nJZ#g=8EJCb450;xgyonT#*B6uE;SpSEN9jE0TcC z6}h11iY!ud1rIe>gpADX$^D5K@D@oi4=3ii4$|hc@uL*<|wX` z&zYDjPMMf1&W)HWVxqX_(CN6w_j@VsLrM3`a9@#hUy1v0(*1JWS0&xAz>?A+;VT$d2`EsSr^PL4|uXR%zcP1j<>yp+g`?Pui&n-Ekv}vg$@+z7J8ykwa`;&|4ez-K@~{dN55^M zLxp?j_bv2lVZhr$uN7v!(L!&Tzi$_|(z`A6LEB`D!3PL&S`X+s6Eu7RsJIU>ZXnW# zit*nhP|tLv-poa+O*s;8PDi56avEzEVLMZ%j|}At`j{SYmXq=Xb`v}3*-==O^+}BW z+CpET^bb?|vT!ebv6<|`tbfMnU(FQ6{g<05y)f$=+!ykU_t3WosUQBv==(zaqTd$k zfcxoLfj%+%xmCzMG2v+@KerJny9*zpZ?iQ;YOcY($F~%8%i07{=FtzeNn^!hSKTJm3%bRlvVbjA;?$QP~xP@8yRvF_~Ys zHzrPA=+@=M#7q>w*h24O@^dly`7L4rH$L`dJT}HXYG|i554~3`*e_1O5Q}1BF-BWb zj*;I&Z0@uc5yDga@**qth^oz$#uahXJI&vnT? z&*T=SCy!(nmo3g_R5X{jo%LOganCJRxH(|eXmi~s&TkPL&}l=fxEPmTShIEPnz@q; zPO-rOCkBd%D|j&c0<2eaQOCqJd{r@VJ!Z2Rx%D^y`gFFU`PTn+I@~EfovrS4wz|{d zXYO>iy3^U}PUm(ro#TULJGyQE)p_tbHj^)4HsP0Koau6>_;h!< z)7|Axm!G-Q-Q`Yq*OAkG@W|=zNlbTde7c+I6n?|;smhm(Vf#f4G}PK69>(kUqZ8;C zWp{d9jEMuxULJO)>F}2l)IK%6&p~S62P`Xz!%T0c9Ol~D;-G3Lev4iEpn4Oy_w+)v zd!bbv;yrAKJyavHh$IdGVt%PySqcRFibK$^VvyH5?Gtabh&S>6H}T3pnyUfNUr=Pu z@n%fC$3)Dn-*;Mni0TjB)?5ZKq}HD%wf>va`g64S+-=R} zq}HFC*1rh#fDdr@C9}ykUWS1jPU4LCcf8;qS%FsZ&l!CW0uTGf#J`)#3kmS81HnSz z!w+cmbK!m|$_ull+9$0R=_}kTeKF}TjL9^Xg^C5YJdJKdc>;9Vi4fb9AQ30y*zpt` zohySpoC;|;9nx~bNwhE_#t0C&^@T3v?*qJ!-dofyE zgS@oOkeqj*<$ma0uR*tZ7d_rbkAKk$`hg-s(@K#`)uMoE#6VglMo^s?PiKf3R4%}`b8~+KNCBC7vrA6n+bUIfC={(tqHpm`y zksMAJ%WE9K3!3F)?1$$RNHat~c6U!d#dD|CbW zkZzR!rJIz8HmeNUqB_vcsyp4H3h7ogh;CD*bcdQl+f_O3P}OuN@_p`7Tc}xWr=99v zx?9DNq;Y`mRR`%l^?SNseL@eYf6yNFA9_fq(O%t-T68ap=~CLK%cxb)p@;P%dPIk5 zzg|U;>Lz+jpG61s_4I_km7dW%=vf`3gZc$}PQOmSg`f9%{a1QHe?^D%cl44a>1E4H zuUM({s?~*Fvj)@a)-Zb0nn-V1v*?f3GJ4xOgWj>$(Yw|L`jd4v{n^?|f3ceBJ!=oW zZylfytl!gz*6Z|<^&Wj}eLJ+<^N&uaRQXFYx6xrqMjxs<;1+(bWkw$hKD?evq;nMOg=f?nk6Oeo2K zvBPFjn#_bAC9#5mG7CDsv?hwvWj2&ZiP1}CJM5{F?SY}oD7R;3Z_f`@gV~jw@B?ka zzr#RW=_TRCv2cVayZIDSLZXM!JYI{O0=HKDKw<9>bQgXX|42Q(^h~?&N&Cm(;Lt$f zl^v8H+9}SVxytFQpbv$CPHvUC(~1sK@1mIOyqUVIk=ctR}8#()BL3vTDEZisiWy}6CIWQ)NPCJqhA0;zPKd+Y=_9(!_Xnat~I)hEO z>3eOA418?A99E7udNua}JC~(k=PX`}AY4mzu7*s2R$9S4FBkIdy@e1XxqVQ}+FBnK zlOy>N#0B^|mssw!0d^d$dcx`Vt^#KuL>i3{=`=-T&|(n)ugS#6FS792ifp<;w4-ez zNIOJ(eCVMAzTl8UkBB^aT68o3AL76!jlJqa8uaB$2lDuv{almmRqqHc1bJjI>d3+Tc$vJh`ApN>ru z8}6b^-j)q9c{)tEW-8^{t#0jBQ`_21J-K$5Tf58DKH3ax2({bI-6L4gvbJ?N^ioKc zD55mchdPSB)I;>6fucWxzyoNq7({c#V0@`!h*?X%Gp)q-EtFLd0&vUI92t=-Nt#uq zlKzo8dSyirdFy{Z)~Oj-+z7=`MCY+BiA#CZ0yTP*pp#cPYEL7rPNsrrxPFo`{MNfP%(-|iqSMt zi~$cAOEbi{q;<_qtZOFLwHA~1fg`MgOUPN5P1cW;<`r*eWMtqyd|1p`7@(z?71MGo zY&|Pz-aavT4sI@-fj<+9HU4fawhDt9DCW>;i@Xe#S3q~TY8wTEpigxOYX1yb3*A5(e_a zB_2FWO)t^y$|DbL$M?QpfAyV*g%Ra5M5rc6_DJEa8uK2+dN;IR*X&5foVxY4-5 zV0e|>QB{%}Ou-npQ`cb1X3ERK?Z#+3`Swk>P+HKxv7LOkdK34>WDabFNDcbgyCVRm zKQ{xd1X%r_@eE!P##TUU0xlmErv=lz+bIvU^>8pvSH|SW@cZ1wMffjX<7#yLYt|)j zpKK;3!G~k=3n;_;o2g6U=}qQodeT!&xo!(+74P^GJ`VY{^G@tYtNeExBl;Of0!Yk5 zXkk93iUpJ{7DCRP0-;?_gTx{#1^XT&PNhlWG$8fqG+itOV_F8puAt?j5_)bJI_`4l zvsL(J(+avoL}-&(N!N&K$fO#4^r)8Z5>eVE>L@18peIE={Z2H{ThN8y6;1SofUYIh z;F#)Kq44#sfLM=jr<^GY@a2VZ;%qSu-(8rG?++}-7Z*a}e586^AXeh@3TKE7=y4IA zUX0R4aTUI*a5KK7a4){4ut!`0!*G*$7`4xft8u3N8u6OA9;eW65bxnL1|Q>K^%rm# z{8MZ(M7e>zXX5KPQHrl!qC}k`MG1)f7CQ8aAj&afkNg%sSq+HXC%;3<16XX4--A4T zR72ej8K*#f%aK1o#`v+)JoyuzreMA8<bx`U@Y)4`0Pqa|rb2s*n)K7H)BOD;Ur_On)&QI*J&Cy&a z(YhGMCwpin6b60JJT2D#T2#jzT~Jk$Qjn9@E$)6L}1(dl3<7^xQq&%mgys-Pq_NAo2uC$*jGmy=R`BlajK z%`M-Cef01w@NhuVLsRC5miur}(nC|`huVEOBbq z6r|q>S)tf^ch=}=-+a>|Ua1mHn=J>8->SWg5-MW~X5r6;oJ82Qd+L0L8(D~Gi-l#ZK)%F(W9cB*I zc++h672ENO*+;#?)ZrC)+lGTw8h_J!+oeq~I}P3p-Od!pXyy)~)#*0$b8 zwcB}Y??gR%w~^0Ejth%d$rgWr5&s$(|Lb7Kf#9!zN z@gBZp_&z-(KAD3%4}V<=jGnDD!4QI?hiHIW#B~P2 zJ5rkc6OB{qgzv}?!FjS$C}ed^$3LjiM(-OvtZ})&=kIV5!l0b zFPb~vR3K7DRW(;}qpIkp`_R*|1JZdE@n1?8-%=;>omrA&cNWK&M8#?`CMe7VHOX`+ z8+<0uoylWZBD*b0A%9L~|L5R7F}1W*_l>EFT)kH<-$ofIS97^`D~vG6to1Q<23pn6 z=z}K>ZJXB}*80`Z$6k%})R^_PlL9%8z1pa%FE zI7Ta2g?A{HL7mF&ptu_f=K6RYtdX6Cpn0b?>;G;M62=Gjx;h6Huu)*J;pTh{!;v8Q z=QeN{BTf2&8>z5{ZR#u2Xej)uW8sb{gU40}IKAR9#qH1UzA7 zaB$I+^1dx_JOI@%W<{YV3YWslLov4~ZfRx!!FTf=+m~&dnFop`_y)bQfPAty^^%2D zD2r&2?3+Y&gAU_PiJxJSfrUY-Kk3C7W7BD@8Uk67GiuQIWK3ZJ?IT4pE zC0>Y(94~YGD&C9u4UqYNMN-7#k~hY9g5&_|A_r1$If#noU>Yxn&@4HWmdIlG1x}(` zIgHlG60=UngRnNS&b0~TT$@17wdzXV5cmxisZA(Z$Y7YMR-)uJ2DilafXKL+qs5DTu0MFBaRI>|Yd4+E+Ip{D_IJ`KkQlP1AsIzyI&Z7iY+ zxtJny39$53YLKTTv5m$Aup1M=Zghbiph{yjJ1o>kDwWM%P$_?(!-0w14fC1f3WKml zxJ|a<@)~T}Y`!Aqlgd}UjV{4L*ycSC;Mp8Jg0XLzl;=Tt#QfTDnk1K|XZ| zAf7?D%X-=&8)&a=q&H;~eJWSePjU^cgSEnv>qUw@Q{>CDL;=3LG*F&nFyJO*W)F&E zfEeUr!0iy4>==;w!~EdC5x`wUqJwozWIdVLGE%T{ld>TLCPE-d_aSxkyj7{Z1bVE^C4 z?}uFLROR9J6TJg8P_$3|js1(P1%0vG@QKV;^<}pDx>fy~JMLzmrUf>#jqdFk)lV?*gDIwXn2Q!Xnd*DQ+gi0y>~9?tgdcc2*}2}JU(IZ$zPL}hoig~ITcaw^ zl+mnmV=#sPMuXPI>P<5@_yyW{Za@5ZT={hu{~|+l3q4m9)4J5ZU;E1UYk&DZZD;E= zGeez$8NxxJGkIpZz2k$}r*leEb5mowBOG4_LVVL@Gt;ovG2IP$Y+RkKdjf>KT^oyS zRGeCxri;^?saH`jJ*Io1{=(_GX->>xcOS45-Mde28Z`0^=_1G@e6r*4n?*qzZL@XX zQV7WY#&_uPlCrj;T_cjigb$}#IxC>hi<0nt-5qG73J#Z$d|20f@SbV z=+V4P=t~2$^;l<(t$I9yx4HTtWoGM1*?KatN|!YgK*M*fdRl@-!0S67Sb9FC<0EJp z@Q%W0y#f~LtUA!#=uP;nJhl&itWucej9L8zA3 z(;2}2MtLKo?@h>I*o+*8n~}e83vv~1MQ*}v$V=Eto8>lYmUmDq@)Zt938xj zIxL&PPId|#=TL(39??nO3zl-f7>)We`GA;>FL^DMyTx+3N1P!a5?A4Ro!l#KkuBnO z854KQec~b6D)z~T#bfdjI8`1M&&bEb3-W+?Sw1e_kWYwr3*>sxfuQQJe01ZEA@Q60IsoTXuMvCr#9M;G{#<<_&rQT?vZZP z#763p!9uw`2j6J5x4lK#KO)4MIu6`$3i;#g?^c1Q;Gp^x1fakPEs>Gc9{%mq$Ua^Z zug>yn7^sr`49o7uB-7V7yM5Do{0KivWohd&s^axs1{ZPn$5P|{&F z#Pkw}L>QADe{g%WCuF>&%8!7_A5#bU33Z0BVf`=P87aOKP`I6x1B~wOgyip1IBy2F zdQ4PcBB(s>CpN6FyF+oOf#M2cda1+t;78X>9cE{^1v`^JDfVXT3a53L870&W@SrL~ zr4g4(9Y$|>3p3dD2Pus&F)l`M=$|wo{h@ufn(`aQ;nexy)$kjzCpNhJrC|4kIP^nUAB@$ZBQK_^6 zcFScdjjmDYvd8LEQw@<$1pcW`9q`m?MAg$t99=fvRj?_bGZ(Z8PTe6KY#b zU$U99*k!O2q+-SN<;_5?biU`CwjD8cwJLzqzBfLMQb<{ zJ9agQ)0dNi><^r!a_Q!M0D+@vOI*zbM9XH(Gc)?`)tdtTnqrN@HKlA>e;x4OflTGh z>`M@x#O#p0DZjRAaKml6K~1qo7JF5(Z-ZClT0w6=4(SG=^>~yiAp^^eb0O5cg@CA; zbolneuLZru9L_EFZtx!dfAsSOy|*5I6P;K?TVHAeLePZNa zS_mPi5QP_Yof<`FA((I;!Uz|s$#e+<23M&vgcYVD3-Dx+*d*Gmrh&|+(|$FBo>DXE z_i7gXSC1R2~Rm@bUi^VEr zkjja;E=iEtk~lBMM~5A$7!cEpNuVfKjMqri0Ks(?BQ?IJ03JVC1oiExgCQWY3`uzr znaa3QKRt(-b<7-UYTm0^*=Wv%V9hROD{nZep|!=?$dpR75Yl@_CkDDL+Hp zF$rc1qCTi|+bOpgRkAleLtboB)=dKBF&CXh%%n zRie4HlS_~cdS|h3zrGi0+x`3W18hcS>j#~fR{$boWK8ejs#p%3E~;a4RqGzQ82?)J zBZFf4v26WhsUO%0nZaFk3(C)FY~vlIvG7{hTs*k5T3Z zeSp^VY<;K{O1QO+OiV96Y_|S=e8jHFBcc}23vqU== zPhXZbZv!&O?ZDN;)qg6s*)~ZF+O7J%;`CrzFnudK35{Xh33e@v=Af$&Sg*n=|?L5bjqnnnILDsZD@KE20g%joJ__G z#t5i0%`}+rHWdfBVpDM@FLN^m`vwDuG^4t25XK7%nZdxuVCF`238vphmY^51R;c!9#2oKHy;nSL6{$cQsL^p{8>MC(aA(KO0Bi>Ngig(p-#AoU{@r}9`341q6uewPF)fSnjZkAou zE%F3)tL&$4lOxnNIbPi^r>Z;TY_(l3QafaYx=Yrmo$?HIw_Ky{k=LvHOQ6qiCP;r+P<*1#2PfHTOIsFr&{|m{ z$3vI*LtFosF{pp=g6mVWvhw97!;Z8Q|6Aeho0 zaX$;COpMjr@^hV>O5Kqq&2^kRGL5nh6Y>QTDmQ|=9L3f5axyW^!`~mFsVv6TpY}77 zR+!J_iNQZH%j@(Vbxi5>6ZH}ZV)+X4Nb+YW9GMPWu$ak-27|vj+o+3SUzQE_HN8)^ zl^(Njpv%gJx%$fPR;#_6T?p4D*syVTT*!&AP~l4~zFbF+cmDI71f)n&Ewes|=)NbuZ1#O$!FN^{4y?r2Ws_wA{?SRxke38;LR8 zZ{CK02nK?wI}SgO<{3vc-(ZWN-TC&zOPZmtvWh;VPcYTY7kc+W8p+b#%x*B3%>qaLc2 zda6et?)Ot)^(fTH$DmOkfY^VW#;YeF_Me2ve~L~~Pt$4Y849assT#@rXQ=0Bz4|R( zpq{5o)eA`cKScZBdpiKH+cWT|y`)~E_teV}@vqRoQTMa@1GMOa5c6+{j`(O{H}w|C z`;TJ0dK={Y4n+IAqEh`yMDVO$y>H0r1q5$_HgR$i4OSt1aX9Br0Ye;;3dG4)5kxxj zy~TK|50DwQfH=tzo2K!|gyl>k&KvJ-v~Dl5DtlOc(Jlpfl|ic?N`4&IxZmoJQYzKb zJw_d{X+2$U4Zz#d=u*1a8i=Rq(7I}45k(tEg6rh*r1Zxlu8Dt`>c%Gx3i$WT0 z`q~1=ajhXR)1CCJrIgVVIsp^~)(kk6BJS0P=?euN6#QeT)oWc^>m$IAXxq?OV*YbZ*N{%>Q;$W>Q22UuKc5+X>r1@<5}=w{K8GxX+ob1Wo}fe*y9#~e00inT;Ah-_;Na!z&38pq{{ zIk1G>@?=)ym^Hn^e;j2Cg*Qu5`Tk9end*#*w3K+UqQu)b2d3cLm;y7Nw@bX-_@m-< zp2v+h(*+1}8lA~&bSA@|5QIODzLb81mNWHNwH(A#Sj!0{w44m1++-xwonXdBC_2z` z+F0Kmh_e9)f&BQuf|eA-mo^KvrXkv*QSix6gg0rHPN60EHs%VA|5Kp0>1>=$xtVINj9)T5!6sru3F9l5MXKN}-sfb^GZJ1^n9i&T) zevpna99QE;*{s^^zU3Du(o74vZQe0g2t&V4e#aIgT9}l#7}3I+Mk9@8rIC7HVl*o!tH>t( z6HHEMM)r^lz~|#czUp@H0!S3<(Ttc}`Lw4BW7ZrH`1GQu5ke_?`f=RPFWhULvia~w zyV1~#ZA`Zo;hM>k5x?!t)#1t}dq^oYq6 zeze#N@~i_nt5MCg80C7DHOe@~E*nv{P#%x+YLq=FpTy(b3{itJ3?pj28T^|ca0c%q zeB;Fp>>YZPZ@3vESoo2P@C5|?K7!v?{x$-B)S?!-8o!PBt;dlRb3*7jO5Ub*azS?4B>J+)frvnR>Kc{}5R zIPW%U2i6L;H_aHE)`n(+9&K%O7L6i8eVD$3Ob2Noc;+nfAZB1AT9Ab}K@K7SJ@tGV zpcl{xxGtyYQ$Qc(6w-^RR^z0rUP5Q-Q|W4b8u;Ytv{Nso`*aBWav9EeR^W_hCC+Sy zaaMCVy{xO~6&;~B^h)|jSJS7uMs(7(qPvcY0$m3S<_vL?Zh*(8NsQ5}MVVeBmg%*k zTCWpp^m=h2J`Z}8K1qBJ{EIxSJEPL$RrN@t11ZeNdh1({yV zr)^lE7oh}ONIZ)K8}>fsh{sLFh?7=RK^+fMPoooJAE4hsWk|)3ZQFb#hjm4#8W+y~ zca=xe5>fFXH4Uxv?zVZ2=e@PG?}|2eKh&BW)0SQ$C7aG!NKa+|+J z4!3nmRsdhy;IC^q97!&VbEDm8oJaZ816_L13Fey2dM+caF9*F}L23F*YOgm@p1um) zbQAT`*Cer?6O&jEj%pspKuMg3nGU{}g|AJyv-}ITg)>_gAXII^2Vz~#2etgri*8_V zwTY^9aD>545G0_ZElwx}VR1M@zy&xhGOaI+DaTw)1Re@oty|kr-XsU<>&VjAlV9He zHQ`1q;wGT`W-8VnfMV&PVx{Wh8o_u*`~3+LXkL1l9o> z9@)g!I@{|}`XJNy6u literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/ContrastPlot.class b/bin/ij/plugin/frame/ContrastPlot.class new file mode 100644 index 0000000000000000000000000000000000000000..aaccc3f818f89c53b5d692676cf206e9798b6241 GIT binary patch literal 3947 zcmai1eQ;D)760AccXyNJned{`ab-3z>N1)0%+&CN?i8}Gd{-iZzH?}9@>7v2*1OgGe-x`Ufw>xp#Y|0Z@V*=hGCH4*}zHoqN zB%+K|!1DmT8v|PINu&fyyRuPr4p~vVg;t4FRA63iV@uK+9&q|n+>?EYSc1XknYAm4 z+Di3VF`IPMiKGV@?ttw?2hwC(ow$<@({WvW4^MATMC<^(SZZPs78gR`b`wP?_O&H` z&AS0gRA-_D#fl4=D0R6NCdw4I3akBS6e!Myu46E<29@;Bn;un$cbZs$$^h=ddOw;3 z3Rt#XNxMH~;%+nx%!%6REhA318#aU6q^w#@EOd|CXrc-QN_LNlY7_*>-D~0lXy^T{ zNaTO&x1=tYvG*u&OMQ=W&|zXTwg~uBcDmbnNad@pvs2}zq|1rhJ4S|j?c`poS7mZ; zSAwziSV>2%GeRTHatbW!dOr}d97Ww}t8Z|-HJq_4al4kwxDvs0-?4J00)rh>Py=6pmQNmu8N$?gWGBWq%h(KO>;=rJ0^}`Gy~8~?+FC+tCbB!W#Ooa+fbqU z;$aiVR1e6}0VmBS`xuV<@o|B&Y&B;LO?(2MWHwnxT@*Ha-c4ufq&%E7@hLn))qW=y z+hg}JTt9E4KwCqt6U&Ld~w^GdH z=gz#t-Q;w3=F;V;$>hRnXK%_*9=2ry9&tMqeN1^c=N{!bMG=6|8<4349evFXu6Ra_QiuK0N>ERdXFn)=> z#vAA}-bBjy6*}p|bnk%=VjEXou4Ed05PrQQupK)xw+?4+UH)zCJdhvNOW1vlmL46I zPuUs2>NK@eXujuj@-ZMqhr~TnnvGIcj(j`zE2B9=Kj6a0!JW!S(aru(!$ZHpvN8 ztG%FHRec2|88#f0O~y$nf|4drPydE`-X`A!mg;@qs-V1rc|l*$xCApJYLX{?BIwil zZ>Ur6pTt4#9Sll2a0QEl@+>MtL3yeiA6CUsdk2Ds(%7jYvU1r!#JL26NHBhnBI6IJ zF#d>Y<4;&*yoDO$&#b^-P;dMd>x{oK`+r9p*V~MLV2ANfR^e?#jhh%I|FH3IoG{+O ztajt`lSfHeQYWdVhD+&hBM5v>@gq2vSEr0R? zDzq!Ls->$8z8bk~ZBmt<@m5J*nW<w9>L(M{ literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/DisplayChangeEvent.class b/bin/ij/plugin/frame/DisplayChangeEvent.class new file mode 100644 index 0000000000000000000000000000000000000000..76362ddee4c6d1869e028f3b7cdbc524730b8f9c GIT binary patch literal 995 zcmaiwUvCmY6vfZL>_U+WqO?|ue`;0Os{7(wO|>FM425V3YTp)E>5%P0*agFP^2Nl) zq!0Z7ekkL;1L{UGKJ3mtGk4DU&HeWM>>R)&JV+oWu%!m}$eZ+)Z+{p&C$9Zm1tZUy zRgN9M?^dU-A0{9L@&jk;*ppCsw%%-Z25vVL5bx-#39MFzeh@l-*mk^$8>a+82k*I{ z6mi5wTtFT*n+IHzrD$m_Zy9Boux@V;cVmz7*lorUolKaTWXG9NJ^Lx03EmGuw+t?;ot~fEKE6$A5?j77+B-*rcmtlQD;S&`xa14(C zvZ}4q^M-}Bz&(^`hjI6)n~hDYK%~DRQ@kr{1o=uktj)WA!I>I~gUd@nihqI}ECsQa YgG8n^2YF2(Z~g(o6Jd}Y{q*}k0E*F(O8@`> literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/DisplayChangeListener.class b/bin/ij/plugin/frame/DisplayChangeListener.class new file mode 100644 index 0000000000000000000000000000000000000000..6f4ca1fb4d18313f3ffd69709f96afff7537ee70 GIT binary patch literal 227 zcmX^0Z`VEs1_l!bZgvJHMh4Z)Ed7F<()7$c{j{RQ+*Ey+%;JKa#7gIk#Ju!WpUmQt z)V$Opb_Nzk27#=^vPAtHpt!z&QdVkm2|EKDBZD|tw6r8MN8hz9HLnDwg^__T1=*++ zMh0~aAH24K^;>I(F*0xk=a&{Gr@Ce4qyp^?uFOjg&&*57FE7>u*~Z4ez{tQ1#H>J% TFall1&cFd;F>nG&CI&75-y1*( literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/Editor.class b/bin/ij/plugin/frame/Editor.class new file mode 100644 index 0000000000000000000000000000000000000000..94989ce7e1b825e91df8fdbc6ab8372d8cd07416 GIT binary patch literal 49762 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/Fitter.class b/bin/ij/plugin/frame/Fitter.class new file mode 100644 index 0000000000000000000000000000000000000000..b45ac04403895c85af3d158f0cd5036b0872720c GIT binary patch literal 12737 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 MN(o1cP~z|Z05Md8LI3~& literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/IJEventMulticaster.class b/bin/ij/plugin/frame/IJEventMulticaster.class new file mode 100644 index 0000000000000000000000000000000000000000..ac153baedfb822c33f12d310f1d693c000fd2e73 GIT binary patch literal 1177 zcmbW0&2JJx7{;H00R*`4<7KMuyTf0Xf^1)+Sj8V}3P`_8++=Xqyde*XIQ9l%{|ClF!SP)Fvm(>+kG`N9`% zY1W=S=}9-(?K**KiB2H>1USR`k?09i^aJxzV}G_H&agF2daOFfjyTyl6z+kv6ro({ zGsN#JR|O9l?iH;N+6|OrhR8of)iXCrbq2oU*)oMF(gqT^z_4g3S3d8yo6_GGO-C|h zEw3e2?6O*_C&92{ zUQZDnZ{Q|2 z7}DgV=F-5p!eQ9`pW8j>ybJ&6hz87;ZLdcY!f@L_L0iiVQe$_F9CFXG>6O~^x_(PO zReEri_Dos4dI-OU>qC(TLy=^;@&z{t{DBk)?&DMf{_YeiNq%q9KB(<1 at#tdF^HE!KQ12&D#Sy9$q8Q4dbmcd3lo^`< literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/LineWidthAdjuster.class b/bin/ij/plugin/frame/LineWidthAdjuster.class new file mode 100644 index 0000000000000000000000000000000000000000..c377da2132e90e35617261ffd6b871a361e33419 GIT binary patch literal 7051 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/PasteController.class b/bin/ij/plugin/frame/PasteController.class new file mode 100644 index 0000000000000000000000000000000000000000..889ae4d1be70df327803d14f37dbd56d0a2f25c6 GIT binary patch literal 2690 zcmZuzNmyG|6#g#+@`w*eQz&YwrPW%9wozM$LQ8EKYG|McEfwnYNG{>gC@}a;v@7(DO|2g+uzW@8nZvbq^ z2SHQ_tT(4(xlAE#TCoYo$jaEDk#}X6ZMlw}$w()NfWX@SY~qQy)vFE!Q7sT&-Z1Db z1)=fSlre3@j2Sm3r={h_;;zglOscn}BT$tyEScfi#L~7wHPazblT%Lg+bJoqYI(V9 z(l(Ql>zZlhUBgOpx$Ly(4p*ypnwIHy3RJeXjR*v~s7J$Ufm$!NmSS)rRH9KwJ=O>` zw)$2whLw&DyN+q4JK9ESAaK5pTGWMbA)13&>qmOnv{Lp=zhN0^>F8L8iv)tM-RlrF zfz?G_DsJDUwh?u{MaKrUv&KlJd<`L7j7>pYLM(`1yidoa*v#l0nN}zfqlR+7x-v#S zPq%_-qm?DldNcM+!kD!SZV0WoOht4#L1>M~9nZaw1;pd8kL1>bzFxX1e~D>M9Xw{+1Z?JG5H;B zWqDkh41)QpUozz-54wguUM6$1YPCy8w@O=(_Eb@6=p}}GGtx?F zxIrM)ZBEcj$qLcX#~Skd&KaC>x6DkME~gXBW%=}+bZiX+3}Vkfw}wH1iUUI$hB$GP z5)P`B6!o*DwBf?I>ljH_!%ZCZ7c#Dy%gkyRBkr+ z9IwvHj7&0N?MUiKfrVUOjAi+Pw3Ola-wVV&W2_qL)t^8M@I_X0$AMULoamxCk>3IU{*Age<0Y%-|Ty zG)Z8kbHIvcc_sRWla7?MQmS5c6a0$zoEDhVaVPHbog9*RbIur7wgatWe#PFc;~vjY ze$t*HGzrs^1BL9kbPoDEBMF=Cju?)q#>L5iJ89+xHYApt(f_tImR#+y%MkjFT=8g_ zC6VPEJKrQ*fMgY~D+6K~{T|iGyeQi7sE)^Qf@PGJ?y#3nnTlH}NkG_n;x%{zr-C@i z9@`&#G^}`owyFlf=1E*pfhq*OZw*3x z$0}5J;5Ve&G{2SH#m+J_nQniBA5G!j`r!#Om4hY`BD8YAm{x~13 zhj9rDZ4*xOv3diW@gX0bPtk_25yiK-3_sv<{D>|1iQ0a~75IgZ#ILvtzhN7G=f53) z;A-4QPjo*qxE~L2^q@cU4_y*LZ$;oC+Vayl=cOH3^y7VGS7m4Ohdl8#N&VE4dPsew zLDC562+1I&Nm){!bc}SIbU*1)(y7YM_s|y(l6QwS^3E!<9pO6iZQ&a7E#VOP=J0vs z8>-2g)#fI(HxgE3J*<;ghig06F$d@H_!-nkp7#BGW)Uwe;!JyBISXS%9DFP_>|n{Z z)7uUlBIZZ1i{5r(0=tpH^>DC<-tNWS=psJd^tJ~l(Tiu9n=`lpXBq43*pGLSz&Z3| z0RtYY+j%{RQxR1qy@FSXqK1$08c$Tv>PO7m>%1rnoPQHg6E2I0T>>WM&A75~Tr z{8Th@4=<>n2l~o3K>an*1-v78cT`{%xR<<+3V|D`Y>38&%TioZbVlGkZ}*H9 AQvd(} literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/PlugInDialog.class b/bin/ij/plugin/frame/PlugInDialog.class new file mode 100644 index 0000000000000000000000000000000000000000..b0bf6b9edb6d618a8e5ddd8991f2c408d56dc564 GIT binary patch literal 2808 zcmah~TU!%X6kP`rhJ*nOA_|HYuMog!>y@ZgQ7n{;6^mATaYznvFwD@&1nm8O{Rj33 z)MtI5e0}I+ANrGgC|&1Fm<+cj`7$||eb!!k?S1zA^Y5R30XTzS4b%x7u;(+2#d5)R zGIO4_ATzf)8Fg;hR?#gOhzYdLTT52PTJ|&Riwy$1s>gx@1{wvrLsPjV9Y1r=cJl7> zxLxw4BRvC2`rBkO;^xYwbqj%pOSWVCR|NL-j%&-J+OS)o(JoC`xvA;<0`3kO=*mIELc_hVPDeuH$R2G;Bq$i9V!xa@n%E9P1rb00ILh260j#W##i56%{xf z;k%vY)J#E~GI1KOGi`*}qQQLj*9fvky3xJ>a&(!U;_5ZE0d5LgGgy=mequ2EN&x}59iYzee6 zcVP|bHH|)O;s$Qgn&=VhD0pDpH)G%yy;iI1=F^;9^ljH+C=5`Z)wN33sVY1%D&>Uy=Wr$}Gkso8m%^L3JDP2^PV$8)406OdqwnG@KzS;spv z=X!bRnRpDFUBT0H0r8*__Zm?ogo=WVK*CW*uE2>(G|`T+-Q^_;IUcAM(Uy=j*OCce z-_yKZTGXzV@YKLEl_%Ta9!^*eI|x~Rf=^j`o?LL3WU!@FsJaN&bOUP*l5j2O+e?-& z^GPh>3lm=|ganIs{dm)y>WsH67G$2K76A+R>RY;=DS`b}XIu73EpR%u$UaQ8%2Z3p zX@!!yHujvY3{rvp4QX$)X01~vVNG(}EwNuEgY=Kss^U}8*T_j#J?LQ%sOL8h0|_=y zlk+D1YzD-j-T~&)t7v(K#46gK^X=3B2ll1gv3~{KT$#{&se!-IJg|bE6ke_3L^{2K ze)Y(#;*DoWreEUR3WjKOfabrUgQiy|2YDtvxPt4?5g&@Zpznux#A+O&-?zwS2rZgZ z8=BF{aW@IUq-`3!x{&54F;z5^Y9v*YP|u$1%pF$tf33BaKXU4YH#Ef zVFlrofRv7nnJD~;7kZ$gY2X%cP&4ZYHbZ6t_cb$t_wb+sQJ{^2=;~j=!}RZHr<~SH z^wot$d`urI;(CZq4%q-IPP6=1 zG!t~@G416l{}x(<+Q%?JKZ6=ug-hcKw!mj0`^(%>?BfLayb6-ki>C;3IzlcUl8aZ! z34DdG38A$7U#K&LI=ds(H-r%QHe|J9TFyrxM94;jY`&`vaiIpp_q8D|)`0k-HpEa3 dh#zZ1WNSeD6lP{eIWO%95oF|NrnL^g{09ZZJWT)q literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/PlugInFrame.class b/bin/ij/plugin/frame/PlugInFrame.class new file mode 100644 index 0000000000000000000000000000000000000000..e0510e2837e83f466d7f87c3280a4ae425ae23c4 GIT binary patch literal 2903 zcma)7TUQfT7~O{?4#WWg5f$))wUq=lR;yOy9mSG@P_bz3MTcaFgJC9}Ou)9b_Ga(* z>#{$feU`4KtUmP7hyJ0i)rZ=B&LqPCvQk-2=A5(7KKt9>_nrCc-#`8Y@DzU3&>+xl zO{a>5a^A92Q?4;%rp7oJvIph6hNwWxv@vU>j5#k=om(T&y12=QYiJbM5+Kc4)AmxA zEj#DT4O=D8v`tq-T%czeWYEc$OErW*%(J|LDbPNwa0SE8r^Y?kvh!(yHODO5@=geB z=v%p*m=K5#I5{(p2-Dw3i+s;hCvl2s9vMQ1PxQfw z2@Pi`WwBV!-O8Fp&vIJ!=>Zw^r4Vh+C%eh~bjBc}}&@ zK*1@o{sp#$3=bEQDrHp!&*QQN_CZ(R=?a{VD|ms$z&wmQWmlGSM=c4e+eRm+&8(-= z@S={F@G@H=O9=$Bp4ckaU}(55uxYtp7tO5W=1f<|B(m&QSB(Y4)kcgpVv0mE7B=WL z+>jtkU{^J!aHBuEw*>|CEEda(z?9+{9kx`g<*ZUs9aRMHomc5U)%u+|V%Y3ksw}}{ ziMi&CGi&-wON(0FcJ!5%mgJu3-P}2k2SAp`Xyn(UA*#9-uk7r-I|ZAa*o*pUfw57UyxA+ygvgzdE;>N?Q1~ z4sB?~20pvcj%`?v-RQs}&ZPNSI*E-K!e;IV@9-#}8Jxo)f90EufZs11B>OXH;Eooe zGd$;6uJ9Dcl9{a!khVjb;cHuh8ff&Zg^0|k7B&-s*= z9p|sKELP)hP}LGwlZUAMaELP&I1-~HUvR6ymx2CO6L=&d&P<&mV nB3*~#oB9;T>QH=JpW=8OitmEVtZL_pRVn<8e9v??;D`SJrkqlD literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/Recorder.class b/bin/ij/plugin/frame/Recorder.class new file mode 100644 index 0000000000000000000000000000000000000000..0496c2981f8666845bd0dc7b17c5dd008e1aeeed GIT binary patch literal 30608 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/RoiManager$1.class b/bin/ij/plugin/frame/RoiManager$1.class new file mode 100644 index 0000000000000000000000000000000000000000..1b0df43a5cca8596494a0c47eefb2a2fed15341b GIT binary patch literal 679 zcmaKq+e+&|7=_ogwXxZ8H`dekus!yoRxk=)2o*s@L8!XXO$G0!=~$Pey$Zw7)PA79F-yhhae*Vr2Qwxp8rHp5)y zJL7VLA-5a0L;(y#4lE4X$fD?=0GA{ zX1}?wyiTQMb)sxcVA8@bhN-XHa4>~wsxRU_>U^wH?+0=kYpL3`<#Pu6G)%NF_N9rZ z*w1#=xJlVleh^0FJ{0j~*m5wB1(LZR%TWCh`Xba_(Mp3#`TRQeL{F&L+es~ZW?eHm zI0s4zy&Lc-5)omj6&eFzu4{s$PoAMsfKBR;^aseKid|i~gHwIL&zsaUOmm(*zza$= zJL%fS2u5i~ox=SjY|?q-v#Ymo*geM2`UE>=ztVkg|Dj|tL+3L1jahoOptTN4tnu{Q1J<+_E%d!K< zYW(02@JAVE3km%o#7p*OW@qM^XJ_X3pP#<~>|n=$#!!?4_k*AIWZ+&zd??)GPpL|8QGy@Q6AWiD5XRwy#xN6PRmLB8&|@u~0>A zN~g)N_aAgVx$#Fy^F9z4JoOVrsY9Lj33F5NdP);kyk^BQnD4?g^2BSY)*?UMsV$X8 zH-~}ehcVUYh@>B$TX+s?RgP+iL>vW&2PZUbdKOL6Y6XFa_I)15B1VN~uG3k7X4`5B z0K1@8fOcA=lY>Rx8>GD;TPK;XZ(d@#Uc7`||Bk|!tH3q-XDJX`AGb(XNfe6PxI>cR zE>bw=-||{?G3_03WepqrfmL2f=BW^`#1qHIGEF7t39) zcwcCL%SMsG8}Kt8gk1H5@123@rZ!3pwLH3)DaAXXU?`<0GVwnUYx`;1CWC#(LtjRx z3}Xhk6VY)$!YrXw>6274{migYe~#n-veiR|Vk_>65inF;*eDxs+{FkS2Not=jAD|( zsvBsvEtU9|MIE6J9viQ<<1P;mxt3->3>M8a40F%);crpPmd=!eH+XAfmSN`U$z9Ch z9Zeqzo@AQz8h@BEAAj{HgR>uJx+}g)BWN|hjZGssMZTzR7$;KocSU*<_gwf`qAKd4 z6-N&qsNEfgANkoEqQplq90tL@Ak^x@L2u81?Kx-5(Wh>-u z)7~OmZLHm3tg(Irukj1Ds|Vl|{YwNy`5e>an<9eZC0>zbc#X*+O_OL$uF|+BmTP#o pm^sQx9CK><0&e;8AEL^T$fABtUki9own%a>VwpULC=Drne*yBypsfG^ literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/RoiManager$4.class b/bin/ij/plugin/frame/RoiManager$4.class new file mode 100644 index 0000000000000000000000000000000000000000..0cd66c9a90db8e14fc219fd69aac2cb5fb25bd4b GIT binary patch literal 1285 zcmaJ=T~8B16g|@q+Ad3}r4&U3l~&qCDPsKuF@|8$q(CA{0#7XMSO(kKW_L^YJwEe* ziGa~W-~B1-i^eC7#yi_q3I>|&&fR(3CVQ3&MR7?-1R6tcQAobnsBLij znYmG=_ z@8{BK)a#b*^72w$4Ov)ney7Ci%Fr;0I75O6k20DYLbMFGb!1^MXvUWr4e!R$0dk9#YH1zr{?p zMIHm9RJ|Z2x96&+<8TL;Nw%M!AiaHRhv<(2HE19fQ`<&gL3(ub(5)z0MY`vsGii+P zqt`gZIm5S)gz*;1eGI*(h!4Z`jw1kQpApf%AgX;uKiMPX*9nKRiTMhh3|BGQLSCXw z6-yXzkO-{Pd*}eER|p1n{k!D%d$>_Lp?yP4`;NHwqXRS1!pwMgS)9OV94+v5lB2-W zC%}`6##r?j*QkyIjPJ3%6Kp?;?H5M1-yLk}<7}#k9Qs;pQ>{|M?=iW0h^dbR*lbIe zQqk?}017vOVvkAftyX@TPNgER zt$E-`fn;#xN%N3SE4|~rXs;v?x0`K&YZZb}Zc@ajPUzEi!fV;Sz{qnxXoNOHqU{m0 z-Ing*uJj2Oos0d*zwIw93rw@^fOpW{%WrM|sd z;Ms1Y)QNpxhk_K}FX~uknuh$q^a7&G3tbg&;cW%06s|wZI^Mw=QQ45ecIbK14Qh^U zkzk18vWj(-6uc)eIXJ00Ht@bc%Cs!$M-o_zQag|+&O@`Kuc!W=SpjJqi5vbS3E!0ye3n*PM4C6B$6;w%s z{!!~^Qeb}~{v(|!eWK$jKBq{&ykIVoFSEy*fL4sa9c zx^;%)K)dPOxPBfu&PC=sBMx_t4T9(#D`_8H{SzF{6!RLAwzJ zG&)YZ0m*&zEC!L|1`vVHaRZ15SV6W6=Qmo%$!d!t&Yt4U4Ib&cP2Zh!yjvw0n}1>J z93L%ZK0U=_KA$?p4%@s;{EAd&_Z)i+G!ABAo>|qhNNE$uYLl4PrjXaBv8>HtUAu)% zZ8ict*9qGNoWdHu!ZQMu#XaofIRXBfyiwzNg-xMgi*hlBZ@Q@O(lMh>8jEN6w#QO` W3OG$Md>{RrIG~ZmB&aMc#Qp`YFp^vV literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/RoiManager.class b/bin/ij/plugin/frame/RoiManager.class new file mode 100644 index 0000000000000000000000000000000000000000..6078557e0bef21ba7bd0a4933ac61c1ceac0358a GIT binary patch literal 71709 zcmb@v33yc1`9J=ibJw{!kdP20vN#}0NLU0E6%>JlB}f1Xf`HqQ3^0;p;>?6aZQb{! zE_DI-HP%`e#E?o|E85!H&92s3t8LY4)vB!<%K!78bMMRqu>C&2=g*_Lcg{WcEbsol z=RN1X@YQ2a5YfTGw>*-9_D!8KuA{Ycb*g>bs!XCSIc`xp)tG2ctWIV;vIPy={rN&v zZfKvybv<$g4LK#TE-@~#J~uA8F4>+NH@!KRO1IBTWpl}PZo@|#yH#n(CEE%W0zrd! ztI(M4%q9ypl%QdItFd%VGTBqI-uC`%Zc73XSbvPb=-jr-j?l!D4LFKy5)�z zM480O)}$bB-h#T5j-I&;kLMMhH{~*Daf+Zmb?NqOF43M_l4$Kr`cx^Xq|Jb9Y-_q% zP~XO5=PhnHsczANg(uZFEb?g}%Gx?xbEz@SnRLh4mQ)6v%s6)O;sx`Y1c?R<$S4tH z*DspBlB zbI)&EXsL1L{A1By-Gau(>GSIal{U&CO4@H?2vn%GI@|n%4^2C(+uP zUf;+w?PyKrA&%N&Ir?o&XOc5Ib2*SkVr8-w9dsnx`C4Yc%mlQMO8{g)rD5C*bQC?+ zB!{O#bGkL1X=rarZoq(AfnGrsx{(O+a~bET8RLhI=@#y0Xs;Ue$yEuAlq!Cb2PW%WqD!H-n5?F!HtpP>HdnR<~nT4ecPFOlL<9Z_AE0LH&zn zlB>Rr4_DHZ#g`Vd-M#Q9b&o-UQHYstwYL zhu2q=;G$%6I@1DnMf(|j-((<(sp$&!ogJ7dW`Q1qTqe<;WiTBFLPZ;e0*C+w#HKTi z$!r!qpgGOQbzoy|vDfd!Iy~>6e?BXnX-nin8ugmjB-%BtJ6Qu_M~hkz`Vq~osrFQE zsvxU+)DjS39RM4H2w1CV9PP_@ZHf*ucWFiAX@XCuV$O{~Q2d$gTp_DyB9-%9r=r75 z)%A+z(F88rsHoB0ovx_G+B z4-4HR^k{uL4@9$QK|?&$6aiUufueKhLNHLZA#E*JL%BI>iANWMfVDvF%~}CnM3?fb zYk0JmD>{;n1;U~x>u)$USs0-eZr_b58a+}*1v zVP3dT(Ih%Fp!?|okG{u};goSpQ|&E~hekIjdXOFh+H&bxnRGh~tZ>9UKO@*iEeU7~ zZT098NEp1|Fjvu|)XnsrZ8QQ0G_pLP9(v59?}OTyKls5oErGzJU{^)k=?4JF>SS(4 zBGKwvf*$>ezqCYMhz@h=1$@MUsm)- zdW8i{OUuzotrFO{5wu?s8jbl=^mq!RRM4x6{!FidDDZMG!vDUUrY51NYuauUfjP`|0nB-ef|Ip(z7LkKRVhd0v|V(V5K5OKePc<^p<)F69}# ztLUHf9^L}#%p=%zMV*?mkkoZgAJB&${R;>Kt+C>neQblGkLY8Nb{1j!u{O3 zbXy11PD$({u( zPl3^=hx0h?RP-f%C1@Zf&UBWyPxyzycEo&ZIlrTY3u#qdp-> zMJa48ZZI0?+$P9?)>by20?}9WgG?0Vf~txTQ~+*6!>A>`=nr9N)H~2C1}L$Q7zh;v z*a$ize6Lu37mSrcOQZHy=!t!cr>hORKvZd2AciP9m=5v8evu|%;Ws;zY6;0r3>Cw8 z@WUbVsvGbR{DXJj^~C`wncmV8Uy(b32Ru@VYF0H&#l~FnMGdB8Y!hFM#(h0(wOrB{ zV^M8UvK`*a*s)`MaS&Q+G)9Flp!odyWL7%|zBm{N08w=$Gr3eUt7}h!XqW}#02C5m zF$;ZPAr4dGa50%piI$dPKSofz2;hczik~yhh$?Xe!}>@;HAR*G2TXl&6olPOSlm4K z=}OFCQK-=W!pOP|=mSv?1`{(u8+&h=MUj}T#2kLv?f{i>!CWPd7V`iT2*9S)X<(rN z5Po@bt%sG?o`pL%B_`&Jg`QXtc}N^}u?nxV7|Xw2tcgus9@Gb7jS?ww3e;?~wu2Y68+`y~ zOD554{~|CONvfrS5UonIX&AGvf=7@-8zroxLVdzoEYGlDQw{CC@G{I?zuizbw49yQ z*eP-?^3X>`OS_&$zLJ@UCpLik^Id!5v|h}~fbql`pyfQmO@Dznoi4y1ai$VyiL+r5 zfY^Hb^@4^M0bp;Qzb5$8>1A&0)j*stF7(6&(1N@8M(TbV&vL7QE zz8uDffSFiNpm0ZfJf66sm>3JZATBrJ<|;)qsE$#2jS}A$*JAEk_vCB#=9PRg2JVaN zL8%Min?MXO>16poG{RX{S1# z7h4a2giO)G)->LNJ`aDhYvPM7=)NhP!IQ0c($tY`POaLghX`x3Vv**t_`-BH#r$Ig zT@UEQSjW2FV`zajpD%s@j2ZJX%}!DrMn47xBT+4m94Gz8Q+Q)evK75QgFY8@L`^gB zo@JSLttXx%0} zMUP7pI`9Mcb`^Xw_Gb6A*$ipVVVL|;iC4s*0IE!9d;Zm4q#wCXu=s`evl6d~zko|R zwfhZV!;IBW-caJN`U#>OPCX2keF>=e6TMM&7K!1UKw z)tLTVO0J7YS)?{K))sZPx9d;?EA7vvqom7|cM$o@ur`yvk}(+oeJx<~1sP_0#z}*o zh*V1U;rf0{Xk0+^%Tg#WSq706(FemkR8Hb_SsD`?WrdRcS#Js$j~nFxCHK+4Hi9)| z74(WI_vD~r&rdssJlbT&mqVbv5yT4Z@ao~AtQkIPN+9>83t}=ZhkJ4ujNm<>qQfNc z9_9WV4FKVA2*sBNFoEV<^yNq(F>EzJYokzK)*$M-7Y%I4sF(*>tqM6>$uS0&Eb_Qy zoRSA|i3RrI-xHLasDE!rXZZIaN>1Y6TK47zqdZIy@#D!#PGPE1I`|R_Nq%sol2iG; zn0~;bl1ry4IlWNI0+UPYl&t4cI}5qV1+$c#%>`c8h)*tSP;xGtDF~3UnUa^F{A`|* zja=U~(Ks$zpyWa>@>?NE;e%(naFLQtTv!U-o6K}*p&C6qR>>v&ES8T+@f*h}xr|Hg zH4v{{utLe>xxh;sDeKD9KCMXwzOuD#bKP5oS;N@*;E{VK}}%m0J@xgyzdj zAiH`!f#)7Rga+Fk!TGrXb#6ACR54=1DY2DohGIg!v1Cii3>TahKGCE1B#fJjSydOheTNVFDylxmsJF$YuynxT0mf_KN$1a9gcG$p%q-~}=;sn-V#Uu zp(vg@m8brIk`J;C31fXVLWS~SCAYBpECt2Xb!MQfjTQ~$R@v>zM=?)ytraUPNhP;Q zm}xF*YasMBsx}&$FJZLRE?$$2v%#}28BeiBj<=+f*?4<87heN2HJ*qYXBT##mJUBq z@^O}(j*fGC@=2)XaJmcAts7UT+tF1ZpO8;0`HV!+kHTk}IEy*3%jonHFSfSYmj6Ge-tiCcP6$)Cxe7rz;6$Fjj&5kSPm0pku=YSq8q2zs*EJQ7PnFxDM8SvW3@gBvLIr|1D?9o8Ua4YpvU_0^J} zSt2BCyLuMuqaD_M%8GL!uuwxY2nf#_&Ysc5JdhE}I)E|R$M^y)`}J~&utqAY+8Tu+ z8v6~NBrB{vh1(K_k6ECLVf`r@hLvjTK;I02ywri3=xdfa3$v41#5Dv2uip_mkDcF-8Gh2pw&$3_^C)?vyzoMFllkyM9X7Vs@}S(<4ZYlM9@ zYP0v4qu_A@ zkTaDv%bIOO>KtB7jv`3~Z>X5NBO11jvKo{%*E$++n)ORTWAk{rTyGgt~6v2 zMOcl&q_BV=vu>9ao^=OkQ1{CGmVb(eVj&WpklfIWKO6DKZQ;3NGI9g|*~ouR z%LZ1D^_XX28P)?E9gC%2Wo@^90Eh!aoe-ntz)>$jil`z%#cr{lRMu10)4(NMAVG)h zPO}9(gyY;(C!#3}kO;ue@dM*J(rE2~=f&1v(W-nrm5n0@B)5@mJ?nX8{m24ymosxC zRs;mMXo}-B2)laJiU!|8e2vtBYG2R2ogCA0I>5H`)p)_NGqu!OB&S-(LH#)1Sqv`AN( zMe#l7zGn$kC))VEvi`uTq))SM9LojSY!cG$SJoeu^@>(98s?0A`^x%&P0tb&$6L@ovwks%J)=RblUbm$ zVe5kWRy2Tk+Om+n0t$ZL$Y9w6v>;z5*f>GgzyPogMA`jez}uC(ty6@os$C&_A7u~ZVOOq6WanUk zI-||b$bN#XwD*P8D2@m2?ZL_(Vh@F$huFyb2@I4XIuAvdSuC^T${xmqQieBr%{(Sf zwD(u`2v%=po!KOt#M$XtZK`J(`Ufg|BvY}?K}+8r1=(g+MqzN+wTPqGqXiva-1FB; zprX-5Q(ppxVG9qv59pl0ZcwjfkceTK5@*l1(vF+aHho&_g}f{<3D;{g#N^_l?K(Suat)H-m; zZeqWI!}-8bgR^0-nRPrj^$lJNK929sRQ6f? z7JI>q!L8iVIcU>97i^@l6opuH;!v=LIXcUZB(R-9SX5>}^8G{1_6iuV6~LrW@TSZH zfqjXxXW6r1v)C}~I_%3e%k0fzcAxw}r=U?_Uu9q8*;li;J!PCupjHTkLz4{T)s3I+Kl8GQdW@4Z|)5ACMPJK3dn*SmeB_d583Qb*yOq6-3lSzywl7b0cs7hRRhU>Z6WBd4E$to~VX#jv z$#+jE`$_w$@Ff`KS{3eLk>z6qsDVK}!$ZlWQ@;Hy5P7U#Ig01faqd{#V~HFj9C9u= zK~|1A%@g&12C=y>Qaz7pw}pwRis1bJbW>knc~?$gI3q$((-Z&t*2k{)pDIibtTIO$qFm zuP~Bcv0qd6U+mYRWY%Y3cLGJ#D`sf%g`xdb*?+VD4jA(qA*$dJR54z9OWALSrd&Fk zY_pp7IpWAYo zhv)2{ZvZfx*=|V784mt)hC8s#{sJ9l|Hr9T&M20XJjpQc9AeUeb@sTd@trY7F7mh` z7oBmST<#xO$rfRyFhr&bz2ii((GiKA!U(4L&Ln_N&w)*A&gF0pW5Y0=YD(sO2X>V` z7PRU+M*vEw2k{*DovEmzb3t%F4OLj&@i=EFr%ngy*k*>=o9oO5&P->v=gh+Viv;OL zWL>r?XO7bV5H_3D58e~dh?R6ro7JP{vTt<0=gf;lL@`wsiwMt|A4x%``p#9Wc-unn z8VjRkKGa|-So}0_7BCtbohD$-f!7tKPNHscJ(h#c(N2@+EG=rSK9Nfl*9x2^P7@=w zi2<@gImdI_n_e?R=EZ9EOQD{e6P0t4bFwiYv2Ju~r&ftuv98iIbb9~TQh4`QYi?mm0@@u0ZQNR!)n_+RZbBc1-I;|KuByo|nkJ5gNabbi# z180fTuAH^4OF%k6K(=H@W1imtHEIGJ4e=PRaspkuBLnm=bMQLETo!xm5J3>_1;FV4v^1WD z!n1gG#kfUtb`DhPIp;7AFH+9M&Lzg+)DlV1sJ)_ZPmDr6iSwPylyf=zk|n&KO1H28 z64}+l2u7vP#q|lm5uIJ7oU5H{j57@GglDI9C8D*^U6I3q2U7y)TIYJtxlS*|M^1dW z^CwI-!eZh=2XH#xU>&dt4=gJwc}aNP9vMC-=W5GZtR zWfPj?#$f^mhdHwrl_5t1@86-EJK13CL4rhIg1I?08kKu z(yoY>&?-zG(hY+wLz$r~FxYt>1v^_?XyF^ZHxDr=zo(oB7?iwAB66XNK+07fLRN_b z8xtX^UJdtx&hoGChV>-G=T;=TYxW!y4rVeIV+&f&x8^b#Y*S8;^L^ku)c+vFh|jdX z&USuZj`MQ} zabPJ9wH}9eWa|6QFF~!-^$xE%#2#<)V$i;%oL@V?F@%ieh0Zo4!0Eu&p3YK(@BEHU zH8z+`wu|rl0YhAwMgl~b{=%dBqjFwhkCrzmU>`>^2SxrWe$HfD+-R)V04O9*AgXDG z68H{`$ubyQiFl4}5tCKnJAcJ{@n$`)EWDcu&fk^urq*ega-@<`{I+s*aF2siEFfpV z9ZM$KJ?C8*AE24NQD5M^17p^C$9Z2lA8@8aU)Gb0((BD4aF#Lqp`znxDRa%otd9PT zRsG#)PzRRx*1T3wH#wgu=TmN6wWP8U&KxCL`oh(JilWB4p`Im zP)Fm(dB??Ggf_a+b1mo&%;RgKfFm?nhqPic&;nPw4#w=dg68Wnf6cp7n5Vwx*hWf(7Rr{7DixRIjF=856mE(dWR|`6N>d;bccgNwwN}q)QbI|Ma%;H+41|Puo|tB0EW5g} zQ5_w0gkX2aDfb|@XE`RbIF-YqIbNQi+=-kw=!2t7O%xuY+)3`CU_R6|Vgc=7jcUa# ze&ulGPUb~8j`y0UbKvT6tiXj~Dyn@KCaD>SkfRrZ#vk2h7}M%3bcRFpaY@4VNU)zuwi){hUcjc8Yv&HUfI`iCS zaMIV02HT+Aq;gmBmM3Hhb|$%CjdD|Z4;1X-WILCvRc)kWo-=+V#yTY5m zJ=;CkbI*ZW6iyrFIeXlkDXQG_-17lEV0Bh%L$alj9dP6y)$>~yD)*w^PZJv$U6&~L zQune@u^|a;WhUIA!XXm$5wRCi7oulVV(u00m7e=87$bYHq1>zJL{M-ChS6%GnsSGG zjdH)uVKWzYAYvZAdmV%l-?1acdwH=K;NGCzO}rRTie29zoJ4ClL6(9@wY#0Vc{Vq* z#J*+EP$lQ&;qh(Cz1_XTuu%9u@CcI=;l!)lyOevkdyipSz^E~?0b{COQIE&IdoSL? zx)&CGu~t|eV5ave_q&lafYX!9dhYiCU#V$&mU9nc230MNHbU(xvO~TapgYY zK8cqgwshil78Nkr74TVft4~97y0Eh<3t2==OkolcAOn6+XYf|P&;=PJ@ayR30gU)flOGxw0K@#QbN>MZN`s@iFDQqPLNSXNR)}-}G4U_lM$6lPcOzOoL2!Rc>R=D&PtI}4cIg`|B?Ruc} zg$R}V-XMt5$T;IfeMG#xAwVuCarxeUKp&j8FuTi`aA7*XHw;PsjCPOr{Ad*hMDiWP zw0D5=4)jK%9iA~K4fQf0jW1oWE`8A!cs1T=&#MLbgQ@f*DBI?>${XX21&zP}LM|*X z73h`;dSpx3o8%qlc`%gv zhGH{x?Ui@9hiF~78I2xb_=F9H-gAt&IIMvDx~<+3$~)475vA9H%sC;jfVCOVHbGPs zBamA~tM3F%3^SBh$IDk%N20~|WagV>$#LC;ctRq*kLS#R?up zrMm$Ow4fyf^IGpo17@kV)p|x~DIJZ?T^VK>IXN~gJ&%nx$m`kIJJnW~~<}E>c zp}7>9HdyuZj#FL}M;cT0QPao_tn$Ij<9_ z=0t#nY*+vvCVB2_Cc<==^42SFgS-Qr12d@=O?sy(?{x1Bmi}l+Tk?XY@8+#CGQXzP zFhI`&5$jxGau}qb_f0g(A_Bn@oy6;%rM$Cwwqefebh9}CBWc4D4bYVWBMV%?{a=%a?hd7eGj&qr?d4Y$NAN( zly^0o4xG`Ef7SE84RIARETk4ti8u-GGxt51ZY71^@xZ&ryMZObro#CUyGw+Kk{K=6 zcsFwDLO1t)v+{0X!ONr5Y~Xpf7116=9yPo1MrFM;?>*#oW5>Guq3`v;%tCAlIX=yL zzfYDeUwGqCXOH2vLX0hY6d+?C%OibUc~5vS;5fay*Q^nF>OE-&?5*C@%6leCwb^6Z z&nj;RhlM!%d@-gSc;EG&SKf~}x^33e^$Y`y`32?ugxg_~(36ZN^CEAGKi>C#j%Uzx zc&xEj?RyBFNdz0bmmq@yl1R%%HCSQ(1{pgZ;J!ES6b7m9{T`q-twz}&X!vEk(7VCt zq3^u{&@?4;aVA@{i!AV7^@a?hfrE$YBcaO*7F+drW4Z6g1dZ0GL*>91g#@_Wlio&iZ(6*P z@Jp1he8{Z6%^Zo1_z=zRzCh3b{pgn}zsyJY45-(A>iT&NqUX4f_x|>_Ie3ScU!nZ| z{3?z*)2{+0crj?tcggeJNBIL81tmJ#G!vWlA(T|kHy^q%0JK_31(zv zKO)~$R2h5H!^ZO^{MIPt*YIYo5-h4WA)eTZ4ftpyJ03&a;{|>&!Slz3+B>u)X2F>P zCiEt(z#j|kjd@N`{zU&^j2YN3uE6UBdL)seEq*v9=8yF!DgRJ+G3aE2W|b_L9j^Sz zx@@BR=6An)|*Nr>~J%_4}eu*JL@Tl%;f2SN3{8viE)YYm$DD5kj1T2 zsUS2Vv@GnT!@9M64bc&Qh4PR0Pk06PPYK+Go0rO=8$ zpFV;S!2*pn;l#QoBu1_ZypQ}AhCe#)wSPTylQ})c_g6!vnnNFaAMhAt?&BTN0T6Mx zMw8?EUtmx0*TRAI5kUa47p>Q8={{u-E$RD+i^b|7X^zF)lT#9{?Q^vNZ){I@9UN;Ub-I8w3B4F3v znH?7f>&LFiwYBcwhF{p?GLpB{ob8|M`RDX9xcrPZ{6toSAnXeVPq4@5i_&Wqz5{>^wfKO5h_m2K|~ z4pjAT2M?^mK`^+z6MS5USTJXE`2OAag~5feDs4-w4bKzRhfL{g-%u3y&blqKXU+Md z2xs`;Q8bHYdj5AcgKO=e4HiTykSCV${qI5d6~S0P4E+222btX-+HH?X$ZiGh(AB4K z1l7OK1L9r@P#G-#B6`)rx@DX4dwB7h&2302 zUNiI`Q?!h?e+vHx%72_wWO4E(pHUDI9iBX?{HL^0f=uhE7^r*(d#B_bzW*%5GxDB5 z6bbx~MQY6xyzGiYa6JFHg8LG#frdHK{toLmNG$b#to#>PX*-FPI6?uYQK$+`@KbMz z&`WoDgUjs5FM7f&^!^gWI;C~g_>FwhF^ zXh4cCHTK-1#QMU?f9!2UztE-zF}RDRYe+^qGh_}@GW41;G)+*S>_A)*JlW#;SaUBr z<1$|@@HvOkf5m@I`G4_W=Wq#U>h(JJAk54ulDT`y)d`D0P;2mh@lmz;zAz*n_B_Ph zAwZ*Hvp2^@Q#}7I03#1lE(`oOd3103?|>BOLcqpW{PR8Kzt3KaYm)NNE?P0|7DYpK ztI&8J3^PPu%)TO6J_g?5Lf3Aa@?k|@LYyBX6%*`{BFA%HdVMlemq6|(ZHZY*-UdgRL<|Xs`^Zgv z=c$;l?S1UDLF#4Bj|D&~tRL*GV(I3K*ZdevW6$ii#rZvd^~Jxv7>wBeb`H(NAXXNu zfUt=5*C|+F{U|Q8+Y0U!77GPo3E^mu^t}zfxXgzo1nblHc$|Zk@6j3`R*M|t%X&Ws z)74~tL~GH0Y=4YgFJ;F$TJOgWV3cqe1%9y~gB9C9`c-^{1Vd(&iq$Z#?e*Bg)(N4nJ*t~=6?O@c7cyDa_KVIak%$%_Vv zVIuKq$$ktTV1_`Cn^fvG*r8_o{BKQ>FnX0r&bfU~_A`{%+)fkx5)`K+7ezW59Y<*}nx zY@Uvmv%Z=I`sC0y<8!`pg4lw+d+*o_;>V5wemQ&^MlzrR#hZR?G4v+J#R)2ix_ht2 zmZ;d$*m2;}7M-sAzZM>hW-8K&rofLahdAVh*&{ct`!NuNE<*|pr^n!MKR4f?bX2-{rAa;K2 zLN9g!ba-C0?_O8M^rAIy&pD3kU!r1{GHAG98C;^+<=7I#5Rok~y=4ogJ;;ogmB;R_ zP4Qz_qc^t3%zip-y)Z&$B=5?AtkL<{QFfg+&;AeKs0Rziu8ZB^#jXb|3NJK*YgBAg zY%>rR9*)Wxq;o-U*cPs7n2~xjR95U3j?45Gsvr|=8jRhhVz)9 z5=19R;PnSp>>=*AKTg`jf$X~3hUkb1@O5=Vcs!2(9wfE+fKm0Rigj}!p1BiYkaa11 zF9Eh7KkC6radH!MAFPt79KTHOI=YCh`>`Kjt90yfmOj1GZ5zx6erB?;;}o?>9( zFn;r~0|xLjD)z$|OvQ3^+zSxecdcFlKm+rwuqvXb9Dhq=NjiU$I3ny})=`ZiFKtq3 z6M$`rBZ_z=8qQQcMlSYa6?=>U#Yb(-#A!t4U?QInCNem*u>At{ex_pju;_t0eAFvV zEuVxaOR-`Z`<05l6#I2Zsys?_py&Z#%el-6I*h{3i~=nHb@YUIxmG5M!e2BdN6k&lY;@z1Rm{?EPX%vwJm28u>%6_#s4$x%*hf z{>>h5-&9tgv$+^+fpDX_(kCkRDL=w-0*%RBf{%3K;?Gs=i`Y&K58|mVIvbh^wwLaT z&W!FQt9Bm`Ok#X~0+ZAyVmAUtm?x}(z0Ffs$xk*Af#n4fQ2Fn5RbWSQMK`c0&DmO5 z9-%FPs{)VbhjU|$OM(oE$Ag%{`YB4WZxDICS*!v!cV-*fO$|s5JqJPY|20S$H#i5I zTP#&U*<5Doav2TVFc_kOp&TQ_kuCbb00bg5o+H@+nhb`iU^uTtF`hL^@;Veh z7@>j#c%{u`*5MNuM#|&KNEKA`3L77ntzXGMsZl{K6L=|G?uC=Iv)tYo6^vzw^IYrq ze0-1!#`9x+Ml5H|8vfunC#vA!;1CuQMMH$mQA9yHCxrcd>%`zt6&%J3>CC82#j6G3 z*C-m=HGKieFrm$7B$zlCK|jDx0cjeJtV7QA+QQG7CJqAer%8F{25f@ky6ZT9k)vVy z$h*+!h>jCBoWNq$fE*-QpzRq!y%*FW(8u`wUvH>jCPSwJz|-ki4cX}}dSRv=XPJp$ zwhC~N4-z7S1{KT=uo4d|m3=|vHUk~`xH*IqpYI+tf<#vM0qkFK0#nK{Dp(XWK{$q# zBB;Jt2xESQ1o$G7>s7v2T0EAik5|@sFx7sr6rPP)6N`v2r2A?NXBoKZ1gyFRE8twi zg$Sbs)x%9)-0xkrUh14SeYJG}78o?*Z zuH%RT;vqqc3RWT>0ymtc|wqmCB{^r3WN zu*MG%m@PWwW(6N7eWV{CLK}>ZuQTUa9SM^+$id_dI@#pK?gq{IMT^go$;(y&+&Iqs zDH6$AL^Ooum{Y6?Cr4om7;inxo3Xtr9TA1Zm@cMH@q*L;yH|a15S%7a`%JHNE z*~OA@L@x~}`i(<+QDfiy^Fg>DT=aVg?uQor-VgWjqTj=CKfLHS*WbVBH`gCg^qcD+ zQ1tsi+>b2!&F@zi{pR;a75%QkeQnWiu0OiyH`gCi^qcz|TlD)toV$yU69E3WuuCMhp zQ(qCa*H?VgM_(JlE5Z!;9q?C=^cCMc(pQ`zq3hvvM193c68ee{7U(O^oYYr*`A1)| zsS4MCcaP{Rw&Lh=gxB>ICwS{Cz7M3YI9F6(@o^x1#ixOA4fw+~`Zqohq_0Sx)K_G4 z>gV_plfL3RO!|tiFyVR``LvqWfRa-9wHjCcS6f#42%RDyp;r45YA^42gfiYEl*5m8 z_Te z3@$5~TC-IQ-Y$kN8)Ey`HWBX@`=jEBZgC(kqs*n&T*jEoICB|qE)&h=5WZ}o3}3c_ z6S~Er+%VsFizz&bO+{s!bXkSao$DcWi)l4Iq7HRt;s1uob`_2md?0fKE4pK~BTDY^UQ3XlLNtXWsxLd=o@}CJ5y$JUtt1a1LEf=Ysstquc0w zd?)Jy`aWGqPvKK-&*SOOz$Cw+OX(HJt2gL!`Ue>1U-T_}n(0d6;2Ujy=xThn=NfSU zHj0j-YsDD4PK>APAzE(0w`(?uCfY1k(2Zgx-6U4i&7y;D6*;<1Yyh({t4%e;6q1}R zI&s!$8l-kHKIPFNP6a{UMt=|)h%pOo&JbC=ZKJh)P;(tPkD0TI?47jH6YIaCKX~{m zk|*Zxq`{upXn{`g@t}}VuZILd2~FHCPG45LO?;zqG~oHo(IDh*aSq>~TdOgB-X`*E zMn5VpVN8RvrYtSMd??7D=q|wfZovB>9E{~eaHgoyDxokI=$IazQb9tIC zJhX1{!-5!E1#A#~57yKJnDc}9^$^ABVc0BNU{^dshtO7t=tm)t?$rqHe&jL2+sV~mJrn| zLsSpkE_N`gpVz4V5sw!5{ISNTQJv-B9-Wps2pU+aJ2c%S*fD2`GdF zLZPa$yTz+0nu{X5tBW83UPsZPD1v0tMWS2$4MihR1i6HwJ~cw3@U1RchB62#lm(#R zce}(c6hM@rpcFz`yx%Q8TG}PvMk%N#YV8vg{T4-_BoskTe!gtG*tx7`oA_#*6fC8> zrQIbSK^0J$ehVcY-ntD%pg3J*nWD>41gVK43_+H3i8IDTe8dDYxLfv}Y~$vVZrN|L z!$@GsC(Emxt+KLP4(t*~qXiHyS{S-r?pq-TZ<9l}$o)2n2^Dfgg*>oFR(FX5Q3>=M zc2nCWDp3Hc4-3Y22^$3<{IFnr7kz{R(0*8Oa2LIf0&oEefDd_Sw>*4l7rlr=@I+Ya zh%Wj83cww@V4FOui@u8j@JU!>Mi*^H0XQZsnAt@aqX4{vf|43{o1D`{>rnzW3L81P zi&mik3>6m4@1kQ-0M-f%j_IO$6oARXg2i1l5d~nkumD3Hjsh@VSg^c{`i-fOC+GqX zus^BB0?p;6l{gS`pCH2>DDW6)Ie`OUr^yyb0SohXWKynzCfb(<$<@$AHs)9^*W`85Xo~No zz!T^1q(S&wUS3{NR#`s0yu9q75oP$-pa1mZKV@YwF8M5Zl2Ec?4rH5MN>l1!5@m6L984yht<-3Ma!#5^4ort2|X^Oh0l0`e8%P zukDuEMs3i@jZ;VS%{_EHE;nzYa`b!#lQhg6@c)@$XV^V*<0kT~F0xT{4zno@NBOOr zXwVSw6-HTpB{QrUusk34#!;}~GCa&3Y`TZOjZ5ziuGJmjA=;k=aiKe!3QJxlF9-hm&=>Lw*c}#KOBsnQK#;>Nl#*BC&e%rt zsPC6l>&dIceE2TAKnOOjp3HF(N!*K}elX-qOi(n0HQYiC5!R{dGvqV0DESj7A)U^~ zmFcv+M_vPk)FZEpsN#N2ZfD`I%QZw}5lNO8ktEtHo5mH8o`Z_)Onn2BVjC?!h^Y>m zV{;b`7;F@!ycq&8Rs*GTbCE%|uO{MSz<`TL!Q3pv*9fb`653BJ)!iCnEFJ-o;z$cJ zK22QSjyq$FIbL^FLaPihV~-e!om|vdNxrcXd7l|Nf7iq` zX1@fC_rc34Q1p)HDOMrB+ateM`@FUW-tQu}M?N&!!SA6}&hzA0_rsl#kIX|sA1J`@ zcgyWdt@~@AmQR3xYM+u%bjznUbG0$gQ#k~~v(HfpU!bijP*hs8Rle{XtYci}Ta(;M z8{@x?V)i6gPO>ZQn`jVf|Mb#HUZrQ>MSi8nj>}fMksp?|+(oVp*6x&l5Y9;NvgIg z=$8N3M2A)4n=#a0%0*msm@Y9d1`yoF-Ao;<+ z!-IcRzK41JXL5i}y5(n^$Q5Q3epS!~(ynr9K~6g-mw>qOtEAha$t6`K&+okh44kW2 zwfxtu?Y6L7zhknh#Ge0=-0yIV#8QBG72lLTNBwHLtqRy8sW|@fGf9LWN9VbxHi#A87Rz7v7WvGGvF+- zk%^mSuIQFa#5TD?^vIRs`!EZ(Yc{?g<`x(9>Xb2gZ zvT6{Vggn6=hih#bBIjA7tuc@lo#JC_EEwN~+uhINV5|q2eBa`5tOv;4ZH>pB5BOYb zO~73Yw!jJ2M97^03TciuUP_>LrdkK%sS-EPFRephIRpZH6F&WzI;}}~gN;K%h+ssY zV1_5R?j(i373JlnJ0Vzi(g0B>J|_|5yJ`5{SO48l2>BJA=wVYLKK(iTQxc1yL0H#d zH5|HT2Q-?C>sDyEP@n9i{-_(9tqr6;_*;qArlD$|SRChw@uy8fY5500g*8I9$2yc% zx;44Sf{DJW_Bk3g*uITMY`2bDR$)!wX4N4YWz~0Eb1JNP_}gvG@3CN1A2ArTKXz}m z7NgeSZfi+}wXCSVHnXf1uo?;$fex^WC&9!|QK@(u-rzHIxcDKgfoCZto&y;_U!-iZ z5vnPx(F*a8J4UNfHZIwvqsAd*%bz3(r+FHl@id1)GJ8GN3G*PH$Gl&>TA7}*VVFF+= zF&xKSkRFf2uMxF?O_pI`G*f^piQj_Ve^=B^-Z~*aB5O!yCQbH`@ud#at~eI$!rG)e zwpi;n(U>9b9aKH~xd?k3fxRIb6Q=_cqaip$gvjq1XPQMRoKnQZY}P`xqld8YwBZm9 zxe-1iNMJ~h^$oOAVV#9n;l64D;CG&fGtQEq%Pba_YBbA47q-M`E^}E30?I5>SrQho zK8qPwyhaB>bssEVr$fXWG)4TCjuL;PdhvH!DBhyQ;%!QccPJ;`1*86xzKKs~o-5vm z{r4|K#y+Im#YY-i+Log`@<_QuGdEMl9U5+IY22aVX3-Q{t=ZDXw-uVLbAU2q%k?Ag zAp1+&7dXRzLvgMtz9n#O9(5N2ag4fggKwn19HG3d+dA)d>bu>#01R@`HtUinxo~i( zKDw<-wbU=ncL4e!@d+sWQ#cZzQ4M%~toWh`+vD=%AD74WIO|(^%C-r~Rv;LBRw~5% zaL@rqSS|-$!>mBxYmXdU7Q3Cw2j45AkH zoW-?SgJuM!tQ#T0EiGM3$#ZwYMuo&1Cid$O8sl$m8FZ^;BBbY0;S@%?e*q1;UPWGz+ zgSD03T`&vyu291bYs9Jm@302nu^H#xMnfTM_XXxuzma8kP#@fN;qG>FebYpRbsspd z(2tR|x-8!^msLgWw?LBhSoas$H5wW}2st-Erz`~7s;P%5_1j4$MePFbdQM8WtJsFY zxc6u`h#IR33UYHf*2Um?0N?$nQkDbH6;v(z(>Pg4hspuK`93s54x}bo1)T3oopKOe zAP3W3atL+Fp|oA@M?aTwD7|3_Hw;H;0nVWuA?C{iH9eeX5E#*eI3{9saZJSOC}RlWQzC^%wER6;$ zA-fM$*dQ1d%*6OIa>zr+0Emd?(3k9K966Xn2Nl+{aCPp7mHXWMG4OR1rns05g9R1V z3zKd9@++*LB1&mJYW+;R6XR>PTEEy#HQTLUEyK!QmA%dSH806@TfgHtr;U2rnX+Ei z!V4|`Y5w3bPuxZW2W`gEj+VZ!;R2oVMi(s~Y`08;x@rWA!f)!L@-da}*h<(h@Q9rA zTv6$cdE)k6|Kza6fj!pWH_^V_L?!goJG{uWgYW*S{ohKALpIQ95+MnBG(L(r4|1=Ou95TUI=O&u!e?*qmdDV&xZi>x);77A9+$_` z^KuFOQZA)m%j4)})PEhHv3*mXMDNIx=|h=7*kYx|^eTiTfv7yDzqJ0PU0jDgwLS#C zC3y8s>s!`GAk9%U+xi%^ZPOyEvi@!T2WY;M7FnO5o{d@7Lp5{QV-uc2fq{SCqyd^< zY%O;uQ`t@g$#zn?XMMJl*y`L#V?FCvpHu8}viIEy)8U&5hm1L9zICuscyRjdII_(45wAteVHdJ-5Y5>7yxUQATxy2-eBh#{(0zS zLN-Ge4XARepqJe$UtctF;s+ZMT^tqwt-AaQY>J{_IEX119Uq}aXMh^NNz>$+G*_MliaeW^%5!LiJQu;O^C&CNhZeqo&XO0> zh4La$ zaSH|M!tJpuI8b^EmBXO52Oy?WWkVg?Rk}7Cxj+k=hedA>!p&2-?zhDrj-V)haljR! zUKS;8m5b<3mD_C}V9?lyW2$y_ZI4}3gu|6I5EA8TnEcm(w7*UL<+ZSyu7gCmo@(&T z-?5129V9o?Ve&?r3HIV071lrD!chY(CF8a2-n>NXN=uUYr z-3KppEBs={pAN|*;ArwN05^LKSPHpy^gw8I#_2kZ7mH?7T4T+o#k5j;#4HH{jb~Tm zY9I38c7p#a!TCEtr_DR0_iL*~8*0)*|=X@hR0l0mFMZpZQ%ENE?f*yCWc z3^KMyWKRz=*qA>CT*AJ@j?sev<$_hM1C;Us0O~8(T5>@nO|)%ol_L>A>ywgk{L&u@7z>U11+mTh_nZ zJ`_@73jUwE331AYa5KGN2$_*}Qy{kig*`M-exLS}kI^vs13fYmIz2EyvI9fIHlV?F zy*(2n<6wba#~$Y4i%rBIx-nz29l%jxloJ7w0>Q$j^VuT=Re1E+gJ^Ii>p-OrKR?5)UOsd%WViOe)J+g>aU9`5g)Uc>(Xty zfm_&kwU@Fjc^e!7UDj=%&_y>?y0=gz*SFN2q-wb>6rasV3^{SKhowqLJOl~-O5eVn zS}L(#!qr!Bj|=r+?xG=FYA?Oh4B=$`t62}>V3eCM!y$`;-Bse1G|`+iKq!~nHLa?@m<^SVhMhSoL7$9tRT zXq4EKVR1i4(_q%y8xekle7*@$MyD#+3a-+~KqZRxhpsB%%c z(z}C3@!egO{vEWRF5^FUo_r&Q#^wCy?vrn#1JR59&9dND8jL8Zrz^EMt(A}}sDL{( z$y0*FsjYIXZu@N5Tz0w(ao{Qks6M+$&O8UwC;1fY#;0MiJOcuKmIlim&=m;&$mhXE zKcac^$FxkoK&$0Xs8#-yz9CMdD^NOC44Q_TzZO5)poy!bcNO0 zkMPCG^pe&}j#y1U)gFb5sOxirQy)FVTc5(J2x5eYsLs&(E+#IeN%r~HL+~E1rg8QK zxGT}@JdgTuSO}8wOPVOezLi4 z$}5iSqLX!XL>xxtg9oTy5=IX>v8ewWQ87=(OUu8B^)z$1$Hoq&myA;PJ)gMB81;#Und$d zzCN{_n!!m9leXJ8E~~I_#_w(Rt@B~%-agsJ-@8H+SbP7r2_-=N&Ow-Co?eii)MIyT zrh|0MIBrxo$0LBEITiN(rlyV~8w2M7HgK$|wH5Y5-S!qJ88kA;LQRJHVtUwtr7`S5=42J3H(S?2_XK!m5ib83RZb6kJRo28<}?oKP|6 zt8ftn#WkRkoLyW+3}9Bwiusx_XRpM4zf;vSJD~Tw|M%Ya9-r>?boW$OSDiZHcS7~2 zr`-W~@<2EW2GRTOV3=S-=tp-bZFYyzc6TpocK611p!FXBpi||G@+Dkd zLsKJloR@bG%Cf$L?TBiY{{&y!OtGGDNOS;c#XQ*v%^N%@-Z~C-1bf0+@^@bX=gNgH zet;Mw;JB1L|HOz-N%=4ZzB7N|9p`HExO`z(lL~7$601}@B5}!;J?m<-T*3m$B3hm<_Oe5SwXn%Yj z>K;nd+`}Ld52w@IsdTP84J3aAUGGk(+ub8U;z!XEcLvCOGA(nDqgC$lwAwv^o^(&7 z=iQm~vU@VU4gbhScNTr=osU@&uLu=_}a6q&S95F#|kz1J+i}7+Buzc8qVvO7Y<}I-_Zj#MF z;1%qiz#zvOQQD)TqwaK#63T)7SWRb}#l-0YgOrP31aunpQBoG0W>cPf85rs1 z)Y`p*I=EM6=o{Trvq;^eSuCO=RSZPRid*>6;2bRgq=cEnPIH#w0KiZTK8W@eYiy@V zOsBx}E8L!u#Rw^KCks7h0_LniUSyMMT^CcO2?@T_pxTGKNbTI4sFQm$7XB@?mwPLXbpHVj={79<+vy;8E}er# zawS|Wx4QEo)9#>pcL6=--bw4-Mf8Gu7j1CwrnlTB^r3qXec|3qKfCwQzuo&qk-Jp1 zbssX*eKH6b^Oc%z(cVll)JxIcOuR#Fu>PzaUQJh-B`6U!dA91U`e3$4(kuf{XgY|F zRMn7d9*A={W5fHjo~EeX)b3d3Z|$@!4seGQ|#s=+D6^+*^C4i z6StMRgV!Y7bvyk?bu??JpeN`~VVgutiK;jH0d|CVN01sge@ zi<&<(l3b0dYf@wI|2U@C{TtMT26bSq*Q6$y^9j$KIcO{sbX9|@tM%QQpq}oC2Ni1? z)s*stu80Rclj=~!>+llgFBW6VGX82CKA@)I4G{RlZ!jJGhFs^@Fo#)5bripc^B@QD z0N@{#2zOqf;jRlbOn6OlV_hO_P&1P1SRkutqzeY}#My!1o-JxV0^d%qry+P1>&$~r zVxE(s2tf8jxbtlg?(Wjs?43?P+VAuoqufd!bybl%WoADwuTh=K4VQSjQJoR>Po0Ce zistD&9Jb8R|II2_h<0ogO<>~hi;u;>u-Z3@cLY=e@tycdKs6BC9LW7N7kC?)0_3iM z8nBWI;ri+Xr&kZSwt6B7t+(4q`?{+k@*jnNya^!f{IkC zKHqkqhDcvWzqsp#>pm-T+~)+>MriH6AiBCQiW>JNag6)2nCZSEZggK23*8Ok9``kI zKki%Qz9AajH^t-bTjEvsZSju#o_OEgC_Z=5bN5}b3E#iN_ut+3#h*Cd=5BP9`+-yF ze(1DvKXQh;A3I~*Pn?PFr_RalXU;k97lzZ=fS~hIoW?mX#c7=LB2LqZ4m4baogU*N zPBWe2hN~zzV;s3nT>!y%w|G}ws4fCmStedm7pqGERjHMxJ8dsmmy8gGs?qvR9v{7$}Cf=+)S;T zkyV(%vat1|r!EJtj5V|J(!!SP1y7CgR7YIiP7@;z*I_fzoMxDade@r`6Kh6j`d`|I z3ShMac9#F$@2rNA{g)OA|I#W3YZrh0u~jg((Q&Z9iFfO?+*4FyY#FjYqoTBWEC(-E zl!Ad=QKR6~OCQG-fbC&b9DAa!W*-Z_wt}ENKCX`hT$WX3OogirAFPU&nTbu#(MY$3d^^Y?|ui1{Cg^Oe}HQIBh=!bXaJmNd%M5T zVNgz{xxeFGzN3>d+o!=Db}`<;!s%zE=#I`X6oYAjeP|&lWRe+bV>iW211(9wR!6Jr zKrss1V~w)z5LPH*x$S_(1z-r4S8s%hl=|fG9+`IJkPl_z%b||KlIHPh9ap6bf!l(N ztvdsd@6y>y#1*r;FX9=%pHg ztQ>YT{b!V+gQ0z^5C_DK3%KtQ);vBPet!bG&Ca*hP#|v@UD2p+$#iI~-pRLZ0%jyI zF{u#)>ruY+DIo(h*w)3@I_jdLIQ0WyY+ZZ?GEaDjMs4coPz2D6Z=jLVdeLQV}Z1@qmgkQ(8oP~w~~49w8$AL+qA_*nMU85aiMmky_3jJs)%dN;4S0tDEV!=` z?H{E9$SdbkaRu^@pjsd#!1%NIDR0Fkx5K`uLyayDgjy}h9ME1EP;>?7onJEu;Bx8! z`#l>e2H+4fm$GCY70P@ng0HSr7E(tUqiR_MG&4?9#z3f46%X0caR?sK1l7503_Zu8S zf688FCasTpOPbiWM96Ix{TSH+Mf9b**8mvpAegK_x*DvMnW~~;i1pz(4UM$SOVxeG z*1L{QQ1_cfH4lId|HAsHj-~?54n=qKc7$Ou1Z&m7&4{1aLOL7HE1WD6u#uo&GNVHY zdUrG8DpAV7e#IqlyPYrDn16t_g=nNG4;QiH=>yS1JsL}dp}|5>+|n@>m2RVYcr4G8 zVLvwb&tS3=F$A(7WlMO{Wq&g^76eQM^`nhwY&eK%Am|-QGo;lDZY5(J?BmX)P^co=H3|q}tMZDNdL*fmDYy3{>QR$tJ{IR6 zODaTo?1rN!%^f*$g_UF7<14|)W9n&~M>0J(0&O#iAJ^08(OtNM6YcBi4fduXTMX{B zT5u{f(J7&=Ci^Vwfq?Fy2R;uC2@HG%osm>8!X{I%te~@^!wu!D)T1~q$I?uy*Q?gd zKtPp}09#`U5$?G8UBF8^Kg>_KNwqQ6`iE$p?K7yBADh3Q;xE)+e%xpFaY^+BJ8Ap# zA9(fx3=`teO((N(PHn*Bl$8S)-vWE$G{UK`**1nr*EvSqmh9*3@V09ETJ`Np^)t{mwIiva z>tMi-0hci@xx<(=Ydvrmn=Ujz+W%gJ>8FIU&4uEPIrY3?zm3J`&@mYnW9<1PFW zQS^s`Uk=Cm906XrAJ*qcipx<{CPxEi9ZOy1IN%rKsY*_ONjj1G$^&SmJdnoANi;=H z2EROr&XaX?g`7fH$%E+zc?jJi52f4X;q*^AmFnd*dQcugFU#rlt~`=Hmq&q59xa4C zMr6quB1ax8I?Ch3ZuqRh_kQvOF-V>$_LV1zk#eROEl&aVHA_s9r-~!wY2tW!hB!^0 zDbA5+i;Ltr;%0fSm@m&0i{<&^VR?aAB`*|D$&18u@?!CdyhLo2mx@p1Z1J7EO#F=F zt@2_=$*Y`vd9_m_uW{PSYn{&WI;Wew-l>u|IDO@f&M@RO|4wbh$Gvq&< zljUvB8S-{0+H;*tuDr~3xK{L zk^sB8S-RLrfO>kyxYIQ-(sOkQ&UxVG$LR!EypNjp&lpLe#m#i1Zf&m4r#XhF`r;`% zLbuVt?hpfBYn&~NjI}iqB@2;u2O2KyIm0Mhm+7{M*f^BB=yu@8K76MqBPxqw3RdSH zpe`AaESrJVm02~J5eX|}AZ=_|zfqqv&O10SoK8bT>j2WL4`KljSS5x*H{51ihdF;z zKH_9ri=hx+9f3N2yPcr^%oo7&m54zm!4&9jg;qNUd`J=z1w7!*ucfovMtEb z4tc|FE4}~&YrF)B69GzR4`SX_-k^bEH0q83BqCl0FpPN_l5KKVXygL<_zOf$C3RQe zhHuw+vc|6$o#TE@PzDB#xYwGX+~)Y`r0xOz2;PAUkRt^auTd&M_e|PU_u(xSNALYY~ZUBXWTonCR6!JbPtrej^K)abw4wlYK6u9ssnn>v~!w)p0DSUlIOf#AS8C08>$mJjBtf!hp zZcGn`3vC`%Sh14GjZm39do%ZmUw`e32@Aq7s}Lj(k2vcj zper14@gKmgG){3TqqPJOv9t{!YW}W#oT(s8g zUuX3Fdp!Y1AvC;??b<`Bj3-PKVm6|X?$HzV0nmsR(;fOi^eGoc`ZanIj`D!gUvBio zd|C&e20O_L%zC_(fD2Y*Od10-?U!pCPXr=)`;&*M@-Va4>3uXU_I8j)_*uCAZGvu+ddx2;gqR2EuX;w=B zTF5Jl*IflLYgP#mlTsy;`Vbfl2Ezk<2=xUk0kJ=Pu%``6$x8U^348AfgXm%5Xi(le zaovK@*I>*@eh8)IBhc2zkZ7Mk^Z%53$bV6#{0y}9IrW!F#*kmqSosz0k8_8~uQRmv zX(_G@gP60aF!waga9}o057txlG-#%IG(oYB%08IEhMy{`q&;9tx5iz)sF#^jj}SL& zX40pIXgP@_xA`*}1pPqui2Q{)oa&^W*vOZwo0@zQ0e+atE9eZ zF2(UF!DQk8*SA_rvPoZDSFu9lc{z=G9-itV$W=Dz`L)FfFCI@6C-uUKd{k{(I?o)g zi5GCTBcEMUQxMNbzLELTsPC#RNtC#KO8ndB!%2^$etx1v_9+Q(TfpCCpMC&k`Dc%L zwIyy%0`(OVCGkW&aYae&_zANUBA!T;fGd6vR63q0nK(Nx;&C=~t+!OyGgB^1>ic%$ z%kd_?v@WI}O2lJ&8N4#p3D2!=&?|TVa^nTuc8OmSCiNq-rnnwH;#Qcn;_9ScjmH#M zucsK!@Dch}Tn(oE*z95vFXrdI!}q%ORu^{$Jd|rEXIR`jX}LB&{6XlgY;UJy6cLdz z3=8dhF!3KKls{sz{RDISXDXJzKz;lbs^V{0T))#~AaGOUzv&41C(V$X>3q3`X5;u; zxH4}B95h!p(?Ugbw-R(8oRtqLmsToC&nZPO16O|xNk1DAg0NKaty82yu&9Ofg;R_p4@mAO`Z@f0Pl@zYJbdKp%;F^eg&Rh>kDBLcIa^ z#b^^^Hu$-5A?ZmYU|NZxG+V!>Uq|hvv#7m(L!XO0#>Lc0ziEz^L}Bv@WU}3^-$H9~ zN704)ZT${{?@A&jSj-Gh>_KH*-NIS59SNdKS3yu%Y*oCCjrxNKE!K<#K*wv;pVmrQnEo81A*R2CXo%@e6QS%H zz~W&8SR}OXT2LXTzhT+>T|%q;ee**DG{p3eEA-DR^zTXiCu_7@ffoWq)Z4*Hnj;*- zbHU6^Ql~ z9zB)AYz<05_0fTcN(Oz6)4D!o9Q)B&crn`@zUl=WBnGuQk-i#FUES%l%Za5a`x(D9 zNeX*$XNqeBWBXUyw`$ES96r7gF%S8#BPSNjRAMv6Q&N|dP4qD+ku{nc18OpOzDYP>jIO%Ug) ziQ-aqfVff}C|0P+;xSbxo>5a;R3fo1#nr^x6ju|;6ju`~Bd%5rliWsUjTGm49pPD0 zG)c^~hN{r?gok=JFjUKs`ol(~kL3J*ioM=eCE9~v2{MOhrZ+RuNL~@@auY4Soet)2 z;%0}&b9JODb^!ixJfH&ByhAyfay*;3b`j~C6KTqRe4o&yq zOI?DthM2Z)!z!!wDzVDUS04xF>!#G#O|{UoeB`6Upz^CZzB({;7a(ZbYxIU@dV3N$ zc{iW5{iqH3&Wgw`GrnYRZzj12L}`~jt12oLx-vvh)p<_j#bCeN$Pk|--&ddLh(m6 zlWtI`&8JSGC2AHutWKr()M>O?oi3C*MdZU52)RhNLsiCvq?Bq=U}yxK8KKln1#D1d z8>({IM)CFo04@=zu+tHr{-h{0* zY8yP4;<8-E8n)6AoAFHHVMB0BX7n%gn#-pSuzGl1tzJ`u96h*M5*UBkb5MC%(mMbF zH(r~h$I*Flm+No=(3yyoi2eli#XZ_U; zW+bf#Yd~rwu>k5Ym#FQ7xw64UC5V1u3GPxm;3Mox@Mt%9!`O_m5w+df?67VCjvWC8 z!+$x>3vfMN(gc(sc@5L!mi!;RuT1>U-uD)~@2z;>f8c#@!~5Qf_XPy6=HY$+`SH-6q${HNWj{H`f46S*rf@nH46a-g;f(}zAGWM_NXzW8xkqXtNd=HzEy7w_ zj9{<3D535)Z_(bigt-E+z?AJBihxH{TL;-4hPkpKVcCT2T6ThO1F8T$GH?*_%q~8} z>RFa{l%cg0UxR}WjLmxDnEO;Ey~C}q z8dRy_oR_S4mRv;T0PIc9O9x>2z~guBUpy<0Ho&gYh4$q?_y@a}xf6B9|2@$l zUSn20hY~TXJ)@r5MCSk&Ff1Ua;=DUQ>78xh05)!VTPfX+vvaxotpJ82xi&!RaXq{z z;ttp`=(^38LExWvKG#=7?=Hl+WT4}bP=YBVdzBL|-=0b*hmTsMU0@dXyRzY8k7?=xOyhy{Oi} ztXWGxsV8ZRdJ3k@Ga^f^gDJBDrp)uAM!g{RQ!k2%>LqcodRZKi^Dpk#(w z68;`|2jt@;KkHe_FGVIo79@Qtmz9?Z!%1s6sDj}UQR|6XU(^P`UPWydk~R8eJ2kmZ zO`cPe@4(oXbA`!9MR;~m|t_2!JlU%;%K#OgfrEQ17%-qk3C#4^{^RN|ZO zrrrq6f^3d?*ReK;rsPz1znPc~2zo7#v}d3cBaSiw8Q|T}V74 zfAOT3t{zxkilW)(siX1NcEp=s!@WZCTS0lnBhCv6?@13;^g1NGwcf&{w+PCKJz7!& zSi+pU7fJ!{dVmXI;miX`@4em_dBdGF!MxmP zli$8?qHsM8MK`%zRKIlQU~McBv#;KOSBLuzukEe23JZH3W31cb54?_Yxmy#fr!CBQ zFetIqco!v#49oclj@lqmgs_6Ps7Mkfib^j>@u@@+4^}+ioQdZ{q$a%e1g~9^qZDwN zk4Etyo-1Vjc}<90FsGe2k}{DfGJ}MajJebgrwfr4$4PFu1tY||++*elK1gq^nfd5S zR?>SqQN&4f+yq9jRY^AJ=QbWn{Pcm6?4-AzFJUM<#d96h0ps(_;<-uh`FLK-_q?R{ z((IUs$KrZXGqR%BBOeY(W@~iU`2$RhQTLp3^Nq{4@;GO4H=$90XU9_Cot5T$Q6h#y zXgs7XIo$C?(R!MV&M!m>wfH1~qdGp@?9pd1U#gJ)%MC@ONWk&)LyfkFs$8 zc`&#>LY$wU&oi3Eg&;1GHF16cpI<1-5njk0>1AxF4wiIYVbaMrq_e@2j&Y}E#k|+! z`HkM2@q%4`@DBd~&J57nEV$Y7JDooq8LZ$~)9uy$b>G9`#fk5k&t!?XEt6 zCG#N-P#@7?^)U@opU^()Q&?`FA%^#JnyS7)MAVmbqS{2KtFP&N^$lGCSpHh|9X+bP zr?u(_dPV&REAA(HNBsgz?pOLu{YGD_-w`nK2mPV`4NLA%;i%0bR9i%m+A7+rZKAu{ zA$qH3QHv6Q!?X~iwIe2JB@WhFoUc7GNBd%l4#fRB6f1SMSgmu!3p!W4uJeEs<%MCcG?&5(|z3;dQW$ju65_={_ahBpu0#9a_`ZD-TU-VccmWYKC1U} zpU``|>-0YEXL`8%t=`Z5Nsn~@(4%CL9wSTjIN4s0m%a4_xrd%8N9zORBy9^i0+(TN zkrJ44vdg8@pl9L8*4V<4w+ZK5IHUHG1A&%FYApvEajJl4?rCgX4TpFy?`xx1SIA25 z8|ZXC^^`rlZ}o2wn0;hd?>mUjkam}yyzg-}i~7m7-VX+m8z9?wKjLaG4VER|PdLis zU?ii(=hHs2(CD`X^a{=Ke)fK$LUFLO&4}?rF^4LQ9UbHPwnk*f#0&J95!ppZHJxcv zG-J-6;&6M;%@(5&+*A#ajL|oTstbmQ9cYuWw`Lk{@hGmb#{V-OXJg2{Upd#qWw#FM zSPtN#=-$m+X?(#JYO{?Ffy8H<$dtHP7lZ|!0@M*~T@0)XlDu2?5EL{3Hiy4lm~9*T zm)0>_S0KV5D=2T*D(K~P_Ym<3q!7~HJo`UBAJ0NVtV*Qx0M-)zw|?hZ{Q=Jbf=uFv z6qE+nwXK~t0K$$ne8}|Q;Yg8^%hhzMOnQGlOQ?F~ZF`pD?09&Vi$8nK9ASj4>zFSZ zeU#pTdQKPNud(3t6jYZzn0$Q*9Os83DC;mP*N4*{a2NI0wseWL1yUBAq&>~-Vq-qc zE|-+Bwm=>s8Y|v+QA55rV!sU9JrV0alzHGdeDFdpHG8&>SbDk=+ z=0NqD5&b?@u*XDEMYavca3R$0P84+Dbd2osn2&myO_P_GS2p-zB?{DJBYH3C=i=3I z(BK!gyt=8?qmLz}k<_b?r%<0jdHO_ZqfY`%Jd-->Q>dq&MSb;YG*F+OVG^aH9~9Y` z2kc4(znP5y1IfZblGZe5R8`F@L}^t&+s}#KqaJU~LAF)yz={m}yM*oUYR>vZJ~=Ao z%yvZD9Q`UIf7V!S`Ydwv*%a63P+Q>H9dKNs&of=LL26Ygj)8p>_rME=+Chw%ngsd~K$c%x ztDz8gP3R~6_9p&-pW?S43#F*ZZ;!m62EQYNW4(d2np<)b8rQitUT*pZo6`ZuTL@b# z>35!N!fx5dIu)Wd3;p!Fb64>?b`f9@QN8yf()wb`*Oy>eFQwLcHnr22!F6y2s9_Fu z*H?lXt^ze&4QjZChU#l+n7*Dy=o@H?zLBQso9HNgGo7e!p|kZr=u&+f&Czq|259p0 z^n6;P@1O_u0$PQ3*62m_lwM3P=)266O)!wmi|HwQFU`ZT$B5PsAA9ch(gLvLN(;*n{oCuJ`lP za~z;4#)hU;t{K$i_pEF1dq?xwm**_$_leR}+Q6*UtNcCUUT^-~6Q4=HzjbWm``|^* zpPC_uv-SCkCU03CJmvndorb&|l7{p>#FYuN^}ST6@592oAC&w6)!=h4oExnlq&jW$ zmFy5rNe|JK^bk#nhRCJKM$)-3F~|D*`1|5*@(^V3Flb(({(EKIb(hF+Wa^J| zTsK6E-xD@pE`JmQW&>mDo#H#F&f-ECl8Eo*yNLMCE*xhNC?7~gWnFEsrUq#@UOm+( zf;bRZ5WKMyZe`eYZw|XbvQMJXt9133+OevSHDin^c(cJev6*dZ_@^N>)u6s z|I!<2lYSq8DId^Q{gIIRW09pl5k>mbmIWr_5%nmMA5l*puzkz6JJZjGJ0gCbelQD6 zQaOF=kM+ladIr(FXj$D$z0INvO(?|m?4)B1mb0cYVXcGpBgTPE%|b?dO={FEGVlVv4`Q6mP;5e~l^rmb&WisF(g8Q6)cQh@w=n zA4UCB!;VTPcG$Zj*}?L)Xfb9EC#vvbeE6km(l0dwFJ-W{m_HGk%z>j}4duh=MgSQE zyb=9)Wy{u07`Ofzdd)8s)4x&zn(oE8x^uFEkAXV~oN%XP1r8i?_D(%jOu|3{E2T>r zJ47bl0047xc^h~p$8e%SQDqrlC`AaLq6kZ@4>M;2MF}$&oGI9<|BcuC6D)KyVsyaJ z^;Rl|FRBYZtMqoNhF^A1-AscayMbsSKGC5%&!rhSp5-aJ#M5R-Y_iOy(E{<2PtFw^ zvF{+A;*5bijJF0%7`Ev`kK5npq#Kv^q*4CifW9T|hjcYY<7~1?H+F~RfT0E+@}%;c zvF9JtD+v8^hTd4qptgX|Fvi1EV+_RD&TJ9(8bP2k@XOK5Q@hC6T63IsvQw=&FTfK* zBoJg_`PuTMc5aKJ7XcFO9N|xm=GG%;Kh&CoPVS6MISAy@=rfa8JL?cZKt3im74S2N zLFg1bhlnMgaTptwQ=?1?u{QcnLUcL`MKU)ZfnaH*@TE7c;|sOs!dy<{$2NBO|2*8l z=OzPi@DP2EnnSi~5;JU5?TOD`(B%DE7xSmd{OksQI{bU=?)HyncQ?RpKR?g-yZx9y z1K5jyT*A-SQD8mJow$O|Vc)dxww#-}g3jCZ+$k&Q0&|XWE+FUz=fZ*7G|2o#xhnor zBk}HqyrV=;Tw)Zs(}2Su-0nwYe9s!z=&#{I^`=l;z=X};Qt&zoLc|zcUjSr0WUSpi zJ}IxR&4yb!+a_k?$Lxi+b=y$59bIXI6LM4yZUQz|lfxhT*W|{t@4zJFE}-s6$3eVZ zHrJ)XC;DYRb0KWA<2j6C0Z2o%K5M6Gi2XpGkIaH^m1xC83HhgDX>nWj+v)rbE!uBq z?zHT7&|o630O`o^$$2qQUlA<3ICbz^AsD!rYP=G7`w}$5YfT4ur8LbeqZ7ThbcWZC z&hgsQHC_k00sg*)UT1p1>q4u%uC&JMM$dWO=?$+3ec+YTS6&7Eyiwv!Z?xEm_CEKm6v=g$Xrv+NHEHr(Mdw<+P4q{J0bPXMPyi2=00WVrcaWK^aw5C@^I3kQyd zVk+`rnN4MhS=1J3Oeh2^Zi8O24(GDP0vh0-hoc;^g!b^y$5F14*{3raz&S@47dWm= zP4`f^T+9$9W1Ur9;@jyg&-)c#dlXxNd{+P+>P%3NK^wrvxVAea6D)KvvK`bGh=cY# z#LB3&s{p6?HZYbglpo9CO>gSbtZqca>at`01*5SAR0TGGVnxbY!)B)q?;toe>(Ead z%wXACb~NCurVlY9%W=k}TV00Vv+Hj+0}QO^T58d@FR-^!b?(A))akhd+cX*OD#tYX z7opy|f7wdek@;&GxOCSve>I{W{yqs$pYI(GNjw#`L8rmDd?a=9j-qbf(b#C^7^?PW z7>cm?K&s%EMEsNuKt&A>e-4d8*I2;kFg{R*ff-?HemZcYN7{;W7W&R2_G>N&Z<*bd zV0H*}@ULFsUkfgUqZ?4{%^ux^f{uIy#)bdExAG8LaOe^sqmBM8%PTo4$iLQn$P?{H zM+^%qIsJhfqnJ9Yx^Y;nTxA{lE|-h*{^Zl^5t%8ub^jmbupb-zx%JlVZ6)z2MqB;) z=n^uKV*UaEu`&NnfVAfPVpEOoujlVJ^(S{a-=aAi^V2&XV+{bpI|-PoOc{50CjJDv9S&P1TqSv0{r8-n~CI@CKC9OgVa#yg)*@GgK*zmU%KF2bt1 zgf8_irK{mUyUDwZZiV}7t~ZD7^sb~v?<(5hT}_+3Yt7tQ@BG(k;`#zBh%v@I+Gtj! zgII-Y%zV1UGtN;9ybEKtL4K^rUrpKD(Lt;Olt$c6{V>O#SJP?>g9|0bKX0$$d?qBu zj{BAiY0L{wwW9w|KY}vp--D1AyCct;crji>J)+Gu$6#Z^bL+6_7&cIR#J{(E75?X4 z6#0O+N;Df3*k#)atDn3Zuv%_{r~GEv+P7ep+?ugEdZhRzLb|!4B$xK=K)jzYI^j%9 zEd`9d-HWHV$-f^me<=vtZSWsLeQ+P}$}PKc3ediXty(-AdSE$4arCG>EWZ{0N<4~? z9}}%L(K%Xc$G2Dpj1)3}$W@ju$j25mY}l?c`ZNk54C3Nol>l0jelmgt;xGvr6p*Lc zJ&KIRirDN$&0m*sC9#aUxb-{9&tZVa$XfvU38%b=8kHVuj(Ur!vv(JD@s=P5<{nUf zJ>=-Uh!eSw_VMlq#otSlyrne7dytOvEFxg3_W0;U6&;;6E)FrannOKMV!M-(K{Xg- zHaH~pLuPf0qS_`Ex5YJWoOP%lj(WCGPZ=`Ch|NxEiQ)i^7{g!Zf{EB24_kCZ=nz?` zC+B?oh`+iG!YO>#3Ayk-R7J3!CG5=4XFReAwIBV}^|UAZCl^qk$TVp1*VNiF+4*d9 z$2E|aCrwhZ8O|TdQFIGjsD)olz~UfpITpwYP}@rAz^kDDJ_6e@N$tD_j9?>p^J-+% zJ__E^L<7CYXqfjnl>as08&A+d-dgNm@+6FawREcY6tt3O43%0V;M_Eoo*OymC7o%; z)dE6ZrX0h?HO&BNR3X#Uj8!Vl`{(kCD#Ney z%PTP4*fq&~wv3#mJv8n;hmm_8-FpEe_acg@VM7t`WzgU&hzWQVJnj|H-Uf`?YoNc^ zQ7+*P8soi*(R<5`)b6&c(MU;Dt+3Lr9b~9AY&&E9GoNHUA#O&+mgO#CisMn)LG7cV z2>-+yfT8Yw>^>i{AujX8rh6&B`G7W$g~rSbE6d8pMaJd7Gqy%Ch_LrAR@8fFdLv-0 z_u;BpJI^z1=Zm*bmt4xm7UEqlv{`fj+FzSLx?!7 z2P^M!5!JFYqK2QD*W|B5WqQo(vyJ|9D8q)k=py{J>eAQXCEoz*{5In;85@(iTuL3- zw*R94QuH#s+b%bZqK6Is%iyiA7SQu|BFGbaavNuY&&&1o7UHqP{3C|>CrsGSARIsn z-fx)b-!mSXR*F$v_z3?^JdEEBWsGAX1^!#u@YPD1ea*uGK&H13UrSJU7;T~;I^F^-2z!w)!*_1o{N^J3HLb$6 zlI{D*RSb7+H6J}~X|-F_D!5Im6+80AZR6*re;xSmrn_%0z|DL}QzKQqII&T3^upy*bExnmRjBfH# zw$T5`te6DvzX)8$n4%SBU=|-w<^{yLeqZgng8cTRd_d-YNA#l;b-`y(zq9F&T|vFm zE2zS(ashp|lmD6jIi@lXRl8bN5Y&C3%Dle)4*)v~{|o=iow~dflgD~S-zNVn^!Dq~ zE=sV*E4a5&E#{>E4Lm@G0YIddC;jgRA})eM`26oP7MtgE6B|BTTLBYEf`1uQG7Cc?Cp#q~nqIrvHxp#RS6&eOP= z2b*b(CpN}?MIZ`)B>i6y-C@?GMcyoB^h?hrZjYYKZ9}_(M;D{=Yr56mQL6~uao4YD zS=_dvK^YC{s3GD1=Kqdgai11r0$6tRV0Jl>5jn2=ehuc`R*r|Nau2Dpr9Abe2xfKo z0k)@RhiiW?%>3RI`uk8Zd<13waB7F|-TeK`gDtnu(B6!G!O@(Zjs3~)y!O%TBUsD; znwHy}sw<(uUaTZ!PWo*#J)yi&!LIK+HsgI$o>1X5c$1>{or?EmCeS*gY&+_m^E*fN z*;|&FUFs9D)DOTCv*x?y!l`O4Xva)^np0&9>Y#2?H%Ct!7d@?*+s7tLk2~u|^C4K~ zA8XY-ew5`0X3s2t3T62Rn-|zw70p5ywm=8N#H)wWjl*AHrjFNc&ghCY1544omG320 zamGzipDb1SWukG2Vg@&5)C#gQI1Mv+1XjUxtb!vc<{yRidnDHH(HS$Cv3`x*WBt2( zG=sd7xp#=a!IhVxP)L9?|CsCC&D*Lv3(Av$u6J&$hy{W9or%JlSMOYgGg;Bou!s0)oXRt&5K{alETxuB!X1ztGA){d{JQe0h5$7%n(Lj?vGq;~ zeiKhO!`py63a45{r+}pJ&fve`RB?0)xC)>87^h0iDc%_zJ9NK-6A7FEZi=JU(WQo< z4fYzxiL&&)ed@(xoM@Xqf&IR3#EEw46T|AoML5wO)m}_%QQa3rH<(?$#dKWkn7(^J zy_kR#oy~~^w6*nOm#{b$1eXb5Sg$*W##WGU$zX!`5bJ zT_2?5zmDO{%C9ypUxhpDF7O!CVE1}D8z*=R_<1xFN49b{=mfv2r$ce3pXoHtG&m#b zu`xg_=pUVt=1f1F85o^W=1e!7;qn2z(@}$iSc6tLF(iEgyDBPoAfDiEKp2JqQ27`= zkpjxZVaTK=KwQed5(4BZtn90)m46Mj_OFEyxejdTdI*vmsNBDa`uI1~K>rpR>fZ_% z^*?Bme;ZBpZ>M7s40)=LO2f9%ZjKXDfNpE^tZe>uzj&zv>>=gw397tX8x zm(CmhSI*o1Cg)xMYv)7%8^f85sqcIoNq#VysOCtv^M>S7+79?sw_untOxsb%U@sh@ zNFb$>b6Dxkr z!QK(uJrd7ggDk(IG1v#jzSuZO;K&pzx5Ce=vOhzW{RMRJD`c&8Hd@s&Kh^iN#e=rY zTR2d`w0R5gdhtQP`W18CyKLL7%Hf8KSPzV^2)|wWK#TlkfF-5rC7_BUVFHM zAPZWjYq95p->Dp+5JgamP#V~62*#l7Jz5%XTIz_qi_>@Kr0+)gV2NA##tG&|02mzJ zYn~_h^h9$yQHk9A#^AuQ>s2bBbK)OdDsCVGdAZ*_-Do`{b&~yM2Vh#&@Iw1(?j3A57 z53=csAcw9Ea_OcZk8TO_X+cmx_XLIXU=X7fK@qJA;%67V9*#OjHT?!gWf zcK2TF#pT(+(=Sj7je}8LaRSuemXxi|6z{x^x(Ya^MF-ugT|4Ywhti3 zboVq0bQ;zfTNl_$3)`&c8VfcO32bM;gDxHkreo6BjSgF(Q;T_S^uVBovV*=<5bQ}M z0kFQH7E{!px(5TyE8F10lqSz?0gDd7i_WA}4+L~Q^nqxu@j(Pf!3QxKYkbI9_6o;e9~lrSY0V5C=0KA#9y)K9^u$X?($r$zYZRL-#X5l%aeTi&>qA+%sHWd%d1I2cNuzyiC0`Sk?b z>J{umeS>{za4;N1IRZqv9~~5oq|1WQbbDY846C2djR+tMWdkiL&Et~Io|;|&^S9E0 zKu(-V!D$gI+yQF`ghM01S&*1&>Lg-YZornDHJA)8;(Qahjf2_Urt`;@J*FpvYb>6r zV0XWRg~riPDNk(6`xu{7FoARcb0s){V!?sbI+#RN_`YXw5Dg6KXjm|Xh6mQZurXmH zQUpvRg6j~i%;Ym7xE@FBP#R_|b*@fs%bI8?^R12j=7C^i?!y5Pdz5$0GGuF!%Tl&| zvsu;el&U-Jx@^pw>_SI{!8kh>q6p7m!}DZ-MlVMDY0Sf}@W{{w*TFtUvEas*;>9R( znJU_D>Z6OXW;=8+Xy_c+9tL|VLM={j2a;Qkx-u%Cq0X%GDwHV1q2XDXwL=Y}85~A3 zI2;r|mD&c=sB17CV|FC%5g->fIGXwg$IxEEu^5%(Knusyfx!thH8_c81T*Qx;AFEh ztc`nedMr;iEP%)IaS#Mmtn zwJgURuv>3P<+IebM;=O>l)w%^c|5bfyxn&bamg%)aw1t`5=B(G!1l#G8iQNsB%LLb zWsAjbOqk#dY89MG?Siwxdd~stwXv`EEm8qTcxsBJ(U9PuFl%^W4*{!U+UOlIvQ&Lk z*)C@YTv2(zC4Nld{&4lX=0{$+Xp;=)Gp%OWy1~Vu)l1Cy*|p@Q$IpvQYBUxsh^U2k zh35Fm^RW>grrXIvF&t;ZpwhBMh&jRXUYiUSDj<-UaNgRG0}!(KG+51IiFgx~4CgBL z*v~Flf@lW-M)pT=OW95C>YG0^0pT1NjzCw`*yS2DlhX|;G}N-(%+9a@wo?nPAU~Kx zL2wo223J#2a1BW4TB^Wu+6@b-21}%Wa1-qdLf8)zJ3hD-tmH;IIJk`t4{oRFs7i25 z0CgNgd|EJ{&I|4^Lt@=!|85*hvggl}OOirDe&Wp=DxB_2Xa(CgmQ=39x5ca4-1#eOTZ3 zgW?~6L|aNdf(NNQco=+s84W<+hN5och+rj+3m(ZB{PF3*A0N>p(Rk-hv!3~A5sqA} zi$P{;Byz9&1gjk`<2nF)xS17BZK64K z4Rn>kU)$Miatut;o8Jb(_?2aiL6h0{1yCGQ|DxuPD}gCJ7Fx`w6)+#J9}3{kJvtez z8D|h5z5{?Q_J|Cgi!EyN}{Eo`A?$3&!vyjm77b z;3?4R(-}iEH9a&_O&SY>K?eov5bDVqdAA78L**4S3`lrD0P(Me{q+ab%R8=0tq1knZ0raO063f&jAmdL4C!75p%KjZ zTc$&|&rS|q;};2DAr-tzVXy(p*Q@B)Yar3rGy0Y)U#$>+ZDR{PXpM|8*-fi(LxQ|U=t+ked$??!M)LR|7$~sZZ)Hk-SGUJx0!OUVU zyr3gSPnSod*%rOK6?p!I_?&A#A@Dr~_6`caq@`!Zb`1UnO8yM9`Z=iBddn|U}mNx`186Q{ha$ylh zXhUQ08u!y!K>>CW<6Kum22j)(Su8?G0HR`Y`NL!vAwY5g?VnDf9b08p+P9$6@@}EA z;GJ{^ZB;CIFB!a#kB^eUCkVGe^bPCF>@W{L zv2io@KmA9rgXSTf`(g}&o#uZ=ETEiDiAb_1+46}F{M$o~Hf*Q46(Lv;Rv!;8%5)-% zw0aBTKr!iGxQR$Eu&)=D>w##gMg7k@mBe&U^=JG0uB$LOn;dSHgQ0{$HoU{27%9FrvQPmP;#*u zEXfc&IYi~9P*VoNbTd>RpEl}|sh9M6+9jgG`KkEEl`r*&Z&bs=nz z+3rBuA)J?CCu$RR#te6%9${DN6?Vfc_n@I+ISmghXk1uH2ZvR3IDT_XXsf>41u-K% zyEDQp(1w6?n`jgVRuaa5QXusro&x}+4Ko?xhuMXjDUWx&+JZic;OXF*XyzV{X3hk> z@YeK5q6ZFDR#Db-J|gwX+;KQ6U4*@0%dlg-32k7{>}Ncj7|Pt1xioIh5Jfq|ewfc% z$_x8bA{;%rW!WOe#BD9Q)vB1HOc>e@uCnJ4< z>(9Z-mY05&!||QFQoxUglZ};DSev6W{K$5(Ao46c00Vp=26&QrKWo>dY9OJWOEjP? zc#QDNGr})y=R{Rs5CvqNlWpOUYQuh5meh-RmC{;h%CKDQHhPS`T!dEaXl8m0GX);+ zz+qnv+_n@x^tA|tumk)d8sN7&0fDTe*>%o99<#<|*#22y_{p%-vj_`aAyhK#S|O8R zkBTU))3DPT`~=+8zYKw%_AspEd(7Fe6aU5@wznU>l3{hmfEn#GJRCDL6@!Jq*zgFf zxanANM}e;%4T?Mle02s^&9QV)cpM!Uo{%x1Cqx7z5roBILiQCAc*8ZIy_&5w1h4vd zO18koU|+S+X1n|?(>In)h<&}nQ>Y-EmGQ8@ZdkyxXOS;&AbbvXJX4#dCX4`M(Gry* z!?4ugYleKb%kgx){28E_GeI$DQEqrP2I3r$Dk?38nYpNm^m0t36_|v1ZC17OOcbeI zj@Vuf>oGv;Ffh4J>CEAg=piq&VzVbxA^80};Fv~Y{v9n{n6WZbfsoM~!<`EblFCTl zD{432wELeMyfmZ16p|k`h~0N-3cZ29unmpzz$B^5+dLxPVfK}Q7KB$|tmb6QODdEB z_k)cYeiQaji#_ZJWyo8!G-H>_RT)i)^w1*F#Yiy`4*2g)T>W2~82I0txaPk!G3dWH zVWW>Nhxxk~85|C=UJ79=SilZV>8D5@E^i3;jj}X^F-n-pB*Ow|UZ|V|u*_eL zoGExeJ(IS)ukrXI20Ug<>v6C*N;R=@UIinMA5qA?sXxjZ`YElzNI_K^*h z;NoMeV&Q1B{YDOs#+n`DP_+*m2S)rHH@N2-Ha~u>0e|;0-LPBqh6g}j#;qv5m{|qn zP@_zTlTsJMgEDH049Wccl+-P5UFsINDs92^@Q~ES@UZm77IAtV;uI;ofxPfW^23|3 zZf~Ym;Vlp`w^IA?HtHVUPE{dtmcn_oTR5Nkg?G^4Z~^U&jm}4echcB!5lsvigR$LB zM}$kDDBeStg!j^w;eB*nct0%;AE3L!rSx$4AUzsBL~Fx`X??hiJ_}dS_HZS_j#ddB zJ|a4W4Ps!}WJt+4`9vzU9&_y+0oE$0yJ%$$Z-@4da0-{kq9#706B3(`J`Cnt2yVs; zawwwNT0ucCgitaV#i}B%<}eNdkb>85dXZ1MaK{xiNAwV=>C7r>y z!!_UnPf!qIqxbMhjQCR!8&6Y3_zXsR9mK|ZC~MD~5ovE9o?hYuqkzJIb~k)96_9~9 zz{WYNuxlwS?-^0ws|@o1KqBg}(a1_2wy+TsrxU(}nSa?lhdGw;26}^{pFV+c<<}k# zK>+^|94;_tg5BZS2#;d#i*S?SF^mxuGRhVnTZ(#7;qeR<;Eod)HGfwwAZ?E+HvtG6 zC%2HDm6;nLHeSO3ybiJPCd9^DAicM#G<=6Tgztjv-l09i_h?YKk@gMW&lrf|>46v? z$;`HhTxvlOd~!o<<}Py4_DN|qtTSGU|4HoKxvL6iZGQ9m?)daCCgA1yH{I<@oQajh z<}h2f;VD>5P@Q7Q@Kj#$rcmszt$uF$Y!$=qQJ+P42E?QTF$ty)LCL~`u}Xe+R#SKm zc5w*L%hbp09DPGUmNLy*E}0cI2RRv@A6@`uI|V`6GZ&ITIOjzw!YZj79>Fu|VWO}{ z%q~5ZD^t^tA6*0;>eDeFN@|~#02p`%f<0qGfv;XoFfSkKL1%mBjw1zjuQ!WcH z|C^tz+w~{bLCJ&TLTd7m&54HY%J3?Dug1RW+wh5h?iTwAhpPDp4aEG@(B{Ld@ylz1 J%fjm@_+KlXG6ZE{tg?nc;%B`C^+Ih) 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/frame/ThresholdPlot.class b/bin/ij/plugin/frame/ThresholdPlot.class new file mode 100644 index 0000000000000000000000000000000000000000..67af72f819721359165929e67d410ecc1ed698e1 GIT binary patch literal 7932 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 S!QHdPdw&&Mv*NAf2>&;xew-fw literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/tool/ArrowTool.class b/bin/ij/plugin/tool/ArrowTool.class new file mode 100644 index 0000000000000000000000000000000000000000..845dfc124b49552999c6b9aa8316c60e53bed998 GIT binary patch literal 2419 zcmb7_Yf~Fl7{~vITs8>{luHVs<=P4f5JC$Lw4s$6dPxlzAtE5COIX4}lEox|ePO>x z9pCqDozc|M8E1Uc7k(zk`ahdxxeV=Sn4Fx;bNxNfIsEm{Z+`%oz_$wO1Uihp*g^g< zXB1-<)6B;nmP+PvhBF0jfzXB1MM|cM@>sy5Nl^hcO`+VO5nD2ign)P2C>oUq0-qVh>n08!==|_685}T9KkK{BFSqY#OK?QBJ za#l&jRkREEb9yDI6^}HAa5Wq`$PgB%-s+f15;0;3fO+9Bk%VlK8^DH+8aFNQ!pJ4aecktlhveXIsQE9S#K zpN{0%_+)x)XJ;%uIl*CT%9?FWotl`}3c3%p5;zTxi)Hh$l+_;_ve6o?6Dw-z27T-i z7au6WFB^rU!a1J~j()CU{I$-8-XQQ3H|n5r^je#KG;prg1ki{kt^}^2nIo0O2IT_W zdof%^%lt?c9dS3u?zkr!bXUN&;h~>#-&IAD-k!*)T~^d_ zE8Yes+K75a+ss%)9Q)9ML0*9b+OUQo3_eQ-XvcGO;CpmhT06B&>@1lRp5ap~^A|{C zj$AyHpT~lHd{M?CK7*UK3s~aJLq9r}IrGxnCNi8U^!Wuo=Z=q_lUU)NpF6-!c;4~O z`&kMFtqMX4`UO7<%Yhm3H+n>mi@sMK(gXA=%eDDGNcY-QI~<@S7xEF=+AFk3f^ShT zf8@@2khd+IT}-Q+$@VaeUMASbzkVil12aq^#n*HZ0}OJI7b$E(+^7ZVK-_d7ZaNTG z9f(T~#Iyr()`2+fK#bcECAu!uy`$^P(KV9UhQlmqGt2x(W;G&V*^uovI;UCLi%mAF z&GV>+tiBm0su7}!()%b;#fWN*U~b_F{}(J^oN3)TgDB%5$~cHJ4x$AIQOZG-bPy#R zMB@&k=tV?N-h-%~h`x4i{{&a^_Iq2t`4wBI*FV5^2Rqb*yH?P(C%|h5EwIZgRC_<7 iwgp$q-fOCyeLbensJa~0{3WV|i>fo1s+yLAgMR@?^0e;& literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/tool/BrushTool$Options.class b/bin/ij/plugin/tool/BrushTool$Options.class new file mode 100644 index 0000000000000000000000000000000000000000..19ec1bec8159273f7b5ce89e224cc7ccb7533d4d GIT binary patch literal 3985 zcmai133yyp75;B#k~f*&Ch0Uy)0Bj?1kBn<30RZZ(j)|eX%bD^+M-{5nR%VOX7XN` zC219RkpPOjPy}}cwOFcUI)Z>}RB!M!3FcS^W7pNILo%Am5>NYJixmBRH z@$}o8b_w`VgK0bgU&t{_6F)UaBh`gGbP)}Trtpohao!m(`g zaypec%CAe6J7(B*uPcaZ)M!{IP+4eQV!g6ROWOT$+e#{{Y9;D$c24j40^63+ds-B- zq3BZ13T%+rs8j_L6ZWiYoIv@YX&FPQsc|DdrjN%AfzY5G)?>T$xT)T~Nxuq>z{WG{ zBHW`%Jv_NXpLWg$(S#Na%>ref`-&ywdR(FvZB!RElKpnvh{o-d73sBOHklA=Y#MZ^ zqt7PWojJnTj`KB;9TmB7IZFwN&A5Q_!|L#uJ(`T0R&=vKL*r5*o>YgB?l#H|;X;Xv z@PZuRJi9Ep_6G1m5^=<^!e%T0lCs$0zlIkHtj%%WRne0&V-X`B#5Qc#aPgw7aCb=b zpqCt&(PJrNc)!4Tp5=L&(^b&+Zd{@$-Or@TKjQh;v@3wi=vKX#lRy(F(fc$vf=?9By^EV$qdnR04^DwJMM^y_EMNOGdHfBazXV$=I{T6mYDsuTSZoU@#macPfq_keEb_ zhnW{Hy#*Q@uQ(+jT}=h%ly*zP#npoI3(`ic?Vw379O8czDK^ziTLEn2PjPT^ReNdpPfd0LzDpljv5?A3h0;_mnX+x-S zVtHz-a0*%ouakH^-jEBYin-mDTxqXCy0vFQNbLh!~_Kifoh1~G^0 zG+awHq~S>1*w1=%J>E(e86j0GRlCa-oBRaGD`F!y0c1v0;|7Vh;~fGrY*iQGZh@fW z3T9RbYnxM7NrK#>Lrj*vQfFyMpK|)f(!$3Le#=?&5y(XXTYdPKjgqEW;gUJ0|Vf_SWYkMg8$nRPP@eozhl)`!4*F>AD@rcCtRFlZs?3N<6@~FfQl-@GWn;zZ=44saK@FR&I z<0m;s*kzY<#L1X;{7mBK_ywzI#0c}sC(w}}FRvHoSmBa4FE`x@by4}1#INxiCK7Y+ zluJo|3VJeF!RByM;&=ExA>pCIkPz?y{zxp`s1O&b2eWX~rUJMgHTiieGUQ(f`O7H;dCR5Eb$fW<3oZ%j`l4Dwy` z?hB~rwb92XF9UV=c`Y zoYkC0{W0h4IsC4pB;a3^@+2zoZ>+^rq{V+wi>I*x|3w2=CFN5p&c%6rK98Sri08Pi zNQ2Nw6>tNuv#uH9R+?7KxhFDc9Hi<^JDQI}X3%jMfzX!QP#o$UQZx6Udrw;iTTh@b zbZHs`Y3#`0B^m5?x39`yzrtNFuf!|d)qU5b5%xBQqW*iaZI7>YGy}6Wjj8PXmF^&s z#*8;b=Up_&fuC(|2qFXYP>~X8gKK4>-=fF(;KJp-q6(xZX|5CivW&_QXCgS+%Hz(L9r5#iZYxO z<#=3FILLRnKzhjg@fdE!2MBX1PU3_35Tm^wkKn@``EV3l@ezEKdR2s~;d=(v8a}3B z+jf4Q!JYip@Cly29iQ|<)yCZ_RM)l6qmIaZDs)F0M-L;E!RLnB<{7U$GWfjzi1M-} zjr(rM;LCi@-;9+m3j1zBl^VHk50q!{ZEw%RPK>$v`x*SOjS&4*A)LW4y{+E)41OE> zgF8-S@RLQ1x_1KCW{LMttul+S@bO+9fT_bbK+9-*Dn-_g}6 z_xVio+WI#QY`o@5f5+g)Yp&?r0_D20+6^S*W^}lTskpDRr`_L`+iy<0zrVe!^O~O4 z?oLe6)!Q!PT~x_b7!)*?MPebQIlWM>t96U@@&edoy^QTP;*r4hPF&+yUiA1e`G%#%RSr4%|6mpKn!T_z{x`AXMp2W@d_KhnR312A^A=ymy z8PSj#0hn8%VrI%>5j{L^*7jOTK%N#GOPT|OOBX8H7`CM zWh$#WGq|cZyQm8ew3KsG5A|u(i;$ZchC02T`q9z2c03S)$Z5iQgFB5-vZ;EjXg#RY zB~&GMn{|2}mAmOG+Nu$P+Xct>#f_naPH&*Aq1Ld0;3=O)t&Zvr5wKk+54l9Z4jB@d zz4KOU-jGhqsme_T4Qn(6C);5ri60Eabs8ZPo(_b=SwdjCplYV8vu~TKH;Wd#bgHKn zP&W`Ch5&?mXjgY&%tl=wjg2Re9Y_=dVYyVvvoa&?6|sId-AHfN=q3c7jggw#lBv_p^cL`3Y-ng86gP}0205$z)flyh-lo&r)55lh zn0M;*uIb&k2xPbDbP>7a?lzq+78f$TN2lBAy@=+}h!NVgJ{Hep-~1VvS+KuPr&_9W z(>}UWqYq$QuCB;qojyn(0&=h`!%a-*TR^=Ab%{p%b-Iui3G{cm>7#T|qmP071pmu* zx{D4Wi_JuLAOwbpB|v1z@vUO}-8y|jY?leUV_|d1G~x*nc&|>2En^Ssv_y=(kv^%@ z1N0!2P8i9Kcwi5Ryge3)!P5m*e*ac+y+NPS>C^NXcmWcDhcO(FjYq?FzY-Xd@u_Nq zx0#OUbd-)Emmt{gS!yScIN$0h;wNZQqm;cG&PwDuO$kT164Ag|VkDMy(`V^XjZT1% zfySn2IHuF*=rMqUxyG?5lvMd82s3f2B6xxx*Xap*62PFFFqORkNYFr!J@k}LUldpi z%|vS?*%8|l741%nAx{bnU)JfFGz@LXtsEGn;}QA!s!m^{Q$TMN{5TkcPJq!8{|%kK zX?ty8#2m^@@NJ#GqlSR2F+>9Cd7ZvX-?KE$i5Z&%W(CR4(jVybLq(`zI8$Qh$2$E) z8s?6Vg#$^WH5wK-BU;Q+AXPLSCoF+jR4TLnJcA2l*~NOjV74U3Xe2nxY8^_(Bi(_- zF61W&$GpL?9|Dmi)^H12ra$QPBK;qN4{>EDnerLag)=j4+I+G&YV^nHF{2_X1D2Tn zXPy2ciQs~G4EEKEF<5RCsP%K3aA5@P(F3+Jsr`btJW;R{qgUq|0=xwy%HMz}9>WT&=jw&GL3`>2+3?R!jP7dvMDh#|8(}aU*Xz7O zSVspZ?UBWM0CDc-mAp#hM)*l7sLJ>5KypOq)w2HN1%j|{u9DRcHw3U4gPXZYr)u&F zl$Yw*qux%BW`ZE!chO%Af4wk|ikt5NB%uLgTs*D%Xz2s)w~Ts0?e*1%WZtn#iQ{rcW|$ z6!hJ}aIGmma3y2bOE+)lT{=h7X)YQFOi`U<0*0jdK*YqX35~DK)fI(IEFwjqo5whz zb29B6wX;LqM5kkD@4|9WYJzxGxhs=}s55zz0Rnxrgf&0tJQ1AME29EzyOH0lF{-a( z;X!M!qgWQ2-OLE)qL7pdgSOfAOc$NG!sqxW9gNh&Z_`=TX9W=>I-DF4bl$1+yVB^i zkHk#u=rq0sVV^O;YIXBozD?(M3%UgslVQ=pnpB)MRcQ$`ol%t&TF8px$R*Z51o`FWef&Wz28`NG!(yP8aj;ijAs^t6YW$IOif76hoj=C63%h_H(D5#v z4@m`7ghYbrv0a8z%f)v?acj%z;(JgbsKo(W8op2G!+bxc+btXUUPR%NTs}?jUS@$5 zVfcV7T$j7}Aw;4_g}_>mgy*Lvpg+Up%N1q2A~$=iCOaKa!hMvF=zLUAb_>B_H<27i z+PcF=BpFa_0W(5oJS4{w^QDWu@wGO%8c(K^STWJyNZbg7UHk~5SQ+fGySq8XCj{q5 znW}SjOdopKTa#Qqll3gckLmn*Nsu`JxG!$T;%4%CX@5fJC#B@cPuRQuq?#LvmV=z7 z4`O5vvQgMu=+ng~(O~(;BaJTpG9s`~cHwI4s~WRo{GnLX#b04+YOh(DKlVuGRH{hOXes)xoY+p@yN3U;{cd47D$>U(vNPurl1$7;Fr6tO~mLYfS5V z>gql6YGb`wKfJyi9nDu6zN6iqbTNUUUs#_EE^wpcQ^wRk2OrC7I zCSgPN)G2*5;pS8PO@aJdvJX*J&TJ!7`fUEpvFAxSlr}Og?*HZMHu^W(uXrmDs zbMf~PE6rw0Xf)hhYqq%fhk#9@ZK#BA|ELPCmX5#dS3JW}R*M&SAzAR-X)#Nr;5C`l8i4j!qe6V1VR zW+bZ5Y2(bOCzz?@J&MdcRk64XfoLO+#o1#HdDI~cHb$OU)MFPwX{j!L0T4ndpVG<) zb%xVZgho)a7#7A_qq{psG40NW`xsdEdvOs z);B}G^u`pb%onkC6)U&>sxI-^E$KLSu+J&X8ajuc&yvMf<2pSC7xOUwFH~R zYaqoyNDc`vs$;4@Tg<1c!f9m;cp^Q1In%ncjDO8+N+%z(N~))S>r_KavtqG&o;4Lw z5~Vd*!L;IkXds3AbOR)p(*hd)$6WqwSJ`*lz@esHc9v<+TubB}z=6HZcU5&{OIbDB zQI;YY;oBzEmf0CsXK&Y>eHeCHaQ8rLJRUOEo3bJmWn?as0s>{aKZ^6$_DBHdUs%U9 zd-v0emJ?NV#knc23o&9_eCUGnNu?8x8PydhL+XkH9d*Simb&5$OI>li zqpmpNR99>+)DwA z>s?cH{z5wy972=(FNF$tpaMT$kR@s z)`i_pA5as!$}L4~C7}{Ul)pytu9fny(uiM07YD8yj;ihJtH9}uCO~NS%CoNQfxS_R zSynreuo~gW^)Pv?$G0>^yHoV0MyK4pHAU}e%=eZ#Cupy?EPsM-_0~+%yJ6ZLPt$w> z;*J!(e}X=InC5sf{s3;L=p*|{(*R6!k~f)x<^KCo1puS zs5b>S?Wa1qD|X>dVVc+I@|8Ku^2=N)y1&eIfZTxep-Fn!>%#CO-aHX4`a zS6kjrhZfgE%1d`%n1)uaoqT?h~|cOJ3g6N%~ih&zYjrzEZ~d zuA?T!`D>jO&Xc5{;KD7FJV%~2Oah?HAU@cH z;KML{X23r~Kx`QJj{sv6A76ITcK9reWHgaMlBf-q=4mw1!$R{CYU6W&gagu6@wp(u zJoLX<&R~G1pfsNxuTZ~66B_N)xbUAU=W$wTfkOWhN1)qw0pS3A^03&~z4WBqE>#v+ z^!T18x9Mr1RH9)&dzyS2PSa4k)TP;ppTn>MDE1;?xW{#foJXMl zYtR&W6da;r&=orG4cAQaaz!vsu*xnOe}LNTcT?O@mXD@e!??Q#}&spkJnEH_}4Ai5bI zc?&%9R(u708-nWX!1W!VmUkjd_kuod0ZrVB(7X)+`)+)5c@N&+j<;s{EfbOR|@ulU1bR4aZfNGwi19XZ$LeJs*&rj)NNcw|F?z`v@bVzx(#R5O$-51kj z0m`w+q6nw|7FnDNO0vje5j|^@g#h_u^#j-!%Dq8!E1%l_fxrA zZzkD~31sEoHpN|83Roh9h%Z_$x(AefFR1rEQ0)Ck`A;Is9{@2th=l*N!oy=Jn!#f< zgU6_ihge)dj(=141^5FEoPOH*{Hz_w>!DuXNfk=mm*W2JrQoKk9_Q^nz7xCytQD+G zaVW*ZDc&hvQ#>k3ZFL%3n_rQS_wkw9A3@$8g$2i`7{0B*ccL0fft)6>z)ZoSM{>Nm zKBN75l4=j9DAdT~yc@Y%K|bEY*Fo0?D#3@A-Ehr1()b2`BiKl6gX>)Ah)-{}VGr0u z!9#qbO1@GwbS^Do4Q}F86to~L-^eFWI9CUdM_3!7wIgf)ip1>!> zCs7wYmBTZ>jP5>u58n01c@Z^PHmA<3UtZjjXrm}T7X0v}T$ByFD#Bll+Y=3T}nfOAz?S9APe7$wK~ihLl3+sc=0? zJD;W!l-OOkTL`8^5`QNfIptD{a5d7(3+!rWJ};#*&fTh%D=Lu<-W)qIZ7|Bfg4my1 zAmhuVlzv1>wM#(seosa354t3zOZi6#VR-aYrTh~sj1l6lk}Fi7pQ`qstKVPZ*Xs9c z_554)`+N2KqWXPF{r*Y){sq4v)GXqv)}Gp4y9XUHqqSXx7-P184XnjF;-Lt=w$p>wC%HglSa>@*I?teOR94q2QtB+ubI zW0pxr{tV2xE@z15kmXGj%NkP&UY}`9u4aX^OoOsv#x$tJBqEe+9Bv0v{>*8DGpET| YLk@>j+YZNEhipG^jK!yLlm&YJFX9mICIA2c literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/tool/MacroToolRunner.class b/bin/ij/plugin/tool/MacroToolRunner.class new file mode 100644 index 0000000000000000000000000000000000000000..9d5f7c1b605c787b20ab19840b3caba195c88b0d GIT binary patch literal 1175 zcma))OK;jh6ot7=aeCALx(sV?EMfU?8LG|Iveu9x zaqM21X~dDykc7q%Z?=XhFl046!5TxZZwd2V;9Q#CIlr_`hC<&RaeK%;OYWn?g#XdH zW2n!$Evt}H)4V{;>Sow%_OJOTZrEImje+l3V%%*F)7U~eiEW1Rswy=Uv9lun0~IE? zqmq3@XV3>eAKjet+bH>rOvw~M*yMvH8Li5(uAzzs!+*w5UdfT`I9yy&XUC?m8gb^i zV~;xwrDjX^&ylynh)%n*6)!a$V2>e9Av3i;jX!n+;j78@q`0G@i57_mZs3i~6H5-Q zG;4Wy)sAFa(4axb>3%YLXxeI}6tPYxDcYSNMn53k{zgydEA-E_iy=pARznnaw4*phWkf5Y6fuVpGvEFK{UP)bCA<^+Uv`&d+37eZk=V$0VDtJoB zXDW(q&}S93$b%C|)>Q~Sbmeh4!R{lI0+|#ET2dzUFluH}h)jrt=VTX~+U3dU1@

_OMXWHo29lpo((S!l1}4DkMjTG(KA80G5;6_woA$a*fElL~i_> Kd`Mw2y!rzh7UZb_ literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/tool/OverlayBrushTool$Options.class b/bin/ij/plugin/tool/OverlayBrushTool$Options.class new file mode 100644 index 0000000000000000000000000000000000000000..6ee184cdf6a1673a6d41fab086f3a4e2cf89f388 GIT binary patch literal 5621 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/tool/OverlayBrushTool.class b/bin/ij/plugin/tool/OverlayBrushTool.class new file mode 100644 index 0000000000000000000000000000000000000000..58fb0aaf8c5fe9c006c6b2a9d1b6ff4fe427500c GIT binary patch literal 6867 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/tool/PixelInspector.class b/bin/ij/plugin/tool/PixelInspector.class new file mode 100644 index 0000000000000000000000000000000000000000..f64498c5a4f2730e1e09a8fad1458313ec4661e1 GIT binary patch literal 16289 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 literal 0 HcmV?d00001 diff --git a/bin/ij/plugin/tool/PlugInTool.class b/bin/ij/plugin/tool/PlugInTool.class new file mode 100644 index 0000000000000000000000000000000000000000..310a914d6f7479ebaa4349c6c1a35ae26f8226a6 GIT binary patch literal 2587 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/AutoThresholder.class b/bin/ij/process/AutoThresholder.class new file mode 100644 index 0000000000000000000000000000000000000000..734bf86e8540a07d54065b31b1d27f1345ae001e GIT binary patch literal 20360 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/BinaryProcessor.class b/bin/ij/process/BinaryProcessor.class new file mode 100644 index 0000000000000000000000000000000000000000..fbdbcfd6fade93ad4c5a597cc572e8ceacc39551 GIT binary patch literal 6758 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/Blitter.class b/bin/ij/process/Blitter.class new file mode 100644 index 0000000000000000000000000000000000000000..cc3f0d14c8668c10ce81eb9e5d92758f15a62224 GIT binary patch literal 703 zcmZ9~-%i^w6bA5fT4+jXfx+0u{<94zo3z~Qj;l0DCrc%*(on%}kXKo#RYzqcx_TrTixS zToyO}`0G;5HPc!+o?J1pVCscsQEOS~nauB06ZOc1n7zrjsCz zlg#AUw;4osNt3BPy%=CYXs57s`}|^*iOJ~7EL~@6MPeqR*r>(i1*jilZ^?%K7G40h#XO%4K zm8?{u!!j=`r2Ub-B>7$YHaa_bspz$-YSV=@o%g!(u&)2TiC`@e@Y BWg7qh literal 0 HcmV?d00001 diff --git a/bin/ij/process/ByteBlitter.class b/bin/ij/process/ByteBlitter.class new file mode 100644 index 0000000000000000000000000000000000000000..c4c6c623d9598490e8680c53438a48ce67d61714 GIT binary patch literal 5084 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 q~Ha*gQZPUUCUH0fWc>s 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 EAI@bDmH+?% literal 0 HcmV?d00001 diff --git a/bin/ij/process/ByteStatistics.class b/bin/ij/process/ByteStatistics.class new file mode 100644 index 0000000000000000000000000000000000000000..7054a31f8b6a51ab7cdd89e513dd6532379927f1 GIT binary patch literal 5786 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 Wvo3iJiqQ#gu^QmXJkPxbUidFs0YfJM literal 0 HcmV?d00001 diff --git a/bin/ij/process/ColorProcessor.class b/bin/ij/process/ColorProcessor.class new file mode 100644 index 0000000000000000000000000000000000000000..2c2920b25880ba5754d9fbaa359420a75e254800 GIT binary patch literal 35577 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/ColorSpaceConverter.class b/bin/ij/process/ColorSpaceConverter.class new file mode 100644 index 0000000000000000000000000000000000000000..eeba99fdd3938ef742effc1d673b5ad224714d1c GIT binary patch literal 9284 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/EllipseFitter.class b/bin/ij/process/EllipseFitter.class new file mode 100644 index 0000000000000000000000000000000000000000..b57c7e9edc21ef9bcd81b28c717abea0332d561c GIT binary patch literal 5888 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/FloatBlitter.class b/bin/ij/process/FloatBlitter.class new file mode 100644 index 0000000000000000000000000000000000000000..879ed87e9b23768cb1f8091f251715ef8f627c44 GIT binary patch literal 4287 zcmb_fdrVu`8ULMoZH&zYV+Y4^0(p_7JemMWaT!k16c~v!LLg+M&@{uC3z!(2*oHvU zge1*UOdNfG~&(cfj-f%P$i=_4osCD&&0+!DB zm|lu1fiec(Il8d(dB!bS{G6%r`L+zy_zZ z+^3-eP8K0G8DWKd4<%-H1#7z|si%%7Lb2pbD51wvo$+WqL9PjW=xnMf%L>SviZDCQ z2wD&@wQzi9p(B!Fq%BJ@3{Hn8^g<&AgFy-7K?H3$v=p+Xni>w{YXTMFcx+Bjq>jh4 z9Vt-5l#6g)Y7~&3cgn{e8;)`lWIIQ$^BDI9cSuxspcnnJgvZ#^b;fFXaa_aKC8vx? z$c9Np;xVatLc=o{5^zLf+-^xaVS?TwVDf$g^}{l5o3uKq;W?ZV(DGJvC(!U{y8@U6 zXAX>D)P@i{y3qVt1T=&(CKr?p#>QpWa*J5%y6cDS3_Px30ve07Am1Vyrm}DI@~uTa z{L8laGWh6fJ{o6*Eepmp%?-@mooErLdel0lY&WZ)OoW56F@2uoSTdC(=LZtuj!=>w zN*D5^ow~XHm3HQakZhyvS%ul8-qkUjJD`Sno>0mseKWZT(;tY>Cc=7mM4k|?;v;dF z#0y)v0aOkvzjk>@@VHUrZq3fzaoL$WD?4+aaJEpZ5-wV~`PN5!`PtxJQw7g^@V
ZOE4hoo>#)2_ zt6}EaUS{x~ZMRn6KxuU<4U6ix*O0C;Xa_6s9=>r6HkC4qzrts|4c+fbt3i*eX3V2a zO2%aM*ofceamjE7W5i$TvHX}xsmDFmu96r>-CAY(R<_A?$Mn*l#bD;@cOwYh*f-SXHYV|G6I|7z0TNDXTM5|t$AnLa`?P#s=s{UYA#_y zS}4Mw)A;);Ru@;ay1R0>UhG{^Ym%p8PkZz`Q3+|y#IFL5Sq}BW(=bR z8lxc*N6h@Ux2rJ)#uKa+!w=tpo6cObT~z zUT0q#)bH7OSxV7zQYDb5rg*&>(tGS;gZf8~Qfu?nP|k~bEl0__UA3cK%OHRX8-xu9 zh2XQ7f7~prBx{!8<@_k2jT?aC10qBGA65{oSze*yQ3?!{S76;9iZ53}1;)s(;`Bo* zlohLd0lRSh8xA>VSfMq`JQmayGck2F9mPn#Y!w{?ATy-ToHyl{uw^w<>0XfuW|-c} zOXa`M{3$5)lnnR7DvH^hmaQeXrVKW(VrsxqnUOz>R>W*cuA9v`%jXO>uHr)ZUm^Yf UxwB<>=zK|wu?`p6^9nBg7l!ycu>b%7 literal 0 HcmV?d00001 diff --git a/bin/ij/process/FloatPolygon.class b/bin/ij/process/FloatPolygon.class new file mode 100644 index 0000000000000000000000000000000000000000..a39d3f1c717a60ba0486e7d0a29fcfd554044eb5 GIT binary patch literal 5752 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/FloatProcessor.class b/bin/ij/process/FloatProcessor.class new file mode 100644 index 0000000000000000000000000000000000000000..ee25fab821c6e1fd1a87f69bcdcd4e8be832b02e GIT binary patch literal 30320 zcmbt-31C&l7516A_w6@(Zr%%7ktNAXfB^DHKoAiT5Ksg`z=fBEuazVnJF< ztW}Vh%c*FOpg?D9w7s*jt!okIC*HWs-5%?yZ|)Q%SEIhidaL5s*@9Bz4NtNw`PRmo zn~!a4ZtD=_UyIDTrM1!KdUUo5jcXc~N1MCx<~}Oc8ta;gtkU`OSI;e5E=WJNt*+ir zf`SHRQAp4cs%YW5=!WQo=*F%IF&@MO1$z;yHc#hPX$xbm^^3b(YU(?dMQgaD%!O^} zae1^O#_x8f-qjex5b_p&EzPD2a&3&&bu|Lj`9QF-KGx9K7-XCr4=#2w#U-`ddC=%+AE;JzybnWYvT6vU-n9kJF144*rlWKaPW za_UHfimX(`AU_2-ugaimnuf`$ZQ;>>JuR8=M;SDO2kPyp@2u}Sxh*CrWocKmwrNqc zUD44)0S_Gm*7{mFAR=asCq_*yQVjSw9tF5Xt_EdA%tOpbJiBward&P8LkmFi($y9x zxS|~$(M=#Y-ccVZ(c_`TRt*IlCG$PB6!@);wbp@YkHK%WM<2kP_~}$y<)M|}jl?9V z^`cIu}O)O)s5Wwu)lO;vPts&G-OL2ZemWsRuO*w$PJ@}PrQ zOE+ds%;C3a55IAfE`v_tChi8+o#IPWQLkz(*LKuLyXw`F$7(1oOU$~;o6ci9+aQBd zIQ2b)&ZYH&40P!9?|!WG)?$bn;u;qkbP*MT*3I2rr?l4rHZMOex!9mfm>LF3jsdz+ z=4;0BQiCp|%P|8ME>*K*T{b?h(p6=vXLFq^4cfxvFk7Nc^>gaiM!TE45{sL`UuDoX z3${PwpWVH7ZGA_5o!tdQP)BFivRDg*fzcHwDOL=oyfb!|k}=mA z)J+>0)=q;q(gydewmLare&+dYGl2R@*X(Dg3BGkI~~84k&yWr?H|r3=cIA32p?Le<>(`2&o2j z?5AJQlT78O1T8&`K1ra4qCc3p(^pyL-CeQf3Gw610t zIiEBGg3>{RL6{3FKAZr9YA7~+!JyyJZy^S1+gdl&cXTalW9DK8T~(H>R_710HPBR} zmkfH2o_EASk|9#W+Tt5q3wqU{-_vX87>Fn62(CXI>qN2Y_YVd&QjD4Cj|RQTGAs*R zSKraz)~rO;qWZ4JwmQb}PX_&&-WHSt89x8;Dy%Fl#S?3X2pD8W3>h!fXWS;@j^8!t zJtaKjnJd`F;F5nB^iM8vOsILv;^3kW4Dwp~<|6|l*zKo((Z47O1d&1A9wDBm4p% z=q0pL(*IOZYKSrsMl-AkuD`5J*+>?r!|Y;)M{5`Ct9tALbU;~acgv*!hgXKDjCY`; zo?c|Eo?o^amlo&7hB%7% zNglL4{ zMnjw{&f`{XY+aTzBckdBhPY5=*Y~vZd#@ob=6AhweMc8(Y&OKD;xe?0*4PUbEGFB4 zDacQ9!$khU)$+`2Gei$>g|uj0oz)ev^|ruhj$ss>-->+6LR0JEUspr4r6r0t zbczPNFK0Nr46%dF?f_3sTl0qc$vu-ff43oS6nnrAs0eB!S1oUgD(!8U4nf zOmc&u*v+$p!BdNEe0>KrgPmH*h+;Pgji;)d9IcB*TRVYbE$A;h@uPROv@@P~08f5k z1pMI9_i$BK9$Ji(T8z`~I^`NbCv}#009ifFv6dLKrb zDX5_YIQq5{OeJ)S(X_V3I(Zi--Z#Vt;zQ(jFs*H6r0ma=!BcH5(>>zjxaE zNN)dcL;Q!gY0wGvb$;<*@tH^bFWx}u+c|CB*z#ceEj~vztddn|EWR|vLGcwv!>STZ zxoAPza`$Xm@8PD9hTl3#dgq>gLpMh$&MJ{aM(~?qI)7 zE1Y70SKHCn4i=CegI=cJp{usFYtb91VW&V2UtM>5Gib%Oth>kdR=GFX2~W?lYPQj0 zQ#_^jU_%J#J=g)_vB`Sdq~6kN8|!PEj%{n}sEf4%CeLS2Cq$vPiGN)^HPKFV0dx7T;rFFy5R)iPB27?|j4 zyJVnm4&;rQ0)sI$=YWiy~Y7b+k30 zD!e7wAF@-6fs8ZccyTqR3LcD&kjAP8L{wd8+*ppr0uVhg?@T>uPy|As5O;OfZ`u69KvyBpJLP8$tnY zd7>dtl1nfYtOe!fV`m&2hF-b!FbZtq7N;0;xm12`F53?=!d`{YWR zY8FD1En%2?@jhSru+GF>~HG)D(HozQvbV^bC<(aYuMu22SP)I9t zRtUcjC2I}YpzI?s7wFW{#hEcfu2T+22*Mbg<($`S$QBAb0tE|u42$aZe*w{#tE zLbz&&Av-zS4VX#``FrNXIxt|hW&_Aot_&;%o}i9eUOeh00M<<^!kbg%wd-L{d3kU? z?6XrRD#?cBryK^VV|GAsSLy|(#RjN3*=tB;i@4S{ zLr7u2BsUxKQZWOr1!XojtHqye+bS!ND_(Ahabi5r;+2Nn!n)F>jBHk67>eA)Ki&MZ z0Y3sN0d^RPZpOmix?=~PB~Mt{zUmT*=SPR zkfqAeTWg(b_-Yb;^2qFsN+z(G+tyLv(9zc2S{Khm`E0n*2WK+gLo?G*b77JnmDS-Y zHc8p9?6^OHEH)a7={0$uA@7ktgASJchTKmnn2?Tzr!1SuIrkg#0rr8pS9Ov=|b~X-xI=QP~8}bDQ=SiINTSLC+bgptTIWuXKb*zJry@GOD?|A=Wy{G-cwG8e>FCtM(D7R^OqvTkg6Tyl!kicg0-|)ur>3ksP9_1`S|H8G5a4jJ9G=Vx9d#=B#ap zyZxI1@@b4F4Y7u|xL&Y@TBPpC9?Q&eAOF$7z@L!DTri`?;4E(E13|y#244i<6vX6U z%CZwQvG?Q-3`0v{n>7tE79}03a9Nt6r8{Lwry3qDbFkJM1lF%*&~f+?{virtP%Ut+ ze`#BHM{PYw4?1TJrrCI2{4^4S(=bFoNj}{|O7hv3dcxA*JJx z*N2ncG@R*X1J7dM7Y2qCfZdV6sv5W*jof2FQXa*T7B0>x<~XC6D=CFTN6A1Hl1sA*9mi{t>n#VIilJdmmgG5j@673oTqlrcKw_aO@c_f0#M3T$_aFJX^X zAyCoR5olSQK$#YSaJFv~Xc;KW3-elxrgR7C_hgQllezpj=8bVY>VnDxv|=X}#?zW9~pFR0Jyqm-aN`Ukto9n_B- z+A=(9AFTh$ggZMrjM)T4_B3A-Yi3OC7Y8I&r|?l|<%iZ88TCSHNlumOMfaSaC`8 z)T;=t0cH_3ATKUgz1~CQmr{(CX$L7^enCEomN*i$D2zTV(N|HDx_u|5mhrC_oU-no zaLG0vZyA2ofJ@b=QPh$c`B+FVqH`cqz6Tu6g)%&!f^-3--G!LLiy(}9G5U+uC@hg& z;V?@DwPVCs8jzbENNHd*)<7ZynFtoK5uAhJaivtH)6jiq90jSA70Vc+D?pMffy@@@ z>8+|e!)j%BCpgx@hB(iLm;tV-+esb;^FCU$RnHZf1Xniyo*R!4?4|J5S9fkaJvgy3-J17mw3tTtZ1vBz? zk{>y}J1G>--vRQW6oXZ*_S5DSc%KR=82l9|-hx+aiOl4R`W~9AMcjR`3|I@gE4NhZ zAsy*+w(p{lN^QOF1_ksi(=C_vQ2>QVYRh`v!gLqx9r#rrRVfe^!+H$w>Bg3zTO(`kq}Swla;8u}?X={D-2+rfW# z&=2WO)V`Z;Mc&WwzkP`Kc#!U+Cy;s#(H(!r`#%s_!Lt5jtXz=Up5YL^O4ov$;a@`- z#Sf5@fPOw8vaG)lrb+`iRtF?rZJg?c?qDtMrUS5_tQ7OfZpsSyXNiLp6#N?yCqgF- zpg>c$4oQA!{sDCCQ90ms{C|K-$!)!Bc=Zebfx+corCneyab?$#!ZFE>4{r-o%Icj> zUqmTc%qI2%`&6KI31-Y}J;Wx7n`X7ZRu#KnD> zDW>cD%X(9UnUcSE;7QI(@PMVuAA!B}C}!d@@W3xY=_hD3Jqi8$6y(O!uqJ;6_3{k( z;aN!B=O9g=2Om5QS@eRM-?eCsk=gUhDV|kq7a+ZORwY`OtGU$BJ_K$n1XT+mDfeK0 z-RP?ZytE!%GJ#gmO-T9C?>ul~A*A0iuvdA`Q@A_RiF{_ttm9)S;EOOIMz_jaJ(9Za2Xii+Fw)xY0N&k!x|PelgpBbtQrW;er9V+ z3c8(PBWBEQ{Wg{q8`NE(^({|PDQ5N7g%8plE1CBAvlsx7?^+ZtuMh|5XZz?D5P0R0r z%WJ^(bx8d`Kpp%MWPB4U@+}bkPgrXEe}Fzt==vj+g!~VFKZUOUFO={9fY4`9>VVcA#jG$ zv64bUQi0HDGD2(;3*g8kY(Pq{Lgs%KHAN#3WZ}QJ{7I!U6+-3x^clK7xQ9}a90=bh zQXWCN?4?V+}+cE6jt95jVd_Jbg5>!gNDD zPTolwJSvkV-zd|C9gR!$dW7lWiri)mwllCkvFijt0o5%evTjMtg_4K@vB(&TA$&N@ z_R|CrpsB)ukV&BvMJmE#(kP0so+cPno3Ir-2k%=@zDJmfGM2{M>oDJ5kqt%@uu|8d zMhckkC|IV}?&fGf$^k>_c%P{xoePq7iU=al4Xby8GVMKT@#T^)K&EksGCd+i2o8!8 zBIx=Iyeve>VSzmW#`4Eu0c}Pfd>Eihe&rnXVCv*oE>VwBOR)dT=WJ}k|Ck_zVgZqh z>Cc0YG@r6X0TqivD#80?QLF}P$&5G)WK$06^ZM~qnHq{EGtx=(4lqs-x%eO4972o$ z6ie2w1UcAclN&C7l#XUU3vij3wDK6GB!g6p0!pL7kz>G-V-*@X766Cdx#S1J=t~q} zR;+F&@H-$W&Pd1)wc15l5<|OeFoNT3?8aw-o=nm5yj@h9$N1eu#reA^pXoOFCdy{= zae887-kQn-qWE_0c2u`~KB?SPOaQi(R3;`R5w6_9xLnOL<5#Zu!Xg~jQyESQMmu<% zXyc)nd{BRAnWUmbB&nCUZB5Y1ypQHyGVES5#2C=fqR<%t&IZ47&%}Wj91tZ-5;LH@ zYhoIZnyz}V#B8QBj!dVAOuGlbgYIYBjt5e1_casP@}&EjaCwSiLq;%D!MA4C@4)wq z3N-@Y?f9=2*+gM`cS>IVa0t%^Ls*Uni(>;0z<>>qU;;Cm5@_NW%0_dc4Vq2-#Qfel z1l74o#7=b}r#hrab%;FyEMO6PwM}f^S~!r$97gE{lqyb0qI8-AKFtB2=1@8fLvSlf zj}u|LqvahW)iPi4tB1n%AMR zS0M=73TtbU4YvixAhTWeINo_ZTAF`(WKszILGNN;Pe)-dlLxj zn_-Xd0Za)yT2_E=Z@Birperd0_luc#l~|_xyb=^l+?>{c{a=boW{cSk^Blt-S(6i` zlQ(ne0)~P4+%M)NcGi}_UJDX6Ksv@CZ`RRx(L@o^ObbOz($uVQNVWolSPw*j9tIKj z6nMy$?lC>}HbT&glUz&tYse?|b zgHEY~PASld&pNbC3D7}Mt&Pqm;K?hV=`+SgrvT{W+UN|{LU(3~6Ii+=BXlJY+CpQ+ zRY^1TP4uy;0*xdVI`aK{pbDcg9w>-KdzAtfC;bQt81GB>Qiz|+e?;k6#bB?oV6%R) zGGWMA`uAEGFF(MgxF05=GTx!hc!xIQZQ9tVi~}l&_qHjLY(z{MYD8e3fTgu9Oc)Ue zU$h%vjs?UzuNvr{wnNzr*gvFT>n2VE-lOiu&qVz3rePtTQ?&9y`-O7u(4y`LfFf}- zl*26uS^Y7n{1X~2ZiSKhQ-y@3HmmL7jV4pD>r}+K4rg2fr<68ct+s|Jwhgc;Y=%e6 zpMs?4W3PDPtOP~X`HZ*=%kplRhWEhi`xzC9dy~56^DOJUt=P83n4Z|JRONWdoY% zP^z3$2(MAxlLPU=R<=OINZheF1wHeOUC<2maE)bU-D{Bov`v@R$ zq$e&b-BzUssyu-zFWiF}YO`3yF2Ehzs-Q7NYQ)zkPBncz$M!A;whuNhfj&{mlWqDd zt6X_MWo**%!~gi0WikF%OA%wT@HQ1zmw3-hfR8zg!X2w**~V4v4dw4v6!& zaaND@uF}maZRM{41eb!aJQ>1#Rpla8eMZoAKwQE=CL^~LPAfkshS$+S7e5L(cl+*m zZmN~*;fF{Wz> z{GOf=uhVnl5A>q=BQEp2Nq-f8q7TKN>0jb6^o4kbz7l^Ex_B2?P5v%Y#Xm&0cpv+Z z4@5DdQA-fy8Wx|3N#ft4T6~ItsQ-#t;xjQ1F{>wvFU3*>t1cIZ#46ZEYsFJyqj*AG zDm7+9F@o7oToz|TM0Q^ePKM4#9!qbD5ne2=fJo3Wp3&k;q+IZhY7o6{{IAbR-RYzb zIH|{-)C*4PZ721illmf_!tUROjNNZMRp6vboK(a~&2UmMs4c8uD#lZ*D9y&y14X(A z7<({Z=fNlFhV$SEu~tcBALeY1+En_fTFg}PI-mqvGld2~A}>4iG%RO{zk`rXUt#sb zA-KYF^=Zh)VGVl(4X@khGcp4KpAqn-x>MZ)kPylrh)Vcpdwvm+kN~OjK8JTkh^@{> zdkV`FEHqD8OWiN7N=Om4O_GRskUk|zETs^)uZ8$N{BG7h;3$A#5#$ZK@-`2Gp?0P+ zkR*wcENVXkobDaWd=}$mDM%LM#UTl-uOn=BNot=mTzig6iCZ&? z_SX&9J}_K+%M&)V|BHufZw%MoiryL8{?g&vrw-RXI2``4AL7d;X(oKb%|z&%X9Anu zVIcX3gOvNtkodxkJtq_GIrW6~;f7R@G^F@?5ojLwwr=)brs^eG@L%UN5|^rmFz2{Ry2s7oI1QN02T@ z;kbM>LaoP85v~VJkR?c;y8DBmV6AKF>g zVB>m`zHfV3Q*k;D*C@nl7M%-i#r9t=^$6U$1bvIB1I81dKvlz!%idTw{On7Wj`7ee zI!SqBy?D3I8htqTuTb7tKcZNyJ(Q()O3CXRAnQC32R_gctP2v(*$^^JIA`IX7uP#B z=h4811->Wj1=(*Hsxu!OCO*$-fI~#KK83i+kw^((d14q~(*f+LuK~uVYivW{ zT9(Cq-YKBTNbE2|fhr?Vl@h2*4OFECs?q~h8G)+IKvh} zGp7Qc=Z}JB;0HU&ZB&^?&}^+T;Ul>j4&iBt!LiM~2+mq;LoX6iteo2?>$%v>RouEJ zGQu3e@4M~O__|1*nP=w0tmJY&)vt@>a}HkNqPQuMza~#SZ4blFsuBJ_cLSh_ajgjdCuR_j9H|xk6KKQs+q|B zCjbZaxiEIQcOU_)y~-W*DlCAEH|VeO1p`%nyFav^VvZOC`#tEY3A$^7o|>SyCg`gP z`fGxL8te$yE$J;3y(5HKXa@HVT-_(8$2-6B|F`ooh~rtOI`*wi3HsNq4F=X$6(QG+ zT%h1WuGh*{XAb6wU{SDWU9dP>z+o zn!+kQ=s`LIXw?KWgFgIb0r8q(b}+E6rV7{v&2?3Lf(NPwiw2?B-kU4TTr;%ytDTM% z6Eh(lNX~%-JB~8tT*{NjQ=yzkqvU*=A{Wr{@&rh(g;XOKQ8S(!qS(q72UEyoFf~>MKUI?kn6=d8NE*QVv z(knN}G`Ud*WsfYBo8*!5EV)3QEu(V1Y?a@`?|H~MUv8Bb$REfH<@NF+d9&=5x5-2x3NTm=b)TrO9z*TPe^fQ|%gbF$#Mu z1V}))Z>BkLgkaAjb*i;f5LQ+SR1LsIg53pnVt<391iK4(gQkevkn+haT4ty2q7_yO zbD)dcQR){H#arSIq!1;nnc_~Q400o?`YxnW$fu=YJRIR+-~$BrAce?Y%?GcD8#{(f z%?;m(8=B8VM8bVYWl{*vwtYxtfp@+V{YYhlgT6rYgB!8SXCl%I97Q?cmdljuCI>uo zF3$W5QF04r^Z-&Jx=l7Ky_E|tS_02VAt?L2oCW_#A?W%RZp=N1R6h7BM?8d70d8z} zshv|Hq|!B3N*)QX*w4{#5jZy}9!9EIZbak;pA{9#kKs#xgiCp^C1L3f;5a(S8zT>5 zG~)l%zRB7=!D#$~(p>{MiXZXy)ewoS^yL6h&il4%tn@gr=Q~uy=4}8p&;M4CahTtw zrUIHn1>X)d4)r@#RbWe~Fb)=Gf%<=fm=={lC}`>dLLjVDl{d5af8t+qRo?w(eMzSv zB+*WGC6XHlFq_WIC@8}K;{If!+TrsYpFm8_qj6K%eFHY^<1mVgY~{UJOyiS!_W0*; zYzML*!{6g7W}k~h#%7cs#A!jc_(fZ7HBM(mDzb!hvS5hX`d_b$_)0crizg5m74jbt zPgNWIc&0ivdN9m?c4qp3FJsjXIqN2IuqCYr^Z8OpA$4sjB|b84$$Xh2`&!!ihmLZK14a9GZS zYAY6IvFW>c;McuHg1d@)rUowW6Rq6y=4wV@^5*K1>MgCIdX!4=xlZ*MRfN0}l~8#& zPXX%XePX0p&NXfKA){VC2z9&-LcOAo-Z3i@s3U2k9tvfP=W#xBA22V1FYflx2rJ`9 zp%I~x$k>IB_>gCAD8#2!?$m1342?n)PiPF%BkbG}s9&OT%Z3tU91f?pE$O2((3uKM zD?leIQD~}Qj=d_f5oS=NKc$5&b^#!jj$! z0ZS6p&g(4~Or1Ej554MWYkCn8!X$r4Ub&qD@;dA}cFnr zp!}H_BkvW}5=RtrpI9#YMV;I)IwX9j@_uodd_Y_)9~3v@qTFNh=i*oLVeyiDM7$** z6@QnHiBIL@(kFi*4f#uH;{IEqd{R!3Psv&GXbrx*GsytN?~|;a~xCO8v93VHGr_ zs)LPPQPrHU)aqe1F-!*_QUPBeX8;VxDtNfMaJ^ccqi*^DY?H66m<%+9bkgrEM!bGn#^}0mC3@zg7!#yxasJzO# z>d`9%Kdp!=b)RMo2cD@Yw1XUQ#&?_mPr91-4qTUzOe#cEz6^WycNCJZ&_wwvO~nP& znesJSBwwc$c&|nIdih7TnKAzgV|&Gi@X^Y5!Lb}kJw@ODKlx2SM`2F5bx5pVb#z%hvx53QK=hyfV6iBjbKBd*>B-}&K?`7J*a|9lEJ z-^52N_ltksEB^U2@yP-4DK0sn0O_q4_lqyG#i4t}CrrheeY7w+d{60pCgK=W@0V24 zgp(j9pbG25ZzcA$d>6^4Vf?gb;3ezcNLTyP&{W6QyZEllDJa;Ik<#L4JaII9#R)v1BR$1nf}mlqK)Xi>2_ zj4>Z%w&<7cg~|s*qdQY>}8vZy)YImNHdL!A@@0!kx7$7`XOHNEJ6^!SOm*9 z>oY!@mi{<>AZ?j{5q7}zQ3v9K>4Ssuio*}alOhD=A&hPSyDdagK#K9oDYhUwqbP7j zVI7T2jOeEL%^mj!K6ip{Js*=}8K{Sur*?*?Bn^j_=alj91{k1hzh-Q1C~0cg3cslj zmXpR$o|ereO7W#9^Im zBpH3greQ{e&p+8PI;g-iqzJdPf?K&_+?AFhrg0=2+rzNMeL_J(lvA~pP%0`h>sM;9G0J08_E+yId=a!7;H%O zKjJVFq}Vob0#0s_G&@ugNNllEtek9M$l7QKK({3pqI2l`h$jG zd97GTZItk9qeX@`MigpeMOZ5l6SYz?Q-f`U^yw&T&?>|_ZJan)!xwn831WvzjmYt=GCnY?O(G~r@+Ex|GpoepMB0wZJ=^^E(#%M9uQC3sVxxMnaDHnN!9!9)NjPyN~WdI zOYruvoJ)mudoFxK891X3Ba)8wZMt}#ZnIObB5Hs&Z-)4o&Xw@tAUN?h_+U(=vP2;w z#@W)%Mu-ka4RC5EtY5Z>bHqRCB0H68E0>TMZ^wJ*icVoD3pRvrzzJo+ju4yaGiAZ% zLnD8nEZ9PLu>P(TQ!y;=ok|^zWJwF((}+W04-Ge?iqsvc&j_l0jJh_%mKuOU`F}+v zmQ|ZJOZiPqHt+0Gs{oa(e@G0#?jEKFSAnc$c%#|G9j3}4AU?}H8`{kPY?Iut?_Z(C zIOJJ2^w8!&+_Ei!Mt}nXhJghXomy%cX82FNLYD5Khh=niEh<#T*aD2wycL#P?b2xT=i z)mEFKl%z_kaawhd8>c1ZtHvpupB_p<9jl#8Cuz$ls+~fO z+H%^Yt)TO@m5_+5ArVi*wW-soUps?-uC1YGv?!!t4ZW$=LK4=|C)!$NDLe|FET7`V zgT4-7O<57Lhp^6A5^*ViFM`9CzmR-)gX2{GEBVHT2cF-LWC->6`xEcUT%`Q4CEk!D zP-?(JdRgWnF9lZlGcq5kG(;mmC<~C{==?je*If?`>hVFOBBV^*{*9-$U>{)R-Gsbi zlm>CbGM;*djQ4~(r_b5-y8#{1-hFEPm8l{6Qr3Gw%C_i4@a>`SQ zYEQoP^yQ(H4sLF%4&+<)+NwGF86eA4sOgQwUcErJEP-3lqf7wK@`X~YEdKOIzbu1= z!cX_K^xSm5G=u{~Hig(`!Ojk6jZrggH<@ORnby7&!Ioyu=KRfV{SxPon?t^aP>M2D zvdx^l#z;_O8)uAV&pf(PZs>EYAyQ znRX$ard>pJS}(O~7gLXR37w~HrmM9}u{`(w5zu}h3bboPsdkl^tX(G}+77Wm+bPb_t`~LM4dQHVx42BZQG8$9BX((f zMW6N~agTPh*st9pey;skJfroA7qy>=H?&*Dd)iOMSK4iowA*Etc8e_3u9s!n-Ey*a z4~qkY3gp2siNwW$j35v}9h@UoiIk*c#lPi5EJh6nNN*!FflZXt#Orc0QXZKzzFvV<+5RDvwu>AVwT*`{m(av$;0~oA2l8USCc!6aQ z#QQFyoZ(t4@pTBW z;8aU=i&0=Yjyu>Io~GmDM8OAT)ygb6ZNEHfT)&*rFONAOk7KcV@FvR7)seyh%YM0F znx3m`ntJV*ixUk`9@G%RSHLHpor~K2a@l?I)O^iujPo2Q3WEhXqNVKh=zwD8Xln*?NNw?$LI>} zaix*1IPb0TB}iN2OAsk*31Zyxba@7rphj0<8RbJWvlc{b&q3vgEDnY;0yyQu7cDJ< zX%j(Ld|E_Q7VJ>5BG`b_L^xVlqamd>zb?B>&x^P@nWu6+RvnSrhZvswu>JOOSw710 zB0eMwACzZW2VE#+k~7`k*e|1flxG(`&f1ZQ%4&^^rdev#&2$Su+t#rjGjaoVD#i3h zQng50A8zxfnV!Z-x)#aE_1`D!v1QFP^+q$*bTzJtWcAVfT*J(&9D&@=X#B1 z#(2}+xaPFVU^c%unHiPq;9@gHZ-y{U&T8E|@N#daz&;ltUs+}*=dHha;NIR;(VHgF zt#9wZ#kqmRl&<5+(Vn7I?PvT|iO&Hp%knpdFV(ld{PP3e! zZ(8#eX9H0gX9J{cHbANiW|W3)+Yz!G>r{t3G+S;!$_2N-569YUtW84jKo3$Lm|h>q zO-OkWXaBZ53n?FM#(~e-@*K#Ts}V%a%oLzq_@n`wd!D;q|)hrco)w{DuW)SHSz+aGI3visXC<3qE`_nav?CuMx4GCW@aje-9_2n zW3gM~Pt6}BU(&B3IN!=C7ArjR%mLi7O;_@ag%18953g$1278YMRWW6MeFbj98$d&f z(VtV+0MP$NaFDyR#4)zK=>^HsvBI{O9FQ0B21PwQrh7LPDlgz9|@m0Io9B7XKKdEj05uZaXwtMPFQEGg`AkdwM5k{059O`3@{aM_j; zGi`Ij0eJ;>5e692Om)Nh!p_1dGXwDgFQO7nKML*)c@x_PM*dDJva0Ro zi#T#mmb~TMov`!+sE4Z=>eC0@oGUfJfTvw;O||$0!dQH_Nd7qCrBc3S?SHuT2cNF? zIgQl5fLQpFCTIs~f%X+d!T>en#@%{7*(=-O5Vk|!igQ^3qBP*V)jH2WXgs0$BvF)$ z@!l3civR(w%V~!Cm{I~0%MYyX3+eclla81j-J_<_x)fzNy&1%}C*TplnQr_>P~yWM zxGkf;FJ!?}G1a;ijBmvx$3OfbG-S%coT0(SG?%yw3CS#Bbt>mjL+!99x0If=1TMFdF0je$5M-+(Ho>-7rSuaBb_^$GNvUPhLjTqy^qD@DzS64{ z<61{B$u0@}H`OpV?1;dB!c4K;AsiR7mYag$Ek1;IL$3HG?-{)KP|p(iNBJi5EThPi z@+B<@eo4^`zXtAq3rA+wWt2Jq;0v+X>B>JpDF3o@zx-<$m+|`L-&Q18$XZ*7pVwzl zmTukCx4r|AbQK#he0<>$uyEMME16UOK#F_(r+nY;aT1{i zS9dVTcNap_c&lc}ac8K?t0ReQU!7 z!#4cl+Z!$%w&9oG-q5g1>OuLHJ;qshvS50DwjGPOu3>E3SZj%GT;F&=s8S*94xl9e+EZS`sUUk331o9aMb{SSE^{BRL1G^0{QHP?$2WE0-uPD<+ERLNT2zxZpUp zeL;7kBQ@Nv;GCJxri`Xh$3PXl3e!$BVu5Ju zP9+LG`P7uTiLUh8JdMg_wW^wh>1^tvp6<1&{PM)wu9QM>VJ?~IT9L@7rChFb6g$%e zg~&+5j$7ba3byn*h31icjx*j1JAz_!or1k(;tDUqXmFukq45(}Y`{dLg1aMCyd>RT z?9?L{c)0pz0}+f;sM=uFs|BKOjDfK-z{3IaQ|XS*qVzq zzbwe$(+!+4G&s>aG+1nm8JLFY3cf;OeQKyhVN^?7+lfq4m|@^7%;ZR#G!kWFPqfOm zHrW$}a}1m-d*qV!l(9D3z#Pm~@LriNp4ZitURNMk#+*po0!!I>2Ih$+)l@b&l`ZCT z>2^Dum?bkUFmQo128g+2PgkOtT9oT%r-Gf0kRFQ+TqHeYKNqFi(}}DdZmVgjfzQCH z5FQ#lJD*C-%O{e8Z(^o<@j8)+Tsylcn4jb`##A@=O+mFO7$OdDq0v2JQ)p>dx;tH5 zo*U6aT}nUG$;r&3#5x@@E?llq|M77rd%ng%0&A^BElKyLx@cmkW#YoC66+IFx)Rxr zDT~)$l}Z-Ra)BVSt(qr;t~Agg$PUmG^T{f?j(nnfS^Aok06cBrDjf{jIq7Ucx_24q zuHb5@JJBh5Iw){pZZZ~kFRn*hpWZ-72 zxItR`4BToJJEgeOz!$_gJNHe>w;TAPRkjS>ZD5Z!er-Biao0Wr`$dI!op^t4t|zN$ z;!Xp1NmtuCJ<*p9+-(*0ME4l@iWD6Q!c1nm*T7eG1?g2!eV>8*rJ|}wY6QLy82A@F zNNO0u=(1vaTWUS2h%wA~Bj%)_?GT_Adb&9SpHl^tbe?#14tp?exKm{z(aUjksjZt2 zl@@^y#oDR@XJTkprL*m+UKhS0hCXfJJF;9iu{^Zc?;7}?R64pD;?nE;2AlHRh|a&Os(FG>mxWDEw#H4ViiA&1EYF4mHoiU4Nr7hdINXo0U_&*ZUfp1j-hD7w zuD3d}C{6wnT3|PBfFyM z-D3F?@rYfoD$fiD2-0PB^hk(*VKmV%W0@39K_kv)X%3okKKosOQ9LO}6X;{HlI~xN zlaOK7lgG&<@A25ibY}-9U^iOuWlSVRr(KN&Alt9brO`j19bxh$Q}9xGYrX7ApzG=(I4DOPcg)zot# ze1kNh$_3>@nE#g%HnY?qr}Q3ECrkumkT7tMsJGi$>#M7A*VNPq5SaB?(5O0{0BIsb zE~ihg_zsdYw0Bbj=oBw!29TBa)dMI-qmQBUN$&tQ_~RabyqeY5&hUk>CGHR5`gnlN zTVp|ctgauM!a|aB%Fs3(b7{_udJY>5Tg+2ljkP%YjlzmFiWvUi69TH3tU2UJ9yGozfF; zW!){T+Y~kZ1K1HY0|U5?by+`QJcdG1gnC)f4p|`Iw{d~}m?Sm)&vMdgsm{u9w(`Ho z$#q>(lu3oOckt1mTp?Ar6L`5estwy*y~us2~CgbOvXKCHM=`A$#}IH zv<7r!ZsksdIJWZu7s8jLJtoBs;n3>Ijx~!*4XV_r!i}t|ICUMjM;tANSDZRi*1mT; zA9rN<_!1#FvW)Od;6Q5_G7H2X%V+_M-46*49 ztvF6&`!t5^Oq@d&JQs^Fi$O3O?F^i5%w@{d#w6%GCNJ}FCqw#f%*TCL!0=kgK%K)A z{9L?-#dr%#@IIFERQU`KiDjw^%T+BdR*hJp#*^_+#Y!~`tJP8_p(}8OT8lMm9TI9I zGtSQ;scvIVxgRO@ZCt6IM~8X|o$4n@tKZ-%^)52%ujsNdMYK7v))vMUwu#8wW};x5 zkD_e}dTf`GPJ|@hCkZjSDkMS4qGzcS8X!qflHhl6T@&z-Py=V~uq1eF7js?hq=w11 zR(zeU9-L-76^~G=#x&amJc@5n`%K$Oc#L&^%(jif0HpxE(?{YOMQXeNH{o$gb@XKl zPf!X`=W6^br7&~i75FBldbL#f@GXuFa-9)8DgD%HCNiQWtggTdT1!1Wb+6XaK!4q- zwU}xPNmH~mYQlO0wS(L|zePfJvXu)-yWNFvlZ!}3A7PAAs%DV<8>sYQ>u?vg0)t#* zZKe5G6}r9ZDC+)(bL#2_38x`FBRlenG;+2if)#ZmW}`*&lli=;t8_SCjqV{ca$9-x zvj`MNxiC4xS4+G+4v1=JZpJT9W z<%XBI)i-Dej{tn^>wa zqB|@OBO?lzY>P$uQD}6@G2se(N*-av%Nm_wcWYk=FC^n$(p#slO&uW674gJ#Gyv=lPy!;$Umn~c|+X1K``&5~P{A}TDqQz5?_N=>TN zth9|4@>|OGgYut=H<@zc?cJU;n>ykVGx7uu$45uaU) zqcfz**P>=9gdbb=!AwU8KQ)^&9Fwe!c}2=inGTMKWT@knjsd(8HS0}33vUt?Z$(XW zsPHQed;2+z7g)W`|F;g|zokpLD9z&z;r9dh)i9PeV+{B3MiR>BNlG_k9ByHT)5qlC zR+8)vM(<9h{$D^Ew=pZZoe29P^P^qtv72|rJve~9q}_d_ggY7dJD3;Uf}b;MdWWR^ zKWzO7_oxsdTj+qmz`c?hoN?q|5)R}*O)x&PO58%u28#s2p*^*DrQ__g4>JSM+~pW<2kf%gST z0`^iXom)XUr83GXl~GQqjB@_?o(NdQ}CKt_BCmU+$vi3#ywf z*UpghVr$H>$9!`05U5s;6mQta5|0$gcelm-a`W)Wk>Uwgm#VFshb!!Do$f5zRoJ}` zfd*&wu6>B`pzcR~gR{Z0>kjz&uG)PUoUAQ*Rmrcye$gkY_07XjC7;q0SI&t|(a81? zKFW;Z)j$y(j68jCUycUKKVa7P0`t8WS$CN9^AazoFY|WyLvG+J%zIyDF8dmcGp&PDyO~H|1b%?4T9TC?};V&eej4C^>1>6B>#x+SboQe-!~ZmBcO?r-wF5D2!sx za51mZl1WyRZmo<{zN=dqr;nqo?9#8n_AoqyyjNE)r^;rN#L&9S)>YC)Rj%foZDZXV?7MzqR=BU&|h%X#WZ-PXk1E`C`rWAs~elzfXj!mZ!S z4;s;sat|o)BNc!;rT$6h441~G+a9xc(`OgxZP8$?K+~f!L}+>iO0k-LT-fLqX!3gs zK}Oy@lf3C^onGBg-6y>HX}&dajtU!%uBYzSv-@OrCP+rfPz}{fnI@HLRJcY|@?X&T zAfYLUl|ZPVb7Q0H2~|TSRb4S3Uk-kpX^K*f&(Yp0uhzA`Y#&wgJCCkzmBKKJv-CFj z714#adEfXg@%1~>(K}24f6vb+?{a(Ja=_BU*G;#V# zS1UDf)-ZOXs-BqlX;j|>FRwD=;X8)q%E!DW$w4MtLzf}Vq0-953P#DHgP8EK?KZ1j ZKIoiaxW@1&PyTmI=jT)<++$PbKL7>DG%5f9 literal 0 HcmV?d00001 diff --git a/bin/ij/process/FloodFiller.class b/bin/ij/process/FloodFiller.class new file mode 100644 index 0000000000000000000000000000000000000000..5ad7c69dfe4086bb09cb6a0b6336d4d07c4151f0 GIT binary patch literal 4888 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 WB7U87R-uA$Q}X`}4|BZ-kNg+yo2KCa literal 0 HcmV?d00001 diff --git a/bin/ij/process/ImageConverter.class b/bin/ij/process/ImageConverter.class new file mode 100644 index 0000000000000000000000000000000000000000..e0e25bfa78f3611805e4c6d55023f774bef752ab GIT binary patch literal 9291 zcmb_i3wTu3wf}oOuvZzt*pT%$(V0 z@3q(Z*INHtXHUNI;R`PTSRgN1$PxrXn=AXGkq##ot8C~Ab~$yC-bJR_e0!fGkOo0sXQZVg7!LJz@$*JO z?&1!92*sChaY^Yq&aI1dI(ZO`wULK>zBh(?oz?w4?M}2c*dC^wg2o6%tP4g%`q>-I zS?q}!S90w3rR(yMjfpl2V5{5{ZTOLsj{piSOcB_b%G#KUX#%UuiMJY^sZ@imI?0BG zTs5UT6cZHc9wTEzd&hLRK($6zM1$KG>0UE!oT8F#N^Xe5oB?oZ2jYT+vD#e_5YEQwbC5`DIX^BVa_L`5;QC$^4jg7@PlPD6Z zI)+MnG#C#>dIeKl1AClctUu~hX56E70W7s~7M5`xb;!6!XPJl?X_;;)sIyTITaXiV zIwH|dMQXW?6any|*g zYO28qX^cd+oY!X})W%wzOYl3QPB89hT*Mf}OexB3V#tR*oDE*Vt?|l`W?W_ZzVTp- zjaHn;P1VL@mbyTXxX^`ly^RfO<>>ytPHM2Mx3fMP+-iYIn3?ECYRZL*>-jc5g$tOf z^oNNTW*|M0Qq+u5yEa^;!5S2d_uQ`9xOYp}<)H%%_)f|pFl(dE#W5Qfn*{HVIWB?> z4?<|6TQE5zzcdOnG^)a88(S3m{7}rOYjCeZkBwfnFfU~M;f?m$xWstZ9TjAS$Wg~9 zaB2r8@F;h#qaIaO>touCHp$r(blpi_N@a;UvudG*?Skp)aBm2QovvVbS+uLa$LWoq zy{*IP(>Tt><$_7)^~U=9`XVIL&SFu*a!vHo@;iH5u|XEk14I zRz+DUt3Juv`M4c-Son-!T-wyyNF?k8du`l_yGWjw2E+Z%nu}?r6321V7%Dg69vk-> z7sfiGp}x3gfQ9?X>ncU)1)8!ktbu7rO;hO)#bSaW0b}rC3Qm?!yj!dC10Rv4`m# z?CW!S8E|tlk#tK{O{QMQY*dqrhZ!TK)bc(&s-pG{li=~#UzeaOKWAe<9%rz}Bkp{` zgk;i~lIclNEIx1JNj$}HcKL_gMKYL_skAqFW9hnFe33*!!`GN#Dw(d;)GN#yL9yu- z#kqKzK}!o%_yHTwYB<@h@T4;SSezulR2{)T!()5?Jf(pl8_(kf=DvnQDApGaZf|Do zcbY<^W|CrkJ`RJ8!;93N$vXnw+W0b#Fg=ym*J+HfoR-wZ zL;f{<-NNevKX+AhbGbc+tPAfq(}w;A@)yGoy?|ERCmw5h^B*?2?CW;!L7ha*AF zZ$ij~(84r=)}({%9<@>G^-6cJQ>ov)qE@wk)5dqTx{so1wV`-DGre2Y{tp}fu4-4| zzx0ZmkIP(Y7$p}!;IL7*xTilBFK%~=J+VKu@!u*|nRk7tQ)LG5BO5=~L4|Rp6YA<_ z&1u6=ZTt*BXV7z$t9iR*Q?33hU1#AJTBSTl%%1sp7yqL?^S^@Pl7@yfnTLi~9Syl( z(eP%6h4Eed+Qx72TdEZ7>}&~#I_SgMBSdOOR3#IRa(++8t%y1fIq(nkeJw*kE&7v< z5AbKoWLO*Z1T#mHd5j4)ZdLn>jSumWTM+9*BwCXM#DP{dm?VPJyWrJ-;Qc5BxbPTT>$JR;)ihrwmiMw|j$1rA^xo#8P zAuIUH_pFW{Ek1_jMl9A8!qn{8VjAB}pVe40#*!0Rt^exc%*b5X!pLlsDYg{KR5vm? zaNzcJWu$qX_Qq3rV>6$~mZeZh|O z^FqE%$3(u!EL&zv30=(?ZH;iXc0HPSMirB;rqxV;WPnJSE#*?-hWz?aZ)aqy@}rs9 z)gN-#y5lwZGFMKugqc=Asl)EtQYEb9nx*;(;dYhB^fGHBq29O=vcQ(pWFfu4Fj(zu z%UF=v-MI7JI9p`P8Cr~evF^xLRlCNP#b%&8)Tx0kOKe$+@doO??aSgU+U@;uCzi)s z%WSFDX2|O3R#J`W^m<#)*7g35a3sb!%=5Uzi85<5+vpJxr$c>w7^Ah>;YbG?*>uRc zI`hWX5Ls=43L1tJLv1y+OvRa5!KvQ<^qmQW)(Ee1gTjy}EFZcB%@uA|(p+eDRW;@EPrHmb@vsljerLJD`@ zmQXnC>Gzs!u_dhRVTEFR5bb9*3P`Ul5mVf0NNXq_HqO1omZ%n5m1zbk$zY&Qdg+qN zJo0e6lnu@NIUfgEn{2aXd(t|tB!^HHX-U@+la@xHCEPq~({i=@YA%zVRG?(ja{Z_D zmF!oUbe;m&@tHBH!2Ee5IgJ^6gDp3jj7_dn0B%amLw+lN)TN8s_YFCse3QDUi%(-$WRbvO;-p=m7xX(m@F1p*6dlb%mT|{rh z`@-V)3hz-5Ns=4DYbK!ehxpic3Sj?2(5p*?Y{vMa%YB-0899 z5qVTG?T<#;s<*O^G%*JBL7$DyDoY-t>`9sOQ;ISD^-(<49V^=G9u?RjTb>v8ak){aD@5x_CKEENZCyARi?O;AC+`VW zpQgOBn%@%Xk9IiALwXJ~-k4LDJQSL%e1Vhs zK8T|9`U3cL24BZ>+X<92(a4(Qu0$~v)t>^RBpFN(S6umWD_5xrZ>2k?MI+yfe0wlT-~D?rTgRpR0LCP6Y61)Oqy8pPoxl>RTe}}BrpVfh ziNogZ$5L8x_5h~vt>Mh_sY7T?pxIr%Za+>wgf^o>0-FYLp>92Vs{|^V4xoJi#T753 zYXE@?O4^MAzJ+#U6yL)9jVMal2IY;I#OnavF@>*%e4R!diZBl+VHq)6LtI)oasg(b z4>S4Lc?zyzUwa*9V;4$r8%psI%J38_@EqpgpLsC#ZB()ut}jXKouN%~AQVhPV|mh$lBEL=!bdr?dK>oTHohlfuijdk%+ zEJneXR~$Hs@rpr~o=|xhJZy3cSa|?94xoMryM}OE;}Gs1z)6R2Ut7VzARb^0Jy@_e zfkzT}%r}H5_M_0;`$F>Gm)v_pcqVPm6qB#Z`MQF+xsvW^pfk_mr=|>bS9rjgI# zAhmMU%{q!o3%7rO6FJ1EHCJ7!V}@C@XOD5wFqbiRG!I5}S#@K%>y5z#4mEh!FZ22g zpF6IJIrW&t{F>e*auq9qygsiK<{%Z;t)(y7gKY74Dx zrBlyiQmmsZ*AtHoylO)yHqx1!5aIFIHm1l`bnmBe5$;Birv~jt$^sfs(>=uIp@%8* zF9c=@=lm=Fjeb3c(!b3sA7#uo?$0rJ+Q`l;{t)#R4rga`1)n2bK=HWe|5dJG-#KBj z2kMAn{#FtQk1!JW&yNY{S+p2Dmb1~t2qvX+x%2yX1@5e(jy=_otdXy}uG|6h?Eh(+`1$?g}kNG5XR?iud z`4tH{-Mv>`;l7)U%|0k+xgYD&-dw^)0C<{~K0`|numO3Nt;Ta~GY+zqNMJq&Sx$#o zMxSTte1SFT5Whz`%$R)CvCDKa9dKRVC@Vm!Vv~DiH z06Rw-8Lt}c0b~_?M1J#Yn9Fnv0ZW=Jnaf)uYf`Yh!s$v~FB|nSV{xJXVT>)zc@+5^ zVfQgcyN&U(Hce(}l5$OMR+Na)c}HYowLjqVdzU$d{(Tr1@R3o>Eo(pWT_UsG&2}mW zPMtO=ZAx3pYym4F=etrb8~_IsqW+qs7r}&dy0^N9Wb=T_r|v*cLi*CUuVXt5e1*7w zl_-3TP1q}J;a=tUf3Fei*NOAjS^2(6oWI3t@P?^daaOLYSha3pwYr-%Yk)QBF?@$H z_$EE~T^zw%8GQA!=Z{=GFUpNV`S*A%wE(wzEOjQXGr8yEj@u1i`MGbe;j0{eW0Y<9 z%Hsa5-qls8HF@juRT*-Q@*yh!BSwcsFY<@<`6MYKjlPeZY=j))>Q5}Y;}3kQ(h19_Cmia;+=)bMngR|(LXUqeP#u$iaDAZ zTXmMY?r(Nct?AIe$3F1;?9zY0uKaD5;UD5uo(Wgu9d?jEVsH6l!u1n29Y1AR`58Ou zpR?$^%L4NYd( zm5jwt0QU|{^nV)eYm#uY3TE>kP;*>#U#rnQjLWZYa%tLqMrD|iz;8-ZMUi&zJV75N z*=&t`C=x$rOAd+DLbc=?ql?|`QZY8oqu|+?;8C#BE!)^h%)(o^$UOT?E31w6HoK_j zu)d+t{|Nm4#f9U9_0Qt#;t8^ANNyc3cMQp0u3f`O78Ode3AtBa29C&s)n01u9FotT zo^wd{wFTHE4$9}0N(&S6M0K7Dl0cr<5hvuSwB4&%kRYS^M;~K&z%!OCa{^|{c$BeO zI#niO5sf)ZPDBG6q16(=2AP5j*)ViU5iXIFNN2^kR!+t(GTmTwzNsO*Jd9|xe96$_ zBD8q4coxp_Xt9Bfqc+ppyv>)V)24Prm^6 eB+WE;03Q!Go=V9u$22m>tkvgok;8gVUi^RPidfVD literal 0 HcmV?d00001 diff --git a/bin/ij/process/ImageProcessor.class b/bin/ij/process/ImageProcessor.class new file mode 100644 index 0000000000000000000000000000000000000000..5e613ce67dbb14684af92b6baa8c43f09559fa2e GIT binary patch literal 57040 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�Iv(CKs&lxLH&N*YQ77HeB-L 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/MedianCut.class b/bin/ij/process/MedianCut.class new file mode 100644 index 0000000000000000000000000000000000000000..fe9dcd7103226314690e29a9ef8f3b7df100aa76 GIT binary patch literal 7588 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/PolygonFiller.class b/bin/ij/process/PolygonFiller.class new file mode 100644 index 0000000000000000000000000000000000000000..7934ad2a3b65852450697ff0519b0b29a465a007 GIT binary patch literal 6889 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/ShortBlitter.class b/bin/ij/process/ShortBlitter.class new file mode 100644 index 0000000000000000000000000000000000000000..41af3d34b832f23f9f44b43f3330d08db8385f7f GIT binary patch literal 4240 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/ShortStatistics.class b/bin/ij/process/ShortStatistics.class new file mode 100644 index 0000000000000000000000000000000000000000..43eb1ee5d7bea657bd38b0011ec470881b558c77 GIT binary patch literal 7039 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 T08?FRTBw3`QyV1}YW80NP2AE2 literal 0 HcmV?d00001 diff --git a/bin/ij/process/StackConverter.class b/bin/ij/process/StackConverter.class new file mode 100644 index 0000000000000000000000000000000000000000..534f330e2e1038dd971e93a43188d1a721e96fa6 GIT binary patch literal 9362 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/StackStatistics.class b/bin/ij/process/StackStatistics.class new file mode 100644 index 0000000000000000000000000000000000000000..1b45fda0464b7bad7f33bd17ff4ab2a2426cee70 GIT binary patch literal 9993 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 literal 0 HcmV?d00001 diff --git a/bin/ij/process/TypeConverter.class b/bin/ij/process/TypeConverter.class new file mode 100644 index 0000000000000000000000000000000000000000..e27534fa4dfade7114e49759c20ed40c70d268e9 GIT binary patch literal 6492 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 literal 0 HcmV?d00001 diff --git a/bin/ij/text/TextCanvas.class b/bin/ij/text/TextCanvas.class new file mode 100644 index 0000000000000000000000000000000000000000..16c8955e69dfa53a6328fa4cf52bbf5926c64bb3 GIT binary patch literal 6266 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 literal 0 HcmV?d00001 diff --git a/bin/ij/text/TextWindow.class b/bin/ij/text/TextWindow.class new file mode 100644 index 0000000000000000000000000000000000000000..02b4fba2dbdecec6744f1b4598a42d472f60791a GIT binary patch literal 12324 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 F{9k#owv_+? literal 0 HcmV?d00001 diff --git a/bin/ij/util/FloatArray.class b/bin/ij/util/FloatArray.class new file mode 100644 index 0000000000000000000000000000000000000000..cd3de35f9f1eafa4363e126ea2d0ef559bc31413 GIT binary patch literal 2276 zcma)7-%}H36g^*lErbX(im@hYRFwQetYU3JZ2^tO1`tDOkr&ovg|$gGlWeH8|3zQx zi!*)fblRB$PIdIXe^lFZcb847edsW=`|X$e-E;1_=YIV8*B@^HOk+bsKp}2Eo$A_V zeX3ZubbGGd)^|086h@!wJNi^zZ*EO3KYnUdZG}+B{LWBNB`#~att$jJiVBe%X4AB9 zDFidwHU6|<)r=?IsCVQyqfvOiKbo5oVN@z`ju>W}M86fav@K^&8;Q4Plx&h;pk>UCpFug|r&x(%ag-+5LwTDEC5HJngL^x`UZ+iY&l zcg=duXb<3|!YS`)x>tXCL2v0*)83s)N0C5E5Kk$LWyh(j4l(}N-)o&K{ zmM0*)P?8gqF@zCO2v_Td-d50T%jvE(B)d_J;tHlUT>V$4E4v-rXvA<0A1e&%Vuq^K z+EvK)*Y5gZw$xYQ;g-|spw*>Yf?I}ohDsDQ%qpDkiC1dYjAzSTd-=({)os=~e!B*6 zQ=z{TPGp+-($wj$o84ImR%2 zfH>chV%SUZJpm*(lRLoCs66^5~50FgmEW^h1*5<-*-d5_2x-?A6Z{f@%VTm&2x zISHKXt0L@Iamg2An$Z#$$p!aNcvpurgh}^hi1-=U3!W1xE1nJsfqxw-^EDC72j%M} zKFa^<6B>3pGfputu;`0@B`3VJ8uzZVTT&10=m9i5fPC^t3?(J+3lV$975hfN*tgya zdh+rw1a-A``&r=KG1&*0_chMrGhgEn0p)SYzhg*ND= LQ&{J*03Q7Vnk9JN literal 0 HcmV?d00001 diff --git a/bin/ij/util/FontUtil.class b/bin/ij/util/FontUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..61c9f6c3a83c245e3652937fbbc4a01746f4d047 GIT binary patch literal 1801 zcmZ`(QBxaL6#j15&2G{J=t3+3(o#xk*qTOL#1<+o#-@#iV7ru>R-IYGLKj2QNfv5- zc6{lBPtLUJi%&j^FSMg$e}M1)2WR{Xj`-c1&`803*uD4MbH4L^=bXF0|MSZW0F(IC zKuDn1U(ap^el?q~*Mi$z7|>{4cemVZ)vc{%7naw(N+1xv?AQEYR-h|Au_U17>MLFp zU5Lb>BP`HU^lM&eb7R?SEV|28PrxeHD{ghkZTNEEHfq6rzbVgN%{wDtta*W?A#gff z>@aYGhF@EoDdZT3|eVh(n?9YL%qiGclwwL9Q!a!{72+MYhrh zDk{*uN>Z(gl-v!~#RHuX(3`;{*$^K;bDWsgk{VB*<5X#E?D1FMJVhikwuA9fW}A^+D%){|7H5WTUezAnwO)+F!a4Nu z=hd%ROtvz?##zQOCtgQJF`J@amb_y;djoG0Gezd-ae-1N=@~)jZ;Tu0jWZc z1dQ-FH#{q*HM=}*zL+w)rounMPMV4E`DtCMO6o%?s*)Mon-!NG2vgVw!W?gd1x|MUvVe5*sZClrrYr?j!@5K~U%t^Bqb4|uvmoXp6 zn0cpcSPL@n=AOT!{P%L)mi{-T{~hUHcD6MIWILT|w&o(ECgCi5oM#7LH?=s^!1>Z+!U)6z~|E*JgM4^^r}KYYZ?={VZyj1FUZTOkn- z5)ru(5%m>sOhTm56HyZFQ%zkSx(zx~_4wRYy!_ZKq& zHsMGR0S(28;n-+4k&1Qh-s@(Ef-p2J9CrV4V<~rJFt%@C*o$X1Xgi2DqJ?NB+KHV+ z2eFIj)KFL-PbEeY*+vbzQ`N7*XiXpUMAPH%R`U6&bO;)369o`r_RNEaBKPryq#`DQ zk{Y_!*Z8LLoTOHmC?wVN_Ttf_SCNy{DicCdkM4V~KiB>zsnsSzlFGh|P2Su8K~j#1 zu%y;^zkGW3fWH9M3zP3gLR*K<3y@G{5w-D#_8 z3$s*{iDopAK7A@98r#qO`>F2AHqlH=5z#%GTmT$ zR?NQdQ$sX8za__Q(cF^XNapvL=vAe>@sqS@Tti^|PYnxtvTi)N*F8DcO}HmLnu&Wm z6SCi;{5`0ZMNz@?>fA3xKj<50h0-h)>5z)FMMZj{A`MWHs;elClX=va(cr8Y7WZAq zw=o3x1h&{%lda2bzuLs;@iIX*6~^Hg?>+iO55np=%%F)I{C zj2uM77@Opgb)`}@)c{JUlT9s_z`;`f50s*VTE(#p=Q+Pj?h49O+pCoMchIL=EWi!) za~9xtixLQ$d64@pLcupUdeIm3>EB@LEjq>L70)Y#S}v0=8_{S|9wl#BUfuFv)HUYH KXgH)+9sU7S<;*7l literal 0 HcmV?d00001 diff --git a/bin/ij/util/Java2.class b/bin/ij/util/Java2.class new file mode 100644 index 0000000000000000000000000000000000000000..311830e98aa45fdd6caa019cbf09412045cf05e9 GIT binary patch literal 2372 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 EFCcLx{Qv*} literal 0 HcmV?d00001 diff --git a/bin/ij/util/ThreadUtil$1.class b/bin/ij/util/ThreadUtil$1.class new file mode 100644 index 0000000000000000000000000000000000000000..a6698f61da7902839de54cdebe43c24ca094dd35 GIT binary patch literal 1099 zcmah{+l~@J5IxOR*ijG>T~}RH)PdbdOg6q?j0W8Vf|@9c%04wiyEGYk%uLTF{*w=y zXiR+Y1Nl=IKX}u35K$qHbX7lW`E)fK0cyt?S2+1 zhT@c;@}|qxN%Q4sDjc0fhG8f2MhM4KPU!nW>E>amLtilDPq|z3MG(50q10Qg&|=6u zluGJH3>WS7aDBj#YI|dm1H)MhS)9ouiGqb3iVR8HBoum5i5KB)B>X-fxpZr(=Q-RR za9^5p>`dv23>YfCO${0%4p)wF=|X#09}=G#|JYGl_h`1}pks?GdBZH@& zGV;InLf;XG(x_7SJJEY)5afBL9M=n|t>;2dym1zHNxXG^EY#6puma6}{X~tso>UBv z?c>et+T3-G@U8kk66UXiG`BiR3BT>~AP@nnG~^`xX|Q6+%$lBMhW;G#M6*OCuJC}YsgOv1GV2oZO@yq>qcPHC zT*76-Fl^&WT%#GCfpp_5tj{ZD-YyYHF9>X7CkD64rUopvwW`JO24oVq%q^(lHrW*Jz{Vc!%xuj4^!)*j Ch}3)l literal 0 HcmV?d00001 diff --git a/bin/ij/util/ThreadUtil.class b/bin/ij/util/ThreadUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..37924d012553ef54c9d662dff204045906958828 GIT binary patch literal 3725 zcma)8ZCe!A8NSc#F0;Fg50mL9;vvfg}7sa%xv&#eqml--c8?{NA zG!kRp+PtSp+cYn2Y16b-(*l+&_Ip3{cl1m9sh_-R>3wE~SK(^+x|lPZbDrn9pZmF= zbLOxA`STqBU&o(RC<3MCSYyF8bB+C@j-d|^@Kr@v;DIsyDZMeLTO*C9GGj*8N(LT zuyY-Tg8eNj5(0bI^x1dC%8oj=Wf$_N3r4|+V<&1f>_$|esMVt~jyU$psRsp8b*WU> z|A@Bs^6{LVy=Yn^UX+&l!59SUG(=D&P?9z+qo**QF`Rx$6NQkrvwCh&cTD-eHyK9= zH7uw*YLdOp>HAg1`fGVtcieVsxXU&zfr`4veKcNeeEl(Y90zeoMbiom`l}jc`;b6! z-f(*z({@bv3S-u#e4~do9FfdKyfD&DvxXKq6CPtg6-NcO-KUz)%UNT>HEk=7RveQ| zJ}R)KZY47NlFXFgR2&!BvE(FWxrXBuCR}59(GiR9cM6#(TO&oToBjp?krGf$a++8c za4n~#Z$qD}XD@c^6CUF#POu5GY+Bdw*=ctioqSY~k2RvkS<)U2r=;d%Y>}Q!$3%e$ z6)WSl$vP%|8or6Nz@rPrTHWa#dd};fmW80JH=HcE1;=P9#vsnAIJ;i|Ecnqdg!3#J z6_%rtR`job60v>1g(p;eo291CQe7IpBa2ots$0W3W3dh@bb+ch8_UO}BFhN(OLfn| zI5OaQvKvoIZkClMKp+JtFe(MtFW<&ATm<2&QfTcG_^$Q%3h0nhVh@xpNof%`>m!qw zier5eL19+Yy1*BO5trM~82Lg@p3?WDrOc?X@?{H)v1nc71eWTPUZogw$2u_s>?$VY zvGY9*PvaRn$QLr>CPTLcOsxxboy#I{OPHpLOU1Z==QaF5svtz6%Kxuwn8b?=mDR1R zL3+b==j+d_cuAmgiNQYC;Td+kVCIGmCyJNZ?wyWfI|*3Bwu|*uPw93ow>dZAE@9QGCemZH`M$hHM_0DD_TyMr|hEg zc#JQ4Gnl+!xdq#8yAOA@hL$ukM#hX&?x?}9k9eP5aI(fpQ(j9Yei;V%33EW=;DwX2 zEW_!@>G`~o$1XN%h*vrDkT)COVu(WJpPT{nmQNrthxpJev}u%CX zn!%ngQG1hn6x8!sLtEew!n~K_cnBH}qXZA5l;^@$&hNw{NP4?vR9@Rbq(+W{^O58X z8kXD@d6Qw!MDsUbANF%&3?QX25o=2NoQ4b*@Skwler|B0hmFA-$oiXG6#)2N|8Vr9^}}F za%NP)OLB_`Fv+Y50v$NPQDMHFI7zSDxz~R?M7ZC;W1LHIMjfg0TN9QFED!SyO zJ;qJEV1nGvag@20Z2J)EEV^5^eT9SRh8vvnQI>j9tN5&@mmS2K@X%EFX6Pjba8~hf z`H@2qQ7_8rAj)2j@)rrs;*9j|SJS6|kLOQ@C0>pf#uaH$&81t&6E5Q z*qES?DE8vJa2TPS81ryRf-HA|a|-PhJcUb)+C(e)=2KaL&^+NNDlV&BWC6#+cPWsV zyaE*=oCsDs&An-Sxt>z>6=tUwG`<;mzA}GHelo;A}J=8N{$I) zk3Vqv>D z3*LDOUf_vzm8&mu<{F83iN(FnljLQRah>PPE3|$U$M70`y^bvR=kNwg`X*_43(w#k zkF>|=g-LrUaEoXZe2G7DlpjnPj{N-> D_b_N= literal 0 HcmV?d00001 diff --git a/bin/ij/util/Tools$1.class b/bin/ij/util/Tools$1.class new file mode 100644 index 0000000000000000000000000000000000000000..469f9f7d51879c0df2008c6a2f7e851f5b172f60 GIT binary patch literal 965 zcmZuvO>fgc5Pj>IxG^qC(>9dv0D;Cy+K|#)OC^+o6e)#6qg-;@Bv!h?^&)@tS8+jt zAaOvP`8|juV%9OL6UEYcXLokqyf-ty{(k=f;1GufG={vGHlt9u&7PEQP-z*^846Q= z#hWhoCe8Q5sWS=?t3JjU5dXVo5&v_SJ4jsS836-JPl_T!V-!=SNV}RM+3`YW;`m1^uI!G99o#i=hY+u2B@q_x;Xa8MUfAbu zM7YG1A|9e_;L$<}=Y_EF7*7~-36Gv+*iJJ^%4K&LG})qBuArn`2MoLExOs!p0+JiW zhM{U<8zlzwTt@!Lc`H=$%(&V6N)$0V7ZZ<%kx!0SsiX_QOI<%sz58Fn6RL9ljW=>- zAiT+$6JE%%fhIL|p&J%jAPu_Dy$>`S)xoJfAk9|C^BlkJ@*r>ml!2JlhiA`+LiE55gg3u#Gg;}b7 zfmNU3_JJ}Q%9!E7C-S7x{={hIc|?Lnv3QC~oS}qgWNC%hl;L@jVK2$BUaSAaPJ`AA iyUV~d%5f?{haFor)Z@K@J=%5b;{{%lrKBri^!)?e3eeI3 literal 0 HcmV?d00001 diff --git a/bin/ij/util/Tools$2.class b/bin/ij/util/Tools$2.class new file mode 100644 index 0000000000000000000000000000000000000000..00de7796fcbc6e858b417e13ef978df7b30383b8 GIT binary patch literal 992 zcmZuv-)|B@5dQX{T%lK?w59boDq4QESlYMNCWdH2LiGW{OP?0l(8cSnxjXt-`C_7& z_+WhYk2204XL=AwmYtiO@0A=Q%1_@*xq>*DtlubakBbE3ty%`9v#|N%p$anORyM69S^Bp;p z!L{@mwmM6qs0e!)mQ2V>IaXv_1!63`vlXsNJA-{ZuyLOl?_@;?2M_Ux#7hjhS%lM@Ak)x+}+u2UCw8-o%nF5HHRDJQnxAnHSXW z>^n7dwND#xDS~S~veBUNF3iQjF-Swwg-WIM(t)HZhLrXipZG4zPh za2BfH;M8W=YZ{|&j2Rw(p_e$D9kLbYh=`4_c!5%wp@5fUS-H2A;Z>C3Fv_r5t^LM+ loz4sgOT-jv$~01vsDxG(weYUvF!UbbHBQJf(v2AU{R1i|*}nh) literal 0 HcmV?d00001 diff --git a/bin/ij/util/Tools.class b/bin/ij/util/Tools.class new file mode 100644 index 0000000000000000000000000000000000000000..4ba0ec4c1927936c5f1a520dc3243b29f0f56e62 GIT binary patch literal 12212 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 literal 0 HcmV?d00001 diff --git a/bin/ij/util/WildcardMatch.class b/bin/ij/util/WildcardMatch.class new file mode 100644 index 0000000000000000000000000000000000000000..e120764707cbdc2c5da02820b6fb0eaaf0383dbf GIT binary patch literal 3549 zcma)8{c{^v6+KV8l45(6D6VD4U%~w#kyj>ep-I!ab+JWCP31VL^FiFS&01PJo5+$P zt=yyqQXr)a{Lq$<0Xx$)lz>|>kPfxoDGbvY$iVO?FvFk0bcSg$=e;K@lC2EG#U-Q-!PC?KW;0;M!3NlxZC3AlVj(oV^pbaN%o z_vT%JCa-j;ly(YkMj+&t(#afA#TZ{6^>Us+EMUa@PY9UFe8z2tfx9g1K(q;r)Ity; zIqR?x#x^eltk*(|KKB_5mY&^hA%g7!J5pZGJzSofc8kZH=_~_kP36;0 z_JmXPU~7#PF{HMGyBo~c??<>(i!I$J>)AKsQ!?r(Pj1>F>l83JC&d@o z8&BQVe$p>8nxU;L{Zq|2h)2SBxUN?8c?&~$RG`Jrr}8hjMd?nUE#AKsrkOepTX-B_ z)R7%_FZz?^X$>Q=r$Htk`(M4QuE8|%1_BVVvt;NPl^Ql zTTt~g)P0f%+)X@#%{R;+dl{br*~!$%KBh)?F*UM> zsgWH_jqG0>cMyU7kk6fbE7P93Az`nf)izepW=9`iLAxEbSFp>Do?bzhZLXk4?(LS3 zzV{T^J$&wA>jc_i;yKPgv++|6 zjUQlQn&CPMdNxyVj1(Nd0|m0yWy)Wmwi>IJf}YKRoNgG%ejL!o;u_VISL~L)5~}v! zN6#W61FLvwJi$lG7`6x2@Yo|}r@4xe1HplqWV`RoDh`dug4Zx%cbdj3#?~;tfKj=% zh@tA*(c$X!_yU>|#5j~9_{btg2tU$$0~T%C77;X-&ER)MU~NDvyE%Z5zeQ2I~tvI6JHW0pla#}1I=N}f030(#-I7RoHgc|63vcLbO4 z3|?d}e~G6qbLVAz7vJU;zk;{%3U1(4{FW=f$M^6dzOM*(qw=Y?Do{En#rs z+|Aa^xG}th*Al(lrU+fvF>G_(y@0T>1d~U1%hyG8{Q+~Q20lQ34JCDY74xfjaoj#d zxy>`|mPp8+U0i>3t1fUs!*B;3iYo>ORL-3;cvU z{*)XpP~EHK`%OHCYZ%3Kj8V4~evZS`?kIji?tjS|SfcLlP`_W{$M`jOmhmorgO8}~ zpYc2V1MlIVT>TeymTlbDR96-Lm{lPaPSI9s9U%I<_@+`=YM6kv^ZwAM2ulS|h9TCu zXmaGcTta|z(P!E1fesq;JE>LWUmX2ah{{W6cPsYli&;0b4^&vXkw>dxMe(glBagCV zL&TE77!cbz<-ezjlQF)@mTPsB(Dv(_Zi{yCN98~_U90NAsyL{?${x-Cbro3op^&g` z5_Z>Hh$#5C^i5WA+-}An)xft^;oIurTM5TMh86gyEAR?-*L9ZqmD)?LzeImw{3M@$8RC>D-0}v@B9~E{gF}t literal 0 HcmV?d00001 diff --git a/images/about.jpg b/images/about.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3ae64145ea089209c6d2dbbe34433a0314395c65 GIT binary patch literal 1940 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 C);Vqf literal 0 HcmV?d00001 diff --git a/macros/AddParticles.txt b/macros/AddParticles.txt new file mode 100644 index 0000000..edd71cf --- /dev/null +++ b/macros/AddParticles.txt @@ -0,0 +1,12 @@ +// Adds particle analyzer ROIs to the ROI Manager + if (nResults==0) + return "Results table is empty"; + if (isNaN(getResult('XStart', 0))) + return "Run the particle analyzer with \"Clear Results\" \nand \"Record Starts\" checked, then try again."; + for (i=0; iInstall Macros) to reinstall after +// making changes. Double click on the tool icon (a circle) +// to set the radius of the circle. +// There is more information about macro tools at +// http://imagej.nih.gov/ij/developer/macro/macros.html#tools +// and many more examples at +// http://imagej.nih.gov/ij/macros/tools/ + +var radius = 20; + +macro "Circle Tool - C00cO11cc" { + getCursorLoc(x, y, z, flags); + makeOval(x-radius, y-radius, radius*2, radius*2); +} + +macro "Circle Tool Options" { + radius = getNumber("Radius: ", radius); +} diff --git a/macros/CommandFinderTool.txt b/macros/CommandFinderTool.txt new file mode 100644 index 0000000..4d673d4 --- /dev/null +++ b/macros/CommandFinderTool.txt @@ -0,0 +1,4 @@ + macro "Command Finder Action Tool - C037T0e17CTce17F" { + run("Find Commands..."); + } + diff --git a/macros/ConvertStackToBinary.txt b/macros/ConvertStackToBinary.txt new file mode 100644 index 0000000..faf0842 --- /dev/null +++ b/macros/ConvertStackToBinary.txt @@ -0,0 +1,19 @@ +// Converts a stack to binary using locally calculated thresholds + + setBatchMode(true); + run("Select None"); + run("8-bit"); + id = getImageID; + for (i=1; i<=nSlices; i++) { + setSlice(i); + run("Duplicate...", "title=temp"); + run("Convert to Mask"); + invertingLUT = is("Inverting LUT"); + run("Copy"); + close; + selectImage(id); + run("Paste"); + if (i==1 && invertingLUT != is("Inverting LUT")) + run("Invert LUT"); + } + run("Select None"); diff --git a/macros/DeveloperMenuTool.txt b/macros/DeveloperMenuTool.txt new file mode 100644 index 0000000..491546f --- /dev/null +++ b/macros/DeveloperMenuTool.txt @@ -0,0 +1,14 @@ + var dCmds = newMenu("Developer Menu Tool", + newArray("Record...", "Capture Screen ", "Monitor Memory...", "Find Commands...", + "Search...", "ImageJ Properties", "List Elements", "Debug Mode", "ImageJ Website...", + "-", "Image...", "Hyperstack...", "Macro", "Open...", + "-", "Blobs", "Fly Brain", "HeLa Cells (48-bit RGB)", "Image with Overlay", + "Mitosis (5D stack)", "T1 Head (16-bits)")); + + macro "Developer Menu Tool - C037T0b14DT9b12eTfb12v" { + cmd = getArgument(); + if (cmd=="Debug Mode") + setOption("DebugMode", true); + else if (cmd!="-") + run(cmd); + } diff --git a/macros/Filter_Plugin.src b/macros/Filter_Plugin.src new file mode 100644 index 0000000..d2389b4 --- /dev/null +++ b/macros/Filter_Plugin.src @@ -0,0 +1,19 @@ +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.plugin.filter.*; + +public class Filter_Plugin implements PlugInFilter { + ImagePlus imp; + + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + return DOES_ALL; + } + + public void run(ImageProcessor ip) { + ip.invert(); + } + +} diff --git a/macros/FloodFillTool.txt b/macros/FloodFillTool.txt new file mode 100644 index 0000000..6d97598 --- /dev/null +++ b/macros/FloodFillTool.txt @@ -0,0 +1,18 @@ + var floodType = call("ij.Prefs.get", "tool.flood", "8-connected"); + var alt = 8; + + macro "Flood Fill Tool -C037B21P085373b75d0L4d1aL3135L4050L6166D57D77D68D09D94Da7C123Da5La9abLb6bc" { + setupUndo(); + getCursorLoc(x, y, z, flags); + if (flags&alt!=0) + setColor(getValue("color.background")); + floodFill(x, y, floodType); + } + + macro 'Flood Fill Tool Options...' { + Dialog.create("Flood Fill Tool"); + Dialog.addChoice("Flood Type:", newArray("4-connected", "8-connected"), floodType); + Dialog.show(); + floodType = Dialog.getChoice(); + call("ij.Prefs.set", "tool.flood", floodType); + } diff --git a/macros/LUTMenuTool.txt b/macros/LUTMenuTool.txt new file mode 100644 index 0000000..2ce2add --- /dev/null +++ b/macros/LUTMenuTool.txt @@ -0,0 +1,19 @@ + var lutdir = getDirectory("startup")+"luts"+File.separator; + var luts = getLutMenu(); + var lCmds = newMenu("LUT Menu Tool", luts); + + macro "LUT Menu Tool - C037T0b10LT6b10UTeb10T" { + cmd = getArgument(); + if (cmd!="-") run(cmd); + } + + function getLutMenu() { + list = getList("LUTs"); + menu = newArray(3+list.length); + menu[0] = "Invert LUT"; + menu[1] = "Apply LUT"; + menu[2] = "-"; + for (i=0; i1 || width!=globalLineWidth) { + lineWidth=width; + globalLineWidth = width; + } + radius = maxOf(4,lineWidth*2); + getCursorLoc(x, y, z, flags); + if (flags&9>0) { // shift or alt + removeLabel(x,y); + exit(); + } + getDateAndTime(yr, mo, dw, d, h, m, s, ms); + uid = "labeltool_"+yr+""+mo+""+d+""+h+""+m+""+s+""+ms; + nbefore = Overlay.size; + getCursorLoc(x1, y1, z, flags); + setLineWidth(lineWidth); + while (flags&16>0) { + getCursorLoc(x1, y1, z, flags); + drawItem(); + wait(30); + while (Overlay.size>nbefore) + Overlay.removeSelection(Overlay.size-1); + } + drawItem(); + label =getString("Enter label", label); + while (Overlay.size>nbefore) + Overlay.removeSelection(Overlay.size-1); + drawItem(); + } + + function drawItem() { + makeOval(x-radius, y-radius, radius*2, radius*2); + Roi.setName(uid); + Overlay.addSelection("",0,""+hexCol()); + makeLine(x, y,x1,y1,x1+(((x1=x)*1))*getStringWidth(label),y1); + Roi.setName(uid); + Overlay.addSelection(""+hexCol(), lineWidth); + setFont("user"); + makeText(label, x1 - (x1=0) { + Overlay.activateSelection(index); + name = Roi.getName(); + Overlay.removeRois(name); + Roi.remove; + } + } + + macro "Label Tool (double click for options) Options" { + Dialog.create("Label Maker Tool Options"); + Dialog.addNumber("Line width:", lineWidth, 0, 3, "pixels"); + m1 = "Shift or alt click to remove a label.\n"; + m2 = "Double click on text tool to change\n"; + m3 = "font size and color."; + Dialog.setInsets(0, 0, 0); + Dialog.addMessage(m1+m2+m3); + Dialog.show(); + lineWidth = Dialog.getNumber(); + } + diff --git a/macros/MagicMontageTools.txt b/macros/MagicMontageTools.txt new file mode 100644 index 0000000..9b28037 --- /dev/null +++ b/macros/MagicMontageTools.txt @@ -0,0 +1,448 @@ +//--version--1.6 +// 1.6 adds the '?' button that points to the wiki page +// panel labels are now drawn on an overlay +// added overlay commands and copy to system clipboard to the rightclick menu +// Montage tools for easy montage manipulation +// jerome.mutterer at ibmp.fr + +var str="ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +var lcas=false; +var antialiasedLabels = true; +var n=0; +var xoffset=0.05; +var yoffset=0.05; +var pos="Clicked quadrant"; + +var commands = newArray("Copy", "Paste","-", "Scale Bar...", + "Brightness/Contrast...", "-", "Extract Selected Panels","Crop Montage [F3]","-", + "Add Panel to Manager [F1]","Selected panels to stack [F2]", "Montage to Stack", "-", + "Fit Clipboard content into panel [F4]","Fill Panel with Clipboard content [F5]","-", + "Set Montage Layout","Change Montage Layout","-", + "Hide Overlay","Show Overlay", "Remove Overlay", "Flatten", "-", + "Copy to System")); + +var toolCmds = newMenu("Magic Montage Menu Tool",commands); +var pmCmds = newMenu("Popup Menu",commands); + +macro "Popup Menu" { + runCommand(); +} + +macro "Auto Montage Action Tool - C66fF0077C6f6F9977Cf66F9077C888F0977" { + setBatchMode(true); + b=bitDepth; + if ((b!=24)&&(nSlices==1)) { exit("Stack, Composite, or RGB image required.");} + if ((b==24)&&(nSlices==1)) { run("Make Composite"); b=8;} + Stack.getDimensions(width, height, channels, slices, frames); + getVoxelSize(xp,yp,zp,unit); + if (channels==1) { channels = channels* frames*slices; Stack.setDimensions(channels,1,1); } + id=getImageID; + t=getTitle; + if (b!=24) { + newImage("tempmont", "RGB", width, height,channels); + id2=getImageID; + for (i=1;i<=channels;i++) { + setPasteMode("copy"); + selectImage(id); + Stack.setChannel(i); + getLut(r,g,b); + run("Duplicate...", "title=temp"+i); + setLut(r,g,b); + run("RGB Color"); + run("Copy"); + selectImage(id2); + setSlice(i); + run("Paste"); + } + } + run("Make Montage...", "scale=1 border=0"); + rename(getTitle+" of "+t); + setVoxelSize(xp,yp,zp,unit); + setBatchMode(false); +} + +macro "Select Panels Tool - Cf44R0077R9077C888R9977R0977"{ + run("Select None"); + setPasteMode("copy"); + w = getWidth; + h = getHeight; + getCursorLoc(x, y, z, flags); + id=getImageID; + t=getTitle; + selectImage(id); + xn = info("xMontage"); + yn = info("yMontage"); + if ((xn==0)||(yn==0)) + exit; + xc = floor(x/(w/xn)); + yc = floor(y/(h/yn)); + makeRectangle(xc*(w/xn),yc*(h/yn),(w/xn),(h/yn)); + xstart = x; ystart = y; + x2=x; y2=y; + x2c=xc;y2c=yc; + while (flags&16 !=0) { + getCursorLoc(x, y, z, flags); + if (x!=x2 || y!=y2) { + x2c = floor(x/(w/xn)); + y2c = floor(y/(h/yn)); + makeRectangle(xc*(w/xn),yc*(h/yn),(w/xn)*(x2c-xc+1),(h/yn)*(y2c-yc+1)); + x2=x; y2=y; + wait(10); + } + } + setPasteMode("add"); +} + +macro "Extract Selected Panels"{ + t=getTitle; + xn = info("xMontage"); + yn = info("yMontage"); + pw = getWidth/xn; + ph = getHeight/yn; + run("Duplicate...", "title=[Extract of "+t+"]"); + setMetadata("Info","xMontage="+getWidth/pw+"\nyMontage="+getHeight/ph+"\n"); +} + +macro "Montage Shuffler Tool - C888R0077R9977C44fR0977R9077"{ + id=getImageID; + run("Select None"); + setPasteMode("copy"); + w = getWidth; + h = getHeight; + getCursorLoc(x, y, z, flags); + xn = info("xMontage"); + yn = info("yMontage"); + if ((xn==0)||(yn==0)) + exit; + xstart = x; ystart = y; + x2=x; y2=y; + while (flags&16 !=0) { + getCursorLoc(x, y, z, flags); + if (x!=x2 || y!=y2) spring(xstart, ystart, x, y); + x2=x; y2=y; + wait(10); + } + if (x!=xstart || y!=ystart) { + xext=0; + yext=0; + if (x>w) xext=1; + if (y>h) yext=1; + if ((xext>0)||(yext>0)) { + run("Canvas Size...", "width="+w+xext*(w/xn)+" height="+h+yext*(h/yn)+" position=Top-Left zero"); + setMetadata("Info","xMontage="+(parseInt(xn)+parseInt(xext))+"\nyMontage="+(parseInt(yn)+parseInt(yext))+"\n"); + exit; + } + sc = floor(xstart/(w/xn)); + tc = floor(x/(w/xn)); + sr = floor(ystart/(h/yn)); + tr = floor(y/(h/yn)); + swap(sc,sr,tc,tr); + + } +} + +macro "Annotation Tool - C700 T2709A T8709B T1f09C T8f09D" { + xn = info("xMontage"); + yn = info("yMontage"); + + getCursorLoc(x, y, z, flags); + iw = getWidth/xn; + ih = getHeight/yn; + + co = floor(x/iw); + li = floor(y/ih); + + fontsize = ih/10; + if (fontsize<12) fontsize=12; + marque = substring(str,n,n+1); + if (lcas==1) marque= toLowerCase(marque); + opt=""; + + if (pos == "Clicked quadrant") { + xoffset=0.05; yoffset=0.05; + if (x>((co+0.5)*iw)) xoffset=0.90; + if (y<((li+0.5)*ih)) yoffset=0.85; + } + + if (antialiasedLabels==true) opt=opt+"antialiased"; + setFont("SanSerif",fontsize, opt); + fg = getValue("rgb.foreground"); + makeText(marque ,co*iw+xoffset*iw,(li+1)*ih-yoffset*ih-getValue("font.height")); + Roi.setStrokeColor(fg&0xff0000>>16,fg&0x00ff00>>8,fg&0x0000ff); + Overlay.addSelection("",0); + run("Select None"); + n++; if (n>lengthOf(str)) n=0; +} + +macro "Annotation Tool Options" { + if (nImages>0) setupUndo; + Dialog.create("Annotation - Options"); + Dialog.addString("Labels",str); + Dialog.addCheckbox("Lowercase labels",lcas); + Dialog.addCheckbox("Reset label counter",true); + Dialog.addCheckbox("Antialiased",true); + Dialog.addChoice("Position",newArray("Clicked quadrant","Lower left","Lower right","Upper right","Upper left"),pos); + Dialog.show; + str = Dialog.getString; + lcas = Dialog.getCheckbox; + resetCounter = Dialog.getCheckbox; + if (resetCounter==true) n=0; + antialiasedLabels = Dialog.getCheckbox; + pos=Dialog.getChoice(); + if (pos=="Lower left") {xoffset=0.05; yoffset=0.05;} + else if (pos=="Lower right") {xoffset=0.90; yoffset=0.05;} + else if (pos=="Upper left") {xoffset=0.05; yoffset=0.85;} + else if (pos=="Upper right") {xoffset=0.90; yoffset=0.85;} + +} + +macro "Montage Sync Tool - C800L07f7L707fG" { + w=getWidth; + h= getHeight; + getCursorLoc(x,y,z,flags); + xn = info("xMontage"); + yn = info("yMontage"); + if ((xn==0)||(yn==0)) { + run("Set Montage Layout"); + exit; + } + xc = floor(x/(w/xn)); + yc = floor(y/(h/yn)); + x0 = x-xc*w/xn; + y0 = y-yc*h/yn; + np = 1*xn*yn; + xp =newArray(np); + yp =newArray(np); + for (i=0;i1) { + xa[0]=x0; + ya[0]=y0; + xa[xa.length-1]=x1; + ya[ya.length-1]=y1; + } + makeSelection("freeline",xa,ya); +} + +macro "Add Panel to Manager [F1]" { + roiManager("Add"); + setOption("Show All",true); +} + +macro "Montage to Stack" { + columns = info("xMontage"); + rows = info("yMontage"); + if (rows==0 || columns==0) { + run("Set Montage Layout"); + columns = info("xMontage"); + rows = info("yMontage"); + } + run("Montage to Stack...", "columns=&columns rows=&rows"); +} + +macro "Selected panels to stack [F2]" { + id=getImageID; + t=getTitle; + selectImage(id); + roiManager("select",0); + getSelectionBounds(x,y,sw,sh); + setBatchMode(true); + newImage("Extracted Panels of "+t, "RGB", sw,sh,roiManager("count")); + id2=getImageID; + setPasteMode("copy"); + for (i=0;iffp) { + run("Size...", "width="+sw+" height="+sw/ffc+" constrain interpolate"); + run("Canvas Size...", "width="+sw+" height="+sh+" position=Center zero"); + } else { + run("Size...", "width="+sh*ffc+" height="+sh+" constrain interpolate"); + run("Canvas Size...", "width="+sw+" height="+sh+" position=Center zero"); + } + run("Copy"); + close; + selectImage(id); + setBatchMode(false); + setPasteMode("Copy"); + run("Paste"); +} + +macro "Fill Panel with Clipboard content [F5]" { + getSelectionBounds(x,y,sw,sh); + id=getImageID; + setBatchMode(true); + ffp=sw/sh; + run("Internal Clipboard"); + run("RGB Color"); + ffc=getWidth/getHeight; + if (ffc>ffp) { + run("Size...", "width="+sw*ffc+" height="+sh+" constrain interpolate"); + run("Canvas Size...", "width="+sw+" height="+sh+" position=Center zero"); + } else { + run("Size...", "width="+sw+" height="+sh/ffc+" constrain interpolate"); + run("Canvas Size...", "width="+sw+" height="+sh+" position=Center zero"); + } + run("Copy"); + close; + selectImage(id); + setBatchMode(false); + setPasteMode("Copy"); + run("Paste"); + +} + +macro "Set Montage Layout" { + columns = info("xMontage"); + rows = info("yMontage"); + if (columns>0 && rows>0) + exit("Layout ("+columns+"x"+rows+") is already set"); + Dialog.create("Set Montage Layout"); + Dialog.addNumber("Width:", 2); + Dialog.addNumber("Height:", 2); + Dialog.show; + mw = Dialog.getNumber; + mh = Dialog.getNumber; + setMetadata("Info","xMontage="+mw+"\nyMontage="+mh+"\n"); +} + +macro "Change Montage Layout" { + columns = info("xMontage"); + rows = info("yMontage"); + if (rows==0 || columns==0) { + run("Set Montage Layout"); + columns = info("xMontage"); + rows = info("yMontage"); + } + id1 = getImageID; + title = getTitle; + getVoxelSize(xp,yp,zp,unit); + Dialog.create("Change Montage Layout"); + Dialog.addNumber("Columns:", columns); + Dialog.addNumber("Rows:", rows); + Dialog.show; + columns2 = Dialog.getNumber; + rows2 = Dialog.getNumber; + run("Montage to Stack...", "columns=&columns rows=&rows"); + id2 = getImageID; + run("Make Montage...", "columns=&columns2 rows=&rows2 scale=1.0"); + rename(title); + setVoxelSize(xp,yp,zp,unit); + selectImage(id1); close; + selectImage(id2); close; +} + diff --git a/macros/MeasureStack.txt b/macros/MeasureStack.txt new file mode 100644 index 0000000..78fcf5d --- /dev/null +++ b/macros/MeasureStack.txt @@ -0,0 +1,121 @@ +// This macro runs "Measure" on a all the slices in a stack. With a +// hyperstack, it runs "Measure" in a user-selected (c,t,z) order. +// Unchecking "Channels", "Slices" or "Frames" will select the +// current channel, slice or frame while running "Measure". +// +// Ved P. Sharma, March 21, 2012 +// Albert Einstein College of Medicine, New York + + saveSettings; + setOption("Stack position", true); + if (!Stack.isHyperstack) { + getVoxelSize(width, height, depth, unit); + for (n=1; n<=nSlices; n++) { + setSlice(n); + run("Measure"); + } + if (unit!="pixels") { + depths = newArray(Table.size); + for (i=0; i 0 ) { + getCursorLoc( x, y, z, flags ); + Overlay.getBounds( id, bx, by, bw, bh ); + if( id < 0 ) break; + Overlay.moveSelection( id, x - bw / 2, y - bh / 2 ); + Overlay.getBounds( id, bx, by, bw, bh ); + d = newArray( bx, bx + bw / 2, bx + bw, by, by + bw / 2, by + bh ); + while( Overlay.size > n ) Overlay.removeSelection( Overlay.size - 1 ); + hit = 0x00; + for( i = 0; i < r.length; i = i + 6 ) { // each element + for( p = 0; p < 6; p ++ ) // each remarkable point + if( abs( d [ p ]- r [ i + p ] )< 5 ) { + hit = hit |( 0x01 << p ); // allows for multiple hits + } + if( hit > 0 ) { + ihit = i; + break; + } + } + for( i = 0; i < 6; i ++ ) { + if( hit &( 1 << i )> 0 ) { + if( i < 3 ) { + Overlay.getBounds( id, bx, by, bw, bh ); + Overlay.drawLine( r [ ihit + i ], 0, r [ ihit + i ], getHeight ); + if( i == 0 ) Overlay.moveSelection( id, r [ ihit + i ], by ); + if( i == 1 ) Overlay.moveSelection( id, r [ ihit + i ]- bw / 2, by ); + if( i == 2 ) Overlay.moveSelection( id, r [ ihit + i ]- bw, by ); + } + if( i >= 3 ) { + Overlay.getBounds( id, bx, by, bw, bh ); + Overlay.drawLine( 0, r [ ihit + i ], getWidth, r [ ihit + i ] ); + if( i == 3 ) Overlay.moveSelection( id, bx, r [ ihit + i ] ); + if( i == 4 ) Overlay.moveSelection( id, bx, r [ ihit + i ]- bh / 2 ); + if( i == 5 ) Overlay.moveSelection( id, bx, r [ ihit + i ]- bh ); + } + } + } + Overlay.show; + wait( 20 ); + } + while( Overlay.size > n ) Overlay.removeSelection( Overlay.size - 1 ); +} +// a tool to select one or more overlay elements +// selection is stored in preferences +// click outside a roi to deselect all +// a menu tool to align selected overlay elements + +macro "Select Overlay Tool - Ce00R22fbC0e0o0044C037L77ffL777aL77a7" { + getCursorLoc( x, y, z, flags ); + Overlay.removeRois( 'ToolSelectedOverlayElement' ); + Overlay.show; + n = Overlay.size; + if( n < 1 ) exit( "Overlay required" ); + id = Overlay.indexAt( x, y ); + if( id < 0 ) { + selectNone( ); + call( 'ij.Prefs.set', 'overlaytoolset.selected', '' ); + exit( ); + } + if( flags & 1 > 0 ) { + Overlay.activateSelection( id ); + if( Roi.getName != 'ToolSelectedOverlayElement' ) + selectElement( id, true ); + } else { + Overlay.activateSelection( id ); + if( Roi.getName != 'ToolSelectedOverlayElement' ) + selectElement( id, false ); + } + run( "Select None" ); + highlightSelectedROIs( ); +} +function highlightSelectedROIs( ) { + Overlay.removeRois( 'ToolSelectedOverlayElement' ); + selected = getSelectedElements( ); + s = split( selected, ',' ); + //print( selected ); + for( i = 0; i < s.length; i ++ ) { + id = s [ i ]; + Overlay.getBounds( id, bx, by, bw, bh ); + makeRectangle( bx, by, bw, bh ); + Roi.setName( 'ToolSelectedOverlayElement' ); + Overlay.addSelection( '#90ff0000', 3 ); + } + run( "Select None" ); + Overlay.show; +} +function selectNone( ) { + Overlay.removeRois( 'ToolSelectedOverlayElement' ); + Overlay.show; + //call( 'ij.Prefs.set', 'overlaytoolset.selected', '' ); +} +function selectElement( id, add ) { + if( add == true ) { + selected = getSelectedElements( ); + s = split( selected, ',' ); + isSelected = false; + for( i = 0; i < s.length; i ++ ) { + if( 1 * s [ i ]== id ) isSelected = true; + } + if( ! isSelected ) { + call( 'ij.Prefs.set', 'overlaytoolset.selected', selected + ',' + id ); + selected = getSelectedElements( ); + } else { + unselectElement( id ); + } + } else { + call( 'ij.Prefs.set', 'overlaytoolset.selected', id ); + } + highlightSelectedROIs( ); +} +function unselectElement( id ) { + selected = getSelectedElements( ); + s = split( selected, ',' ); + selected = ''; + for( i = 0; i < s.length; i ++ ) { + if( s [ i ]!= id ) selected = selected + s [ i ]+ ','; + } + call( 'ij.Prefs.set', 'overlaytoolset.selected', selected ); + selected = getSelectedElements( ); + highlightSelectedROIs( ); + run( "Select None" ); +} +function getSelectedElements( ) { + selected = call( 'ij.Prefs.get', 'overlaytoolset.selected', '' ); + while( selected.endsWith( "," ) ) selected = substring( selected, 0, lengthOf( selected )- 1 ); + while( selected.startsWith( "," ) ) selected = substring( selected, 1, lengthOf( selected ) ); + return selected; +} +var dCmds = newMenu( "Align Overlay Menu Tool", + newArray( "Top", "Middle", "Bottom", "-", "Left", "Center", "Right" ) ); +// a menu tool to align selected ROIs +macro "Align Overlay Menu Tool - C037L00f0R2244R8248" { + cmd = getArgument( ); + if( cmd == "Top" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + tops = newArray( s.length ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + tops [ i ]= y; + } + //Array.print( tops ); + Array.getStatistics( tops, min, max, mean, stdDev ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], x, min ); + } + Overlay.show( ); + } else if( cmd == "Bottom" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + tops = newArray( s.length ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + tops [ i ]= y + h; + } + Array.getStatistics( tops, min, max, mean, stdDev ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], x, max - h ); + } + selectNone( ); + } else if( cmd == "Left" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + tops = newArray( s.length ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + tops [ i ]= x; + } + Array.getStatistics( tops, min, max, mean, stdDev ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], min, y ); + } + selectNone( ); + } else if( cmd == "Right" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + tops = newArray( s.length ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + tops [ i ]= x + w; + } + Array.getStatistics( tops, min, max, mean, stdDev ); + for( i = 0; i < tops.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], max - w, y ); + } + selectNone( ); + } else if( cmd == "Middle" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + Overlay.getBounds( s [ 0 ], x, y, w, h ); + middle = y + h / 2; + for( i = 0; i < s.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], x, middle - h / 2 ); + } + selectNone( ); + } else if( cmd == "Center" ) { + s = getSelectedElements( ); + s = split( s, ',' ); + if( s.length < 1 ) exit( ); + Overlay.getBounds( s [ 0 ], x, y, w, h ); + center = x + w / 2; + for( i = 0; i < s.length; i ++ ) { + Overlay.getBounds( s [ i ], x, y, w, h ); + Overlay.moveSelection( s [ i ], center - w / 2, y ); + } + selectNone( ); + } + highlightSelectedROIs( ); +} +// Click to delete Overlay element +// A confirm dialog is shown +// you can choose not to show it. +macro "Delete Overlay Tool - C037R00ddB58Cd00L0088L0880" { + getCursorLoc( x, y, z, flags ); + selectNone( ); + call( 'ij.Prefs.set', 'overlaytoolset.selected', '' ); + id = Overlay.indexAt( x, y ); + if( id != - 1 ) { + showWarning = call( "ij.Prefs.get", "overlaytoolset.deletewarning", true ); + mustDelete = true; + if( showWarning == true ) { + Dialog.create( "Delete ROI tool options" ); + Dialog.addCheckbox( "Delete this ROI", true ); + Dialog.addCheckbox( "Show this dialog", showWarning ); + Dialog.show( ); + mustDelete = Dialog.getCheckbox( ); + showWarning = Dialog.getCheckbox( ); + call( "ij.Prefs.set", "overlaytoolset.deletewarning", showWarning ); + } + if( mustDelete ) Overlay.removeSelection( id ); + } +} +macro "Overlay Toolset Help Action Tool - C037T3f18?" { + html = "" + + "Toolset Description
" + + "

* Move overlay tool" + + "
Click and drag an overlay element;" + + "
It snaps to alignment guides to other elements.

" + + "

* Overlay select tool" + + "
Click an overlay element to select it." + + "
Shift-click to add elements to the selection." + + "
Shift-click a selected element to deselect it.

" + + "

* Overlay align tool menu" + + "
Use this menu to align selected elements

" + + "

* Overlay delete tool" + + "
Click to remove an overlay element

" + + "

* Overlay toolset help action tool" + + "
This dialog. You can leave it open to try toolset functions.

" + + ""; + Dialog.createNonBlocking( "Overlay Toolset Help" ); + Dialog.addMessage( html ); + Dialog.show( ); +} diff --git a/macros/Plugin_Frame.src b/macros/Plugin_Frame.src new file mode 100644 index 0000000..f31fc1a --- /dev/null +++ b/macros/Plugin_Frame.src @@ -0,0 +1,18 @@ +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.plugin.frame.*; + +public class Plugin_Frame extends PlugInFrame { + + public Plugin_Frame() { + super("Plugin_Frame"); + TextArea ta = new TextArea(15, 50); + add(ta); + pack(); + GUI.center(this); + show(); + } + +} diff --git a/macros/Prototype_Tool.src b/macros/Prototype_Tool.src new file mode 100644 index 0000000..7cf2850 --- /dev/null +++ b/macros/Prototype_Tool.src @@ -0,0 +1,24 @@ +// Prototype plugin tool. There are more plugin tools at +// http://imagej.nih.gov/ij/plugins/index.html#tools +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.plugin.tool.PlugInTool; +import java.awt.event.*; + +public class Prototype_Tool extends PlugInTool { + + public void mousePressed(ImagePlus imp, MouseEvent e) { + IJ.log("mouse pressed: "+e); + } + + public void mouseDragged(ImagePlus imp, MouseEvent e) { + IJ.log("mouse dragged: "+e); + } + + public void showOptionsDialog() { + IJ.log("icon double-clicked"); + } + +} diff --git a/macros/RoiMenuTool.txt b/macros/RoiMenuTool.txt new file mode 100644 index 0000000..2b3699f --- /dev/null +++ b/macros/RoiMenuTool.txt @@ -0,0 +1,88 @@ + var rCmds = newMenu("ROI Menu Tool", + newArray("Set Default Group...", "Set Default Stroke Width...", "-", + "Set Group of Selected ROIs", "Select Group", "-", "Properties..." , "Install Keypad Shortcuts") ); + + macro "ROI Menu Tool - C037T0d15RT8c12oTfc12i" { + cmd = getArgument(); + if (cmd=="Set Default Group...") + setDefaultRoiGroup(); + else if (cmd=="Set Default Stroke Width...") + setDefaultRoiStrokeWidth(); + else if (cmd=="Set Group of Selected ROIs") + setRoiGroup(); + else if (cmd=="Select Group") + selectRoiGroup(); + else if (cmd=="Properties...") + properties(); + else if (cmd=="Install Keypad Shortcuts") + call("ij.plugin.MacroInstaller.installFromJar", "/macros/RoiMenuTool.txt+"); + } + + // Numeric keypad shortcuts used to set the default ROI group + macro "Keypad shortcuts for setting default group" { } + macro "Group 0 (none) [n0]" { npad(0); } + macro "Group 1 [n1]" { npad(1); } + macro "Group 2 [n2]" { npad(2); } + macro "Group 3 [n3]" { npad(3); } + macro "Group 4 [n4]" { npad(4); } + macro "Group 5 [n5]" { npad(5); } + macro "Group 6 [n6]" { npad(6); } + macro "Group 7 [n7]" { npad(7); } + macro "Group 8 [n8]" { npad(8); } + macro "Group 9 [n9]" { npad(9); } + + function npad(digit) { + Roi.setDefaultGroup(digit); + } + + function properties() { + if (selectionType==-1) { + showMessage("Selection required"); + exit; + } + run("Properties... "); + } + + function setDefaultRoiGroup() { + group = Roi.getDefaultGroup; + Dialog.create("Set Default Group"); + Dialog.addNumber("Default group", group); + Dialog.show; + group = Dialog.getNumber(); + Roi.setDefaultGroup(group); + call("ij.plugin.frame.Recorder.recordString", "Roi.setDefaultGroup("+group+");\n"); + } + + function setDefaultRoiStrokeWidth() { + width = Roi.getDefaultStrokeWidth; + Dialog.create("Set Default Stroke Width"); + Dialog.addNumber("Default stroke width", width); + Dialog.show; + width = Dialog.getNumber(); + Roi.setDefaultStrokeWidth(width); + call("ij.plugin.frame.Recorder.recordString", "Roi.setDefaultStrokeWidth("+width+");\n"); + } + + function setRoiGroup() { + Dialog.create("Set Group"); + Dialog.addString("Group", "1"); + Dialog.show; + group = Dialog.getString(); + RoiManager.setGroup(group); + if (call("ij.plugin.frame.Recorder.scriptMode")=="true") + call("ij.plugin.frame.Recorder.recordString", "rm.setGroup("+group+");\n"); + else + call("ij.plugin.frame.Recorder.recordString", "RoiManager.setGroup("+group+");\n"); + } + + function selectRoiGroup() { + Dialog.create("Select group"); + Dialog.addString("Group", "0"); + Dialog.show; + group = Dialog.getString(); + RoiManager.selectGroup(group); + if (call("ij.plugin.frame.Recorder.scriptMode")=="true") + call("ij.plugin.frame.Recorder.recordString", "rm.selectGroup("+group+");\n"); + else + call("ij.plugin.frame.Recorder.recordString", "RoiManager.selectGroup("+group+");\n"); + } diff --git a/macros/Search.txt b/macros/Search.txt new file mode 100644 index 0000000..2cf005b --- /dev/null +++ b/macros/Search.txt @@ -0,0 +1,139 @@ +// "Search" +// This macro searches for text in files contained in a directory. +// TF, 2011.02 Added support for scripts; Recordable. + + str = ""; + contents = true; + ignore = false; + search = "Macros"; + firstLine = true; + arg = getArgument; + if (arg!="") { + args = split(arg, "|"); + if (args.length==4) { + str = args[0]; + contents = parseInt(args[1]); + ignore = parseInt(args[2]); + search = args[3]; + } + } + extensions = newArray(".java", ".txt", ".ijm", ".js", ".py", ".rb", ".clj", ".bsh", ".html"); + IJdir = getDirectory("imagej"); + + Dialog.create("Search"); + Dialog.addString("_", str, 20); + items = newArray("Macros", "Scripts", "Java", "ImageJ folder", "Choose..."); + Dialog.setInsets(2,20,0); + Dialog.addRadioButtonGroup("Search:", items, 5, 1, search); + Dialog.setInsets(0, 20, 0); + Dialog.addCheckbox("Search_contents", contents); + Dialog.addCheckbox("Ignore case", ignore); + Dialog.setInsets(10, 0, 0); + Dialog.addMessage("In the Log window, to open a file,\ndouble-click on its file path."); + Dialog.show(); + str = Dialog.getString(); + contents = Dialog.getCheckbox(); + ignore = Dialog.getCheckbox(); + search = Dialog.getRadioButton(); + if (str=="") + exit("Search string is empty"); + + sourceExists = File.exists(IJdir+"source"); + searchNames = false; + dir1=""; dir2=""; dir3=""; + if (search=="Scripts") { + dir1 = getDirectory("macros"); + dir2 = getDirectory("plugins"); + dir3 = IJdir+"scripts/"; + extensions = newArray(".js", ".py", ".rb", ".clj", ".bsh"); + } else if (search=="Java") { + dir1 = getDirectory("plugins"); + if (sourceExists) + dir2 = IJdir+"source"+"/"; + extensions = newArray(".java"); + } else if (search=="ImageJ folder") { + dir1 = getDirectory("imagej"); + searchNames = true; + } else if (search=="Choose...") { + dir1 = getDirectory("Choose a Directory"); + searchNames = true; + } else { + dir1 = getDirectory("macros"); + dir2 = getDirectory("plugins"); + extensions = newArray(".txt", ".ijm"); + } + if (ignore) + str = toLowerCase(str); + count = 0; + if (dir1!="") find(dir1); + if (dir2!="") find(dir2); + if (dir3!="") find(dir3); + if (indexOf(str, "|")==-1) + return ""+str+"|"+contents+"|"+ignore+"|"+search; + exit; + + function find(dir) { + list = getFileList(dir); + for (i=0; iLookup Tables menu. + +saveSettings(); +list = getList("LUTs"); +setBatchMode(true); +newImage("ramp", "8-bit Ramp", 256, 32, 1); +newImage("luts", "RGB White", 256, 48, 1); +count = 0; +setForegroundColor(255, 255, 255); +setBackgroundColor(255, 255, 255); +setFont("SansSerif", 12,"antialiased"); +for (i=0; i 1 px"); +if (selectionType != 4) + exit("No traced selection found"); +getThreshold(lowThr, hiThr); +getSelectionCoordinates(xx, yy); +len = xx.length; +polyX = newArray(len); +polyY = newArray(len); +count = 0; +for(jj = 0; jj < len; jj++) { + x1 = xx[jj]; + y1 = yy[jj]; + x2 = xx[(jj+1)%len]; + y2 = yy[(jj+1)%len]; + dd = 1; + if (y1 == y2) {//horizontal separator + if (x1 > x2) dd = -1; + for(x = x1; x != x2; x+= dd){ + processPixelPair(x, y1, x+dd, y1); + } + } + else {//vertical separator + if (y1 > y2) dd = -1; + for(y = y1; y != y2; y+= dd){ + processPixelPair(x1, y, x1, y+dd); + } + } +} +polyX = Array.trim(polyX, count); +polyY = Array.trim(polyY, count); +makeSelection("polygon", polyX, polyY); +run("Interpolate", "interval=1 adjust"); //after button released + +//processes neighbors of this separator line and adds vertex +function processPixelPair(x1, y1, x2, y2) { + if (x1 == x2) { + val1 = getPixel(x1, minOf(y1, y2)); + val2 = getPixel(x1 - 1, minOf(y1, y2)); + } + if (y1 == y2){ + val1 = getPixel(minOf(x1, x2), y1); + val2 = getPixel(minOf(x1, x2), y1 -1); + } + bright = maxOf(val1, val2); + dark = minOf(val1, val2); + + if (bright>=lowThr && dark<=lowThr) + thr = lowThr; + if (bright>=hiThr && dark<=hiThr) + thr = hiThr; + if (dark==bright) + fraction = 0.5; + else + fraction = (thr - dark)/(bright - dark); + if (val1 < val2) + fraction = 1 - fraction; + if (y1 == y2) { + newY = minOf(y1, y2) + fraction - 0.5; + newX = (x1 + x2)/2; + } + if (x1 == x2) { + newX = minOf(x1, x2) + fraction - 0.5; + newY = (y1 + y2)/2; + } + polyX[count] = newX; + polyY[count] = newY; + count++; + if (count == polyX.length){ + polyX = Array.concat(polyX, polyX); + polyY = Array.concat(polyY, polyY); + } +} diff --git a/macros/SprayCanTool.txt b/macros/SprayCanTool.txt new file mode 100644 index 0000000..8ed59b9 --- /dev/null +++ b/macros/SprayCanTool.txt @@ -0,0 +1,41 @@ +// Spray Can Tool + + var width=100, dotSize=1, rate=6; + + macro 'Spray Can Tool - C123D20D22D24D41D43D62D82Da2C037L93b3D84Dc4L75d5L757f Ld5dfLa7d7LabdbLa9d9LacdcLa7ac' { + setLineWidth(dotSize); + radius=width/2; radius2=radius*radius; + start = getTime(); + autoUpdate(false); + n = 25*exp(0.9*(10-rate)); + if (n<=5) n = 0; + while (true) { + getCursorLoc(x, y, z, flags); + if (flags&16==0) exit(); + x2 = (random()-0.5)*width; + y2 = (random()-0.5)*width; + if (x2*x2+y2*y2start+50) { + updateDisplay(); + start = getTime(); + } + } + for (i=0; i10) rate = 10; + } diff --git a/macros/StacksMenuTool.txt b/macros/StacksMenuTool.txt new file mode 100644 index 0000000..636dc6e --- /dev/null +++ b/macros/StacksMenuTool.txt @@ -0,0 +1,15 @@ + var sCmds = newMenu("Stacks Menu Tool", + newArray("Add Slice", "Delete Slice", "Set Slice...", + "-", "3D Project...", "Grouped Z Project...", "Z Project...", "Orthogonal Views", "Plot Z-axis Profile", "Reslice [/]...", + "-", "Images to Stack", "Stack to Images", "Make Montage...", + "-", "Make Substack...", "Stack to Hyperstack...", + "-", "Combine...", "Concatenate...", "Flip Z", "Label...", "Animation Options...", + "-", "T1 Head (2.4M, 16-bits)")); + + macro "Stacks Menu Tool - C037T0b11ST8b09tTcb09k" { + cmd = getArgument(); + if (cmd=="Images to Stack") + run(cmd, " "); + else if (cmd!="-") + run(cmd); + } diff --git a/macros/StartupMacros.txt b/macros/StartupMacros.txt new file mode 100644 index 0000000..4e7059d --- /dev/null +++ b/macros/StartupMacros.txt @@ -0,0 +1,7 @@ +// Default startup macros + + macro "Developer Menu Built-in Tool" {} + macro "Brush Built-in Tool" {} + macro "Flood Filler Built-in Tool" {} + macro "Arrow Built-in Tool" {} + diff --git a/macros/TimeStamp.ijm b/macros/TimeStamp.ijm new file mode 100644 index 0000000..9f8f915 --- /dev/null +++ b/macros/TimeStamp.ijm @@ -0,0 +1,13 @@ +x=20; y=30; size=18; +interval=1; //seconds +i = floor(i*interval); // 'i' is the image index +setFont("SansSerif", size, "antialiased"); +setColor("white"); +s = ""+pad(floor(i/3600))+":"+pad(floor((i/60)%60))+":"+pad(i%60); +drawString(s, x, y); +function pad(n) { + str = toString(n); + if (lengthOf(str)==1) str="0"+str; + return str; +} + diff --git a/plugins/Decarburization_Measurements.jar b/plugins/Decarburization_Measurements.jar new file mode 100644 index 0000000000000000000000000000000000000000..0bf116d16772f4e3202b1aea03dc0625fe6b6913 GIT binary patch literal 1518 zcmWIWW@h1HVBlb2c$HWi%zy+q8CV#6T|*poJ^kGD|D9rB2!JZ(V0Z~s`t?%PlO&+> zLLe3aVz^RYM?X(D*WeI6U$@V`XHNTg>*`(P_14uocjo-&AcHH$51tmCaTY4n@$fn4 z5z5fTsLIRxg@?Ct=bY{!Veg>elDS%qR%f>M`ih#wOfEL!?rMA$^oiTc$h+7SsK(2< z*y}*nrDEZ3t%{u$JJH;9(Ioj64+{fBB^v{S63|U9smX~&Nu@=ZRf#2;`FZiasfop< zMX9-|c_qbq$vKI|#kHZi#loQ?b$OpR%|5%WcdMY&Y$x4>OHR&?i`ABH6PcLcrQNC! zw&Zq-@Ae$?!p+@>dhT1*ePn-7UT9_2XM8F`qVx}AUGX2ze_WCq?8=2-Wo(r{cenWb zYuodd>EEW?*Z+__&>azWLDit*+8U{MUGJmHdCq59m_PGb{B-xTT`wziSrZ$N%ql#6 z;$~a$`LpZS%nbE+3{l8k`*hZ}`0aD<{GRr9W?o+7_DRKhJ2*HzRU6;kY`gr4=lE)g zZTW38EL8;NZjm`XTZkidlW60IoW*Hb91FwUWvY0X&c6s)!1ZOC?(sb@)~}!OE;ae6 z8SAf8y{i(#d9F|T-e>S;ZIo`~!cTEmMOc+|FMpoRq&)M}{ZQSibx*H!r-p?@nZHuw z&J^8TQ+=c0weHNq>1!RPZ`Hf-*0WQerk0+0sFZB_clpLeJ)x6?Z>JV^uzhowe%Ri9QT2{n8TZtzo!6`KO?}Y7F~6j{ zWOB;HYdbW$=LPPyOgCws7*^ow5S{ZQ@9Gn;jk6C|_kP=%7DJ z{Fb~_P_I24|K6p8?b_eQ=L@%Od#t(huLtWdJ(&XzANm}Z+`f2Z{gZn_%(NNA_P=ej=Etixx1De67uTD$CGlHF8;^NSi+KN0Eszq?}g zoKq31F^|pN_6JwA&pB4i;{D{&7A8N3yU!knvN|1fRODy9&iebOgy2UOLq)mI-zQYD zeC3y^5aBsj?YKGr4{LxoBa;X-?!pjQpa6jY!&^rX4KME?MI%fr_QDaQ0R)ybY6D5Q pR-~d5VL8ME^r8%`9c;pWAW67j3-D$I7J>{6%s_Y+NOQA-cmSqofm#3n literal 0 HcmV?d00001 diff --git a/plugins/MacAdapter.class b/plugins/MacAdapter.class new file mode 100644 index 0000000000000000000000000000000000000000..ab5202edf266d89c919aedea446806ca637f749c GIT binary patch literal 2005 zcmah~Yg5}+5Iw>+GO~cgJOZ?U14(0SVz&ux(n8ZxN{Xo+2v8{WDXbe?#FkuHrjH-f z|Il{EWXepZpZlXaJy$Z81C!><)z#JR*|TSNum1V>&%XdHp{XH-hbrFJ(1(X|`9Lnq zajai&eS%M8h=JRD2r8x&|GOV^ey4uA&%6Ny8|fNRKTQ z+ZrY${xqJd*pV;0D)!_%gMXo7U&VonFI5~Wh&2qa+EkD%o*1V_zU5i9{JtoAj=QX& zf5oyb?~#H?dSPEdbj>k^g5jcNizlu6v2gc{;~GyQZp&6MmJTp#hF!_;dah+xmZe*w zYS?B?tR6cpPrO|4aN24J`>`csTn`Sm zUGYq~!Y&Jv)`GLAEsMZ|yPi>gUNRaTQez=PPXq-wg7Y0Av0wT!d=bL3y&!0OrCjSs zpzS&>w=9D5;gV5aHI0TR+?>=+$6L6gVCvPG={UkyI=;p?6wsFG0HkjF!yVIZG2t#s;)3h)7IsCFiQ7EtyQt?d??_BrJvr`@d=xcN1^359SUr(xmSx@ zw3?n|#5N9<+G*K#(4Lb`;jR07v!{JZOq(gDk7SRI8>ZRQ3nAd0|7+I^GrwbFq+Hlk zFkm`sPQA{aS1|i(&VH-2g~@HhpAVWRD#C9(+o-eZqv?gK1I1w@)l0(`sJ6Utq+p?U z2rnd9zKY`yCc|yJmg8F9&;E=TNP&F9vuL^?4P#Bua9Nd@a9xKg&-VuDOnOnKFFTs` zYWjjio7OK(qu&*E=aic#sIf%BoYMOh_jwtgAxRl?;Wpad@yd6`B@HvNh zD2U5Z;pLj(o+HRL&9`Yj6+UB`Gbq1tLtVJ``-ekZGstq4rVn@lNBA5$N8jNYBBjM> z8_~r~8?n$ai6~8yj5dNnUbIP!(R~qjJ0Mwt^wA=b?0*3@ON4mnJmlj~Acje3n4(Lr z>sTVNjDGuf3}oBT+DIInLqE(U2hT86T3l>nIBK?WjhpKsvEaoq2zL< vcK2v@|Mh0~X}0uwvn3Yv-EJ!Len=Ma9MjBT<_`>n`3J2tOx*W;ZV&zgozJu% literal 0 HcmV?d00001 diff --git a/plugins/MacAdapter.source b/plugins/MacAdapter.source new file mode 100644 index 0000000..7eff785 --- /dev/null +++ b/plugins/MacAdapter.source @@ -0,0 +1,54 @@ +package ij.plugin; +import ij.plugin.*; +import ij.*; +import ij.io.*; +import com.apple.eawt.*; +import java.util.Vector; + +/** This Mac-specific plugin is designed to handle the “About ImageJ", + Preferences and “Quit ImageJ" commands in the ImageJ menu, and to + open files dropped on ImageJ.app and to open double-clicked files + with creator code "imgJ". With Java 8, the “About ImageJ" and + “Quit ImageJ” commands work without MacAdapter. +*/ +public class MacAdapter implements PlugIn, ApplicationListener, Runnable { + static Vector paths = new Vector(); + + public void run(String arg) { + Application app = new Application(); + app.setEnabledPreferencesMenu(true); + app.addApplicationListener(this); + } + + public void handleAbout(ApplicationEvent event) { + IJ.doCommand("About ImageJ..."); + event.setHandled(true); + } + + public void handleOpenFile(ApplicationEvent event) { + paths.add(event.getFilename()); + Thread thread = new Thread(this, "Open"); + thread.setPriority(thread.getPriority()-1); + thread.start(); + } + + public void handlePreferences(ApplicationEvent event) { + IJ.error("The ImageJ preferences are in the Edit>Options menu."); + } + + public void handleQuit(ApplicationEvent event) { + new Executer("Quit", null); // works with the CommandListener + //IJ.getInstance().quit(); + } + + public void run() { + if (paths.size() > 0) { + (new Opener()).openAndAddToRecent((String) paths.remove(0)); + } + } + + public void handleOpenApplication(ApplicationEvent event) {} + public void handleReOpenApplication(ApplicationEvent event) {} + public void handlePrintFile(ApplicationEvent event) {} + +} diff --git a/plugins/MacAdapter9.class b/plugins/MacAdapter9.class new file mode 100644 index 0000000000000000000000000000000000000000..44ef7dd31b5e5273ffaf3f44ef0c55ddad8ebb3f GIT binary patch literal 2272 zcmZuySyLQU6#nio&@gleApsK&dn7;z;}S#2AO-`Gi7W|V5aQO%T!)6Cd(sObafxv^ z`UiaR)n~P8NTkZji!c5p%TmAF)3X3mQ+2zi?>XN+=R4my_pg6{{{z4U{18I~H5#Hi zYEdW9ykTq`@vLEG;`3`8GUW->jhU9|jSJND58R5O9t|2|IvSxD&5gTWd{Vlbo}Ckj zWu#a7bE1D>CP1t(Nhpja9Y@hD(BevOJ}0efW>!ucRyr#kfxiB-Q@Vf~FB-X_RiY3} zi;m+sA#jv96Kl5bl|Xu`fK*z90Bt&+!AXH80^Ib?vVV6K{|W=T*W+0p9AI`Ow~UOu zK0G`e!)df@=+M!LE`eHeBc8l2(2%yT+FM)9P@pwL4p?6F9Mj4K!StY4!xNtlXrWe4Kw&`tp(lI=nbhqvE zPKMu!FpRi{7j(Rcmv}}Qp=ctYtsCx~++j2n$ExzUpyL%>6o^{E=aK*_B$d!`Nk9+q zOgpY}XuxIg^O%lTL8Y~lwOrQEm{xq&NKK@T9D^Dqn>oW z6sy#HBkQ|?E+>)FkyepKUGu)w@FA;5E7ndWES(r6Y80!cjty*bWH@rm-j+)0$#4Md zum9FQ3JWJI6kA6QcUWMXz!O$_BAs5c7i20BeAT{LRp)YmQG(57)iCx<($cUk(0-_q zDyM*k9f9scCI_dDhWi4i51CcW0}US+dn8D;SZ%icqM5M_&v(d2-yz|S9ah75f=pkv z)3iT2V_I^~-&&K-lF|<4;P@6xyB^HSAQOSEDx)gc3MK_>$RHG<;j}nY#k`bo+2zQh zXQVb~ja+zgYvD|l^MbtNOV?XARgaBVfm22Wq(XXX{1@j~0l>}KmMeqU>c>*q@QR5o z+P;&LDp1z5qW6at_vq%Bh)}vNeB8x7d;|_MH8S}8g!fqlpF-n1uQIL=_@$js!DsE@ zF2pZ<5%`R&+6J1rK13H+WjdyALOxGChBiAin8y*d98*hc*|Uw{v_TZDXh0j9czqnl zNn(7CFG8dqJ|ncMYdQ58&y{`ET?^h1wLw^4;;Yd95z!QC@?0L>PjFUXcIXMt3;c*n ze0f>mDMsdg#pwAwUd!X^@*a{agDr3FV)k*tzbm2k2HY{&JxmhJ`3 paths = new Vector(); + + public void run(String arg) { + Desktop dtop = Desktop.getDesktop(); + dtop.setOpenFileHandler(this); + dtop.setAboutHandler(this); + dtop.setQuitHandler(this); + } + + @Override + public void handleAbout(AboutEvent e) { + IJ.doCommand("About ImageJ..."); + } + + @Override + public void openFiles(OpenFilesEvent e) { + for (File file: e.getFiles()) { + paths.add(file.getPath()); + Thread thread = new Thread(this, "Open"); + thread.setPriority(thread.getPriority()-1); + thread.start(); + } + } + + @Override + public void handleQuitRequestWith(QuitEvent e, QuitResponse response) { + new Executer("Quit", null); // works with the CommandListener + } + + // Not adding preference handling + // because we don't have the equivalent of app.setEnabledPreferencesMenu(true); + // @Override + // public void handlePreferences(PreferencesEvent e) { + // IJ.error("The ImageJ preferences are in the Edit>Options menu."); + // } + + public void run() { + if (paths.size() > 0) { + (new Opener()).openAndAddToRecent(paths.remove(0)); + } + } +} diff --git a/plugins/TESTPlugin_.jar b/plugins/TESTPlugin_.jar new file mode 100644 index 0000000000000000000000000000000000000000..8436d7ed34e7375ff4572d2ff97d845adb1f88ea GIT binary patch literal 759 zcmWIWW@h1HVBlb2U}&ih29j{V$-u(k>l)&y>*?pF|L+t7LjY7M2gBdy>fo=Js-7eP z6&C`r2oS@S`a1e~y1532==r*R_C0gj$6HtLBCofu*10q1HwPJ9F@Es0=!~;ap^k^o zIge0=E=E;e-Y-17l{@Ej2MK!z1((d#YP34DwbxhFBxZ845qDSPtDsNZUPj)HL-vMv=1cWYJbtk{Xb z^*`hwGN*M~lIEv{56YFyKP2eiGl=PN^I5DWur7^z_O6uNv$tNo|Fq=e1O5;0lJ~`g zJZ!$4-FJOv<>ud?e}CnDz~j!x#WVHe(~O$+n>>#3^XatxnzO$s{-edG_lI`QSjOjN zGPU@>W%|!Hn;uE8=SI_BHY~jH)>mRpcZyn!NZ`i@3(f>^r|9HuULNqmkbi1I2HVLg zt2wV<`f_^VRJ&8+ZCM+{miRBTS}eOZZ$p={*VdlJb@@TZ#G1=>r+D?<7h0PpE3W&- zBE$HGNJ+)xL~-7%mTR4A#_NYs4Au@dVLy<>i1Gk$RyL3lW+2=Hq*a+fJOHsR|Kb1u literal 0 HcmV?d00001 diff --git a/src/ij/CommandListener.java b/src/ij/CommandListener.java new file mode 100644 index 0000000..7ec2870 --- /dev/null +++ b/src/ij/CommandListener.java @@ -0,0 +1,16 @@ +package ij; + + /** Plugins that implement this interface are notified when ImageJ + is about to run a menu command. There is an example plugin at + http://imagej.nih.gov/ij/plugins/download/misc/Command_Listener.java + */ + public interface CommandListener { + + /* The method is called when ImageJ is about to run a menu command, + where 'command' is the name of the command. Return this string + and ImageJ will run the command, return a different command name + and ImageJ will run that command, or return null to not run a command. + */ + public String commandExecuting(String command); + +} diff --git a/src/ij/CompositeImage.java b/src/ij/CompositeImage.java new file mode 100644 index 0000000..b0a2cc3 --- /dev/null +++ b/src/ij/CompositeImage.java @@ -0,0 +1,666 @@ +package ij; +import ij.process.*; +import ij.gui.*; +import ij.plugin.*; +import ij.plugin.frame.*; +import ij.io.FileInfo; +import java.awt.*; +import java.awt.image.*; + +public class CompositeImage extends ImagePlus { + + /** Display modes (note: TRANSPARENT mode has not yet been implemented) */ + public static final int COMPOSITE=1, COLOR=2, GRAYSCALE=3, TRANSPARENT=4; + public static final int MAX_CHANNELS = 7; + int[] rgbPixels; + boolean newPixels; + MemoryImageSource imageSource; + Image awtImage; + WritableRaster rgbRaster; + SampleModel rgbSampleModel; + BufferedImage rgbImage; + ColorModel rgbCM; + ImageProcessor[] cip; + Color[] colors = {Color.red, Color.green, Color.blue, Color.white, Color.cyan, Color.magenta, Color.yellow}; + LUT[] lut; + int currentChannel = -1; + int previousChannel; + int currentSlice = 1; + int currentFrame = 1; + boolean singleChannel; + boolean[] active = new boolean[MAX_CHANNELS]; + int mode = COLOR; + int bitDepth; + double[] displayRanges; + byte[][] channelLuts; + boolean customLuts; + boolean syncChannels; + + public CompositeImage(ImagePlus imp) { + this(imp, COLOR); + } + + public CompositeImage(ImagePlus imp, int mode) { + if (modeGRAYSCALE) + mode = COLOR; + this.mode = mode; + int channels = imp.getNChannels(); + bitDepth = getBitDepth(); + if (IJ.debugMode) IJ.log("CompositeImage: "+imp+" "+mode+" "+channels); + ImageStack stack2; + boolean isRGB = imp.getBitDepth()==24; + if (isRGB) { + if (imp.getImageStackSize()>1) + throw new IllegalArgumentException("RGB stacks not supported"); + stack2 = getRGBStack(imp); + } else + stack2 = imp.getImageStack(); + int stackSize = stack2.getSize(); + if (channels==1 && isRGB) + channels = 3; + if (channels==1 && stackSize<=MAX_CHANNELS && !imp.dimensionsSet) + channels = stackSize; + if (channels<1 || (stackSize%channels)!=0) + throw new IllegalArgumentException("stacksize not multiple of channels"); + if (mode==COMPOSITE && channels>MAX_CHANNELS) + this.mode = COLOR; + compositeImage = true; + int z = imp.getNSlices(); + int t = imp.getNFrames(); + if (channels==stackSize || channels*z*t!=stackSize) + setDimensions(channels, stackSize/channels, 1); + else + setDimensions(channels, z, t); + setStack(imp.getTitle(), stack2); + setCalibration(imp.getCalibration()); + FileInfo fi = imp.getOriginalFileInfo(); + if (fi!=null) { + displayRanges = fi.displayRanges; + channelLuts = fi.channelLuts; + } + setFileInfo(fi); + Object info = imp.getProperty("Info"); + if (info!=null) + setProperty("Info", imp.getProperty("Info")); + setProperties(imp.getPropertiesAsArray()); + if (mode==COMPOSITE) { + for (int i=0; i0 && (stack2.getProcessor(1) instanceof ColorProcessor)) { // RGB? + cip = null; + lut = null; + return; + } + setupLuts(channels); + if (mode==COMPOSITE) { + cip = new ImageProcessor[channels]; + for (int i=0; iMAX_CHANNELS?createLutFromColor(Color.white):null; + for (int i=0; istack2.getSize() || channels>MAX_CHANNELS) + return; + for (int i=0; inChannels) ch = nChannels; + boolean newChannel = false; + if (ch-1!=currentChannel) { + previousChannel = currentChannel; + currentChannel = ch-1; + newChannel = true; + } + + ImageProcessor ip = getProcessor(); + if (mode!=COMPOSITE) { + if (newChannel) { + setupLuts(nChannels); + LUT cm = lut[currentChannel]; + if (ip!=null && !(ip instanceof ColorProcessor)) { + if (mode==COLOR) + ip.setLut(cm); + if (!(cm.min==0.0&&cm.max==0.0)) + ip.setMinAndMax(cm.min, cm.max); + } + if (!IJ.isMacro()) ContrastAdjuster.update(); + for (int i=0; i=nChannels) { + setSlice(1); + currentChannel = 0; + newChannel = true; + } + bitDepth = getBitDepth(); + } + + if (newChannel) { + getProcessor().setMinAndMax(cip[currentChannel].getMin(), cip[currentChannel].getMax()); + if (!IJ.isMacro()) ContrastAdjuster.update(); + } + //IJ.log(nChannels+" "+ch+" "+currentChannel+" "+newChannel); + + if (getSlice()!=currentSlice || getFrame()!=currentFrame) { + currentSlice = getSlice(); + currentFrame = getFrame(); + int position = getStackIndex(1, currentSlice, currentFrame); + if (cip==null) return; + for (int i=0; icip.length) + return; + for (int i=1; icip.length) + return null; + else + return cip[channel-1]; + } + + public boolean[] getActiveChannels() { + return active; + } + + public synchronized void setMode(int mode) { + if (modeGRAYSCALE) + return; + if (mode==COMPOSITE && getNChannels()>MAX_CHANNELS) + mode = COLOR; + for (int i=0; ilut.length) + throw new IllegalArgumentException("Channel out of range: "+channel); + return lut[channel-1]; + } + + /* Returns the LUT used by the current channel. */ + public LUT getChannelLut() { + int c = getChannelIndex(); + return lut[c]; + } + + /* Returns a copy of this image's channel LUTs as an array. */ + public LUT[] getLuts() { + int channels = getNChannels(); + if (lut==null) + setupLuts(channels); + LUT[] luts = new LUT[channels]; + for (int i=0; iMAX_CHANNELS && getMode()==COMPOSITE) + setMode(COLOR); + setup(nChannels, getImageStack()); + } + + public void completeReset() { + cip = null; + lut = null; + } + + /* Sets the LUT of the current channel. */ + public void setChannelLut(LUT table) { + int c = getChannelIndex(); + double min = lut[c].min; + double max = lut[c].max; + lut[c] = table; + lut[c].min = min; + lut[c].max = max; + if (mode==COMPOSITE && cip!=null && clut.length) + throw new IllegalArgumentException("Channel out of range"); + lut[channel-1] = (LUT)table.clone(); + if (getWindow()!=null && channel==getChannel()) + getProcessor().setLut(lut[channel-1]); + if (cip!=null && cip.length>=channel && cip[channel-1]!=null) + cip[channel-1].setLut(lut[channel-1]); + else + cip = null; + customLuts = true; + } + + /* Sets the IndexColorModel of the current channel. */ + public void setChannelColorModel(IndexColorModel cm) { + setChannelLut(new LUT(cm,0.0,0.0)); + } + + public void setDisplayRange(double min, double max) { + ip.setMinAndMax(min, max); + int c = getChannelIndex(); + lut[c].min = min; + lut[c].max = max; + if (getWindow()==null && cip!=null && c0) synchronized (listeners) { + for (int i=0; i0 && command.charAt(len-1)!=']') + IJ.setKeyUp(IJ.ALL_KEYS); // set keys up except for "<", ">", "+" and "-" shortcuts + } catch(Throwable e) { + IJ.showStatus(""); + IJ.showProgress(1, 1); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) imp.unlock(); + String msg = e.getMessage(); + if (e instanceof OutOfMemoryError) + IJ.outOfMemory(command); + else if (e instanceof RuntimeException && msg!=null && msg.equals(Macro.MACRO_CANCELED)) + ; //do nothing + else { + CharArrayWriter caw = new CharArrayWriter(); + PrintWriter pw = new PrintWriter(caw); + e.printStackTrace(pw); + String s = caw.toString(); + if (IJ.isMacintosh()) { + if (s.indexOf("ThreadDeath")>0) + return; + s = Tools.fixNewLines(s); + } + int w=500, h=340; + if (s.indexOf("UnsupportedClassVersionError")!=-1) { + if (s.indexOf("version 49.0")!=-1) { + s = e + "\n \nThis plugin requires Java 1.5 or later."; + w=700; h=150; + } + if (s.indexOf("version 50.0")!=-1) { + s = e + "\n \nThis plugin requires Java 1.6 or later."; + w=700; h=150; + } + if (s.indexOf("version 51.0")!=-1) { + s = e + "\n \nThis plugin requires Java 1.7 or later."; + w=700; h=150; + } + if (s.indexOf("version 52.0")!=-1) { + s = e + "\n \nThis plugin requires Java 1.8 or later."; + w=700; h=150; + } + } + if (IJ.getInstance()!=null) { + s = IJ.getInstance().getInfo()+"\n \n"+s; + new TextWindow("Exception", s, w, h); + } else + IJ.log(s); + } + } finally { + if (thread!=null) + WindowManager.setTempCurrentImage(null); + } + } + + void runCommand(String cmd) { + Hashtable table = Menus.getCommands(); + String className = (String)table.get(cmd); + if (className!=null) { + String arg = ""; + if (className.endsWith("\")")) { + // extract string argument (e.g. className("arg")) + int argStart = className.lastIndexOf("(\""); + if (argStart>0) { + arg = className.substring(argStart+2, className.length()-2); + className = className.substring(0, argStart); + } + } + if (Prefs.nonBlockingFilterDialogs) { + // we have the plugin class name, let us see whether it is allowed to run it + ImagePlus imp = WindowManager.getCurrentImage(); + boolean imageLocked = imp!=null && imp.isLockedByAnotherThread(); + if (imageLocked && !allowedWithLockedImage(className)) { + IJ.beep(); + IJ.showStatus("\""+cmd + "\" blocked because \"" + imp.getTitle() + "\" is locked"); + return; + } + } + // run the plugin + if (IJ.shiftKeyDown() && className.startsWith("ij.plugin.Macro_Runner") && !Menus.getShortcuts().contains("*"+cmd)) + IJ.open(IJ.getDirectory("plugins")+arg); + else + IJ.runPlugIn(cmd, className, arg); + } else { // command is not a plugin + // is command in the Plugins>Macros menu? + if (MacroInstaller.runMacroCommand(cmd)) + return; + // is it in the Image>Lookup Tables menu? + if (loadLut(cmd)) + return; + // is it in the File>Open Recent menu? + if (openRecent(cmd)) + return; + // is it an example in Help>Examples menu? + if (Editor.openExample(cmd)) + return; + if ("Auto Threshold".equals(cmd)&&(String)table.get("Auto Threshold...")!=null) + runCommand("Auto Threshold..."); + else if ("Enhance Local Contrast (CLAHE)".equals(cmd)&&(String)table.get("CLAHE ")!=null) + runCommand("CLAHE "); + else { + if ("Table...".equals(cmd)) + IJ.runPlugIn("ij.plugin.NewPlugin", "table"); + else { + if (repeatingCommand) + IJ.runMacro(previousCommand); + else { + if (!extraCommand(cmd)) + IJ.error("Unrecognized command: \"" + cmd+"\""); + } + } + } + } + } + + private boolean extraCommand(String cmd) { + if (cmd!=null && cmd.equals("Duplicate Image...")) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + Duplicator.ignoreNextSelection(); + IJ.run(imp, "Duplicate...", ""); + } else + IJ.noImage(); + return true; + } else + return false; + } + + /** If the foreground image is locked during a filter operation with NonBlockingGenericDialog, + * the following plugins are allowed */ + boolean allowedWithLockedImage(String className) { + return className.equals("ij.plugin.Zoom") || + className.equals("ij.plugin.frame.ContrastAdjuster") || + className.equals("ij.plugin.SimpleCommands") || //includes Plugins>Utiltites>Reset (needed to reset a locked image) + className.equals("ij.plugin.WindowOrganizer") || + className.equals("ij.plugin.URLOpener"); + } + + /** Opens a .lut file from the ImageJ/luts directory and returns 'true' if successful. */ + public static boolean loadLut(String name) { + String path = IJ.getDirectory("luts")+name.replace(" ","_")+".lut"; + File f = new File(path); + if (!f.exists()) { + path = IJ.getDirectory("luts")+name+".lut"; + f = new File(path); + } + if (!f.exists()) { + path = IJ.getDirectory("luts")+name.toLowerCase().replace(" ","_")+".lut"; + f = new File(path); + } + if (!f.exists() && Character.isLowerCase(name.charAt(0))) { + String name2 = name.substring(0,1).toUpperCase()+name.substring(1); + path = IJ.getDirectory("luts")+name2+".lut"; + f = new File(path); + } + if (!f.exists() && name.toLowerCase().equals("viridis")) { + path = IJ.getDirectory("luts")+"mpl-viridis.lut"; //Fiji version + f = new File(path); + } + if (f.exists()) { + String dir = OpenDialog.getLastDirectory(); + IJ.open(path); + OpenDialog.setLastDirectory(dir); + return true; + } + return false; + } + + /** Opens a file from the File/Open Recent menu + and returns 'true' if successful. */ + boolean openRecent(String cmd) { + Menu menu = Menus.getOpenRecentMenu(); + if (menu==null) return false; + for (int i=0; imacro + on the current thread. Returns any string value returned by + the macro, null if the macro does not return a value, or + "[aborted]" if the macro was aborted due to an error. The + equivalent macro function is eval(). */ + public static String runMacro(String macro) { + return runMacro(macro, ""); + } + + /** Runs the macro contained in the string macro + on the current thread. The optional string argument can be + retrieved in the called macro using the getArgument() macro + function. Returns any string value returned by the macro, null + if the macro does not return a value, or "[aborted]" if the + macro was aborted due to an error. */ + public static String runMacro(String macro, String arg) { + Macro_Runner mr = new Macro_Runner(); + return mr.runMacro(macro, arg); + } + + /** Runs the specified macro or script file in the current thread. + The file is assumed to be in the macros folder + unless name is a full path. + The optional string argument (arg) can be retrieved in the called + macro or script using the getArgument() function. + Returns any string value returned by the macro, or null. Scripts always return null. + The equivalent macro function is runMacro(). */ + public static String runMacroFile(String name, String arg) { + Macro_Runner mr = new Macro_Runner(); + return mr.runMacroFile(name, arg); + } + + /** Runs the specified macro file. */ + public static String runMacroFile(String name) { + return runMacroFile(name, null); + } + + /** Runs the specified plugin using the specified image. */ + public static Object runPlugIn(ImagePlus imp, String className, String arg) { + if (imp!=null) { + ImagePlus temp = WindowManager.getTempCurrentImage(); + WindowManager.setTempCurrentImage(imp); + Object o = runPlugIn("", className, arg); + WindowManager.setTempCurrentImage(temp); + return o; + } else + return runPlugIn(className, arg); + } + + /** Runs the specified plugin and returns a reference to it. */ + public static Object runPlugIn(String className, String arg) { + return runPlugIn("", className, arg); + } + + /** Runs the specified plugin and returns a reference to it. */ + public static Object runPlugIn(String commandName, String className, String arg) { + if (arg==null) arg = ""; + if (IJ.debugMode) + IJ.log("runPlugIn: "+className+argument(arg)); + // Load using custom classloader if this is a user + // plugin and we are not running as an applet + if (!className.startsWith("ij.") && applet==null) + return runUserPlugIn(commandName, className, arg, false); + Object thePlugIn=null; + try { + Class c = Class.forName(className); + thePlugIn = c.newInstance(); + if (thePlugIn instanceof PlugIn) + ((PlugIn)thePlugIn).run(arg); + else + new PlugInFilterRunner(thePlugIn, commandName, arg); + } catch (ClassNotFoundException e) { + if (!(className!=null && className.startsWith("ij.plugin.MacAdapter"))) { + log("Plugin or class not found: \"" + className + "\"\n(" + e+")"); + String path = Prefs.getCustomPropsPath(); + if (path!=null); + log("Error may be due to custom properties at " + path); + } + } + catch (InstantiationException e) {log("Unable to load plugin (ins)");} + catch (IllegalAccessException e) {log("Unable to load plugin, possibly \nbecause it is not public.");} + redirectErrorMessages = false; + return thePlugIn; + } + + static Object runUserPlugIn(String commandName, String className, String arg, boolean createNewLoader) { + if (IJ.debugMode) + IJ.log("runUserPlugIn: "+className+", arg="+argument(arg)); + if (applet!=null) return null; + if (checkForDuplicatePlugins) { + // check for duplicate classes and jars in the plugins folder + IJ.runPlugIn("ij.plugin.ClassChecker", ""); + checkForDuplicatePlugins = false; + } + if (createNewLoader) + classLoader = null; + ClassLoader loader = getClassLoader(); + Object thePlugIn = null; + try { + thePlugIn = (loader.loadClass(className)).newInstance(); + if (thePlugIn instanceof PlugIn) + ((PlugIn)thePlugIn).run(arg); + else if (thePlugIn instanceof PlugInFilter) + new PlugInFilterRunner(thePlugIn, commandName, arg); + } + catch (ClassNotFoundException e) { + if (className.startsWith("macro:")) + runMacro(className.substring(6)); + else if (className.contains("_") && !suppressPluginNotFoundError) + error("Plugin or class not found: \"" + className + "\"\n(" + e+")"); + } + catch (NoClassDefFoundError e) { + int dotIndex = className.indexOf('.'); + if (dotIndex>=0 && className.contains("_")) { + // rerun plugin after removing folder name + if (debugMode) IJ.log("runUserPlugIn: rerunning "+className); + return runUserPlugIn(commandName, className.substring(dotIndex+1), arg, createNewLoader); + } + if (className.contains("_") && !suppressPluginNotFoundError) + error("Run User Plugin", "Class not found while attempting to run \"" + className + "\"\n \n " + e); + } + catch (InstantiationException e) {error("Unable to load plugin (ins)");} + catch (IllegalAccessException e) {error("Unable to load plugin, possibly \nbecause it is not public.");} + if (thePlugIn!=null && !"HandleExtraFileTypes".equals(className)) + redirectErrorMessages = false; + suppressPluginNotFoundError = false; + return thePlugIn; + } + + private static String argument(String arg) { + return arg!=null && !arg.equals("") && !arg.contains("\n")?"(\""+arg+"\")":""; + } + + static void wrongType(int capabilities, String cmd) { + String s = "\""+cmd+"\" requires an image of type:\n \n"; + if ((capabilities&PlugInFilter.DOES_8G)!=0) s += " 8-bit grayscale\n"; + if ((capabilities&PlugInFilter.DOES_8C)!=0) s += " 8-bit color\n"; + if ((capabilities&PlugInFilter.DOES_16)!=0) s += " 16-bit grayscale\n"; + if ((capabilities&PlugInFilter.DOES_32)!=0) s += " 32-bit (float) grayscale\n"; + if ((capabilities&PlugInFilter.DOES_RGB)!=0) s += " RGB color\n"; + error(s); + } + + /** Runs a menu command on a separete thread and returns immediately. */ + public static void doCommand(String command) { + new Executer(command, null); + } + + /** Runs a menu command on a separete thread, using the specified image. */ + public static void doCommand(ImagePlus imp, String command) { + new Executer(command, imp); + } + + /** Runs an ImageJ command. Does not return until + the command has finished executing. To avoid "image locked", + errors, plugins that call this method should implement + the PlugIn interface instead of PlugInFilter. */ + public static void run(String command) { + run(command, null); + } + + /** Runs an ImageJ command, with options that are passed to the + GenericDialog and OpenDialog classes. Does not return until + the command has finished executing. To generate run() calls, + start the recorder (Plugins/Macro/Record) and run commands + from the ImageJ menu bar. + */ + public static void run(String command, String options) { + //IJ.log("run1: "+command+" "+Thread.currentThread().hashCode()+" "+options); + if (ij==null && Menus.getCommands()==null) + init(); + Macro.abort = false; + Macro.setOptions(options); + Thread thread = Thread.currentThread(); + if (previousThread==null || thread!=previousThread) { + String name = thread.getName(); + if (!name.startsWith("Run$_")) + thread.setName("Run$_"+name); + } + command = convert(command); + previousThread = thread; + macroRunning = true; + Executer e = new Executer(command); + e.run(); + macroRunning = false; + Macro.setOptions(null); + testAbort(); + macroInterpreter = null; + //IJ.log("run2: "+command+" "+Thread.currentThread().hashCode()); + } + + /** The macro interpreter uses this method to run commands. */ + public static void run(Interpreter interpreter, String command, String options) { + macroInterpreter = interpreter; + run(command, options); + macroInterpreter = null; + } + + /** Converts commands that have been renamed so + macros using the old names continue to work. */ + private static String convert(String command) { + if (commandTable==null) { + commandTable = new Hashtable(30); + commandTable.put("New...", "Image..."); + commandTable.put("Threshold", "Make Binary"); + commandTable.put("Display...", "Appearance..."); + commandTable.put("Start Animation", "Start Animation [\\]"); + commandTable.put("Convert Images to Stack", "Images to Stack"); + commandTable.put("Convert Stack to Images", "Stack to Images"); + commandTable.put("Convert Stack to RGB", "Stack to RGB"); + commandTable.put("Convert to Composite", "Make Composite"); + commandTable.put("RGB Split", "Split Channels"); + commandTable.put("RGB Merge...", "Merge Channels..."); + commandTable.put("Channels...", "Channels Tool..."); + commandTable.put("New... ", "Table..."); + commandTable.put("Arbitrarily...", "Rotate... "); + commandTable.put("Measurements...", "Results... "); + commandTable.put("List Commands...", "Find Commands..."); + commandTable.put("Capture Screen ", "Capture Screen"); + commandTable.put("Add to Manager ", "Add to Manager"); + commandTable.put("In", "In [+]"); + commandTable.put("Out", "Out [-]"); + commandTable.put("Enhance Contrast", "Enhance Contrast..."); + commandTable.put("XY Coodinates... ", "XY Coordinates... "); + commandTable.put("Statistics...", "Statistics"); + commandTable.put("Channels Tool... ", "Channels Tool..."); + commandTable.put("Profile Plot Options...", "Plots..."); + commandTable.put("AuPbSn 40 (56K)", "AuPbSn 40"); + commandTable.put("Bat Cochlea Volume (19K)", "Bat Cochlea Volume"); + commandTable.put("Bat Cochlea Renderings (449K)", "Bat Cochlea Renderings"); + commandTable.put("Blobs (25K)", "Blobs"); + commandTable.put("Boats (356K)", "Boats"); + commandTable.put("Cardio (768K, RGB DICOM)", "Cardio (RGB DICOM)"); + commandTable.put("Cell Colony (31K)", "Cell Colony"); + commandTable.put("Clown (14K)", "Clown"); + commandTable.put("Confocal Series (2.2MB)", "Confocal Series"); + commandTable.put("CT (420K, 16-bit DICOM)", "CT (16-bit DICOM)"); + commandTable.put("Dot Blot (7K)", "Dot Blot"); + commandTable.put("Embryos (42K)", "Embryos"); + commandTable.put("Fluorescent Cells (400K)", "Fluorescent Cells"); + commandTable.put("Fly Brain (1MB)", "Fly Brain"); + commandTable.put("Gel (105K)", "Gel"); + commandTable.put("HeLa Cells (1.3M, 48-bit RGB)", "HeLa Cells (48-bit RGB)"); + commandTable.put("Leaf (36K)", "Leaf"); + commandTable.put("Line Graph (21K)", "Line Graph"); + commandTable.put("Mitosis (26MB, 5D stack)", "Mitosis (5D stack)"); + commandTable.put("MRI Stack (528K)", "MRI Stack"); + commandTable.put("M51 Galaxy (177K, 16-bits)", "M51 Galaxy (16-bits)"); + commandTable.put("Neuron (1.6M, 5 channels)", "Neuron (5 channels)"); + commandTable.put("Nile Bend (1.9M)", "Nile Bend"); + commandTable.put("Organ of Corti (2.8M, 4D stack)", "Organ of Corti (4D stack)"); + commandTable.put("Particles (75K)", "Particles"); + commandTable.put("T1 Head (2.4M, 16-bits)", "T1 Head (16-bits)"); + commandTable.put("T1 Head Renderings (736K)", "T1 Head Renderings"); + commandTable.put("TEM Filter (112K)", "TEM Filter"); + commandTable.put("Tree Rings (48K)", "Tree Rings"); + } + String command2 = (String)commandTable.get(command); + if (command2!=null) + return command2; + else + return command; + } + + /** Runs an ImageJ command using the specified image and options. + To generate run() calls, start the recorder (Plugins/Macro/Record) + and run commands from the ImageJ menu bar.*/ + public static void run(ImagePlus imp, String command, String options) { + if (ij==null && Menus.getCommands()==null) + init(); + if (imp!=null) { + ImagePlus temp = WindowManager.getTempCurrentImage(); + WindowManager.setTempCurrentImage(imp); + run(command, options); + WindowManager.setTempCurrentImage(temp); + } else + run(command, options); + } + + static void init() { + Menus m = new Menus(null, null); + Prefs.load(m, null); + m.addMenuBar(); + } + + private static void testAbort() { + if (Macro.abort) + abort(); + } + + /** Returns true if the run(), open() or newImage() method is executing. */ + public static boolean macroRunning() { + return macroRunning; + } + + /** Returns true if a macro is running, or if the run(), open() + or newImage() method is executing. */ + public static boolean isMacro() { + return macroRunning || Interpreter.getInstance()!=null; + } + + /**Returns the Applet that created this ImageJ or null if running as an application.*/ + public static java.applet.Applet getApplet() { + return applet; + } + + /**Displays a message in the ImageJ status bar. If 's' starts + with '!', subsequent showStatus() calls in the current + thread (without "!" in the message) are suppressed. */ + public static void showStatus(String s) { + if ((Interpreter.getInstance()==null&&statusBarThread==null) + || (statusBarThread!=null&&Thread.currentThread()!=statusBarThread)) + protectStatusBar(false); + boolean doProtect = s.startsWith("!"); // suppress subsequent showStatus() calls + if (doProtect) { + protectStatusBar(true); + statusBarThread = Thread.currentThread(); + s = s.substring(1); + } + if (doProtect || !protectStatusBar) { + if (ij!=null) + ij.showStatus(s); + ImagePlus imp = WindowManager.getCurrentImage(); + ImageCanvas ic = imp!=null?imp.getCanvas():null; + if (ic!=null) + ic.setShowCursorStatus(s.length()==0?true:false); + } + } + + /**Displays a message in the status bar and flashes + * either the status bar or the active image.
+ * See: http://wsr.imagej.net/macros/FlashingStatusMessages.txt + */ + public static void showStatus(String message, String options) { + showStatus(message); + if (options==null) + return; + options = options.replace("flash", ""); + options = options.replace("ms", ""); + Color optionalColor = null; + int index1 = options.indexOf("#"); + if (index1>=0) { // hex color? + int index2 = options.indexOf(" ", index1); + if (index2==-1) index2 = options.length(); + String hexColor = options.substring(index1, index2); + optionalColor = Colors.decode(hexColor, null); + options = options.replace(hexColor, ""); + } + if (optionalColor==null) { // "red", "green", etc. + for (String c : Colors.colors) { + if (options.contains(c)) { + optionalColor = Colors.getColor(c, ImageJ.backgroundColor); + options = options.replace(c, ""); + break; + } + } + } + boolean flashImage = options.contains("image"); + Color defaultColor = new Color(255,255,245); + int defaultDelay = 500; + ImagePlus imp = WindowManager.getCurrentImage(); + if (flashImage) { + options = options.replace("image", ""); + if (imp!=null && imp.getWindow()!=null) { + defaultColor = Color.black; + defaultDelay = 100; + } + else + flashImage = false; + } + Color color = optionalColor!=null?optionalColor:defaultColor; + int delay = (int)Tools.parseDouble(options, defaultDelay); + if (delay>8000) + delay = 8000; + String colorString = null; + ImageJ ij = IJ.getInstance(); + if (flashImage) { + Color previousColor = imp.getWindow().getBackground(); + imp.getWindow().setBackground(color); + if (delay>0) { + wait(delay); + imp.getWindow().setBackground(previousColor); + } + } else if (ij!=null) { + ij.getStatusBar().setBackground(color); + wait(delay); + ij.getStatusBar().setBackground(ij.backgroundColor); + } + } + + /** + * @deprecated + * replaced by IJ.log(), ResultsTable.setResult() and TextWindow.append(). + * There are examples at + * http://imagej.nih.gov/ij/plugins/sine-cosine.html + */ + public static void write(String s) { + if (textPanel==null && ij!=null) + showResults(); + if (textPanel!=null) + textPanel.append(s); + else + System.out.println(s); + } + + private static void showResults() { + TextWindow resultsWindow = new TextWindow("Results", "", 400, 250); + textPanel = resultsWindow.getTextPanel(); + textPanel.setResultsTable(Analyzer.getResultsTable()); + } + + public static synchronized void log(String s) { + if (s==null) return; + if (logPanel==null && ij!=null) { + TextWindow logWindow = new TextWindow("Log", "", 400, 250); + logPanel = logWindow.getTextPanel(); + logPanel.setFont(new Font("SansSerif", Font.PLAIN, 16)); + } + if (logPanel!=null) { + if (s.startsWith("\\")) + handleLogCommand(s); + else { + if (s.endsWith("\n")) { + if (s.equals("\n\n")) + s= "\n \n "; + else if (s.endsWith("\n\n")) + s = s.substring(0, s.length()-2)+"\n \n "; + else + s = s+" "; + } + logPanel.append(s); + } + } else { + LogStream.redirectSystem(false); + System.out.println(s); + } + } + + static void handleLogCommand(String s) { + if (s.equals("\\Closed")) + logPanel = null; + else if (s.startsWith("\\Update:")) { + int n = logPanel.getLineCount(); + String s2 = s.substring(8, s.length()); + if (n==0) + logPanel.append(s2); + else + logPanel.setLine(n-1, s2); + } else if (s.startsWith("\\Update")) { + int cindex = s.indexOf(":"); + if (cindex==-1) + {logPanel.append(s); return;} + String nstr = s.substring(7, cindex); + int line = (int)Tools.parseDouble(nstr, -1); + if (line<0 || line>25) + {logPanel.append(s); return;} + int count = logPanel.getLineCount(); + while (line>=count) { + log(""); + count++; + } + String s2 = s.substring(cindex+1, s.length()); + logPanel.setLine(line, s2); + } else if (s.equals("\\Clear")) { + logPanel.clear(); + } else if (s.startsWith("\\Heading:")) { + logPanel.updateColumnHeadings(s.substring(10)); + } else if (s.equals("\\Close")) { + Frame f = WindowManager.getFrame("Log"); + if (f!=null && (f instanceof TextWindow)) + ((TextWindow)f).close(); + } else + logPanel.append(s); + } + + /** Returns the contents of the Log window or null if the Log window is not open. */ + public static synchronized String getLog() { + if (logPanel==null || ij==null) + return null; + else + return logPanel.getText(); + } + +/** Clears the "Results" window and sets the column headings to + those in the tab-delimited 'headings' String. Writes to + System.out.println if the "ImageJ" frame is not present.*/ + public static void setColumnHeadings(String headings) { + if (textPanel==null && ij!=null) + showResults(); + if (textPanel!=null) + textPanel.setColumnHeadings(headings); + else + System.out.println(headings); + } + + /** Returns true if the "Results" window is open. */ + public static boolean isResultsWindow() { + return textPanel!=null; + } + + /** Renames a results window. */ + public static void renameResults(String title) { + Frame frame = WindowManager.getFrontWindow(); + if (frame!=null && (frame instanceof TextWindow)) { + TextWindow tw = (TextWindow)frame; + if (tw.getResultsTable()==null) { + IJ.error("Rename", "\""+tw.getTitle()+"\" is not a results table"); + return; + } + tw.rename(title); + } else if (isResultsWindow()) { + TextPanel tp = getTextPanel(); + TextWindow tw = (TextWindow)tp.getParent(); + tw.rename(title); + } + } + + /** Changes the name of a table window from 'oldTitle' to 'newTitle'. */ + public static void renameResults(String oldTitle, String newTitle) { + Frame frame = WindowManager.getFrame(oldTitle); + if (frame==null) { + error("Rename", "\""+oldTitle+"\" not found"); + return; + } else if (frame instanceof TextWindow) { + TextWindow tw = (TextWindow)frame; + if (tw.getResultsTable()==null) { + error("Rename", "\""+oldTitle+"\" is not a table"); + return; + } + tw.rename(newTitle); + } else + error("Rename", "\""+oldTitle+"\" is not a table"); + } + + /** Deletes 'row1' through 'row2' of the "Results" window, where + 'row1' and 'row2' must be in the range 0-Analyzer.getCounter()-1. */ + public static void deleteRows(int row1, int row2) { + ResultsTable rt = Analyzer.getResultsTable(); + int tableSize = rt.size(); + rt.deleteRows(row1, row2); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) + Overlay.updateTableOverlay(imp, row1, row2, tableSize); + rt.show("Results"); + } + + /** Returns a measurement result, where 'measurement' is "Area", + * "Mean", "StdDev", "Mode", "Min", "Max", "X", "Y", "XM", "YM", + * "Perim.", "BX", "BY", "Width", "Height", "Major", "Minor", "Angle", + * "Circ.", "Feret", "IntDen", "Median", "Skew", "Kurt", "%Area", + * "RawIntDen", "Ch", "Slice", "Frame", "FeretX", "FeretY", + * "FeretAngle", "MinFeret", "AR", "Round", "Solidity", "MinThr" + * or "MaxThr". Add " raw" to the argument to disable calibration, + * for example IJ.getValue("Mean raw"). Add " limit" to enable + * the "limit to threshold" option. + */ + public static double getValue(ImagePlus imp, String measurement) { + String options = ""; + int index = measurement.indexOf(" "); + if (index>0) { + if (index>>>>>>>>>>>>>>>>>>>>>>>>>>"); + log(""); + if (!memMessageDisplayed) { + log(""); + log(""); + log("Options>Memory & Threads command.>"); + log(">>>>>>>>>>>>>>>>>>>>>>>>>>>"); + memMessageDisplayed = true; + } + Macro.abort(); + } + + /** Updates the progress bar, where 0<=progress<=1.0. The progress bar is + not shown in BatchMode and erased if progress>=1.0. The progress bar is + updated only if more than 90 ms have passes since the last call. Does nothing + if the ImageJ window is not present. */ + public static void showProgress(double progress) { + if (progressBar!=null) progressBar.show(progress, false); + } + + /** Updates the progress bar, where the length of the bar is set to + * (currentValue+1)/finalValue of the maximum bar length. + * The bar is erased if currentValue>=finalValue. + * The bar is updated only if more than 90 ms have passed since the + * last call. Displays subordinate progress bars as dots if + * 'currentIndex' is negative (example: Plugins/Utilities/Benchmark). + */ + public static void showProgress(int currentIndex, int finalIndex) { + if (progressBar!=null) { + progressBar.show(currentIndex, finalIndex); + if (currentIndex==finalIndex) + progressBar.setBatchMode(false); + } + } + + /** Displays a message in a dialog box titled "Message". + Writes the Java console if ImageJ is not present. */ + public static void showMessage(String msg) { + showMessage("Message", msg); + } + + /** Displays a message in a dialog box with the specified title. + Displays HTML formatted text if 'msg' starts with "". + There are examples at + "http://imagej.nih.gov/ij/macros/HtmlDialogDemo.txt". + Writes to the Java console if ImageJ is not present. */ + public static void showMessage(String title, String msg) { + if (ij!=null) { + if (msg!=null && (msg.startsWith("")||msg.startsWith(""))) { + HTMLDialog hd = new HTMLDialog(title, msg); + if (isMacro() && hd.escapePressed()) + throw new RuntimeException(Macro.MACRO_CANCELED); + } else { + MessageDialog md = new MessageDialog(ij, title, msg); + if (isMacro() && md.escapePressed()) + throw new RuntimeException(Macro.MACRO_CANCELED); + } + } else + System.out.println(msg); + } + + /** Displays a message in a dialog box titled "ImageJ". If a + macro or JavaScript is running, it is aborted. Writes to the + Java console if the ImageJ window is not present.*/ + public static void error(String msg) { + error(null, msg); + if (Thread.currentThread().getName().endsWith("JavaScript")) + throw new RuntimeException(Macro.MACRO_CANCELED); + else + Macro.abort(); + } + + /** Displays a message in a dialog box with the specified title. If a + macro or JavaScript is running, it is aborted. Writes to the + Java console if the ImageJ window is not present. */ + public static void error(String title, String msg) { + if (macroInterpreter!=null) { + macroInterpreter.abort(msg); + macroInterpreter = null; + return; + } + if (msg!=null && msg.endsWith(Macro.MACRO_CANCELED)) + return; + String title2 = title!=null?title:"ImageJ"; + boolean abortMacro = title!=null; + lastErrorMessage = msg; + if (redirectErrorMessages) { + IJ.log(title2 + ": " + msg); + if (abortMacro && (title.contains("Open")||title.contains("Reader"))) + abortMacro = false; + } else + showMessage(title2, msg); + redirectErrorMessages = false; + if (abortMacro) + Macro.abort(); + } + + /** Aborts any currently running JavaScript, or use IJ.error(string) + to abort a JavaScript with a message. */ + public static void exit() { + if (Thread.currentThread().getName().endsWith("JavaScript")) + throw new RuntimeException(Macro.MACRO_CANCELED); + } + + /** + * Returns the last error message written by IJ.error() or null if there + * was no error since the last time this method was called. + * @see #error(String) + */ + public static String getErrorMessage() { + String msg = lastErrorMessage; + lastErrorMessage = null; + return msg; + } + + /** Displays a message in a dialog box with the specified title. + Returns false if the user pressed "Cancel". */ + public static boolean showMessageWithCancel(String title, String msg) { + GenericDialog gd = new GenericDialog(title); + gd.addMessage(msg); + gd.showDialog(); + return !gd.wasCanceled(); + } + + public static final int CANCELED = Integer.MIN_VALUE; + + /** Allows the user to enter a number in a dialog box. Returns the + value IJ.CANCELED (-2,147,483,648) if the user cancels the dialog box. + Returns 'defaultValue' if the user enters an invalid number. */ + public static double getNumber(String prompt, double defaultValue) { + GenericDialog gd = new GenericDialog(""); + int decimalPlaces = (int)defaultValue==defaultValue?0:2; + gd.addNumericField(prompt, defaultValue, decimalPlaces); + gd.showDialog(); + if (gd.wasCanceled()) + return CANCELED; + double v = gd.getNextNumber(); + if (gd.invalidNumber()) + return defaultValue; + else + return v; + } + + /** Allows the user to enter a string in a dialog box. Returns + "" if the user cancels the dialog box. */ + public static String getString(String prompt, String defaultString) { + GenericDialog gd = new GenericDialog(""); + gd.addStringField(prompt, defaultString, 20); + gd.showDialog(); + if (gd.wasCanceled()) + return ""; + return gd.getNextString(); + } + + /**Delays 'msecs' milliseconds.*/ + public static void wait(int msecs) { + try {Thread.sleep(msecs);} + catch (InterruptedException e) { } + } + + /** Emits an audio beep. */ + public static void beep() { + java.awt.Toolkit.getDefaultToolkit().beep(); + } + + /** Returns a string something like "64K of 256MB (25%)" + * that shows how much of the available memory is in use. + * This is the string displayed when the user clicks in the + * status bar. + */ + public static String freeMemory() { + long inUse = currentMemory(); + String inUseStr = inUse<10000*1024?inUse/1024L+"K":inUse/1048576L+"MB"; + String maxStr=""; + long max = maxMemory(); + if (max>0L) { + double percent = inUse*100/max; + maxStr = " of "+max/1048576L+"MB ("+(percent<1.0?"<1":d2s(percent,0)) + "%)"; + } + return inUseStr + maxStr; + } + + /** Returns the amount of memory currently being used by ImageJ. */ + public static long currentMemory() { + long freeMem = Runtime.getRuntime().freeMemory(); + long totMem = Runtime.getRuntime().totalMemory(); + return totMem-freeMem; + } + + /** Returns the maximum amount of memory available to ImageJ or + zero if ImageJ is unable to determine this limit. */ + public static long maxMemory() { + if (maxMemory==0L) { + Memory mem = new Memory(); + maxMemory = mem.getMemorySetting(); + if (maxMemory==0L) maxMemory = mem.maxMemory(); + } + return maxMemory; + } + + public static void showTime(ImagePlus imp, long start, String str) { + showTime(imp, start, str, 1); + } + + public static void showTime(ImagePlus imp, long start, String str, int nslices) { + if (Interpreter.isBatchMode()) + return; + double seconds = (System.currentTimeMillis()-start)/1000.0; + if (seconds<=0.5 && macroRunning()) + return; + double pixels = (double)imp.getWidth() * imp.getHeight(); + double rate = pixels*nslices/seconds; + String str2; + if (rate>1000000000.0) + str2 = ""; + else if (rate<1000000.0) + str2 = ", "+d2s(rate,0)+" pixels/second"; + else + str2 = ", "+d2s(rate/1000000.0,1)+" million pixels/second"; + showStatus(str+seconds+" seconds"+str2); + } + + /** Experimental */ + public static String time(ImagePlus imp, long startNanoTime) { + double planes = imp.getStackSize(); + double seconds = (System.nanoTime()-startNanoTime)/1000000000.0; + double mpixels = imp.getWidth()*imp.getHeight()*planes/1000000.0; + String time = seconds<1.0?d2s(seconds*1000.0,0)+" ms":d2s(seconds,1)+" seconds"; + return time+", "+d2s(mpixels/seconds,1)+" million pixels/second"; + } + + /** Converts a number to a formatted string using + 2 digits to the right of the decimal point. */ + public static String d2s(double n) { + return d2s(n, 2); + } + + /** Converts a number to a rounded formatted string. + The 'decimalPlaces' argument specifies the number of + digits to the right of the decimal point (0-9). Uses + scientific notation if 'decimalPlaces is negative. */ + public static String d2s(double n, int decimalPlaces) { + if (Double.isNaN(n)||Double.isInfinite(n)) + return ""+n; + if (n==Float.MAX_VALUE) // divide by 0 in FloatProcessor + return "3.4e38"; + double np = n; + if (n<0.0) np = -n; + if (decimalPlaces<0) synchronized(IJ.class) { + decimalPlaces = -decimalPlaces; + if (decimalPlaces>9) decimalPlaces=9; + if (sf==null) { + if (dfs==null) + dfs = new DecimalFormatSymbols(Locale.US); + sf = new DecimalFormat[10]; + sf[1] = new DecimalFormat("0.0E0",dfs); + sf[2] = new DecimalFormat("0.00E0",dfs); + sf[3] = new DecimalFormat("0.000E0",dfs); + sf[4] = new DecimalFormat("0.0000E0",dfs); + sf[5] = new DecimalFormat("0.00000E0",dfs); + sf[6] = new DecimalFormat("0.000000E0",dfs); + sf[7] = new DecimalFormat("0.0000000E0",dfs); + sf[8] = new DecimalFormat("0.00000000E0",dfs); + sf[9] = new DecimalFormat("0.000000000E0",dfs); + } + return sf[decimalPlaces].format(n); // use scientific notation + } + if (decimalPlaces<0) decimalPlaces = 0; + if (decimalPlaces>9) decimalPlaces = 9; + return df[decimalPlaces].format(n); + } + + /** Converts a number to a rounded formatted string. + * The 'significantDigits' argument specifies the minimum number + * of significant digits, which is also the preferred number of + * digits behind the decimal. Fewer decimals are shown if the + * number would have more than 'maxDigits'. + * Exponential notation is used if more than 'maxDigits' would be needed. + */ + public static String d2s(double x, int significantDigits, int maxDigits) { + double log10 = Math.log10(Math.abs(x)); + double roundErrorAtMax = 0.223*Math.pow(10, -maxDigits); + int magnitude = (int)Math.ceil(log10+roundErrorAtMax); + int decimals = x==0 ? 0 : maxDigits - magnitude; + if (decimals<0 || magnitudesignificantDigits) + decimals = Math.max(significantDigits, decimals-maxDigits+significantDigits); + return IJ.d2s(x, decimals); + } + } + + /** Pad 'n' with leading zeros to the specified number of digits. */ + public static String pad(int n, int digits) { + String str = ""+n; + while (str.length()= 6; + } + + /** Returns true if ImageJ is running on a Java 1.7 or greater JVM. */ + public static boolean isJava17() { + return javaVersion >= 7; + } + + /** Returns true if ImageJ is running on a Java 1.8 or greater JVM. */ + public static boolean isJava18() { + return javaVersion >= 8; + } + + /** Returns true if ImageJ is running on a Java 1.9 or greater JVM. */ + public static boolean isJava19() { + return javaVersion >= 9; + } + + /** Returns true if ImageJ is running on Linux. */ + public static boolean isLinux() { + return isLinux; + } + + /** Obsolete; always returns false. */ + public static boolean isVista() { + return false; + } + + /** Returns true if ImageJ is running a 64-bit version of Java. */ + public static boolean is64Bit() { + if (osarch==null) + osarch = System.getProperty("os.arch"); + return osarch!=null && osarch.indexOf("64")!=-1; + } + + /** Displays an error message and returns true if the + ImageJ version is less than the one specified. */ + public static boolean versionLessThan(String version) { + boolean lessThan = ImageJ.VERSION.compareTo(version)<0; + if (lessThan) + error("This plugin or macro requires ImageJ "+version+" or later. Use\nHelp>Update ImageJ to upgrade to the latest version."); + return lessThan; + } + + /** Displays a "Process all images?" dialog. Returns + 'flags'+PlugInFilter.DOES_STACKS if the user selects "Yes", + 'flags' if the user selects "No" and PlugInFilter.DONE + if the user selects "Cancel". + */ + public static int setupDialog(ImagePlus imp, int flags) { + if (imp==null || (ij!=null&&ij.hotkey)) { + if (ij!=null) ij.hotkey=false; + return flags; + } + int stackSize = imp.getStackSize(); + if (stackSize>1) { + String macroOptions = Macro.getOptions(); + if (imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.COMPOSITE) { + if (macroOptions==null || !macroOptions.contains("slice")) + return flags | PlugInFilter.DOES_STACKS; + } + if (macroOptions!=null) { + if (macroOptions.indexOf("stack ")>=0) + return flags | PlugInFilter.DOES_STACKS; + else + return flags; + } + if (hideProcessStackDialog) + return flags; + String note = ((flags&PlugInFilter.NO_CHANGES)==0)?" There is\nno Undo if you select \"Yes\".":""; + YesNoCancelDialog d = new YesNoCancelDialog(getInstance(), + "Process Stack?", "Process all "+stackSize+" images?"+note); + if (d.cancelPressed()) + return PlugInFilter.DONE; + else if (d.yesPressed()) { + if (imp.getStack().isVirtual() && ((flags&PlugInFilter.NO_CHANGES)==0)) { + int size = (stackSize*imp.getWidth()*imp.getHeight()*imp.getBytesPerPixel()+524288)/1048576; + String msg = + "Use the Process>Batch>Virtual Stack command\n"+ + "to process a virtual stack or convert it into a\n"+ + "normal stack using Image>Duplicate, which\n"+ + "will require "+size+"MB of additional memory."; + error(msg); + return PlugInFilter.DONE; + } + if (Recorder.record) + Recorder.recordOption("stack"); + return flags | PlugInFilter.DOES_STACKS; + } + if (Recorder.record) + Recorder.recordOption("slice"); + } + return flags; + } + + /** Creates a rectangular selection. Removes any existing + selection if width or height are less than 1. */ + public static void makeRectangle(int x, int y, int width, int height) { + if (width<=0 || height<0) + getImage().deleteRoi(); + else { + ImagePlus img = getImage(); + if (Interpreter.isBatchMode()) + img.setRoi(new Roi(x,y,width,height), false); + else + img.setRoi(x, y, width, height); + } + } + + /** Creates a subpixel resolution rectangular selection. */ + public static void makeRectangle(double x, double y, double width, double height) { + if (width<=0 || height<0) + getImage().deleteRoi(); + else + getImage().setRoi(new Roi(x,y,width,height), !Interpreter.isBatchMode()); + } + + /** Creates an oval selection. Removes any existing + selection if width or height are less than 1. */ + public static void makeOval(int x, int y, int width, int height) { + if (width<=0 || height<0) + getImage().deleteRoi(); + else { + ImagePlus img = getImage(); + img.setRoi(new OvalRoi(x, y, width, height)); + } + } + + /** Creates an subpixel resolution oval selection. */ + public static void makeOval(double x, double y, double width, double height) { + if (width<=0 || height<0) + getImage().deleteRoi(); + else + getImage().setRoi(new OvalRoi(x, y, width, height)); + } + + /** Creates a straight line selection. */ + public static void makeLine(int x1, int y1, int x2, int y2) { + getImage().setRoi(new Line(x1, y1, x2, y2)); + } + + /** Creates a straight line selection using floating point coordinates. */ + public static void makeLine(double x1, double y1, double x2, double y2) { + getImage().setRoi(new Line(x1, y1, x2, y2)); + } + + /** Creates a point selection using integer coordinates.. */ + public static void makePoint(int x, int y) { + ImagePlus img = getImage(); + Roi roi = img.getRoi(); + if (shiftKeyDown() && roi!=null && roi.getType()==Roi.POINT) { + Polygon p = roi.getPolygon(); + p.addPoint(x, y); + img.setRoi(new PointRoi(p.xpoints, p.ypoints, p.npoints)); + IJ.setKeyUp(KeyEvent.VK_SHIFT); + } else if (altKeyDown() && roi!=null && roi.getType()==Roi.POINT) { + ((PolygonRoi)roi).deleteHandle(x, y); + IJ.setKeyUp(KeyEvent.VK_ALT); + } else + img.setRoi(new PointRoi(x, y)); + } + + /** Creates a point selection using floating point coordinates. */ + public static void makePoint(double x, double y) { + ImagePlus img = getImage(); + Roi roi = img.getRoi(); + if (shiftKeyDown() && roi!=null && roi.getType()==Roi.POINT) { + Polygon p = roi.getPolygon(); + p.addPoint((int)Math.round(x), (int)Math.round(y)); + img.setRoi(new PointRoi(p.xpoints, p.ypoints, p.npoints)); + IJ.setKeyUp(KeyEvent.VK_SHIFT); + } else if (altKeyDown() && roi!=null && roi.getType()==Roi.POINT) { + ((PolygonRoi)roi).deleteHandle(x, y); + IJ.setKeyUp(KeyEvent.VK_ALT); + } else + img.setRoi(new PointRoi(x, y)); + } + + /** Creates an Roi. */ + public static Roi Roi(double x, double y, double width, double height) { + return new Roi(x, y, width, height); + } + + /** Creates an OvalRoi. */ + public static OvalRoi OvalRoi(double x, double y, double width, double height) { + return new OvalRoi(x, y, width, height); + } + + /** Sets the display range (minimum and maximum displayed pixel values) of the current image. */ + public static void setMinAndMax(double min, double max) { + setMinAndMax(getImage(), min, max, 7); + } + + /** Sets the display range (minimum and maximum displayed pixel values) of the specified image. */ + public static void setMinAndMax(ImagePlus img, double min, double max) { + setMinAndMax(img, min, max, 7); + } + + /** Sets the minimum and maximum displayed pixel values on the specified RGB + channels, where 4=red, 2=green and 1=blue. */ + public static void setMinAndMax(double min, double max, int channels) { + setMinAndMax(getImage(), min, max, channels); + } + + private static void setMinAndMax(ImagePlus img, double min, double max, int channels) { + Calibration cal = img.getCalibration(); + min = cal.getRawValue(min); + max = cal.getRawValue(max); + if (channels==7) + img.setDisplayRange(min, max); + else + img.setDisplayRange(min, max, channels); + img.updateAndDraw(); + } + + /** Resets the minimum and maximum displayed pixel values of the + current image to be the same as the min and max pixel values. */ + public static void resetMinAndMax() { + resetMinAndMax(getImage()); + } + + /** Resets the minimum and maximum displayed pixel values of the + specified image to be the same as the min and max pixel values. */ + public static void resetMinAndMax(ImagePlus img) { + img.resetDisplayRange(); + img.updateAndDraw(); + } + + /** Sets the lower and upper threshold levels and displays the image + using red to highlight thresholded pixels. May not work correctly on + 16 and 32 bit images unless the display range has been reset using IJ.resetMinAndMax(). + */ + public static void setThreshold(double lowerThreshold, double upperThresold) { + setThreshold(lowerThreshold, upperThresold, null); + } + + /** Sets the lower and upper threshold levels and displays the image using + the specified displayMode ("Red", "Black & White", "Over/Under" or "No Update"). */ + public static void setThreshold(double lowerThreshold, double upperThreshold, String displayMode) { + setThreshold(getImage(), lowerThreshold, upperThreshold, displayMode); + } + + /** Sets the lower and upper threshold levels of the specified image. */ + public static void setThreshold(ImagePlus img, double lowerThreshold, double upperThreshold) { + setThreshold(img, lowerThreshold, upperThreshold, "Red"); + } + + /** Sets the lower and upper threshold levels of the specified image and updates the display using + the specified displayMode ("Red", "Black & White", "Over/Under" or "No Update"). + With calibrated images, 'lowerThreshold' and 'upperThreshold' must be density calibrated values. + Use setRawThreshold() to set the threshold using raw (uncalibrated) values. */ + public static void setThreshold(ImagePlus img, double lowerThreshold, double upperThreshold, String displayMode) { + Calibration cal = img.getCalibration(); + if (displayMode==null || !displayMode.contains("raw")) { + lowerThreshold = cal.getRawValue(lowerThreshold); + upperThreshold = cal.getRawValue(upperThreshold); + } + setRawThreshold(img, lowerThreshold, upperThreshold, displayMode); + } + + /** This is a version of setThreshold() that always uses raw (uncalibrated) values + in the range 0-255 for 8-bit images and 0-65535 for 16-bit images. */ + public static void setRawThreshold(ImagePlus img, double lowerThreshold, double upperThreshold, String displayMode) { + int mode = ImageProcessor.RED_LUT; + if (displayMode!=null) { + displayMode = displayMode.toLowerCase(Locale.US); + if (displayMode.contains("black")) + mode = ImageProcessor.BLACK_AND_WHITE_LUT; + else if (displayMode.contains("over")) + mode = ImageProcessor.OVER_UNDER_LUT; + else if (displayMode.contains("no")) + mode = ImageProcessor.NO_LUT_UPDATE; + } + img.getProcessor().setThreshold(lowerThreshold, upperThreshold, mode); + if (mode!=ImageProcessor.NO_LUT_UPDATE && img.getWindow()!=null) { + img.getProcessor().setLutAnimation(true); + img.updateAndDraw(); + ThresholdAdjuster.update(); + } + } + + public static void setAutoThreshold(ImagePlus imp, String method) { + ImageProcessor ip = imp.getProcessor(); + if (ip instanceof ColorProcessor) + throw new IllegalArgumentException("Non-RGB image required"); + ip.setRoi(imp.getRoi()); + if (method!=null) { + try { + if (method.indexOf("stack")!=-1) + setStackThreshold(imp, ip, method); + else + ip.setAutoThreshold(method); + } catch (Exception e) { + log(e.getMessage()); + } + } else + ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.RED_LUT); + imp.updateAndDraw(); + } + + private static void setStackThreshold(ImagePlus imp, ImageProcessor ip, String method) { + boolean darkBackground = method.indexOf("dark")!=-1; + int measurements = Analyzer.getMeasurements(); + Analyzer.setMeasurements(Measurements.AREA+Measurements.MIN_MAX); + ImageStatistics stats = new StackStatistics(imp); + Analyzer.setMeasurements(measurements); + AutoThresholder thresholder = new AutoThresholder(); + double min=0.0, max=255.0; + if (imp.getBitDepth()!=8) { + min = stats.min; + max = stats.max; + } + int threshold = thresholder.getThreshold(method, stats.histogram); + double lower, upper; + if (darkBackground) { + if (ip.isInvertedLut()) + {lower=0.0; upper=threshold;} + else + {lower=threshold+1; upper=255.0;} + } else { + if (ip.isInvertedLut()) + {lower=threshold+1; upper=255.0;} + else + {lower=0.0; upper=threshold;} + } + if (lower>255) lower = 255; + if (max>min) { + lower = min + (lower/255.0)*(max-min); + upper = min + (upper/255.0)*(max-min); + } else + lower = upper = min; + ip.setMinAndMax(min, max); + ip.setThreshold(lower, upper, ImageProcessor.RED_LUT); + imp.updateAndDraw(); + } + + /** Disables thresholding on the current image. */ + public static void resetThreshold() { + resetThreshold(getImage()); + } + + /** Disables thresholding on the specified image. */ + public static void resetThreshold(ImagePlus img) { + ImageProcessor ip = img.getProcessor(); + ip.resetThreshold(); + ip.setLutAnimation(true); + img.updateAndDraw(); + ThresholdAdjuster.update(); + } + + /** For IDs less than zero, activates the image with the specified ID. + For IDs greater than zero, activates the Nth image. */ + public static void selectWindow(int id) { + if (id>0) + id = WindowManager.getNthImageID(id); + ImagePlus imp = WindowManager.getImage(id); + if (imp==null) + error("Macro Error", "Image "+id+" not found or no images are open."); + if (Interpreter.isBatchMode()) { + ImagePlus impT = WindowManager.getTempCurrentImage(); + ImagePlus impC = WindowManager.getCurrentImage(); + if (impC!=null && impC!=imp && impT!=null) + impC.saveRoi(); + WindowManager.setTempCurrentImage(imp); + Interpreter.activateImage(imp); + WindowManager.setWindow(null); + } else { + if (imp==null) + return; + ImageWindow win = imp.getWindow(); + if (win!=null) { + win.toFront(); + win.setState(Frame.NORMAL); + WindowManager.setWindow(win); + } + long start = System.currentTimeMillis(); + // timeout after 1 second unless current thread is event dispatch thread + String thread = Thread.currentThread().getName(); + int timeout = thread!=null&&thread.indexOf("EventQueue")!=-1?0:1000; + if (IJ.isMacOSX() && IJ.isJava18() && timeout>0) + timeout = 250; //work around OS X/Java 8 window activation bug + while (true) { + wait(10); + imp = WindowManager.getCurrentImage(); + if (imp!=null && imp.getID()==id) + return; // specified image is now active + if ((System.currentTimeMillis()-start)>timeout && win!=null) { + WindowManager.setCurrentWindow(win); + return; + } + } + } + } + + /** Activates the window with the specified title. */ + public static void selectWindow(String title) { + if (title.equals("ImageJ")&&ij!=null) { + ij.toFront(); + return; + } + long start = System.currentTimeMillis(); + while (System.currentTimeMillis()-start<3000) { // 3 sec timeout + Window win = WindowManager.getWindow(title); + if (win!=null && !(win instanceof ImageWindow)) { + selectWindow(win); + return; + } + int[] wList = WindowManager.getIDList(); + int len = wList!=null?wList.length:0; + for (int i=0; i1000) { + WindowManager.setWindow(win); + return; // 1 second timeout + } + } + } + + /** Sets the foreground color. */ + public static void setForegroundColor(int red, int green, int blue) { + setColor(red, green, blue, true); + } + + /** Sets the background color. */ + public static void setBackgroundColor(int red, int green, int blue) { + setColor(red, green, blue, false); + } + + static void setColor(int red, int green, int blue, boolean foreground) { + Color c = Colors.toColor(red, green, blue); + if (foreground) { + Toolbar.setForegroundColor(c); + ImagePlus img = WindowManager.getCurrentImage(); + if (img!=null) + img.getProcessor().setColor(c); + } else + Toolbar.setBackgroundColor(c); + } + + /** Switches to the specified tool, where id = Toolbar.RECTANGLE (0), + Toolbar.OVAL (1), etc. */ + public static void setTool(int id) { + Toolbar.getInstance().setTool(id); + } + + /** Switches to the specified tool, where 'name' is "rect", "elliptical", + "brush", etc. Returns 'false' if the name is not recognized. */ + public static boolean setTool(String name) { + return Toolbar.getInstance().setTool(name); + } + + /** Returns the name of the current tool. */ + public static String getToolName() { + return Toolbar.getToolName(); + } + + /** Equivalent to clicking on the current image at (x,y) with the + wand tool. Returns the number of points in the resulting ROI. */ + public static int doWand(int x, int y) { + return doWand(getImage(), x, y, 0, null); + } + + /** Traces the boundary of the area with pixel values within + * 'tolerance' of the value of the pixel at the starting location. + * 'tolerance' is in uncalibrated units. + * 'mode' can be "4-connected", "8-connected" or "Legacy". + * "Legacy" is for compatibility with previous versions of ImageJ; + * it is ignored if 'tolerance' > 0. + */ + public static int doWand(int x, int y, double tolerance, String mode) { + return doWand(getImage(), x, y, tolerance, mode); + } + + /** This version of doWand adds an ImagePlus argument. */ + public static int doWand(ImagePlus img, int x, int y, double tolerance, String mode) { + ImageProcessor ip = img.getProcessor(); + if ((img.getType()==ImagePlus.GRAY32) && Double.isNaN(ip.getPixelValue(x,y))) + return 0; + int imode = Wand.LEGACY_MODE; + boolean smooth = false; + if (mode!=null) { + if (mode.startsWith("4")) + imode = Wand.FOUR_CONNECTED; + else if (mode.startsWith("8")) + imode = Wand.EIGHT_CONNECTED; + smooth = mode.contains("smooth"); + + } + Wand w = new Wand(ip); + double t1 = ip.getMinThreshold(); + if (t1==ImageProcessor.NO_THRESHOLD || (ip.getLutUpdateMode()==ImageProcessor.NO_LUT_UPDATE&& tolerance>0.0)) { + w.autoOutline(x, y, tolerance, imode); + smooth = false; + } else + w.autoOutline(x, y, t1, ip.getMaxThreshold(), imode); + if (w.npoints>0) { + Roi previousRoi = img.getRoi(); + Roi roi = new PolygonRoi(w.xpoints, w.ypoints, w.npoints, Roi.TRACED_ROI); + img.deleteRoi(); + img.setRoi(roi); + if (previousRoi!=null) + roi.update(shiftKeyDown(), altKeyDown()); // add/subtract ROI to previous one if shift/alt key down + Roi roi2 = img.getRoi(); + if (smooth && roi2!=null && roi2.getType()==Roi.TRACED_ROI) { + Rectangle bounds = roi2.getBounds(); + if (bounds.width>1 && bounds.height>1) { + if (smoothMacro==null) + smoothMacro = BatchProcessor.openMacroFromJar("SmoothWandTool.txt"); + if (EventQueue.isDispatchThread()) + new MacroRunner(smoothMacro); // run on separate thread + else + Macro.eval(smoothMacro); + } + } + } + return w.npoints; + } + + /** Sets the transfer mode used by the Edit/Paste command, where mode is "Copy", "Blend", "Average", "Difference", + "Transparent", "Transparent2", "AND", "OR", "XOR", "Add", "Subtract", "Multiply", or "Divide". */ + public static void setPasteMode(String mode) { + Roi.setPasteMode(stringToPasteMode(mode)); + } + + public static int stringToPasteMode(String mode) { + if (mode==null) + return Blitter.COPY; + mode = mode.toLowerCase(Locale.US); + int m = Blitter.COPY; + if (mode.startsWith("ble") || mode.startsWith("ave")) + m = Blitter.AVERAGE; + else if (mode.startsWith("diff")) + m = Blitter.DIFFERENCE; + else if (mode.indexOf("zero")!=-1) + m = Blitter.COPY_ZERO_TRANSPARENT; + else if (mode.startsWith("tran")) + m = Blitter.COPY_TRANSPARENT; + else if (mode.startsWith("and")) + m = Blitter.AND; + else if (mode.startsWith("or")) + m = Blitter.OR; + else if (mode.startsWith("xor")) + m = Blitter.XOR; + else if (mode.startsWith("sub")) + m = Blitter.SUBTRACT; + else if (mode.startsWith("add")) + m = Blitter.ADD; + else if (mode.startsWith("div")) + m = Blitter.DIVIDE; + else if (mode.startsWith("mul")) + m = Blitter.MULTIPLY; + else if (mode.startsWith("min")) + m = Blitter.MIN; + else if (mode.startsWith("max")) + m = Blitter.MAX; + return m; + } + + /** Returns a reference to the active image, or displays an error + message and aborts the plugin or macro if no images are open. */ + public static ImagePlus getImage() { + ImagePlus img = WindowManager.getCurrentImage(); + if (img==null) { + IJ.noImage(); + if (ij==null) + System.exit(0); + else + abort(); + } + return img; + } + + /**The macro interpreter uses this method to call getImage().*/ + public static ImagePlus getImage(Interpreter interpreter) { + macroInterpreter = interpreter; + ImagePlus imp = getImage(); + macroInterpreter = null; + return imp; + } + + /** Returns the active image or stack slice as an ImageProcessor, or displays + an error message and aborts the plugin or macro if no images are open. */ + public static ImageProcessor getProcessor() { + ImagePlus imp = IJ.getImage(); + return imp.getProcessor(); + } + + /** Switches to the specified stack slice, where 1<='slice'<=stack-size. */ + public static void setSlice(int slice) { + getImage().setSlice(slice); + } + + /** Returns the ImageJ version number as a string. */ + public static String getVersion() { + return ImageJ.VERSION; + } + + /** Returns the ImageJ version and build number as a String, for + example "1.46n05", or 1.46n99 if there is no build number. */ + public static String getFullVersion() { + String build = ImageJ.BUILD; + if (build.length()==0) + build = "99"; + else if (build.length()==1) + build = "0" + build; + return ImageJ.VERSION+build; + } + + /** Returns the path to the specified directory if title is + "home" ("user.home"), "downloads", "startup", "imagej" (ImageJ directory), + "plugins", "macros", "luts", "temp", "current", "default", + "image" (directory active image was loaded from), "file" + (directory most recently used to open or save a file) or "cwd" + (current working directory), otherwise displays a dialog and + returns the path to the directory selected by the user. Returns + null if the specified directory is not found or the user cancels the + dialog box. Also aborts the macro if the user cancels the + dialog box.*/ + public static String getDirectory(String title) { + String dir = null; + String title2 = title.toLowerCase(Locale.US); + if (title2.equals("plugins")) + dir = Menus.getPlugInsPath(); + else if (title2.equals("macros")) + dir = Menus.getMacrosPath(); + else if (title2.equals("luts")) { + String ijdir = Prefs.getImageJDir(); + if (ijdir!=null) + dir = ijdir + "luts" + File.separator; + else + dir = null; + } else if (title2.equals("home")) + dir = System.getProperty("user.home"); + else if (title2.equals("downloads")) + dir = System.getProperty("user.home")+File.separator+"Downloads"; + else if (title2.equals("startup")) + dir = Prefs.getImageJDir(); + else if (title2.equals("imagej")) + dir = Prefs.getImageJDir(); + else if (title2.equals("current") || title2.equals("default")) + dir = OpenDialog.getDefaultDirectory(); + else if (title2.equals("temp")) { + dir = System.getProperty("java.io.tmpdir"); + if (isMacintosh()) dir = "/tmp/"; + } else if (title2.equals("image")) { + ImagePlus imp = WindowManager.getCurrentImage(); + FileInfo fi = imp!=null?imp.getOriginalFileInfo():null; + if (fi!=null && fi.directory!=null) { + dir = fi.directory; + } else + dir = null; + } else if (title2.equals("file")) + dir = OpenDialog.getLastDirectory(); + else if (title2.equals("cwd")) + dir = System.getProperty("user.dir"); + else { + DirectoryChooser dc = new DirectoryChooser(title); + dir = dc.getDirectory(); + if (dir==null) Macro.abort(); + } + dir = addSeparator(dir); + return dir; + } + + public static String addSeparator(String path) { + if (path==null) + return null; + if (path.length()>0 && !(path.endsWith(File.separator)||path.endsWith("/"))) { + if (IJ.isWindows()&&path.contains(File.separator)) + path += File.separator; + else + path += "/"; + } + return path; + } + + /** Alias for getDirectory(). */ + public static String getDir(String title) { + return getDirectory(title); + } + + + /** Displays an open file dialog and returns the path to the + choosen file, or returns null if the dialog is canceled. */ + public static String getFilePath(String dialogTitle) { + OpenDialog od = new OpenDialog(dialogTitle); + return od.getPath(); + } + + /** Displays a file open dialog box and then opens the tiff, dicom, + fits, pgm, jpeg, bmp, gif, lut, roi, or text file selected by + the user. Displays an error message if the selected file is not + in one of the supported formats, or if it is not found. */ + public static void open() { + open(null); + } + + /** Opens and displays a tiff, dicom, fits, pgm, jpeg, bmp, gif, lut, + roi, or text file. Displays an error message if the specified file + is not in one of the supported formats, or if it is not found. + With 1.41k or later, opens images specified by a URL. + */ + public static void open(String path) { + if (ij==null && Menus.getCommands()==null) + init(); + Opener o = new Opener(); + macroRunning = true; + if (path==null || path.equals("")) + o.open(); + else + o.open(path); + macroRunning = false; + } + + /** Opens and displays the nth image in the specified tiff stack. */ + public static void open(String path, int n) { + if (ij==null && Menus.getCommands()==null) + init(); + ImagePlus imp = openImage(path, n); + if (imp!=null) imp.show(); + } + + /** Opens the specified file as a tiff, bmp, dicom, fits, pgm, gif, jpeg + or text image and returns an ImagePlus object if successful. + Calls HandleExtraFileTypes plugin if the file type is not recognised. + Displays a file open dialog if 'path' is null or an empty string. + Note that 'path' can also be a URL. Some reader plugins, including + the Bio-Formats plugin, display the image and return null. + Use IJ.open() to display a file open dialog box. + */ + public static ImagePlus openImage(String path) { + macroRunning = true; + ImagePlus imp = (new Opener()).openImage(path); + macroRunning = false; + return imp; + } + + /** Opens the nth image of the specified tiff stack. */ + public static ImagePlus openImage(String path, int n) { + return (new Opener()).openImage(path, n); + } + + /** Opens the specified tiff file as a virtual stack. */ + public static ImagePlus openVirtual(String path) { + return FileInfoVirtualStack.openVirtual(path); + } + + /** Opens an image using a file open dialog and returns it as an ImagePlus object. */ + public static ImagePlus openImage() { + return openImage(null); + } + + /** Opens a URL and returns the contents as a string. + Returns "" if there an error, including + host or file not found. */ + public static String openUrlAsString(String url) { + //if (!trustManagerCreated && url.contains("nih.gov")) trustAllCerts(); + url = Opener.updateUrl(url); + if (debugMode) log("OpenUrlAsString: "+url); + StringBuffer sb = null; + url = url.replaceAll(" ", "%20"); + try { + //if (url.contains("nih.gov")) addRootCA(); + URL u = new URL(url); + URLConnection uc = u.openConnection(); + long len = uc.getContentLength(); + if (len>5242880L) + return ""; + InputStream in = u.openStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + sb = new StringBuffer() ; + String line; + while ((line=br.readLine()) != null) + sb.append (line + "\n"); + in.close (); + } catch (Exception e) { + return(""); + } + if (sb!=null) + return new String(sb); + else + return ""; + } + + /* + public static void addRootCA() throws Exception { + String path = "/Users/wayne/Downloads/Certificates/lets-encrypt-x1-cross-signed.pem"; + InputStream fis = new BufferedInputStream(new FileInputStream(path)); + Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(fis); + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null, null); + ks.setCertificateEntry(Integer.toString(1), ca); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, tmf.getTrustManagers(), null); + HttpsURLConnection.setDefaultSSLSocketFactory(ctx.getSocketFactory()); + } + */ + + /* + // Create a new trust manager that trust all certificates + // http://stackoverflow.com/questions/10135074/download-file-from-https-server-using-java + private static void trustAllCerts() { + trustManagerCreated = true; + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + public void checkClientTrusted (java.security.cert.X509Certificate[] certs, String authType) { + } + public void checkServerTrusted (java.security.cert.X509Certificate[] certs, String authType) { + } + } + }; + // Activate the new trust manager + try { + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } catch (Exception e) { + IJ.log(""+e); + } + } + */ + + /** Saves the current image, lookup table, selection or text window to the specified file path. + The path must end in ".tif", ".jpg", ".gif", ".zip", ".raw", ".avi", ".bmp", ".fits", ".pgm", ".png", ".lut", ".roi" or ".txt". */ + public static void save(String path) { + save(null, path); + } + + /** Saves the specified image, lookup table or selection to the specified file path. + The file path should end with ".tif", ".jpg", ".gif", ".zip", ".raw", ".avi", ".bmp", + ".fits", ".pgm", ".png", ".lut", ".roi" or ".txt". The specified image is saved in + TIFF format if there is no extension. */ + public static void save(ImagePlus imp, String path) { + ImagePlus imp2 = imp; + if (imp2==null) + imp2 = WindowManager.getCurrentImage(); + int dotLoc = path.lastIndexOf('.'); + if (dotLoc==-1 && imp2!=null) { + path = path + ".tif"; // save as TIFF if file name does not have an extension + dotLoc = path.lastIndexOf('.'); + } + if (dotLoc!=-1) { + String title = imp2!=null?imp2.getTitle():null; + saveAs(imp, path.substring(dotLoc+1), path); + if (title!=null) + imp2.setTitle(title); + } else + error("The file path passed to IJ.save() method or save()\nmacro function is missing the required extension.\n \n\""+path+"\""); + } + + /* Saves the active image, lookup table, selection, measurement results, selection XY + coordinates or text window to the specified file path. The format argument must be "tiff", + "jpeg", "gif", "zip", "raw", "avi", "bmp", "fits", "pgm", "png", "text image", "lut", "selection", "measurements", + "xy Coordinates" or "text". If path is null or an emply string, a file + save dialog is displayed. */ + public static void saveAs(String format, String path) { + saveAs(null, format, path); + } + + /* Saves the specified image. The format argument must be "tiff", + "jpeg", "gif", "zip", "raw", "avi", "bmp", "fits", "pgm", "png", + "text image", "lut", "selection" or "xy Coordinates". */ + public static void saveAs(ImagePlus imp, String format, String path) { + if (format==null) + return; + if (path!=null && path.length()==0) + path = null; + format = format.toLowerCase(Locale.US); + Roi roi2 = imp!=null?imp.getRoi():null; + if (roi2!=null) + roi2.endPaste(); + if (format.indexOf("tif")!=-1) { + saveAsTiff(imp, path); + return; + } else if (format.indexOf("jpeg")!=-1 || format.indexOf("jpg")!=-1) { + path = updateExtension(path, ".jpg"); + JpegWriter.save(imp, path, FileSaver.getJpegQuality()); + return; + } else if (format.indexOf("gif")!=-1) { + path = updateExtension(path, ".gif"); + GifWriter.save(imp, path); + return; + } else if (format.indexOf("text image")!=-1) { + path = updateExtension(path, ".txt"); + format = "Text Image..."; + } else if (format.indexOf("text")!=-1 || format.indexOf("txt")!=-1) { + if (path!=null && !path.endsWith(".xls") && !path.endsWith(".csv") && !path.endsWith(".tsv")) + path = updateExtension(path, ".txt"); + format = "Text..."; + } else if (format.indexOf("zip")!=-1) { + path = updateExtension(path, ".zip"); + format = "ZIP..."; + } else if (format.indexOf("raw")!=-1) { + //path = updateExtension(path, ".raw"); + format = "Raw Data..."; + } else if (format.indexOf("avi")!=-1) { + path = updateExtension(path, ".avi"); + format = "AVI... "; + } else if (format.indexOf("bmp")!=-1) { + path = updateExtension(path, ".bmp"); + format = "BMP..."; + } else if (format.indexOf("fits")!=-1) { + path = updateExtension(path, ".fits"); + format = "FITS..."; + } else if (format.indexOf("png")!=-1) { + path = updateExtension(path, ".png"); + format = "PNG..."; + } else if (format.indexOf("pgm")!=-1) { + path = updateExtension(path, ".pgm"); + format = "PGM..."; + } else if (format.indexOf("lut")!=-1) { + path = updateExtension(path, ".lut"); + format = "LUT..."; + } else if (format.contains("results") || format.contains("measurements") || format.contains("table")) { + format = "Results..."; + } else if (format.contains("selection") || format.contains("roi")) { + path = updateExtension(path, ".roi"); + format = "Selection..."; + } else if (format.indexOf("xy")!=-1 || format.indexOf("coordinates")!=-1) { + path = updateExtension(path, ".txt"); + format = "XY Coordinates..."; + } else + error("Unsupported save() or saveAs() file format: \""+format+"\"\n \n\""+path+"\""); + if (path==null) + run(format); + else { + if (path.contains(" ")) + run(imp, format, "save=["+path+"]"); + else + run(imp, format, "save="+path); + } + } + + /** Saves the specified image in TIFF format. Displays a file save dialog + if 'path' is null or an empty string. Returns 'false' if there is an + error or if the user selects "Cancel" in the file save dialog. */ + public static boolean saveAsTiff(ImagePlus imp, String path) { + if (imp==null) + imp = getImage(); + if (path==null || path.equals("")) + return (new FileSaver(imp)).saveAsTiff(); + if (!path.endsWith(".tiff")) + path = updateExtension(path, ".tif"); + FileSaver fs = new FileSaver(imp); + boolean ok; + if (imp.getStackSize()>1) + ok = fs.saveAsTiffStack(path); + else + ok = fs.saveAsTiff(path); + if (ok) + fs.updateImagePlus(path, FileInfo.TIFF); + return ok; + } + + static String updateExtension(String path, String extension) { + if (path==null) return null; + int dotIndex = path.lastIndexOf("."); + int separatorIndex = path.lastIndexOf(File.separator); + if (dotIndex>=0 && dotIndex>separatorIndex && (path.length()-dotIndex)<=5) { + if (dotIndex+1Type should contain "8-bit", "16-bit", "32-bit" or "RGB". + In addition, it can contain "white", "black" or "ramp". Width + and height specify the width and height of the image in pixels. + Depth specifies the number of stack slices. */ + public static ImagePlus createImage(String title, String type, int width, int height, int depth) { + type = type.toLowerCase(Locale.US); + int bitDepth = 8; + if (type.contains("16")) + bitDepth = 16; + boolean signedInt = type.contains("32-bit int"); + if (type.contains("32")) + bitDepth = 32; + if (type.contains("24") || type.contains("rgb") || signedInt) + bitDepth = 24; + int options = NewImage.FILL_WHITE; + if (bitDepth==16 || bitDepth==32) + options = NewImage.FILL_BLACK; + if (type.contains("white")) + options = NewImage.FILL_WHITE; + else if (type.contains("black")) + options = NewImage.FILL_BLACK; + else if (type.contains("ramp")) + options = NewImage.FILL_RAMP; + else if (type.contains("noise") || type.contains("random")) + options = NewImage.FILL_NOISE; + options += NewImage.CHECK_AVAILABLE_MEMORY; + if (signedInt) + options += NewImage.SIGNED_INT; + return NewImage.createImage(title, width, height, depth, bitDepth, options); + } + + /** Creates a new hyperstack. + * @param title image name + * @param type "8-bit", "16-bit", "32-bit" or "RGB". May also + * contain "white" , "black" (the default), "ramp", "composite-mode", + * "color-mode", "grayscale-mode or "label". + * @param width image width in pixels + * @param height image height in pixels + * @param channels number of channels + * @param slices number of slices + * @param frames number of frames + */ + public static ImagePlus createImage(String title, String type, int width, int height, int channels, int slices, int frames) { + if (type.contains("label")) + type += "ramp"; + if (!(type.contains("white")||type.contains("ramp"))) + type += "black"; + ImagePlus imp = IJ.createImage(title, type, width, height, channels*slices*frames); + imp.setDimensions(channels, slices, frames); + int mode = IJ.COLOR; + if (type.contains("composite")) + mode = IJ.COMPOSITE; + if (type.contains("grayscale")) + mode = IJ.GRAYSCALE; + if (channels>1 && imp.getBitDepth()!=24) + imp = new CompositeImage(imp, mode); + imp.setOpenAsHyperStack(true); + if (type.contains("label")) + HyperStackMaker.labelHyperstack(imp); + return imp; + } + + /** Creates a new hyperstack. + * @param title image name + * @param width image width in pixels + * @param height image height in pixels + * @param channels number of channels + * @param slices number of slices + * @param frames number of frames + * @param bitdepth 8, 16, 32 (float) or 24 (RGB) + */ + public static ImagePlus createHyperStack(String title, int width, int height, int channels, int slices, int frames, int bitdepth) { + ImagePlus imp = createImage(title, width, height, channels*slices*frames, bitdepth); + imp.setDimensions(channels, slices, frames); + if (channels>1 && bitdepth!=24) + imp = new CompositeImage(imp, IJ.COMPOSITE); + imp.setOpenAsHyperStack(true); + return imp; + } + + /** Opens a new image. Type should contain "8-bit", "16-bit", "32-bit" or "RGB". + In addition, it can contain "white", "black" or "ramp". Width + and height specify the width and height of the image in pixels. + Depth specifies the number of stack slices. */ + public static void newImage(String title, String type, int width, int height, int depth) { + ImagePlus imp = createImage(title, type, width, height, depth); + if (imp!=null) { + macroRunning = true; + imp.show(); + macroRunning = false; + } + } + + /** Returns true if the Esc key was pressed since the + last ImageJ command started to execute or since resetEscape() was called. */ + public static boolean escapePressed() { + return escapePressed; + } + + /** This method sets the Esc key to the "up" position. + The Executer class calls this method when it runs + an ImageJ command in a separate thread. */ + public static void resetEscape() { + escapePressed = false; + } + + /** Causes IJ.error() output to be temporarily redirected to the "Log" window. */ + public static void redirectErrorMessages() { + redirectErrorMessages = true; + lastErrorMessage = null; + } + + /** Set 'true' and IJ.error() output will be temporarily redirected to the "Log" window. */ + public static void redirectErrorMessages(boolean redirect) { + redirectErrorMessages = redirect; + lastErrorMessage = null; + } + + /** Returns the state of the 'redirectErrorMessages' flag, which is set by File/Import/Image Sequence. */ + public static boolean redirectingErrorMessages() { + return redirectErrorMessages; + } + + /** Temporarily suppress "plugin not found" errors. */ + public static void suppressPluginNotFoundError() { + suppressPluginNotFoundError = true; + } + + /** Returns the class loader ImageJ uses to run plugins or the + system class loader if Menus.getPlugInsPath() returns null. */ + public static ClassLoader getClassLoader() { + if (classLoader==null) { + String pluginsDir = Menus.getPlugInsPath(); + if (pluginsDir==null) { + String home = System.getProperty("plugins.dir"); + if (home!=null) { + if (!home.endsWith(Prefs.separator)) home+=Prefs.separator; + pluginsDir = home+"plugins"+Prefs.separator; + if (!(new File(pluginsDir)).isDirectory()) + pluginsDir = home; + } + } + if (pluginsDir==null) + return IJ.class.getClassLoader(); + else { + if (Menus.jnlp) + classLoader = new PluginClassLoader(pluginsDir, true); + else + classLoader = new PluginClassLoader(pluginsDir); + } + } + return classLoader; + } + + /** Returns the size, in pixels, of the primary display. */ + public static Dimension getScreenSize() { + Rectangle bounds = GUI.getScreenBounds(); + return new Dimension(bounds.width, bounds.height); + } + + /** Returns, as an array of strings, a list of the LUTs in the + * Image/Lookup Tables menu. + * @see ij.plugin#LutLoader.getLut + * See also: Help>Examples>JavaScript/Show all LUTs + * and Image/Color/Display LUTs + */ + public static String[] getLuts() { + ArrayList list = new ArrayList(); + Hashtable commands = Menus.getCommands(); + Menu lutsMenu = Menus.getImageJMenu("Image>Lookup Tables"); + if (commands==null || lutsMenu==null) + return new String[0]; + for (int i=0; i +ImageJ is a work of the United States Government. It is in the public domain +and open source. There is no copyright. You are free to do anything you want +with this source but I like to get credit for my work and I would like you to +offer your changes to me so I can possibly add them to the "official" version. + +
+The following command line options are recognized by ImageJ:
+
+  "file-name"
+     Opens a file
+     Example 1: blobs.tif
+     Example 2: /Users/wayne/images/blobs.tif
+     Example 3: e81*.tif
+
+  -macro path [arg]
+     Runs a macro or script (JavaScript, BeanShell or Python), passing an
+     optional string argument, which the macro or script can be retrieve
+     using the getArgument() function. The macro or script is assumed to 
+     be in the ImageJ/macros folder if 'path' is not a full directory path.
+     Example 1: -macro analyze.ijm
+     Example 2: -macro script.js /Users/wayne/images/stack1
+     Example 2: -macro script.py '1.2 2.4 3.8'
+
+  -batch path [arg]
+    Runs a macro or script (JavaScript, BeanShell or Python) in
+    batch (no GUI) mode, passing an optional argument.
+    ImageJ exits when the macro finishes.
+
+  -eval "macro code"
+     Evaluates macro code
+     Example 1: -eval "print('Hello, world');"
+     Example 2: -eval "return getVersion();"
+
+  -run command
+     Runs an ImageJ menu command
+     Example: -run "About ImageJ..."
+     
+  -ijpath path
+     Specifies the path to the directory containing the plugins directory
+     Example: -ijpath /Applications/ImageJ
+
+  -port
+     Specifies the port ImageJ uses to determine if another instance is running
+     Example 1: -port1 (use default port address + 1)
+     Example 2: -port2 (use default port address + 2)
+     Example 3: -port0 (don't check for another instance)
+
+  -debug
+     Runs ImageJ in debug mode
+
+@author Wayne Rasband (rasband@gmail.com) +*/ +public class ImageJ extends Frame implements ActionListener, + MouseListener, KeyListener, WindowListener, ItemListener, Runnable { + + /** Plugins should call IJ.getVersion() or IJ.getFullVersion() to get the version string. */ + public static final String VERSION = "1.53n"; + public static final String BUILD = ""; //28 + public static Color backgroundColor = new Color(237,237,237); + /** SansSerif, 12-point, plain font. */ + public static final Font SansSerif12 = new Font("SansSerif", Font.PLAIN, 12); + /** Address of socket where Image accepts commands */ + public static final int DEFAULT_PORT = 57294; + + /** Run as normal application. */ + public static final int STANDALONE = 0; + + /** Run embedded in another application. */ + public static final int EMBEDDED = 1; + + /** Run embedded and invisible in another application. */ + public static final int NO_SHOW = 2; + + /** Run ImageJ in debug mode. */ + public static final int DEBUG = 256; + + private static final String IJ_X="ij.x",IJ_Y="ij.y"; + private static int port = DEFAULT_PORT; + private static String[] arguments; + + private Toolbar toolbar; + private Panel statusBar; + private ProgressBar progressBar; + private JLabel statusLine; + private boolean firstTime = true; + private java.applet.Applet applet; // null if not running as an applet + private Vector classes = new Vector(); + private boolean exitWhenQuitting; + private boolean quitting; + private boolean quitMacro; + private long keyPressedTime, actionPerformedTime; + private String lastKeyCommand; + private boolean embedded; + private boolean windowClosed; + private static String commandName; + private static boolean batchMode; + + boolean hotkey; + + /** Creates a new ImageJ frame that runs as an application. */ + public ImageJ() { + this(null, STANDALONE); + } + + /** Creates a new ImageJ frame that runs as an application in the specified mode. */ + public ImageJ(int mode) { + this(null, mode); + } + + /** Creates a new ImageJ frame that runs as an applet. */ + public ImageJ(java.applet.Applet applet) { + this(applet, STANDALONE); + } + + /** If 'applet' is not null, creates a new ImageJ frame that runs as an applet. + If 'mode' is ImageJ.EMBEDDED and 'applet is null, creates an embedded + (non-standalone) version of ImageJ. */ + public ImageJ(java.applet.Applet applet, int mode) { + super("ImageJ"); + if ((mode&DEBUG)!=0) + IJ.setDebugMode(true); + mode = mode & 255; + if (IJ.debugMode) IJ.log("ImageJ starting in debug mode: "+mode); + embedded = applet==null && (mode==EMBEDDED||mode==NO_SHOW); + this.applet = applet; + String err1 = Prefs.load(this, applet); + setBackground(backgroundColor); + Menus m = new Menus(this, applet); + String err2 = m.addMenuBar(); + m.installPopupMenu(this); + setLayout(new BorderLayout()); + + // Tool bar + toolbar = new Toolbar(); + toolbar.addKeyListener(this); + add("Center", toolbar); + + // Status bar + statusBar = new Panel(); + statusBar.setLayout(new BorderLayout()); + statusBar.setForeground(Color.black); + statusBar.setBackground(backgroundColor); + statusLine = new JLabel(); + double scale = Prefs.getGuiScale(); + statusLine.setFont(new Font("SansSerif", Font.PLAIN, (int)(13*scale))); + statusLine.addKeyListener(this); + statusLine.addMouseListener(this); + statusBar.add("Center", statusLine); + progressBar = new ProgressBar((int)(ProgressBar.WIDTH*scale), (int)(ProgressBar.HEIGHT*scale)); + progressBar.addKeyListener(this); + progressBar.addMouseListener(this); + statusBar.add("East", progressBar); + add("South", statusBar); + + IJ.init(this, applet); + addKeyListener(this); + addWindowListener(this); + setFocusTraversalKeysEnabled(false); + m.installStartupMacroSet(); //add custom tools + + Point loc = getPreferredLocation(); + Dimension tbSize = toolbar.getPreferredSize(); + setCursor(Cursor.getDefaultCursor()); // work-around for JDK 1.1.8 bug + if (mode!=NO_SHOW) { + if (IJ.isWindows()) try {setIcon();} catch(Exception e) {} + setResizable(false); + setAlwaysOnTop(Prefs.alwaysOnTop); + pack(); + setLocation(loc.x, loc.y); + setVisible(true); + Dimension size = getSize(); + if (size!=null) { + if (IJ.debugMode) IJ.log("size: "+size); + if (IJ.isWindows() && (size.height>108||IJ.javaVersion()>=10)) { + // workaround for IJ window layout and FileDialog freeze problems with Windows 10 Creators Update + IJ.wait(10); + pack(); + if (IJ.debugMode) IJ.log("pack()"); + if (!Prefs.jFileChooserSettingChanged) + Prefs.useJFileChooser = true; + } else if (IJ.isMacOSX()) { + Rectangle maxBounds = GUI.getMaxWindowBounds(this); + if (loc.x+size.width>maxBounds.x+maxBounds.width) + setLocation(loc.x, loc.y); + } + } + } + if (err1!=null) + IJ.error(err1); + if (err2!=null) { + IJ.error(err2); + IJ.runPlugIn("ij.plugin.ClassChecker", ""); + } + if (IJ.isMacintosh()&&applet==null) { + try { + if (IJ.javaVersion()>8) // newer JREs use different drag-drop, about mechanism + IJ.runPlugIn("ij.plugin.MacAdapter9", ""); + else + IJ.runPlugIn("ij.plugin.MacAdapter", ""); + } catch(Throwable e) {} + } + if (applet==null) + IJ.runPlugIn("ij.plugin.DragAndDrop", ""); + if (!getTitle().contains("Fiji")) { + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler()); + System.setProperty("sun.awt.exception.handler",ExceptionHandler.class.getName()); + } + String str = m.getMacroCount()==1?" macro":" macros"; + configureProxy(); + if (applet==null) + loadCursors(); + (new ij.macro.StartupRunner()).run(batchMode); // run RunAtStartup and AutoRun macros + IJ.showStatus(version()+ m.getPluginCount() + " commands; " + m.getMacroCount() + str); + } + + private void loadCursors() { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + String path = Prefs.getImageJDir()+"images/crosshair-cursor.gif"; + File f = new File(path); + if (!f.exists()) + return; + //Image image = toolkit.getImage(path); + ImageIcon icon = new ImageIcon(path); + Image image = icon.getImage(); + if (image==null) + return; + int width = icon.getIconWidth(); + int height = icon.getIconHeight(); + Point hotSpot = new Point(width/2, height/2); + Cursor crosshairCursor = toolkit.createCustomCursor(image, hotSpot, "crosshair-cursor.gif"); + ImageCanvas.setCursor(crosshairCursor, 0); + } + + void configureProxy() { + if (Prefs.useSystemProxies) { + try { + System.setProperty("java.net.useSystemProxies", "true"); + } catch(Exception e) {} + } else { + String server = Prefs.get("proxy.server", null); + if (server==null||server.equals("")) + return; + int port = (int)Prefs.get("proxy.port", 0); + if (port==0) return; + Properties props = System.getProperties(); + props.put("proxySet", "true"); + props.put("http.proxyHost", server); + props.put("http.proxyPort", ""+port); + props.put("https.proxyHost", server); + props.put("https.proxyPort", ""+port); + } + //new ProxySettings().logProperties(); + } + + void setIcon() throws Exception { + URL url = this.getClass().getResource("/microscope.gif"); + if (url==null) return; + Image img = createImage((ImageProducer)url.getContent()); + if (img!=null) setIconImage(img); + } + + public Point getPreferredLocation() { + int ijX = Prefs.getInt(IJ_X,-99); + int ijY = Prefs.getInt(IJ_Y,-99); + Rectangle maxBounds = GUI.getMaxWindowBounds(); + //System.out.println("getPreferredLoc1: "+ijX+" "+ijY+" "+maxBounds); + if (ijX>=maxBounds.x && ijY>=maxBounds.y && ijX<(maxBounds.x+maxBounds.width-75) + && ijY<(maxBounds.y+maxBounds.height-75)) + return new Point(ijX, ijY); + Dimension tbsize = toolbar.getPreferredSize(); + int ijWidth = tbsize.width+10; + double percent = maxBounds.width>832?0.8:0.9; + ijX = (int)(percent*(maxBounds.width-ijWidth)); + if (ijX<10) ijX = 10; + return new Point(ijX, maxBounds.y); + } + + void showStatus(String s) { + statusLine.setText(s); + } + + public ProgressBar getProgressBar() { + return progressBar; + } + + public Panel getStatusBar() { + return statusBar; + } + + /** Starts executing a menu command in a separate thread. */ + void doCommand(String name) { + new Executer(name, null); + } + + public void runFilterPlugIn(Object theFilter, String cmd, String arg) { + new PlugInFilterRunner(theFilter, cmd, arg); + } + + public Object runUserPlugIn(String commandName, String className, String arg, boolean createNewLoader) { + return IJ.runUserPlugIn(commandName, className, arg, createNewLoader); + } + + /** Return the current list of modifier keys. */ + public static String modifiers(int flags) { //?? needs to be moved + String s = " [ "; + if (flags == 0) return ""; + if ((flags & Event.SHIFT_MASK) != 0) s += "Shift "; + if ((flags & Event.CTRL_MASK) != 0) s += "Control "; + if ((flags & Event.META_MASK) != 0) s += "Meta "; + if ((flags & Event.ALT_MASK) != 0) s += "Alt "; + s += "] "; + return s; + } + + /** Handle menu events. */ + public void actionPerformed(ActionEvent e) { + if ((e.getSource() instanceof MenuItem)) { + MenuItem item = (MenuItem)e.getSource(); + String cmd = e.getActionCommand(); + Frame frame = WindowManager.getFrontWindow(); + if (frame!=null && (frame instanceof Fitter)) { + ((Fitter)frame).actionPerformed(e); + return; + } + commandName = cmd; + ImagePlus imp = null; + if (item.getParent()==Menus.getOpenRecentMenu()) { + new RecentOpener(cmd); // open image in separate thread + return; + } else if (item.getParent()==Menus.getPopupMenu()) { + Object parent = Menus.getPopupMenu().getParent(); + if (parent instanceof ImageCanvas) + imp = ((ImageCanvas)parent).getImage(); + } + int flags = e.getModifiers(); + hotkey = false; + actionPerformedTime = System.currentTimeMillis(); + long ellapsedTime = actionPerformedTime-keyPressedTime; + if (cmd!=null && (ellapsedTime>=200L||!cmd.equals(lastKeyCommand))) { + if ((flags & Event.ALT_MASK)!=0) + IJ.setKeyDown(KeyEvent.VK_ALT); + if ((flags & Event.SHIFT_MASK)!=0) + IJ.setKeyDown(KeyEvent.VK_SHIFT); + new Executer(cmd, imp); + } + lastKeyCommand = null; + if (IJ.debugMode) IJ.log("actionPerformed: time="+ellapsedTime+", "+e); + } + } + + /** Handles CheckboxMenuItem state changes. */ + public void itemStateChanged(ItemEvent e) { + MenuItem item = (MenuItem)e.getSource(); + MenuComponent parent = (MenuComponent)item.getParent(); + String cmd = e.getItem().toString(); + if ("Autorun Examples".equals(cmd)) // Examples>Autorun Examples + Prefs.autoRunExamples = e.getStateChange()==1; + else if ((Menu)parent==Menus.window) + WindowManager.activateWindow(cmd, item); + else + doCommand(cmd); + } + + public void mousePressed(MouseEvent e) { + Undo.reset(); + if (!Prefs.noClickToGC) + System.gc(); + IJ.showStatus(version()+IJ.freeMemory()); + if (IJ.debugMode) + IJ.log("Windows: "+WindowManager.getWindowCount()); + } + + public String getInfo() { + return version()+System.getProperty("os.name")+" "+System.getProperty("os.version")+"; "+IJ.freeMemory(); + } + + private String version() { + return "ImageJ "+VERSION+BUILD + "; "+"Java "+System.getProperty("java.version")+(IJ.is64Bit()?" [64-bit]; ":" [32-bit]; "); + } + + public void mouseReleased(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + public void mouseClicked(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + + public void keyPressed(KeyEvent e) { + if (e.isConsumed()) + return; + int keyCode = e.getKeyCode(); + IJ.setKeyDown(keyCode); + hotkey = false; + if (keyCode==KeyEvent.VK_CONTROL || keyCode==KeyEvent.VK_SHIFT) + return; + char keyChar = e.getKeyChar(); + int flags = e.getModifiers(); + if (IJ.debugMode) IJ.log("keyPressed: code=" + keyCode + " (" + KeyEvent.getKeyText(keyCode) + + "), char=\"" + keyChar + "\" (" + (int)keyChar + "), flags=" + + KeyEvent.getKeyModifiersText(flags)); + boolean shift = (flags & KeyEvent.SHIFT_MASK) != 0; + boolean control = (flags & KeyEvent.CTRL_MASK) != 0; + boolean alt = (flags & KeyEvent.ALT_MASK) != 0; + boolean meta = (flags & KeyEvent.META_MASK) != 0; + if (keyCode==KeyEvent.VK_H && meta && IJ.isMacOSX()) + return; // Allow macOS to run ImageJ>Hide ImageJ command + String cmd = null; + ImagePlus imp = WindowManager.getCurrentImage(); + boolean isStack = (imp!=null) && (imp.getStackSize()>1); + + if (imp!=null && !meta && ((keyChar>=32 && keyChar<=255) || keyChar=='\b' || keyChar=='\n')) { + Roi roi = imp.getRoi(); + if (roi!=null && roi instanceof TextRoi) { + if (imp.getOverlay()!=null && (control || alt || meta) + && (keyCode==KeyEvent.VK_BACK_SPACE || keyCode==KeyEvent.VK_DELETE)) { + if (deleteOverlayRoi(imp)) + return; + } + if ((flags & KeyEvent.META_MASK)!=0 && IJ.isMacOSX()) + return; + if (alt) { + switch (keyChar) { + case 'u': case 'm': keyChar = IJ.micronSymbol; break; + case 'A': keyChar = IJ.angstromSymbol; break; + default: + } + } + ((TextRoi)roi).addChar(keyChar); + return; + } + } + + // Handle one character macro shortcuts + if (!control && !meta) { + Hashtable macroShortcuts = Menus.getMacroShortcuts(); + if (macroShortcuts.size()>0) { + if (shift) + cmd = (String)macroShortcuts.get(new Integer(keyCode+200)); + else + cmd = (String)macroShortcuts.get(new Integer(keyCode)); + if (cmd!=null) { + commandName = cmd; + MacroInstaller.runMacroShortcut(cmd); + return; + } + } + } + + if (keyCode==KeyEvent.VK_SEPARATOR) + keyCode = KeyEvent.VK_DECIMAL; + boolean functionKey = keyCode>=KeyEvent.VK_F1 && keyCode<=KeyEvent.VK_F12; + boolean numPad = keyCode==KeyEvent.VK_DIVIDE || keyCode==KeyEvent.VK_MULTIPLY + || keyCode==KeyEvent.VK_DECIMAL + || (keyCode>=KeyEvent.VK_NUMPAD0 && keyCode<=KeyEvent.VK_NUMPAD9); + if ((!Prefs.requireControlKey||control||meta||functionKey||numPad) && keyChar!='+') { + Hashtable shortcuts = Menus.getShortcuts(); + if (shift && !functionKey) + cmd = (String)shortcuts.get(new Integer(keyCode+200)); + else + cmd = (String)shortcuts.get(new Integer(keyCode)); + } + + if (cmd==null) { + switch (keyChar) { + case '<': case ',': if (isStack) cmd="Previous Slice [<]"; break; + case '>': case '.': case ';': if (isStack) cmd="Next Slice [>]"; break; + case '+': case '=': cmd="In [+]"; break; + case '-': cmd="Out [-]"; break; + case '/': cmd="Reslice [/]..."; break; + default: + } + } + + if (cmd==null) { + switch (keyCode) { + case KeyEvent.VK_TAB: WindowManager.putBehind(); return; + case KeyEvent.VK_BACK_SPACE: case KeyEvent.VK_DELETE: + if (!(shift||control||alt||meta)) { + if (deleteOverlayRoi(imp)) + return; + if (imp!=null&&imp.getOverlay()!=null&&imp==GelAnalyzer.getGelImage()) + return; + cmd="Clear"; + hotkey=true; + } + break; + //case KeyEvent.VK_BACK_SLASH: cmd=IJ.altKeyDown()?"Animation Options...":"Start Animation"; break; + case KeyEvent.VK_EQUALS: cmd="In [+]"; break; + case KeyEvent.VK_MINUS: cmd="Out [-]"; break; + case KeyEvent.VK_SLASH: case 0xbf: cmd="Reslice [/]..."; break; + case KeyEvent.VK_COMMA: case 0xbc: if (isStack) cmd="Previous Slice [<]"; break; + case KeyEvent.VK_PERIOD: case 0xbe: if (isStack) cmd="Next Slice [>]"; break; + case KeyEvent.VK_LEFT: case KeyEvent.VK_RIGHT: case KeyEvent.VK_UP: case KeyEvent.VK_DOWN: // arrow keys + if (imp==null) return; + Roi roi = imp.getRoi(); + if (shift&&imp==Orthogonal_Views.getImage()) + return; + if (IJ.isMacOSX() && IJ.isJava18()) { + RoiManager rm = RoiManager.getInstance(); + boolean rmActive = rm!=null && rm==WindowManager.getActiveWindow(); + if (rmActive && (keyCode==KeyEvent.VK_DOWN||keyCode==KeyEvent.VK_UP)) + rm.repaint(); + } + boolean stackKey = imp.getStackSize()>1 && (roi==null||shift); + boolean zoomKey = roi==null || shift || control; + if (stackKey && keyCode==KeyEvent.VK_RIGHT) + cmd="Next Slice [>]"; + else if (stackKey && keyCode==KeyEvent.VK_LEFT) + cmd="Previous Slice [<]"; + else if (zoomKey && keyCode==KeyEvent.VK_DOWN && !ignoreArrowKeys(imp) && Toolbar.getToolId()1 && win!=null && win.getClass().getName().startsWith("loci")) + return true; + return false; + } + + public void keyTyped(KeyEvent e) { + char keyChar = e.getKeyChar(); + int flags = e.getModifiers(); + //if (IJ.debugMode) IJ.log("keyTyped: char=\"" + keyChar + "\" (" + (int)keyChar + // + "), flags= "+Integer.toHexString(flags)+ " ("+KeyEvent.getKeyModifiersText(flags)+")"); + if (keyChar=='\\' || keyChar==171 || keyChar==223) { + if (((flags&Event.ALT_MASK)!=0)) + doCommand("Animation Options..."); + else + doCommand("Start Animation [\\]"); + } + } + + public void keyReleased(KeyEvent e) { + IJ.setKeyUp(e.getKeyCode()); + } + + /** called when escape pressed */ + void abortPluginOrMacro(ImagePlus imp) { + if (imp!=null) { + ImageWindow win = imp.getWindow(); + if (win!=null) { + Roi roi = imp.getRoi(); + if (roi!=null && roi.getState()!=Roi.NORMAL) { + roi.abortModification(imp); + return; + } else { + win.running = false; + win.running2 = false; + } + } + } + Macro.abort(); + Interpreter.abort(); + if (Interpreter.getInstance()!=null) + IJ.beep(); + } + + public void windowClosing(WindowEvent e) { + if (Executer.getListenerCount()>0) + doCommand("Quit"); + else { + quit(); + windowClosed = true; + } + } + + public void windowActivated(WindowEvent e) { + if (IJ.isMacintosh() && !quitting) { + IJ.wait(10); // may be needed for Java 1.4 on OS X + MenuBar mb = Menus.getMenuBar(); + if (mb!=null && mb!=getMenuBar()) { + setMenuBar(mb); + Menus.setMenuBarCount++; + if (IJ.debugMode) IJ.log("setMenuBar: "+Menus.setMenuBarCount); + } + } + } + + public void windowClosed(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + + /** Adds the specified class to a Vector to keep it from being + garbage collected, causing static fields to be reset. */ + public void register(Class c) { + if (!classes.contains(c)) + classes.addElement(c); + } + + /** Called by ImageJ when the user selects Quit. */ + public void quit() { + quitMacro = IJ.macroRunning(); + Thread thread = new Thread(this, "Quit"); + thread.setPriority(Thread.NORM_PRIORITY); + thread.start(); + IJ.wait(10); + } + + /** Returns true if ImageJ is exiting. */ + public boolean quitting() { + return quitting; + } + + /** Returns true if ImageJ is quitting as a result of a run("Quit") macro call. */ + public boolean quittingViaMacro() { + return quitting && quitMacro; + } + + /** Called once when ImageJ quits. */ + public void savePreferences(Properties prefs) { + Point loc = getLocation(); + prefs.put(IJ_X, Integer.toString(loc.x)); + prefs.put(IJ_Y, Integer.toString(loc.y)); + } + + public static void main(String args[]) { + boolean noGUI = false; + int mode = STANDALONE; + arguments = args; + int nArgs = args!=null?args.length:0; + boolean commandLine = false; + for (int i=0; i0 && DEFAULT_PORT+delta<65536) + port = DEFAULT_PORT+delta; + } + } + // If existing ImageJ instance, pass arguments to it and quit. + boolean passArgs = mode==STANDALONE && !noGUI; + if (IJ.isMacOSX() && !commandLine) + passArgs = false; + if (passArgs && isRunning(args)) + return; + ImageJ ij = IJ.getInstance(); + if (!noGUI && (ij==null || (ij!=null && !ij.isShowing()))) { + ij = new ImageJ(null, mode); + ij.exitWhenQuitting = true; + } else if (batchMode && noGUI) + Prefs.load(null, null); + int macros = 0; + for (int i=0; i0 && arg.indexOf("ij.ImageJ")==-1) { + File file = new File(arg); + IJ.open(file.getAbsolutePath()); + } + } + if (IJ.debugMode && IJ.getInstance()==null) + new JavaProperties().run(""); + if (noGUI) System.exit(0); + } + + // Is there another instance of ImageJ? If so, send it the arguments and quit. + static boolean isRunning(String args[]) { + return OtherInstance.sendArguments(args); + } + + /** Returns the port that ImageJ checks on startup to see if another instance is running. + * @see ij.OtherInstance + */ + public static int getPort() { + return port; + } + + /** Returns the command line arguments passed to ImageJ. */ + public static String[] getArgs() { + return arguments; + } + + /** ImageJ calls System.exit() when qutting when 'exitWhenQuitting' is true.*/ + public void exitWhenQuitting(boolean ewq) { + exitWhenQuitting = ewq; + } + + /** Quit using a separate thread, hopefully avoiding thread deadlocks. */ + public void run() { + quitting = true; + boolean changes = false; + int[] wList = WindowManager.getIDList(); + if (wList!=null) { + for (int i=0; iMenus.WINDOW_MENU_ITEMS && !(IJ.macroRunning()&&WindowManager.getImageCount()==0)) { + GenericDialog gd = new GenericDialog("ImageJ", this); + gd.addMessage("Are you sure you want to quit ImageJ?"); + gd.showDialog(); + quitting = !gd.wasCanceled(); + windowClosed = false; + } + if (!quitting) + return; + if (!WindowManager.closeAllWindows()) { + quitting = false; + return; + } + if (applet==null) { + saveWindowLocations(); + Prefs.set(ImageWindow.LOC_KEY,null); // don't save image window location + Prefs.savePreferences(); + } + IJ.cleanup(); + dispose(); + if (exitWhenQuitting) + System.exit(0); + } + + void saveWindowLocations() { + Window win = WindowManager.getWindow("B&C"); + if (win!=null) + Prefs.saveLocation(ContrastAdjuster.LOC_KEY, win.getLocation()); + win = WindowManager.getWindow("Threshold"); + if (win!=null) + Prefs.saveLocation(ThresholdAdjuster.LOC_KEY, win.getLocation()); + win = WindowManager.getWindow("Results"); + if (win!=null) { + Prefs.saveLocation(TextWindow.LOC_KEY, win.getLocation()); + Dimension d = win.getSize(); + Prefs.set(TextWindow.WIDTH_KEY, d.width); + Prefs.set(TextWindow.HEIGHT_KEY, d.height); + } + win = WindowManager.getWindow("Log"); + if (win!=null) { + Prefs.saveLocation(TextWindow.LOG_LOC_KEY, win.getLocation()); + Dimension d = win.getSize(); + Prefs.set(TextWindow.LOG_WIDTH_KEY, d.width); + Prefs.set(TextWindow.LOG_HEIGHT_KEY, d.height); + } + win = WindowManager.getWindow("ROI Manager"); + if (win!=null) + Prefs.saveLocation(RoiManager.LOC_KEY, win.getLocation()); + } + + public static String getCommandName() { + return commandName!=null?commandName:"null"; + } + + public static void setCommandName(String name) { + commandName = name; + } + + public void resize() { + double scale = Prefs.getGuiScale(); + toolbar.init(); + statusLine.setFont(new Font("SansSerif", Font.PLAIN, (int)(13*scale))); + progressBar.init((int)(ProgressBar.WIDTH*scale), (int)(ProgressBar.HEIGHT*scale)); + pack(); + } + + /** Handles exceptions on the EDT. */ + public static class ExceptionHandler implements Thread.UncaughtExceptionHandler { + + // for EDT exceptions + public void handle(Throwable thrown) { + handleException(Thread.currentThread().getName(), thrown); + } + + // for other uncaught exceptions + public void uncaughtException(Thread thread, Throwable thrown) { + handleException(thread.getName(), thrown); + } + + protected void handleException(String tname, Throwable e) { + if (Macro.MACRO_CANCELED.equals(e.getMessage())) + return; + CharArrayWriter caw = new CharArrayWriter(); + PrintWriter pw = new PrintWriter(caw); + e.printStackTrace(pw); + String s = caw.toString(); + if (s!=null && s.contains("ij.")) { + if (IJ.getInstance()!=null) + s = IJ.getInstance().getInfo()+"\n"+s; + IJ.log(s); + } + } + + } // inner class ExceptionHandler + +} diff --git a/src/ij/ImageJApplet.java b/src/ij/ImageJApplet.java new file mode 100644 index 0000000..db5cb6e --- /dev/null +++ b/src/ij/ImageJApplet.java @@ -0,0 +1,44 @@ +package ij; +import java.applet.Applet; + +/** + Runs ImageJ as an applet and optionally opens up to + nine images using URLs passed as a parameters. +

+ Here is an example applet tag that launches ImageJ as an applet + and passes it the URLs of two images: +

+	<applet archive="../ij.jar" code="ij.ImageJApplet.class" width=0 height=0>
+	<param name=url1 value="http://imagej.nih.gov/ij/images/FluorescentCells.jpg">
+	<param name=url2 value="http://imagej.nih.gov/ij/images/blobs.gif">
+	</applet>
+	
+ To use plugins, add them to ij.jar and add entries to IJ_Props.txt file (in ij.jar) that will + create commands for them in the Plugins menu, or a submenu. There are examples + of such entries in IJ.Props.txt, in the "Plugins installed in the Plugins menu" section. +

+ * This is mainly relevant to linux: Swing components scale automatically on + * most platforms, specially since Java 8. However there are still exceptions to + * this on linux: e.g., In Ubuntu, Swing components do scale, but only under the + * GTK L&F. (On the other hand AWT components do not scale at all on + * hiDPI screens on linux). + *

+ *

+ * This method tries to avoid exaggerated font sizes by detecting if a component + * has been already scaled by the UIManager, applying only + * {@link #getGuiScale()} to the component's font if not. + *

+ * + * @param component the component to be scaled + * @return true, if component's font was resized + */ + public static boolean scale(final JComponent component) { + final double guiScale = Prefs.getGuiScale(); + if (guiScale == 1d) + return false; + Font font = component.getFont(); + if (font == null && component instanceof JList) + font = UIManager.getFont("List.font"); + else if (font == null && component instanceof JTable) + font = UIManager.getFont("Table.font"); + else if (font == null) + font = UIManager.getFont("Label.font"); + if (font.getSize() > DEFAULT_FONT.getSize()) + return false; + if (component instanceof JTable) + ((JTable) component).setRowHeight((int) (((JTable) component).getRowHeight() * guiScale * 0.9)); + else if (component instanceof JList) + ((JList) component).setFixedCellHeight((int) (((JList) component).getFixedCellHeight() * guiScale * 0.9)); + component.setFont(font.deriveFont((float) guiScale * font.getSize())); + return true; + } + + /** Works around an OpenJDK bug on Windows that + * causes the scrollbar thumb color and background + * color to be almost identical. + */ + public static final void fixScrollbar(Scrollbar sb) { + if (IJ.isWindows()) + sb.setBackground(scrollbarBackground); + } + + /** Returns a new NonBlockingGenericDialog with the given title, + * except when Java is running in headless mode, in which case + * a GenericDialog is be returned. + */ + public static GenericDialog newNonBlockingDialog(String title) { + if (GraphicsEnvironment.isHeadless()) + return new GenericDialog(title); + else + return new NonBlockingGenericDialog(title); + } + + /** Returns a new NonBlockingGenericDialog with the given title + * if Prefs.nonBlockingFilterDialogs is 'true' and 'imp' is + * displayed, otherwise returns a GenericDialog. + * @param title Dialog title + * @param imp The image associated with this dialog + */ + public static GenericDialog newNonBlockingDialog(String title, ImagePlus imp) { + if (Prefs.nonBlockingFilterDialogs && imp!=null && imp.getWindow()!=null) { + NonBlockingGenericDialog gd = new NonBlockingGenericDialog(title); + gd.imp = imp; + return gd; + } else + return new GenericDialog(title); + } + + +} diff --git a/src/ij/gui/GenericDialog.java b/src/ij/gui/GenericDialog.java new file mode 100644 index 0000000..0995d71 --- /dev/null +++ b/src/ij/gui/GenericDialog.java @@ -0,0 +1,1918 @@ +package ij.gui; +import ij.*; +import ij.plugin.frame.Recorder; +import ij.plugin.ScreenGrabber; +import ij.plugin.filter.PlugInFilter; +import ij.plugin.filter.PlugInFilterRunner; +import ij.util.Tools; +import ij.macro.*; +import ij.io.OpenDialog; +import java.awt.*; +import java.io.*; +import java.awt.event.*; +import java.util.*; +import java.awt.datatransfer.*; +import java.awt.dnd.*; + + +/** + * This class is a customizable modal dialog box. Here is an example + * GenericDialog with one string field and two numeric fields: + *
+ *  public class Generic_Dialog_Example implements PlugIn {
+ *    static String title="Example";
+ *    static int width=512,height=512;
+ *    public void run(String arg) {
+ *      GenericDialog gd = new GenericDialog("New Image");
+ *      gd.addStringField("Title: ", title);
+ *      gd.addNumericField("Width: ", width, 0);
+ *      gd.addNumericField("Height: ", height, 0);
+ *      gd.showDialog();
+ *      if (gd.wasCanceled()) return;
+ *      title = gd.getNextString();
+ *      width = (int)gd.getNextNumber();
+ *      height = (int)gd.getNextNumber();
+ *      IJ.newImage(title, "8-bit", width, height, 1);
+ *   }
+ * }
+ * 
+* To work with macros, the first word of each component label must be +* unique. If this is not the case, add underscores, which will be converted +* to spaces when the dialog is displayed. For example, change the checkbox labels +* "Show Quality" and "Show Residue" to "Show_Quality" and "Show_Residue". +*/ +public class GenericDialog extends Dialog implements ActionListener, TextListener, +FocusListener, ItemListener, KeyListener, AdjustmentListener, WindowListener { + + protected Vector numberField, stringField, checkbox, choice, slider, radioButtonGroups; + protected TextArea textArea1, textArea2; + protected Vector defaultValues,defaultText,defaultStrings,defaultChoiceIndexes; + protected Component theLabel; + private Button okay; + private Button cancel; + private Button no, help; + private String helpLabel = "Help"; + private boolean wasCanceled, wasOKed; + private int nfIndex, sfIndex, cbIndex, choiceIndex, textAreaIndex, radioButtonIndex; + private GridBagConstraints c; + private boolean firstNumericField=true; + private boolean firstSlider=true; + private boolean invalidNumber; + private String errorMessage; + private Hashtable labels; + private boolean macro; + private String macroOptions; + private boolean addToSameRow; + private boolean addToSameRowCalled; + private int topInset, leftInset, bottomInset; + private boolean customInsets; + private Vector sliderIndexes, sliderScales, sliderDigits; + private Checkbox previewCheckbox; // the "Preview" Checkbox, if any + private Vector dialogListeners; // the Objects to notify on user input + private PlugInFilterRunner pfr; // the PlugInFilterRunner for automatic preview + private String previewLabel = " Preview"; + private final static String previewRunning = "wait..."; + private boolean recorderOn; // whether recording is allowed (after the dialog is closed) + private char echoChar; + private boolean hideCancelButton; + private boolean centerDialog = true; + private String helpURL; + private boolean smartRecording; + private Vector imagePanels; + protected static GenericDialog instance; + private boolean firstPaint = true; + private boolean fontSizeSet; + private boolean showDialogCalled; + private boolean optionsRecorded; // have dialogListeners been called to record options? + private Label lastLabelAdded; + private int[] windowIDs; + private String[] windowTitles; + + + /** Creates a new GenericDialog with the specified title. Uses the current image + image window as the parent frame or the ImageJ frame if no image windows + are open. Dialog parameters are recorded by ImageJ's command recorder but + this requires that the first word of each label be unique. */ + public GenericDialog(String title) { + this(title, null); + } + + private static Frame getParentFrame() { + return null; + } + + /** Creates a new GenericDialog using the specified title and parent frame. */ + public GenericDialog(String title, Frame parent) { + super(parent, title, true); + ImageJ ij = IJ.getInstance(); + if (ij!=null) setFont(ij.getFont()); + okay = new Button(" OK "); + cancel = new Button("Cancel"); + if (Prefs.blackCanvas) { + setForeground(SystemColor.controlText); + setBackground(SystemColor.control); + } + //if (IJ.isMacOSX() && System.getProperty("java.vendor").contains("Azul")) + // setForeground(Color.black); // work around bug on Azul Java 8 on Apple Silicon + GridBagLayout grid = new GridBagLayout(); + c = new GridBagConstraints(); + setLayout(grid); + macroOptions = Macro.getOptions(); + macro = macroOptions!=null; + addKeyListener(this); + addWindowListener(this); + } + + /** Adds a numeric field. The first word of the label must be + unique or command recording will not work. + * @param label the label + * @param defaultValue value to be initially displayed + */ + public void addNumericField(String label, double defaultValue) { + int decimalPlaces = (int)defaultValue==defaultValue?0:3; + int columnWidth = decimalPlaces==3?8:6; + addNumericField(label, defaultValue, decimalPlaces, columnWidth, null); + } + + /** Adds a numeric field. The first word of the label must be + unique or command recording will not work. + * @param label the label + * @param defaultValue value to be initially displayed + * @param digits number of digits to right of decimal point + */ + public void addNumericField(String label, double defaultValue, int digits) { + addNumericField(label, defaultValue, digits, 6, null); + } + + /** Adds a numeric field. The first word of the label must be + unique or command recording will not work. + * @param label the label + * @param defaultValue value to be initially displayed + * @param digits number of digits to right of decimal point + * @param columns width of field in characters + * @param units a string displayed to the right of the field + */ + public void addNumericField(String label, double defaultValue, int digits, int columns, String units) { + String label2 = label; + if (label2.indexOf('_')!=-1) + label2 = label2.replace('_', ' '); + Label fieldLabel = makeLabel(label2); + this.lastLabelAdded = fieldLabel; + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + c.insets.left = 10; + } else { + c.gridx = 0; c.gridy++; + if (firstNumericField) + c.insets = getInsets(5, 0, 3, 0); // top, left, bottom, right + else + c.insets = getInsets(0, 0, 3, 0); + } + c.anchor = GridBagConstraints.EAST; + c.gridwidth = 1; + //IJ.log("x="+c.gridx+", y= "+c.gridy+", width="+c.gridwidth+", ancher= "+c.anchor+" "+c.insets); + add(fieldLabel, c); + if (addToSameRow) { + c.insets.left = 0; + addToSameRow = false; + } + if (numberField==null) { + numberField = new Vector(5); + defaultValues = new Vector(5); + defaultText = new Vector(5); + } + if (IJ.isWindows()) columns -= 2; + if (columns<1) columns = 1; + String defaultString = IJ.d2s(defaultValue, digits); + if (Double.isNaN(defaultValue)) + defaultString = ""; + TextField tf = new TextField(defaultString, columns); + if (IJ.isLinux()) tf.setBackground(Color.white); + tf.addActionListener(this); + tf.addTextListener(this); + tf.addFocusListener(this); + tf.addKeyListener(this); + numberField.addElement(tf); + defaultValues.addElement(new Double(defaultValue)); + defaultText.addElement(tf.getText()); + c.gridx = GridBagConstraints.RELATIVE; + c.anchor = GridBagConstraints.WEST; + tf.setEditable(true); + //if (firstNumericField) tf.selectAll(); + firstNumericField = false; + if (units==null||units.equals("")) { + add(tf, c); + } else { + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + panel.add(tf); + panel.add(new Label(" "+units)); + add(panel, c); + } + if (Recorder.record || macro) + saveLabel(tf, label); + } + + private Label makeLabel(String label) { + if (IJ.isMacintosh()) + label += " "; + return new Label(label); + } + + /** Saves the label for given component, for macro recording and for accessing the component in macros. */ + private void saveLabel(Object component, String label) { + if (labels==null) + labels = new Hashtable(); + if (label.length()>0) + label = Macro.trimKey(label.trim()); + if (label.length()>0 && hasLabel(label)) { // not a unique label? + label += "_0"; + for (int n=1; hasLabel(label); n++) { // while still not a unique label + label = label.substring(0, label.lastIndexOf('_')); //remove counter + label += "_"+n; + } + } + labels.put(component, label); + } + + /** Returns whether the list of labels for macro recording or macro creation contains a given label. */ + private boolean hasLabel(String label) { + for (Object o : labels.keySet()) + if (labels.get(o).equals(label)) return true; + return false; + } + + /** Adds an 8 column text field. + * @param label the label + * @param defaultText the text initially displayed + */ + public void addStringField(String label, String defaultText) { + addStringField(label, defaultText, 8); + } + + /** Adds a text field. + * @param label the label + * @param defaultText text initially displayed + * @param columns width of the text field. If columns is 8 or more, additional items may be added to this line with addToSameRow() + */ + public void addStringField(String label, String defaultText, int columns) { + if (addToSameRow && label.equals("_")) + label = ""; + String label2 = label; + if (label2.indexOf('_')!=-1) + label2 = label2.replace('_', ' '); + Label fieldLabel = makeLabel(label2); + this.lastLabelAdded = fieldLabel; + boolean custom = customInsets; + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + addToSameRow = false; + } else { + c.gridx = 0; c.gridy++; + if (stringField==null) + c.insets = getInsets(5, 0, 5, 0); // top, left, bottom, right + else + c.insets = getInsets(0, 0, 5, 0); + } + c.anchor = GridBagConstraints.EAST; + c.gridwidth = 1; + add(fieldLabel, c); + if (stringField==null) { + stringField = new Vector(4); + defaultStrings = new Vector(4); + } + + TextField tf = new TextField(defaultText, columns); + if (IJ.isLinux()) tf.setBackground(Color.white); + tf.setEchoChar(echoChar); + echoChar = 0; + tf.addActionListener(this); + tf.addTextListener(this); + tf.addFocusListener(this); + tf.addKeyListener(this); + c.gridx = GridBagConstraints.RELATIVE; + c.anchor = GridBagConstraints.WEST; + c.gridwidth = columns <= 8 ? 1 : GridBagConstraints.REMAINDER; + c.insets.left = 0; + tf.setEditable(true); + add(tf, c); + stringField.addElement(tf); + defaultStrings.addElement(defaultText); + tf.setDropTarget(null); + new DropTarget(tf, new TextDropTarget(tf)); + if (Recorder.record || macro) + saveLabel(tf, label); + } + + /** Sets the echo character for the next string field. */ + public void setEchoChar(char echoChar) { + this.echoChar = echoChar; + } + + /** Adds a directory text field and "Browse" button, where the + * field width is determined by the length of 'defaultPath', with + * a minimum of 25 columns. Use getNextString to retrieve the + * directory path. Based on the addDirectoryField() method in + * Fiji's GenericDialogPlus class. + */ + public void addDirectoryField(String label, String defaultPath) { + int columns = defaultPath!=null?Math.max(defaultPath.length(),25):25; + addDirectoryField(label, defaultPath, columns); + } + + public void addDirectoryField(String label, String defaultPath, int columns) { + defaultPath = IJ.addSeparator(defaultPath); + addStringField(label, defaultPath, columns); + if (GraphicsEnvironment.isHeadless()) + return; + TextField text = (TextField)stringField.lastElement(); + GridBagLayout layout = (GridBagLayout)getLayout(); + GridBagConstraints constraints = layout.getConstraints(text); + Button button = new TrimmedButton("Browse",IJ.isMacOSX()?10:0); + BrowseButtonListener listener = new BrowseButtonListener(label, text, "dir"); + button.addActionListener(listener); + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + panel.add(text); + panel.add(button); + layout.setConstraints(panel, constraints); + add(panel); + if (Recorder.record || macro) + saveLabel(panel, label); + } + + /** Adds a file text field and "Browse" button, where the + * field width is determined by the length of 'defaultPath', + * with a minimum of 25 columns. Use getNextString to + * retrieve the file path. Based on the addFileField() method + * in Fiji's GenericDialogPlus class. + */ + public void addFileField(String label, String defaultPath) { + int columns = defaultPath!=null?Math.max(defaultPath.length(),25):25; + addFileField(label, defaultPath, columns); + } + + public void addFileField(String label, String defaultPath, int columns) { + addStringField(label, defaultPath, columns); + if (GraphicsEnvironment.isHeadless()) + return; + TextField text = (TextField)stringField.lastElement(); + GridBagLayout layout = (GridBagLayout)getLayout(); + GridBagConstraints constraints = layout.getConstraints(text); + Button button = new TrimmedButton("Browse",IJ.isMacOSX()?10:0); + BrowseButtonListener listener = new BrowseButtonListener(label, text, "file"); + button.addActionListener(listener); + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + panel.add(text); + panel.add(button); + layout.setConstraints(panel, constraints); + add(panel); + if (Recorder.record || macro) + saveLabel(panel, label); + } + + /** + * Add button to the dialog + * @param label button label + * @param listener listener to handle the action when pressing the button + */ + public void addButton(String label, ActionListener listener) { + if (GraphicsEnvironment.isHeadless()) + return; + Button button = new Button(label); + button.addActionListener(listener); + button.addKeyListener(this); + GridBagLayout layout = (GridBagLayout)getLayout(); + Panel panel = new Panel(); + addPanel(panel); + GridBagConstraints constraints = layout.getConstraints(panel); + remove(panel); + layout.setConstraints(button, constraints); + add(button); + } + + /** Adds a popup menu that lists the currently open images. + * Call getNextImage() to retrieve the selected + * image. Based on the addImageChoice() + * method in Fiji's GenericDialogPlus class. + * @param label the label + * @param defaultImage the image title initially selected in the menu + * or the first image if null + */ + public void addImageChoice(String label, String defaultImage) { + if (windowTitles==null) { + windowIDs = WindowManager.getIDList(); + if (windowIDs==null) + windowIDs = new int[0]; + windowTitles = new String[windowIDs.length]; + for (int i=0; ienum class of the specified default item (enum constant). + * The default item is automatically set. Calls the original (string-based) + * {@link GenericDialog#addChoice(String, String[], String)} method. + * + * @param the generic enum type containing the items to chose from + * @param label the label displayed for this choice group + * @param defaultItem the menu item initially selected + */ + public > void addEnumChoice(String label, Enum defaultItem) { + Class enumClass = defaultItem.getDeclaringClass(); + E[] enums = enumClass.getEnumConstants(); + String[] items = new String[enums.length]; + for (int i = 0; i < enums.length; i++) { + items[i] = enums[i].name(); + } + this.addChoice(label, items, defaultItem.name()); + } + + /** + * Returns the selected item in the next enum choice menu. + * Note that 'enumClass' is required to infer the proper enum type. + * Throws {@code IllegalArgumentException} if the selected item is not a defined + * constant in the specified enum class. + * + * @param the generic enum type + * @param enumClass the enum type + * @return the selected item + */ + public > E getNextEnumChoice(Class enumClass) { + String choiceString = this.getNextChoice(); + return Enum.valueOf(enumClass, choiceString); + } + + /** Adds a checkbox. + * @param label the label + * @param defaultValue the initial state + */ + public void addCheckbox(String label, boolean defaultValue) { + addCheckbox(label, defaultValue, false); + } + + /** Adds a checkbox; does not make it recordable if isPreview is true. + * With isPreview true, the checkbox can be referred to as previewCheckbox + * from hereon. + */ + private void addCheckbox(String label, boolean defaultValue, boolean isPreview) { + String label2 = label; + if (label2.indexOf('_')!=-1) + label2 = label2.replace('_', ' '); + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + c.insets.left = 10; + addToSameRow = false; + } else { + c.gridx = 0; c.gridy++; + if (checkbox==null) + c.insets = getInsets(15, 20, 0, 0); // top, left, bottom, right + else + c.insets = getInsets(0, 20, 0, 0); + } + c.anchor = GridBagConstraints.WEST; + c.gridwidth = 2; + if (checkbox==null) + checkbox = new Vector(4); + Checkbox cb = new Checkbox(label2); + cb.setState(defaultValue); + cb.addItemListener(this); + cb.addKeyListener(this); + add(cb, c); + c.insets.left = 0; + checkbox.addElement(cb); + if (!isPreview &&(Recorder.record || macro)) //preview checkbox is not recordable + saveLabel(cb, label); + if (isPreview) previewCheckbox = cb; + } + + /** Adds a checkbox labelled "Preview" for "automatic" preview. + * The reference to this checkbox can be retrieved by getPreviewCheckbox() + * and it provides the additional method previewRunning for optical + * feedback while preview is prepared. + * PlugInFilters can have their "run" method automatically called for + * preview under the following conditions: + * - the PlugInFilter must pass a reference to itself (i.e., "this") as an + * argument to the AddPreviewCheckbox + * - it must implement the DialogListener interface and set the filter + * parameters in the dialogItemChanged method. + * - it must have DIALOG and PREVIEW set in its flags. + * A previewCheckbox is always off when the filter is started and does not get + * recorded by the Macro Recorder. + * + * @param pfr A reference to the PlugInFilterRunner calling the PlugInFilter + * if automatic preview is desired, null otherwise. + */ + public void addPreviewCheckbox(PlugInFilterRunner pfr) { + if (previewCheckbox != null) + return; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null && imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.COMPOSITE) + return; + this.pfr = pfr; + addCheckbox(previewLabel, false, true); + } + + /** Add the preview checkbox with user-defined label; for details see the + * addPreviewCheckbox method with standard "Preview" label. + * Adds the checkbox when the current image is a CompositeImage + * in "Composite" mode, unlike the one argument version. + * Note that a GenericDialog can have only one PreviewCheckbox. + */ + public void addPreviewCheckbox(PlugInFilterRunner pfr, String label) { + if (previewCheckbox!=null) + return; + previewLabel = label; + this.pfr = pfr; + addCheckbox(previewLabel, false, true); + } + + /** Adds a group of checkboxs using a grid layout. + * @param rows the number of rows + * @param columns the number of columns + * @param labels the labels + * @param defaultValues the initial states + */ + public void addCheckboxGroup(int rows, int columns, String[] labels, boolean[] defaultValues) { + addCheckboxGroup(rows, columns, labels, defaultValues, null); + } + + /** Adds a group of checkboxs using a grid layout. + * @param rows the number of rows + * @param columns the number of columns + * @param labels the labels + * @param defaultValues the initial states + * @param headings the column headings + * Example: http://imagej.nih.gov/ij/plugins/multi-column-dialog/index.html + */ + public void addCheckboxGroup(int rows, int columns, String[] labels, boolean[] defaultValues, String[] headings) { + Panel panel = new Panel(); + int nRows = headings!=null?rows+1:rows; + panel.setLayout(new GridLayout(nRows, columns, 6, 0)); + int startCBIndex = cbIndex; + if (checkbox==null) + checkbox = new Vector(12); + if (headings!=null) { + Font font = new Font("SansSerif", Font.BOLD, 12); + for (int i=0; iheadings.length-1 || headings[i]==null) + panel.add(new Label("")); + else { + Label label = new Label(headings[i]); + label.setFont(font); + panel.add(label); + } + } + } + int i1 = 0; + int[] index = new int[labels.length]; + for (int row=0; row=labels.length) break; + index[i1] = i2; + String label = labels[i1]; + if (label==null || label.length()==0) { + Label lbl = new Label(""); + panel.add(lbl); + i1++; + continue; + } + if (label.indexOf('_')!=-1) + label = label.replace('_', ' '); + Checkbox cb = new Checkbox(label); + checkbox.addElement(cb); + cb.setState(defaultValues[i1]); + cb.addItemListener(this); + if (Recorder.record || macro) + saveLabel(cb, labels[i1]); + if (IJ.isLinux()) { + Panel panel2 = new Panel(); + panel2.setLayout(new BorderLayout()); + panel2.add("West", cb); + panel.add(panel2); + } else + panel.add(cb); + i1++; + } + } + c.gridx = 0; c.gridy++; + c.gridwidth = GridBagConstraints.REMAINDER; + c.anchor = GridBagConstraints.WEST; + c.insets = getInsets(10, 0, 0, 0); + addToSameRow = false; + add(panel, c); + } + + /** Adds a radio button group. + * @param label group label (or null) + * @param items radio button labels + * @param rows number of rows + * @param columns number of columns + * @param defaultItem button initially selected + */ + public void addRadioButtonGroup(String label, String[] items, int rows, int columns, String defaultItem) { + addToSameRow = false; + Panel panel = new Panel(); + int n = items.length; + panel.setLayout(new GridLayout(rows, columns, 0, 0)); + CheckboxGroup cg = new CheckboxGroup(); + for (int i=0; i=0) + theLabel = new MultiLineLabel(text); + else + theLabel = new Label(text); + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + addToSameRow = false; + } else { + c.gridx = 0; c.gridy++; + c.insets = getInsets("".equals(text)?0:10, 20, 0, 0); // top, left, bottom, right + } + c.gridwidth = GridBagConstraints.REMAINDER; + c.anchor = GridBagConstraints.WEST; + c.fill = GridBagConstraints.HORIZONTAL; + if (font!=null) { + if (Prefs.getGuiScale()>1.0) + font = font.deriveFont((float)(font.getSize()*Prefs.getGuiScale())); + theLabel.setFont(font); + } + if (color!=null) + theLabel.setForeground(color); + add(theLabel, c); + c.fill = GridBagConstraints.NONE; + } + + /** Adds one or two (side by side) text areas. + * Append "SCROLLBARS_VERTICAL_ONLY" to the text of + * the first text area to get vertical scrollbars + * and "SCROLLBARS_BOTH" to get both vertical and + * horizontal scrollbars. + * @param text1 initial contents of the first text area + * @param text2 initial contents of the second text area or null + * @param rows the number of rows + * @param columns the number of columns + */ + public void addTextAreas(String text1, String text2, int rows, int columns) { + if (textArea1!=null) return; + Panel panel = new Panel(); + int scrollbars = TextArea.SCROLLBARS_NONE; + if (text1!=null && text1.endsWith("SCROLLBARS_BOTH")) { + scrollbars = TextArea.SCROLLBARS_BOTH; + text1 = text1.substring(0, text1.length()-15); + } + if (text1!=null && text1.endsWith("SCROLLBARS_VERTICAL_ONLY")) { + scrollbars = TextArea.SCROLLBARS_VERTICAL_ONLY; + text1 = text1.substring(0, text1.length()-24); + } + Font font = new Font("SansSerif", Font.PLAIN, (int)(14*Prefs.getGuiScale())); + textArea1 = new TextArea(text1,rows,columns,scrollbars); + if (IJ.isLinux()) textArea1.setBackground(Color.white); + textArea1.setFont(font); + textArea1.addTextListener(this); + panel.add(textArea1); + if (text2!=null) { + textArea2 = new TextArea(text2,rows,columns,scrollbars); + if (IJ.isLinux()) textArea2.setBackground(Color.white); + textArea2.setFont(font); + panel.add(textArea2); + } + c.gridx = 0; c.gridy++; + c.gridwidth = GridBagConstraints.REMAINDER; + c.anchor = GridBagConstraints.WEST; + c.insets = getInsets(15, 20, 0, 0); + addToSameRow = false; + add(panel, c); + } + + /** + * Adds a slider (scroll bar) to the dialog box. + * Floating point values are used if (maxValue-minValue)<=5.0 + * and either defaultValue or minValue are non-integer. + * @param label the label + * @param minValue the minimum value of the slider + * @param maxValue the maximum value of the slider + * @param defaultValue the initial value of the slider + */ + public void addSlider(String label, double minValue, double maxValue, double defaultValue) { + if (defaultValuemaxValue) defaultValue=maxValue; + int digits = 0; + double scale = 1.0; + if ((maxValue-minValue)<=5.0 && (minValue!=(int)minValue||maxValue!=(int)maxValue||defaultValue!=(int)defaultValue)) { + scale = 50.0; + minValue *= scale; + maxValue *= scale; + defaultValue *= scale; + digits = 2; + } + addSlider( label, minValue, maxValue, defaultValue, scale, digits); + } + + /** This vesion of addSlider() adds a 'stepSize' argument.
+ * Example: http://wsr.imagej.net/macros/SliderDemo.txt + */ + public void addSlider(String label, double minValue, double maxValue, double defaultValue, double stepSize) { + if ( stepSize <= 0 ) stepSize = 1; + int digits = digits(stepSize); + if (digits==1 && "Angle:".equals(label)) + digits = 2; + double scale = 1.0 / Math.abs( stepSize ); + if ( scale <= 0 ) scale = 1; + if ( defaultValue < minValue ) defaultValue = minValue; + if ( defaultValue > maxValue ) defaultValue = maxValue; + minValue *= scale; + maxValue *= scale; + defaultValue *= scale; + addSlider(label, minValue, maxValue, defaultValue, scale, digits); + } + + /** Author: Michael Kaul */ + private static int digits(double d) { + if (d == (int)d) + return 0; + String s = Double.toString(d); + int ePos = s.indexOf("E"); + if (ePos==-1) + ePos = s.indexOf("e"); + int dotPos = s.indexOf( "." ); + int digits = 0; + if (ePos==-1 ) + digits = s.substring(dotPos+1).length(); + else { + String number = s.substring( dotPos + 1, ePos ); + if (!number.equals( "0" )) + digits += number.length( ); + digits = digits - Integer.valueOf(s.substring(ePos+1)); + } + return digits; + } + + private void addSlider(String label, double minValue, double maxValue, double defaultValue, double scale, int digits) { + int columns = 4 + digits - 2; + if ( columns < 4 ) columns = 4; + if (minValue<0.0) columns++; + String mv = IJ.d2s(maxValue,0); + if (mv.length()>4 && digits==0) + columns += mv.length()-4; + String label2 = label; + if (label2.indexOf('_')!=-1) + label2 = label2.replace('_', ' '); + Label fieldLabel = makeLabel(label2); + this.lastLabelAdded = fieldLabel; + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + c.insets.bottom += 3; + addToSameRow = false; + } else { + c.gridx = 0; c.gridy++; + c.insets = getInsets(0, 0, 3, 0); // top, left, bottom, right + } + c.anchor = GridBagConstraints.EAST; + c.gridwidth = 1; + add(fieldLabel, c); + + if (slider==null) { + slider = new Vector(5); + sliderIndexes = new Vector(5); + sliderScales = new Vector(5); + sliderDigits = new Vector(5); + } + Scrollbar s = new Scrollbar(Scrollbar.HORIZONTAL, (int)defaultValue, 1, (int)minValue, (int)maxValue+1); + GUI.fixScrollbar(s); + slider.addElement(s); + s.addAdjustmentListener(this); + s.setUnitIncrement(1); + if (IJ.isMacOSX()) + s.addKeyListener(this); + + if (numberField==null) { + numberField = new Vector(5); + defaultValues = new Vector(5); + defaultText = new Vector(5); + } + if (IJ.isWindows()) columns -= 2; + if (columns<1) columns = 1; + //IJ.log("scale=" + scale + ", columns=" + columns + ", digits=" + digits); + TextField tf = new TextField(IJ.d2s(defaultValue/scale, digits), columns); + if (IJ.isLinux()) tf.setBackground(Color.white); + tf.addActionListener(this); + tf.addTextListener(this); + tf.addFocusListener(this); + tf.addKeyListener(this); + numberField.addElement(tf); + sliderIndexes.add(new Integer(numberField.size()-1)); + sliderScales.add(new Double(scale)); + sliderDigits.add(new Integer(digits)); + defaultValues.addElement(new Double(defaultValue/scale)); + defaultText.addElement(tf.getText()); + tf.setEditable(true); + firstSlider = false; + + Panel panel = new Panel(); + GridBagLayout pgrid = new GridBagLayout(); + GridBagConstraints pc = new GridBagConstraints(); + panel.setLayout(pgrid); + pc.gridx = 0; pc.gridy = 0; + pc.gridwidth = 1; + pc.ipadx = 85; + pc.anchor = GridBagConstraints.WEST; + panel.add(s, pc); + pc.ipadx = 0; // reset + // text field + pc.gridx = 1; + pc.insets = new Insets(5, 5, 0, 0); + pc.anchor = GridBagConstraints.EAST; + panel.add(tf, pc); + + c.gridx = GridBagConstraints.RELATIVE; + c.gridwidth = 1; + c.anchor = GridBagConstraints.WEST; + c.insets.left = 0; + c.insets.bottom -= 3; + add(panel, c); + if (Recorder.record || macro) + saveLabel(tf, label); + } + + /** Adds a Panel to the dialog. */ + public void addPanel(Panel panel) { + addPanel(panel, GridBagConstraints.WEST, addToSameRow ? c.insets : getInsets(5,0,0,0)); + } + + /** Adds a Panel to the dialog with custom contraint and insets. The + defaults are GridBagConstraints.WEST (left justified) and + "new Insets(5, 0, 0, 0)" (5 pixels of padding at the top). */ + public void addPanel(Panel panel, int constraints, Insets insets) { + if (addToSameRow) { + c.gridx = GridBagConstraints.RELATIVE; + addToSameRow = false; + } else { + c.gridx = 0; c.gridy++; + } + c.gridwidth = 2; + c.anchor = constraints; + c.insets = insets; + add(panel, c); + } + + /** Adds an image to the dialog. */ + public void addImage(ImagePlus image) { + ImagePanel imagePanel = new ImagePanel(image); + addPanel(imagePanel); + if (imagePanels==null) + imagePanels = new Vector(); + imagePanels.add(imagePanel); + } + + /** Set the insets (margins), in pixels, that will be + used for the next component added to the dialog + (except components added to the same row with addToSameRow) +
+    Default insets:
+        addMessage: 0,20,0 (empty string) or 10,20,0
+        addCheckbox: 15,20,0 (first checkbox) or 0,20,0
+        addCheckboxGroup: 10,0,0
+        addRadioButtonGroup: 5,10,0
+        addNumericField: 5,0,3 (first field) or 0,0,3
+        addStringField: 5,0,5 (first field) or 0,0,5
+        addChoice: 5,0,5 (first field) or 0,0,5
+     
+ */ + public void setInsets(int top, int left, int bottom) { + topInset = top; + leftInset = left; + bottomInset = bottom; + customInsets = true; + } + + /** Makes the next item appear in the same row as the previous. + * May be used for addNumericField, addSlider, addChoice, addCheckbox, addStringField, + * addMessage, addPanel, and before the showDialog() method + * (in the latter case, the buttons appear to the right of the previous item). + * Note that addMessage (and addStringField, if its column width is more than 8) use + * the remaining width, so it must be the last item of a row. + */ + public void addToSameRow() { + addToSameRow = true; + addToSameRowCalled = true; + } + + /** Sets a replacement label for the "OK" button. */ + public void setOKLabel(String label) { + okay.setLabel(label); + } + + /** Sets a replacement label for the "Cancel" button. */ + public void setCancelLabel(String label) { + cancel.setLabel(label); + } + + /** Sets a replacement label for the "Help" button. */ + public void setHelpLabel(String label) { + helpLabel = label; + } + + /** Unchanged parameters are not recorder in 'smart recording' mode. */ + public void setSmartRecording(boolean smartRecording) { + this.smartRecording = smartRecording; + } + + /** Make this a "Yes No Cancel" dialog. */ + public void enableYesNoCancel() { + enableYesNoCancel(" Yes ", " No "); + } + + /** Make this a "Yes No Cancel" dialog with custom labels. Here is an example: +
+        GenericDialog gd = new GenericDialog("YesNoCancel Demo");
+        gd.addMessage("This is a custom YesNoCancel dialog");
+        gd.enableYesNoCancel("Do something", "Do something else");
+        gd.showDialog();
+        if (gd.wasCanceled())
+            IJ.log("User clicked 'Cancel'");
+        else if (gd.wasOKed())
+            IJ. log("User clicked 'Yes'");
+        else
+            IJ. log("User clicked 'No'");
+    	
+ */ + public void enableYesNoCancel(String yesLabel, String noLabel) { + okay.setLabel(yesLabel); + if (no != null) + no.setLabel(noLabel); + else if (noLabel!=null) + no = new Button(noLabel); + } + + /** Do not display "Cancel" button. */ + public void hideCancelButton() { + hideCancelButton = true; + } + + Insets getInsets(int top, int left, int bottom, int right) { + if (customInsets) { + customInsets = false; + return new Insets(topInset, leftInset, bottomInset, 0); + } else + return new Insets(top, left, bottom, right); + } + + /** Add an Object implementing the DialogListener interface. This object will + * be notified by its dialogItemChanged method of input to the dialog. The first + * DialogListener will be also called after the user has typed 'OK' or if the + * dialog has been invoked by a macro; it should read all input fields of the + * dialog. + * For other listeners, the OK button will not cause a call to dialogItemChanged; + * the CANCEL button will never cause such a call. + * @param dl the Object that wants to listen. + */ + public void addDialogListener(DialogListener dl) { + if (dialogListeners == null) + dialogListeners = new Vector(); + dialogListeners.addElement(dl); + } + + /** Returns true if the user clicked on "Cancel". */ + public boolean wasCanceled() { + if (wasCanceled && !Thread.currentThread().getName().endsWith("Script_Macro$")) + Macro.abort(); + return wasCanceled; + } + + /** Returns true if the user has clicked on "OK" or a macro is running. */ + public boolean wasOKed() { + return wasOKed || macro; + } + + /** Returns the contents of the next numeric field, + or NaN if the field does not contain a number. */ + public double getNextNumber() { + if (numberField==null) + return -1.0; + TextField tf = (TextField)numberField.elementAt(nfIndex); + String theText = tf.getText(); + String label=null; + if (macro) { + label = (String)labels.get((Object)tf); + theText = Macro.getValue(macroOptions, label, theText); + } + String originalText = (String)defaultText.elementAt(nfIndex); + double defaultValue = ((Double)(defaultValues.elementAt(nfIndex))).doubleValue(); + double value; + boolean skipRecording = false; + if (theText.equals(originalText)) { + value = defaultValue; + if (smartRecording) skipRecording=true; + } else { + Double d = getValue(theText); + if (d!=null) + value = d.doubleValue(); + else { + // Is the value a macro variable? + if (theText.startsWith("&")) theText = theText.substring(1); + Interpreter interp = Interpreter.getInstance(); + value = interp!=null?interp.getVariable2(theText):Double.NaN; + if (Double.isNaN(value)) { + invalidNumber = true; + errorMessage = "\""+theText+"\" is an invalid number"; + value = Double.NaN; + if (macro) { + IJ.error("Macro Error", "Numeric value expected in run() function\n \n" + +" Dialog box title: \""+getTitle()+"\"\n" + +" Key: \""+label.toLowerCase(Locale.US)+"\"\n" + +" Value or variable name: \""+theText+"\""); + } + } + } + } + if (recorderOn && !skipRecording) { + recordOption(tf, trim(theText)); + } + nfIndex++; + return value; + } + + private String trim(String value) { + if (value.endsWith(".0")) + value = value.substring(0, value.length()-2); + if (value.endsWith(".00")) + value = value.substring(0, value.length()-3); + return value; + } + + private void recordOption(Object component, String value) { + String label = (String)labels.get(component); + if (value.equals("")) value = "[]"; + Recorder.recordOption(label, value); + } + + private void recordCheckboxOption(Checkbox cb) { + String label = (String)labels.get((Object)cb); + if (label!=null) { + if (cb.getState()) // checked + Recorder.recordOption(label); + else if (Recorder.getCommandOptions()==null) + Recorder.recordOption(" "); + } + } + + protected Double getValue(String text) { + Double d; + try {d = new Double(text);} + catch (NumberFormatException e){ + d = null; + } + return d; + } + + public double parseDouble(String s) { + if (s==null) return Double.NaN; + double value = Tools.parseDouble(s); + if (Double.isNaN(value)) { + if (s.startsWith("&")) s = s.substring(1); + Interpreter interp = Interpreter.getInstance(); + value = interp!=null?interp.getVariable2(s):Double.NaN; + } + return value; + } + + /** Returns true if one or more of the numeric fields contained an + invalid number. Must be called after one or more calls to getNextNumber(). */ + public boolean invalidNumber() { + boolean wasInvalid = invalidNumber; + invalidNumber = false; + return wasInvalid; + } + + /** Returns an error message if getNextNumber was unable to convert a + string into a number, otherwise, returns null. */ + public String getErrorMessage() { + return errorMessage; + } + + /** Returns the contents of the next text field. */ + public String getNextString() { + String theText; + if (stringField==null) + return ""; + TextField tf = (TextField)(stringField.elementAt(sfIndex)); + theText = tf.getText(); + String label = labels!=null?(String)labels.get((Object)tf):""; + if (macro) { + theText = Macro.getValue(macroOptions, label, theText); + if (theText!=null && (theText.startsWith("&")||label.toLowerCase(Locale.US).startsWith(theText))) { + // Is the value a macro variable? + if (theText.startsWith("&")) theText = theText.substring(1); + Interpreter interp = Interpreter.getInstance(); + String s = interp!=null?interp.getVariableAsString(theText):null; + if (s!=null) theText = s; + } + } + if (recorderOn && !label.equals("")) { + String s = theText; + if (s!=null&&s.length()>=3&&Character.isLetter(s.charAt(0))&&s.charAt(1)==':'&&s.charAt(2)=='\\') + s = s.replaceAll("\\\\", "/"); // replace "\" with "/" in Windows file paths + s = Recorder.fixString(s); + if (!smartRecording || !s.equals((String)defaultStrings.elementAt(sfIndex))) + recordOption(tf, s); + else if (Recorder.getCommandOptions()==null) + Recorder.recordOption(" "); + } + sfIndex++; + return theText; + } + + /** Returns the state of the next checkbox. */ + public boolean getNextBoolean() { + if (checkbox==null) + return false; + Checkbox cb = (Checkbox)(checkbox.elementAt(cbIndex)); + if (recorderOn) + recordCheckboxOption(cb); + boolean state = cb.getState(); + if (macro) { + String label = (String)labels.get((Object)cb); + String key = Macro.trimKey(label); + state = isMatch(macroOptions, key+" "); + } + cbIndex++; + return state; + } + + // Returns true if s2 is in s1 and not in a bracketed literal (e.g., "[literal]") + boolean isMatch(String s1, String s2) { + if (s1.startsWith(s2)) + return true; + s2 = " " + s2; + int len1 = s1.length(); + int len2 = s2.length(); + boolean match, inLiteral=false; + char c; + for (int i=0; i1&&s1.charAt(i-1)=='=')) + continue; + match = true; + for (int j=0; j0) { + resetCounters(); + ((DialogListener)dialogListeners.elementAt(0)).dialogItemChanged(this,null); + recorderOn = false; + } + resetCounters(); + } + + @Override + public void setFont(Font font) { + super.setFont(!fontSizeSet&&Prefs.getGuiScale()!=1.0&&font!=null?font.deriveFont((float)(font.getSize()*Prefs.getGuiScale())):font); + fontSizeSet = true; + } + + /** Reset the counters before reading the dialog parameters */ + void resetCounters() { + nfIndex = 0; // prepare for readout + sfIndex = 0; + cbIndex = 0; + choiceIndex = 0; + textAreaIndex = 0; + radioButtonIndex = 0; + invalidNumber = false; + } + + /** Returns the Vector containing the numeric TextFields. */ + public Vector getNumericFields() { + return numberField; + } + + /** Returns the Vector containing the string TextFields. */ + public Vector getStringFields() { + return stringField; + } + + /** Returns the Vector containing the Checkboxes. */ + public Vector getCheckboxes() { + return checkbox; + } + + /** Returns the Vector containing the Choices. */ + public Vector getChoices() { + return choice; + } + + /** Returns the Vector containing the sliders (Scrollbars). */ + public Vector getSliders() { + return slider; + } + + /** Returns the Vector that contains the RadioButtonGroups. */ + public Vector getRadioButtonGroups() { + return radioButtonGroups; + } + + /** Returns a reference to textArea1. */ + public TextArea getTextArea1() { + return textArea1; + } + + /** Returns a reference to textArea2. */ + public TextArea getTextArea2() { + return textArea2; + } + + /** Returns a reference to the Label or MultiLineLabel created by the + * last addMessage() call. Otherwise returns null. */ + public Component getMessage() { + return theLabel; + } + + /** Returns a reference to the Preview checkbox. */ + public Checkbox getPreviewCheckbox() { + return previewCheckbox; + } + + /** Returns 'true' if this dialog has a "Preview" checkbox and it is enabled. */ + public boolean isPreviewActive() { + return previewCheckbox!=null && previewCheckbox.getState(); + } + + /** Returns references to the "OK" ("Yes"), "Cancel", + and if present, "No" buttons as an array. */ + public Button[] getButtons() { + Button[] buttons = new Button[3]; + buttons[0] = okay; + buttons[1] = cancel; + buttons[2] = no; + return buttons; + } + + /** Used by PlugInFilterRunner to provide visable feedback whether preview + is running or not by switching from "Preview" to "wait..." + */ + public void previewRunning(boolean isRunning) { + if (previewCheckbox!=null) { + previewCheckbox.setLabel(isRunning ? previewRunning : previewLabel); + if (IJ.isMacOSX()) repaint(); //workaround OSX 10.4 refresh bug + } + } + + /** Display dialog centered on the primary screen. */ + public void centerDialog(boolean b) { + centerDialog = b; + } + + /* Display the dialog at the specified location. */ + public void setLocation(int x, int y) { + super.setLocation(x, y); + centerDialog = false; + } + + public void setDefaultString(int index, String str) { + if (defaultStrings!=null && index>=0 && index". There is an example at + http://imagej.nih.gov/ij/macros/js/DialogWithHelp.js + */ + public void addHelp(String url) { + helpURL = url; + } + + void showHelp() { + if (helpURL.startsWith("")) { + String title = getTitle()+" "+helpLabel; + if (this instanceof NonBlockingGenericDialog) + new HTMLDialog(title, helpURL, false); // non blocking + else + new HTMLDialog(this, title, helpURL); //modal + } else { + String macro = "call('ij.plugin.BrowserLauncher.open', '"+helpURL+"');"; + new MacroRunner(macro); // open on separate thread using BrowserLauncher + } + } + + protected boolean isMacro() { + return macro; + } + + public static GenericDialog getInstance() { + return instance; + } + + /** Closes the dialog; records the options */ + public void dispose() { + super.dispose(); + instance = null; + + if (!macro) { + recorderOn = Recorder.record; + IJ.wait(25); + } + resetCounters(); + finalizeRecording(); + resetCounters(); + } + + /** Returns a reference to the label of the most recently + added numeric field, string field, choice or slider. */ + public Label getLabel() { + return lastLabelAdded; + } + + public void windowActivated(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + + @SuppressWarnings("unchecked") + static String getString(DropTargetDropEvent event) + throws IOException, UnsupportedFlavorException { + String text = null; + DataFlavor fileList = DataFlavor.javaFileListFlavor; + + if (event.isDataFlavorSupported(fileList)) { + event.acceptDrop(DnDConstants.ACTION_COPY); + java.util.List list = (java.util.List)event.getTransferable().getTransferData(fileList); + text = list.get(0).getAbsolutePath(); + } + else if (event.isDataFlavorSupported(DataFlavor.stringFlavor)) { + event.acceptDrop(DnDConstants.ACTION_COPY); + text = (String)event.getTransferable() + .getTransferData(DataFlavor.stringFlavor); + if (text.startsWith("file://")) + text = text.substring(7); + text = stripSuffix(stripSuffix(text, "\n"), + "\r").replaceAll("%20", " "); + } + else { + event.rejectDrop(); + return null; + } + + event.dropComplete(text != null); + return text; + } + + static String stripSuffix(String s, String suffix) { + return !s.endsWith(suffix) ? s : + s.substring(0, s.length() - suffix.length()); + } + + static class TextDropTarget extends DropTargetAdapter { + TextField text; + DataFlavor flavor = DataFlavor.stringFlavor; + + public TextDropTarget(TextField text) { + this.text = text; + } + + @Override + public void drop(DropTargetDropEvent event) { + try { + text.setText(getString(event)); + } catch (Exception e) { e.printStackTrace(); } + } + } + + private class BrowseButtonListener implements ActionListener { + private String label; + private TextField textField; + private String mode; + + public BrowseButtonListener(String label, TextField textField, String mode) { + this.label = label; + this.textField = textField; + this.mode = mode; + } + + public void actionPerformed(ActionEvent e) { + String path = null; + if (mode.equals("dir")) { + path = IJ.getDir("Select a Folder"); + } else { + OpenDialog od = new OpenDialog("Select a File", null); + String directory = od.getDirectory(); + String name = od.getFileName(); + if (name!=null) + path = directory+name; + } + if (path!=null) + this.textField.setText(path); + } + + } + +} diff --git a/src/ij/gui/HTMLDialog.java b/src/ij/gui/HTMLDialog.java new file mode 100644 index 0000000..a08fe4d --- /dev/null +++ b/src/ij/gui/HTMLDialog.java @@ -0,0 +1,149 @@ +package ij.gui; +import ij.*; +import ij.plugin.URLOpener; +import ij.macro.MacroRunner; +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.text.*; +import javax.swing.text.html.*; +import javax.swing.event.HyperlinkListener; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkEvent.EventType; +import java.net.URL; + +/** This is modal or non-modal dialog box that displays HTML formated text. */ +public class HTMLDialog extends JDialog implements ActionListener, KeyListener, HyperlinkListener, WindowListener { + private boolean escapePressed; + private JEditorPane editorPane; + private boolean modal = true; + + public HTMLDialog(String title, String message) { + super(ij.IJ.getInstance(), title, true); + init(message); + } + + public HTMLDialog(Dialog parent, String title, String message) { + super(parent, title, true); + init(message); + } + + public HTMLDialog(String title, String message, boolean modal) { + super(ij.IJ.getInstance(), title, modal); + this.modal = modal; + init(message); + } + + private void init(String message) { + ij.util.Java2.setSystemLookAndFeel(); + Container container = getContentPane(); + container.setLayout(new BorderLayout()); + if (message==null) message = ""; + editorPane = new JEditorPane("text/html",""); + editorPane.setEditable(false); + HTMLEditorKit kit = new HTMLEditorKit(); + editorPane.setEditorKit(kit); + StyleSheet styleSheet = kit.getStyleSheet(); + styleSheet.addRule("body{font-family:Verdana,sans-serif; font-size:11.5pt; margin:5px 10px 5px 10px;}"); //top right bottom left + styleSheet.addRule("h1{font-size:18pt;}"); + styleSheet.addRule("h2{font-size:15pt;}"); + styleSheet.addRule("dl dt{font-face:bold;}"); + editorPane.setText(message); //display the html text with the above style + editorPane.getActionMap().put("insert-break", new AbstractAction(){ + public void actionPerformed(ActionEvent e) {} + }); //suppress beep on key + JScrollPane scrollPane = new JScrollPane(editorPane); + container.add(scrollPane); + JButton button = new JButton("OK"); + button.addActionListener(this); + button.addKeyListener(this); + editorPane.addKeyListener(this); + editorPane.addHyperlinkListener(this); + JPanel panel = new JPanel(); + panel.add(button); + container.add(panel, "South"); + setForeground(Color.black); + addWindowListener(this); + pack(); + Dimension screenD = IJ.getScreenSize(); + Dimension dialogD = getSize(); + int maxWidth = (int)(Math.min(0.70*screenD.width, 800)); //max 70% of screen width, but not more than 800 pxl + if (maxWidth>400 && dialogD.width>maxWidth) + dialogD.width = maxWidth; + if (dialogD.height > 0.80*screenD.height && screenD.height>400) //max 80% of screen height + dialogD.height = (int)(0.80*screenD.height); + setSize(dialogD); + GUI.centerOnImageJScreen(this); + if (!modal) { + WindowManager.addWindow(this); + show(); + } + final JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar(); + if (verticalScrollBar!=null) { + EventQueue.invokeLater(new Runnable() { + public void run() { + verticalScrollBar.setValue(verticalScrollBar.getMinimum()); //start scrollbar at top + } + }); + } + if (modal) show(); + } + + public void actionPerformed(ActionEvent e) { + dispose(); + } + + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + ij.IJ.setKeyDown(keyCode); + escapePressed = keyCode==KeyEvent.VK_ESCAPE; + if (keyCode==KeyEvent.VK_C) { + if (editorPane.getSelectedText()==null || editorPane.getSelectedText().length()==0) + editorPane.selectAll(); + editorPane.copy(); + editorPane.select(0,0); + } else if (keyCode==KeyEvent.VK_ENTER || keyCode==KeyEvent.VK_W || escapePressed) + dispose(); + } + + public void keyReleased(KeyEvent e) { + int keyCode = e.getKeyCode(); + ij.IJ.setKeyUp(keyCode); + } + + public void keyTyped(KeyEvent e) {} + + public boolean escapePressed() { + return escapePressed; + } + + public void hyperlinkUpdate(HyperlinkEvent e) { + if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + String url = e.getDescription(); //getURL does not work for relative links within document such as "#top" + if (url==null) return; + if (url.startsWith("#")) + editorPane.scrollToReference(url.substring(1)); + else { + String macro = "run('URL...', 'url="+url+"');"; + new MacroRunner(macro); + } + } + } + + public void dispose() { + super.dispose(); + if (!modal) WindowManager.removeWindow(this); + } + + public void windowClosing(WindowEvent e) { + dispose(); + } + + public void windowActivated(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + +} diff --git a/src/ij/gui/HistogramPlot.java b/src/ij/gui/HistogramPlot.java new file mode 100644 index 0000000..9e346fc --- /dev/null +++ b/src/ij/gui/HistogramPlot.java @@ -0,0 +1,382 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.plugin.filter.Analyzer; +import ij.measure.*; +import ij.macro.Interpreter; +import java.awt.*; +import java.awt.image.*; + +public class HistogramPlot extends ImagePlus { + static final double SCALE = Prefs.getGuiScale(); + static final int HIST_WIDTH = (int)(SCALE*256); + static final int HIST_HEIGHT = (int)(SCALE*128); + static final int XMARGIN = (int)(20*SCALE); + static final int YMARGIN = (int)(10*SCALE); + static final int WIN_WIDTH = HIST_WIDTH + (int)(44*SCALE); + static final int WIN_HEIGHT = HIST_HEIGHT + (int)(118*SCALE); + static final int BAR_HEIGHT = (int)(SCALE*12); + static final int INTENSITY1=0, INTENSITY2=1, RGB=2, RED=3, GREEN=4, BLUE=5; + static final Color frameColor = new Color(30,60,120); + + int rgbMode = -1; + ImageStatistics stats; + boolean stackHistogram; + Calibration cal; + long[] histogram; + LookUpTable lut; + int decimalPlaces; + int digits; + long newMaxCount; + boolean logScale; + int yMax; + int srcImageID; + Rectangle frame; + Font font = new Font("SansSerif",Font.PLAIN,(int)(12*SCALE)); + boolean showBins; + int col1, col2, row1, row2, row3, row4, row5; + + public HistogramPlot() { + setImage(NewImage.createRGBImage("Histogram", WIN_WIDTH, WIN_HEIGHT, 1, NewImage.FILL_WHITE)); + } + + /** Plots a histogram using the specified title and number of bins. + Currently, the number of bins must be 256 expect for 32 bit images. */ + public void draw(String title, ImagePlus imp, int bins) { + draw(imp, bins, 0.0, 0.0, 0); + } + + /** Plots a histogram using the specified title, number of bins and histogram range. + Currently, the number of bins must be 256 and the histogram range range must be + the same as the image range expect for 32 bit images. */ + public void draw(ImagePlus imp, int bins, double histMin, double histMax, int yMax) { + boolean limitToThreshold = (Analyzer.getMeasurements()&LIMIT)!=0; + ImageProcessor ip = imp.getProcessor(); + if (ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD + && ip.getLutUpdateMode()==ImageProcessor.NO_LUT_UPDATE) + limitToThreshold = false; // ignore invisible thresholds + if (imp.isRGB() && rgbMode maxCount2) && (i != stats.mode)) { + maxCount2 = histogram[i]; + mode2 = i; + } + } + newMaxCount = histogram[stats.mode]; + if ((newMaxCount>(maxCount2 * 2)) && (maxCount2 != 0)) + newMaxCount = (int)(maxCount2 * 1.5); + if (logScale) + drawLogPlot(yMax>0?yMax:newMaxCount, ip); + drawPlot(yMax>0?yMax:newMaxCount, ip); + histogram[stats.mode] = saveModalCount; + x = XMARGIN + 1; + y = YMARGIN + HIST_HEIGHT + 2; + if (imp==null) + lut.drawUnscaledColorBar(ip, x-1, y, HIST_WIDTH, BAR_HEIGHT); + else + drawAlignedColorBar(imp, xMin, xMax, ip, x-1, y, HIST_WIDTH, BAR_HEIGHT); + y += BAR_HEIGHT+(int)(15*SCALE); + drawText(ip, x, y, fixedRange); + srcImageID = imp.getID(); + } + + void drawAlignedColorBar(ImagePlus imp, double xMin, double xMax, ImageProcessor ip, int x, int y, int width, int height) { + ImageProcessor ipSource = imp.getProcessor(); + float[] pixels = null; + ImageProcessor ipRamp = null; + if (rgbMode>=INTENSITY1) { + ipRamp = new FloatProcessor(width, height); + if (rgbMode==RED) + ipRamp.setColorModel(LUT.createLutFromColor(Color.red)); + else if (rgbMode==GREEN) + ipRamp.setColorModel(LUT.createLutFromColor(Color.green)); + else if (rgbMode==BLUE) + ipRamp.setColorModel(LUT.createLutFromColor(Color.blue)); + pixels = (float[])ipRamp.getPixels(); + } else + pixels = new float[width*height]; + for (int j=0; jipSource.getPixelCount()) { // stack histogram + cm = LUT.createLutFromColor(Color.white); + min = stats.min; + max = stats.max; + } else + cm = ((CompositeImage)imp).getChannelLut(); + } else if (ipSource.getMinThreshold()==ImageProcessor.NO_THRESHOLD) + cm = ipSource.getColorModel(); + else + cm = ipSource.getCurrentColorModel(); + ipRamp = new FloatProcessor(width, height, pixels, cm); + } + ipRamp.setMinAndMax(min,max); + ImageProcessor bar = null; + if (ip instanceof ColorProcessor) + bar = ipRamp.convertToRGB(); + else + bar = ipRamp.convertToByte(true); + ip.insert(bar, x,y); + ip.setColor(Color.black); + ip.drawRect(x-1, y, width+2, height); + } + + /** Scales a threshold level to the range 0-255. */ + int scaleDown(ImageProcessor ip, double threshold) { + double min = ip.getMin(); + double max = ip.getMax(); + if (max>min) + return (int)(((threshold-min)/(max-min))*255.0); + else + return 0; + } + + void drawPlot(long maxCount, ImageProcessor ip) { + if (maxCount==0) maxCount = 1; + frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT); + if (histogram.length==256) { + double scale2 = HIST_WIDTH/256.0; + int barWidth = 1; + if (SCALE>1) barWidth=2; + if (SCALE>2) barWidth=3; + for (int i = 0; i < 256; i++) { + int x =(int)(i*scale2); + int y = (int)(((double)HIST_HEIGHT*(double)histogram[i])/maxCount); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + for (int j = 0; jHIST_HEIGHT) y = HIST_HEIGHT; + ip.drawLine(i+XMARGIN, YMARGIN+HIST_HEIGHT, i+XMARGIN, YMARGIN+HIST_HEIGHT-y); + } + } else { + double xscale = (double)HIST_WIDTH/histogram.length; + for (int i=0; i0L) { + int y = (int)(((double)HIST_HEIGHT*(double)value)/maxCount); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + int x = (int)(i*xscale)+XMARGIN; + ip.drawLine(x, YMARGIN+HIST_HEIGHT, x, YMARGIN+HIST_HEIGHT-y); + } + } + } + ip.setColor(frameColor); + ip.drawRect(frame.x-1, frame.y, frame.width+2, frame.height+1); + ip.setColor(Color.black); + } + + void drawLogPlot (long maxCount, ImageProcessor ip) { + frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT); + ip.drawRect(frame.x-1, frame.y, frame.width+2, frame.height+1); + double max = Math.log(maxCount); + ip.setColor(Color.gray); + if (histogram.length==256) { + double scale2 = HIST_WIDTH/256.0; + int barWidth = 1; + if (SCALE>1) barWidth=2; + if (SCALE>2) barWidth=3; + for (int i=0; i < 256; i++) { + int x =(int)(i*scale2); + int y = histogram[i]==0?0:(int)(HIST_HEIGHT*Math.log(histogram[i])/max); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + for (int j = 0; jHIST_HEIGHT) y = HIST_HEIGHT; + ip.drawLine(i+XMARGIN, YMARGIN+HIST_HEIGHT, i+XMARGIN, YMARGIN+HIST_HEIGHT-y); + } + } else { + double xscale = (double)HIST_WIDTH/histogram.length; + for (int i=0; i0L) { + int y = (int)(HIST_HEIGHT*Math.log(value)/max); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + int x = (int)(i*xscale)+XMARGIN; + ip.drawLine(x, YMARGIN+HIST_HEIGHT, x, YMARGIN+HIST_HEIGHT-y); + } + } + } + ip.setColor(Color.black); + } + + void drawText(ImageProcessor ip, int x, int y, boolean fixedRange) { + ip.setFont(font); + ip.setAntialiasedText(true); + double hmin = cal.getCValue(stats.histMin); + double hmax = cal.getCValue(stats.histMax); + double range = hmax-hmin; + if (fixedRange&&!cal.calibrated()&&hmin==0&&hmax==255) + range = 256; + ip.drawString(d2s(hmin), x - 4, y); + ip.drawString(d2s(hmax), x + HIST_WIDTH - getWidth(hmax, ip) + 10, y); + if (rgbMode>=INTENSITY1) { + x += HIST_WIDTH/2; + y += 1; + ip.setJustification(ImageProcessor.CENTER_JUSTIFY); + boolean weighted = ((ColorProcessor)ip).weightedHistogram(); + switch (rgbMode) { + case INTENSITY1: ip.drawString((weighted?"Intensity (weighted)":"Intensity (unweighted)"), x, y); break; + case INTENSITY2: ip.drawString((weighted?"Intensity (unweighted)":"Intensity (weighted)"), x, y); break; + case RGB: ip.drawString("R+G+B", x, y); break; + case RED: ip.drawString("Red", x, y); break; + case GREEN: ip.drawString("Green", x, y); break; + case BLUE: ip.drawString("Blue", x, y); break; + } + ip.setJustification(ImageProcessor.LEFT_JUSTIFY); + } + double binWidth = range/stats.nBins; + binWidth = Math.abs(binWidth); + showBins = binWidth!=1.0 || !fixedRange; + col1 = XMARGIN + 5; + col2 = XMARGIN + HIST_WIDTH/2; + row1 = y+(int)(25*SCALE); + if (showBins) row1 -= (int)(8*SCALE); + row2 = row1 + (int)(15*SCALE); + row3 = row2 + (int)(15*SCALE); + row4 = row3 + (int)(15*SCALE); + row5 = row4 + (int)(15*SCALE); + long count = stats.longPixelCount>0?stats.longPixelCount:stats.pixelCount; + String modeCount = " (" + stats.maxCount + ")"; + if (modeCount.length()>12) modeCount = ""; + + ip.drawString("N: " + count, col1, row1); + ip.drawString("Min: " + d2s(stats.min), col2, row1); + ip.drawString("Mean: " + d2s(stats.mean), col1, row2); + ip.drawString("Max: " + d2s(stats.max), col2, row2); + ip.drawString("StdDev: " + d2s(stats.stdDev), col1, row3); + ip.drawString("Mode: " + d2s(stats.dmode) + modeCount, col2, row3); + if (showBins) { + ip.drawString("Bins: " + d2s(stats.nBins), col1, row4); + ip.drawString("Bin Width: " + d2s(binWidth), col2, row4); + } + } + + private String d2s(double d) { + if ((int)d==d) + return IJ.d2s(d, 0); + else + return IJ.d2s(d, 3, 8); + } + + int getWidth(double d, ImageProcessor ip) { + return ip.getStringWidth(d2s(d)); + } + + public int[] getHistogram() { + int[] hist = new int[histogram.length]; + for (int i=0; i=frame.x && x<=(frame.x+frame.width)) { + x = (x - frame.x); + int index = (int)(x*(SCALE*histogram.length)/HIST_WIDTH/SCALE); + if (index>=histogram.length) index = histogram.length-1; + double value = cal.getCValue(stats.histMin+index*stats.binSize); + drawValueAndCount(ip, value, histogram[index]); + } else + drawValueAndCount(ip, Double.NaN, -1); + this.imp.updateAndDraw(); + } + + protected void drawHistogram(ImageProcessor ip, boolean fixedRange) { + drawHistogram(null, ip, fixedRange, 0.0, 0.0); + } + + void drawHistogram(ImagePlus imp, ImageProcessor ip, boolean fixedRange, double xMin, double xMax) { + int x, y; + long maxCount2 = 0; + int mode2 = 0; + long saveModalCount; + ip.setColor(Color.black); + ip.setLineWidth(1); + decimalPlaces = Analyzer.getPrecision(); + digits = cal.calibrated()||stats.binSize!=1.0?decimalPlaces:0; + saveModalCount = histogram[stats.mode]; + for (int i = 0; i maxCount2) && (i != stats.mode)) { + maxCount2 = histogram[i]; + mode2 = i; + } + } + newMaxCount = histogram[stats.mode]; + if ((newMaxCount>(maxCount2 * 2)) && (maxCount2 != 0)) + newMaxCount = (int)(maxCount2 * 1.5); + if (logScale || IJ.shiftKeyDown() && !liveMode()) + drawLogPlot(yMax>0?yMax:newMaxCount, ip); + drawPlot(yMax>0?yMax:newMaxCount, ip); + histogram[stats.mode] = saveModalCount; + x = XMARGIN + 1; + y = YMARGIN + HIST_HEIGHT + 2; + if (imp==null) + lut.drawUnscaledColorBar(ip, x-1, y, HIST_WIDTH, BAR_HEIGHT); + else + drawAlignedColorBar(imp, xMin, xMax, ip, x-1, y, HIST_WIDTH, BAR_HEIGHT); + y += BAR_HEIGHT+(int)(15*SCALE); + drawText(ip, x, y, fixedRange); + srcImageID = imp.getID(); + } + + void drawAlignedColorBar(ImagePlus imp, double xMin, double xMax, ImageProcessor ip, int x, int y, int width, int height) { + ImageProcessor ipSource = imp.getProcessor(); + float[] pixels = null; + ImageProcessor ipRamp = null; + if (rgbMode>=INTENSITY1) { + ipRamp = new FloatProcessor(width, height); + if (rgbMode==RED) + ipRamp.setColorModel(LUT.createLutFromColor(Color.red)); + else if (rgbMode==GREEN) + ipRamp.setColorModel(LUT.createLutFromColor(Color.green)); + else if (rgbMode==BLUE) + ipRamp.setColorModel(LUT.createLutFromColor(Color.blue)); + pixels = (float[])ipRamp.getPixels(); + } else + pixels = new float[width*height]; + for (int j=0; jipSource.getPixelCount()) { // stack histogram + cm = LUT.createLutFromColor(Color.white); + min = stats.min; + max = stats.max; + } else + cm = ((CompositeImage)imp).getChannelLut(); + } else if (ipSource.getMinThreshold()==ImageProcessor.NO_THRESHOLD) + cm = ipSource.getColorModel(); + else + cm = ipSource.getCurrentColorModel(); + ipRamp = new FloatProcessor(width, height, pixels, cm); + } + ipRamp.setMinAndMax(min,max); + ImageProcessor bar = null; + if (ip instanceof ColorProcessor) + bar = ipRamp.convertToRGB(); + else + bar = ipRamp.convertToByte(true); + ip.insert(bar, x,y); + ip.setColor(Color.black); + ip.drawRect(x-1, y, width+2, height); + } + + /** Scales a threshold level to the range 0-255. */ + int scaleDown(ImageProcessor ip, double threshold) { + double min = ip.getMin(); + double max = ip.getMax(); + if (max>min) + return (int)(((threshold-min)/(max-min))*255.0); + else + return 0; + } + + void drawPlot(long maxCount, ImageProcessor ip) { + if (maxCount==0) maxCount = 1; + frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT); + ip.drawRect(frame.x-1, frame.y, frame.width+2, frame.height+1); + if (histogram.length==256) { + double scale2 = HIST_WIDTH/256.0; + int barWidth = 1; + if (SCALE>1) barWidth=2; + if (SCALE>2) barWidth=3; + for (int i = 0; i < 256; i++) { + int x =(int)(i*scale2); + int y = (int)(((double)HIST_HEIGHT*(double)histogram[i])/maxCount); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + for (int j = 0; jHIST_HEIGHT) y = HIST_HEIGHT; + ip.drawLine(i+XMARGIN, YMARGIN+HIST_HEIGHT, i+XMARGIN, YMARGIN+HIST_HEIGHT-y); + } + } else { + double xscale = (double)HIST_WIDTH/histogram.length; + for (int i=0; i0L) { + int y = (int)(((double)HIST_HEIGHT*(double)value)/maxCount); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + int x = (int)(i*xscale)+XMARGIN; + ip.drawLine(x, YMARGIN+HIST_HEIGHT, x, YMARGIN+HIST_HEIGHT-y); + } + } + } + } + + void drawLogPlot (long maxCount, ImageProcessor ip) { + frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT); + ip.drawRect(frame.x-1, frame.y, frame.width+2, frame.height+1); + double max = Math.log(maxCount); + ip.setColor(Color.gray); + if (histogram.length==256) { + double scale2 = HIST_WIDTH/256.0; + int barWidth = 1; + if (SCALE>1) barWidth=2; + if (SCALE>2) barWidth=3; + for (int i=0; i < 256; i++) { + int x =(int)(i*scale2); + int y = histogram[i]==0?0:(int)(HIST_HEIGHT*Math.log(histogram[i])/max); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + for (int j = 0; jHIST_HEIGHT) y = HIST_HEIGHT; + ip.drawLine(i+XMARGIN, YMARGIN+HIST_HEIGHT, i+XMARGIN, YMARGIN+HIST_HEIGHT-y); + } + } else { + double xscale = (double)HIST_WIDTH/histogram.length; + for (int i=0; i0L) { + int y = (int)(HIST_HEIGHT*Math.log(value)/max); + if (y>HIST_HEIGHT) y = HIST_HEIGHT; + int x = (int)(i*xscale)+XMARGIN; + ip.drawLine(x, YMARGIN+HIST_HEIGHT, x, YMARGIN+HIST_HEIGHT-y); + } + } + } + ip.setColor(Color.black); + } + + void drawText(ImageProcessor ip, int x, int y, boolean fixedRange) { + ip.setFont(font); + ip.setAntialiasedText(true); + double hmin = cal.getCValue(stats.histMin); + double hmax = cal.getCValue(stats.histMax); + double range = hmax-hmin; + if (fixedRange&&!cal.calibrated()&&hmin==0&&hmax==255) + range = 256; + ip.drawString(d2s(hmin), x - 4, y); + ip.drawString(d2s(hmax), x + HIST_WIDTH - getWidth(hmax, ip) + 10, y); + if (rgbMode>=INTENSITY1) { + x += HIST_WIDTH/2; + y += 1; + ip.setJustification(ImageProcessor.CENTER_JUSTIFY); + boolean weighted = ((ColorProcessor)ip).weightedHistogram(); + switch (rgbMode) { + case INTENSITY1: ip.drawString((weighted?"Intensity (weighted)":"Intensity (unweighted)"), x, y); break; + case INTENSITY2: ip.drawString((weighted?"Intensity (unweighted)":"Intensity (weighted)"), x, y); break; + case RGB: ip.drawString("R+G+B", x, y); break; + case RED: ip.drawString("Red", x, y); break; + case GREEN: ip.drawString("Green", x, y); break; + case BLUE: ip.drawString("Blue", x, y); break; + } + ip.setJustification(ImageProcessor.LEFT_JUSTIFY); + } + double binWidth = range/stats.nBins; + binWidth = Math.abs(binWidth); + showBins = binWidth!=1.0 || !fixedRange; + col1 = XMARGIN + 5; + col2 = XMARGIN + HIST_WIDTH/2; + row1 = y+(int)(25*SCALE); + if (showBins) row1 -= (int)(8*SCALE); + row2 = row1 + (int)(15*SCALE); + row3 = row2 + (int)(15*SCALE); + row4 = row3 + (int)(15*SCALE); + row5 = row4 + (int)(15*SCALE); + long count = stats.longPixelCount>0?stats.longPixelCount:stats.pixelCount; + String modeCount = " (" + stats.maxCount + ")"; + if (modeCount.length()>12) modeCount = ""; + + ip.drawString("N: " + count, col1, row1); + ip.drawString("Min: " + d2s(stats.min), col2, row1); + ip.drawString("Mean: " + d2s(stats.mean), col1, row2); + ip.drawString("Max: " + d2s(stats.max), col2, row2); + ip.drawString("StdDev: " + d2s(stats.stdDev), col1, row3); + ip.drawString("Mode: " + d2s(stats.dmode) + modeCount, col2, row3); + if (showBins) { + ip.drawString("Bins: " + d2s(stats.nBins), col1, row4); + ip.drawString("Bin Width: " + d2s(binWidth), col2, row4); + } + drawValueAndCount(ip, Double.NaN, -1); + } + + private void drawValueAndCount(ImageProcessor ip, double value, long count) { + int y = showBins?row4:row3; + ip.setRoi(0, y, WIN_WIDTH, WIN_HEIGHT-y); + ip.setColor(Color.white); + ip.fill(); + ip.setColor(Color.black); + String sValue = Double.isNaN(value)?"---":d2s(value); + String sCount = count==-1?"---":""+count; + int row = showBins?row5:row4; + ip.drawString("Value: " + sValue, col1, row); + ip.drawString("Count: " + sCount, col2, row); + } + + private String d2s(double d) { + if ((int)d==d) + return IJ.d2s(d, 0); + else + return IJ.d2s(d, 3, 8); + } + + int getWidth(double d, ImageProcessor ip) { + return ip.getStringWidth(d2s(d)); + } + + /** Returns the histogram values as a ResultsTable. */ + public ResultsTable getResultsTable() { + ResultsTable rt = new ResultsTable(); + rt.setPrecision(digits); + String vheading = stats.binSize==1.0?"value":"bin start"; + if (cal.calibrated() && !cal.isSigned16Bit()) { + for (int i=0; i0?yMax:newMaxCount, ip); + drawPlot(yMax>0?yMax:newMaxCount, ip); + } else + drawPlot(yMax>0?yMax:newMaxCount, ip); + this.imp.updateAndDraw(); + } + + public void actionPerformed(ActionEvent e) { + Object b = e.getSource(); + if (b==live) + toggleLiveMode(); + else if (b==rgb) + changeChannel(); + else if (b==list) + showList(); + else if (b==copy) + copyToClipboard(); + else if (b==log) { + logScale = !logScale; + replot(); + } + } + + public void lostOwnership(Clipboard clipboard, Transferable contents) {} + + public int[] getHistogram() { + int[] hist = new int[histogram.length]; + for (int i=0; iBLUE) rgbMode=INTENSITY1; + ColorProcessor cp = (ColorProcessor)imp.getProcessor(); + boolean weighted = cp.weightedHistogram(); + if (rgbMode==INTENSITY2) { + double[] weights = cp.getRGBWeights(); + if (weighted) + cp.setRGBWeights(1d/3d, 1d/3d, 1d/3d); + else + cp.setRGBWeights(0.299, 0.587, 0.114); + showHistogram(imp, 256); + cp.setRGBWeights(weights); + } else + showHistogram(imp, 256); + } + } + + private boolean liveMode() { + return live!=null && live.getForeground()==Color.red; + } + + private void enableLiveMode() { + if (bgThread==null) { + srcImp = WindowManager.getImage(srcImageID); + if (srcImp==null) return; + bgThread = new Thread(this, "Live Histogram"); + bgThread.setPriority(Math.max(bgThread.getPriority()-3, Thread.MIN_PRIORITY)); + bgThread.start(); + imageUpdated(srcImp); + } + createListeners(); + if (srcImp!=null) + imageUpdated(srcImp); + } + + // Unused + public void imageOpened(ImagePlus imp) { + } + + // This listener is called if the source image content is changed + public synchronized void imageUpdated(ImagePlus imp) { + if (imp==srcImp) { + doUpdate = true; + notify(); + } + } + + public synchronized void roiModified(ImagePlus img, int id) { + if (img==srcImp) { + doUpdate=true; + notify(); + } + } + + // If either the source image or this image are closed, exit + public void imageClosed(ImagePlus imp) { + if (imp==srcImp || imp==this.imp) { + if (bgThread!=null) + bgThread.interrupt(); + bgThread = null; + removeListeners(); + srcImp = null; + } + } + + // the background thread for live plotting. + public void run() { + while (true) { + if (doUpdate && srcImp!=null) { + if (srcImp.getRoi()!=null) + IJ.wait(50); //delay to make sure the roi has been updated + if (srcImp!=null) { + if (srcImp.getBitDepth()==16 && ImagePlus.getDefault16bitRange()!=0) + showHistogram(srcImp, 256, 0, Math.pow(2,ImagePlus.getDefault16bitRange())-1); + else + showHistogram(srcImp, 256); + } + } + synchronized(this) { + if (doUpdate) { + doUpdate = false; //and loop again + } else { + try {wait();} //notify wakes up the thread + catch(InterruptedException e) { //interrupted tells the thread to exit + return; + } + } + } + } + } + + private void createListeners() { + if (srcImp==null) + return; + ImagePlus.addImageListener(this); + Roi.addRoiListener(this); + if (live!=null) { + Font font = live.getFont(); + live.setFont(new Font(font.getName(), Font.BOLD, font.getSize())); + live.setForeground(Color.red); + } + } + + private void removeListeners() { + if (srcImp==null) + return; + ImagePlus.removeImageListener(this); + Roi.removeRoiListener(this); + if (live!=null) { + Font font = live.getFont(); + live.setFont(new Font(font.getName(), Font.PLAIN, font.getSize())); + live.setForeground(Color.black); + } + } + +} diff --git a/src/ij/gui/ImageCanvas.java b/src/ij/gui/ImageCanvas.java new file mode 100644 index 0000000..410dc1d --- /dev/null +++ b/src/ij/gui/ImageCanvas.java @@ -0,0 +1,1816 @@ +package ij.gui; + +import java.awt.*; +import java.util.Properties; +import java.awt.image.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.*; +import ij.plugin.frame.Recorder; +import ij.plugin.frame.RoiManager; +import ij.plugin.filter.Analyzer; +import ij.plugin.tool.PlugInTool; +import ij.macro.*; +import ij.*; +import ij.util.*; +import ij.text.*; +import java.awt.event.*; +import java.util.*; +import java.awt.geom.*; +import java.util.concurrent.atomic.AtomicBoolean; + + +/** This is a Canvas used to display images in a Window. */ +public class ImageCanvas extends Canvas implements MouseListener, MouseMotionListener, Cloneable { + + protected static Cursor defaultCursor = new Cursor(Cursor.DEFAULT_CURSOR); + protected static Cursor handCursor = new Cursor(Cursor.HAND_CURSOR); + protected static Cursor moveCursor = new Cursor(Cursor.MOVE_CURSOR); + protected static Cursor crosshairCursor = new Cursor(Cursor.CROSSHAIR_CURSOR); + + public static boolean usePointer = Prefs.usePointerCursor; + + protected ImagePlus imp; + protected boolean imageUpdated; + protected Rectangle srcRect; + protected int imageWidth, imageHeight; + protected int xMouse; // current cursor offscreen x location + protected int yMouse; // current cursor offscreen y location + + private boolean showCursorStatus = true; + private int sx2, sy2; + private boolean disablePopupMenu; + private static Color zoomIndicatorColor; + private static Font smallFont, largeFont; + private Font font; + private Rectangle[] labelRects; + private boolean maxBoundsReset; + private Overlay showAllOverlay; + private static final int LIST_OFFSET = 100000; + private static Color showAllColor = Prefs.getColor(Prefs.SHOW_ALL_COLOR, new Color(0, 255, 255)); + private Color defaultColor = showAllColor; + private static Color labelColor, bgColor; + private int resetMaxBoundsCount; + private Roi currentRoi; + private int mousePressedX, mousePressedY; + private long mousePressedTime; + private boolean overOverlayLabel; + + /** If the mouse moves less than this in screen pixels, successive zoom operations are on the same image pixel */ + protected final static int MAX_MOUSEMOVE_ZOOM = 10; + /** Screen coordinates where the last zoom operation was done (initialized to impossible value) */ + protected int lastZoomSX = -9999999; + protected int lastZoomSY = -9999999; + /** Image (=offscreen) coordinates where the cursor was moved to for zooming */ + protected int zoomTargetOX = -1; + protected int zoomTargetOY; + + protected ImageJ ij; + protected double magnification; + protected int dstWidth, dstHeight; + + protected int xMouseStart; + protected int yMouseStart; + protected int xSrcStart; + protected int ySrcStart; + protected int flags; + + private Image offScreenImage; + private int offScreenWidth = 0; + private int offScreenHeight = 0; + private boolean mouseExited = true; + private boolean customRoi; + private boolean drawNames; + private AtomicBoolean paintPending; + private boolean scaleToFit; + private boolean painted; + private boolean hideZoomIndicator; + private boolean flattening; + private Timer pressTimer; + private PopupMenu roiPopupMenu; + private static int longClickDelay = 1000; //ms + + + public ImageCanvas(ImagePlus imp) { + this.imp = imp; + paintPending = new AtomicBoolean(false); + ij = IJ.getInstance(); + int width = imp.getWidth(); + int height = imp.getHeight(); + imageWidth = width; + imageHeight = height; + srcRect = new Rectangle(0, 0, imageWidth, imageHeight); + setSize(imageWidth, imageHeight); + magnification = 1.0; + addMouseListener(this); + addMouseMotionListener(this); + addKeyListener(ij); // ImageJ handles keyboard shortcuts + setFocusTraversalKeysEnabled(false); + //setScaleToFit(true); + } + + void updateImage(ImagePlus imp) { + this.imp = imp; + int width = imp.getWidth(); + int height = imp.getHeight(); + imageWidth = width; + imageHeight = height; + srcRect = new Rectangle(0, 0, imageWidth, imageHeight); + setSize(imageWidth, imageHeight); + magnification = 1.0; + } + + /** Update this ImageCanvas to have the same zoom and scale settings as the one specified. */ + void update(ImageCanvas ic) { + if (ic==null || ic==this || ic.imp==null) + return; + if (ic.imp.getWidth()!=imageWidth || ic.imp.getHeight()!=imageHeight) + return; + srcRect = new Rectangle(ic.srcRect.x, ic.srcRect.y, ic.srcRect.width, ic.srcRect.height); + setMagnification(ic.magnification); + setSize(ic.dstWidth, ic.dstHeight); + } + + /** Sets the region of the image (in pixels) to be displayed. */ + public void setSourceRect(Rectangle r) { + if (r==null) + return; + r = new Rectangle(r.x, r.y, r.width, r.height); + imageWidth = imp.getWidth(); + imageHeight = imp.getHeight(); + if (r.x<0) r.x = 0; + if (r.y<0) r.y = 0; + if (r.width<1) + r.width = 1; + if (r.height<1) + r.height = 1; + if (r.width>imageWidth) + r.width = imageWidth; + if (r.height>imageHeight) + r.height = imageHeight; + if (r.x+r.width>imageWidth) + r.x = imageWidth-r.width; + if (r.y+r.height>imageHeight) + r.y = imageHeight-r.height; + if (srcRect==null) + srcRect = r; + else { + srcRect.x = r.x; + srcRect.y = r.y; + srcRect.width = r.width; + srcRect.height = r.height; + } + if (dstWidth==0) { + Dimension size = getSize(); + dstWidth = size.width; + dstHeight = size.height; + } + magnification = (double)dstWidth/srcRect.width; + imp.setTitle(imp.getTitle()); + if (IJ.debugMode) IJ.log("setSourceRect: "+magnification+" "+(int)(srcRect.height*magnification+0.5)+" "+dstHeight+" "+srcRect); + } + + void setSrcRect(Rectangle srcRect) { + setSourceRect(srcRect); + } + + public Rectangle getSrcRect() { + return srcRect; + } + + /** Obsolete; replaced by setSize() */ + public void setDrawingSize(int width, int height) { + dstWidth = width; + dstHeight = height; + setSize(dstWidth, dstHeight); + } + + public void setSize(int width, int height) { + super.setSize(width, height); + dstWidth = width; + dstHeight = height; + } + + /** ImagePlus.updateAndDraw calls this method to force the paint() + method to update the image from the ImageProcessor. */ + public void setImageUpdated() { + imageUpdated = true; + } + + public void setPaintPending(boolean state) { + paintPending.set(state); + } + + public boolean getPaintPending() { + return paintPending.get(); + } + + public void update(Graphics g) { + paint(g); + } + + //public void repaint() { + // super.repaint(); + // //if (IJ.debugMode) IJ.log("repaint: "+imp); + //} + + public void paint(Graphics g) { + // if (IJ.debugMode) IJ.log("paint: "+imp); + painted = true; + Roi roi = imp.getRoi(); + Overlay overlay = imp.getOverlay(); + if (roi!=null || overlay!=null || showAllOverlay!=null || Prefs.paintDoubleBuffered || (IJ.isLinux() && magnification<0.25)) { + // Use double buffering to avoid flickering of ROIs and to work around + // a Linux problem with large images not showing at low magnification. + if (roi!=null) + roi.updatePaste(); + if (imageWidth!=0) { + paintDoubleBuffered(g); + setPaintPending(false); + return; + } + } + try { + if (imageUpdated) { + imageUpdated = false; + imp.updateImage(); + } + setInterpolation(g, Prefs.interpolateScaledImages); + Image img = imp.getImage(); + if (img!=null) + g.drawImage(img, 0, 0, (int)(srcRect.width*magnification+0.5), (int)(srcRect.height*magnification+0.5), + srcRect.x, srcRect.y, srcRect.x+srcRect.width, srcRect.y+srcRect.height, null); + if (overlay!=null) + drawOverlay(overlay, g); + if (showAllOverlay!=null) + drawOverlay(showAllOverlay, g); + if (roi!=null) drawRoi(roi, g); + if (srcRect.width1.0) { + Object value = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; + ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_INTERPOLATION, value); + } + } + + private void drawRoi(Roi roi, Graphics g) { + if (Interpreter.isBatchMode()) + return; + if (roi==currentRoi) { + Color lineColor = roi.getStrokeColor(); + Color fillColor = roi.getFillColor(); + float lineWidth = roi.getStrokeWidth(); + roi.setStrokeColor(null); + roi.setFillColor(null); + boolean strokeSet = roi.getStroke()!=null; + if (strokeSet) + roi.setStrokeWidth(1); + roi.draw(g); + roi.setStrokeColor(lineColor); + if (strokeSet) + roi.setStrokeWidth(lineWidth); + roi.setFillColor(fillColor); + currentRoi = null; + } else + roi.draw(g); + } + + public int getSliceNumber(String label) { + if (label==null) return 0; + int slice = 0; + if (label.length()>=14 && label.charAt(4)=='-' && label.charAt(9)=='-') + slice = (int)Tools.parseDouble(label.substring(0,4),0); + else if (label.length()>=17 && label.charAt(5)=='-' && label.charAt(11)=='-') + slice = (int)Tools.parseDouble(label.substring(0,5),0); + else if (label.length()>=20 && label.charAt(6)=='-' && label.charAt(13)=='-') + slice = (int)Tools.parseDouble(label.substring(0,6),0); + return slice; + } + + private void drawOverlay(Overlay overlay, Graphics g) { + if (imp!=null && imp.getHideOverlay() && overlay!=showAllOverlay) + return; + flattening = imp!=null && ImagePlus.flattenTitle.equals(imp.getTitle()); + if (imp!=null && showAllOverlay!=null && overlay!=showAllOverlay) + overlay.drawLabels(false); + Color labelColor = overlay.getLabelColor(); + if (labelColor==null) labelColor = Color.white; + initGraphics(overlay, g, labelColor, Roi.getColor()); + int n = overlay.size(); + //if (IJ.debugMode) IJ.log("drawOverlay: "+n); + int currentImage = imp!=null?imp.getCurrentSlice():-1; + int stackSize = imp.getStackSize(); + if (stackSize==1) + currentImage = -1; + int channel=0, slice=0, frame=0; + boolean hyperstack = imp.isHyperStack(); + if (hyperstack) { + channel = imp.getChannel(); + slice = imp.getSlice(); + frame = imp.getFrame(); + } + drawNames = overlay.getDrawNames() && overlay.getDrawLabels(); + boolean drawLabels = drawNames || overlay.getDrawLabels(); + if (drawLabels) + labelRects = new Rectangle[n]; + else + labelRects = null; + font = overlay.getLabelFont(); + if (overlay.scalableLabels() && font!=null) { + double mag = getMagnification(); + if (mag!=1.0) + font = font.deriveFont((float)(font.getSize()*mag)); + } + Roi activeRoi = imp.getRoi(); + boolean roiManagerShowAllMode = overlay==showAllOverlay && !Prefs.showAllSliceOnly; + for (int i=0; i0) { + if (z==0 && imp.getNSlices()>1) + z = position; + else if (t==0) + t = position; + } + if (((c==0||c==channel) && (z==0||z==slice) && (t==0||t==frame)) || roiManagerShowAllMode) + drawRoi(g, roi, drawLabels?i+LIST_OFFSET:-1); + } else { + int position = stackSize>1?roi.getPosition():0; + if (position==0 && stackSize>1) + position = getSliceNumber(roi.getName()); + if (position>0 && imp.getCompositeMode()==IJ.COMPOSITE) + position = 0; + //IJ.log(position+" "+currentImage+" "+roiManagerShowAllMode); + if (position==0 || position==currentImage || roiManagerShowAllMode) + drawRoi(g, roi, drawLabels?i+LIST_OFFSET:-1); + } + } + ((Graphics2D)g).setStroke(Roi.onePixelWide); + drawNames = false; + font = null; + } + + void drawOverlay(Graphics g) { + drawOverlay(imp.getOverlay(), g); + } + + private void initGraphics(Overlay overlay, Graphics g, Color textColor, Color defaultColor) { + if (smallFont==null) { + smallFont = new Font("SansSerif", Font.PLAIN, 9); + largeFont = IJ.font12; + } + if (textColor!=null) { + labelColor = textColor; + if (overlay!=null && overlay.getDrawBackgrounds()) { + double brightness = (labelColor.getRed()+labelColor.getGreen()+labelColor.getBlue())/3.0; + if (labelColor==Color.green) brightness = 255; + bgColor = brightness<=85?Color.white:Color.black; + } else + bgColor = null; + } else { + int red = defaultColor.getRed(); + int green = defaultColor.getGreen(); + int blue = defaultColor.getBlue(); + if ((red+green+blue)/3<128) + labelColor = Color.white; + else + labelColor = Color.black; + bgColor = defaultColor; + } + this.defaultColor = defaultColor; + Font font = overlay!=null?overlay.getLabelFont():null; + if (font!=null && font.getSize()>12) + ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setColor(defaultColor); + } + + void drawRoi(Graphics g, Roi roi, int index) { + ImagePlus imp2 = roi.getImage(); + roi.setImage(imp); + Color saveColor = roi.getStrokeColor(); + if (saveColor==null) + roi.setStrokeColor(defaultColor); + if (roi.getStroke()==null) + ((Graphics2D)g).setStroke(Roi.onePixelWide); + if (roi instanceof TextRoi) + ((TextRoi)roi).drawText(g); + else + roi.drawOverlay(g); + roi.setStrokeColor(saveColor); + if (index>=0) { + if (roi==currentRoi) + g.setColor(Roi.getColor()); + else + g.setColor(defaultColor); + drawRoiLabel(g, index, roi); + } + if (imp2!=null) + roi.setImage(imp2); + else + roi.setImage(null); + } + + void drawRoiLabel(Graphics g, int index, Roi roi) { + if (roi.isCursor()) + return; + boolean pointRoi = roi instanceof PointRoi; + Rectangle r = roi.getBounds(); + int x = screenX(r.x); + int y = screenY(r.y); + double mag = getMagnification(); + int width = (int)(r.width*mag); + int height = (int)(r.height*mag); + int size = width>40 || height>40?12:9; + int pointSize = 0; + int crossSize = 0; + if (pointRoi) { + pointSize = ((PointRoi)roi).getSize(); + switch (pointSize) { + case 0: case 1: size=9; break; + case 2: case 3: size=10; break; + case 4: size=12; break; + } + crossSize = pointSize + 10 + 2*pointSize; + } + if (font!=null) { + g.setFont(font); + size = font.getSize(); + } else if (size==12) + g.setFont(largeFont); + else + g.setFont(smallFont); + boolean drawingList = index >= LIST_OFFSET; + if (drawingList) index -= LIST_OFFSET; + String label = "" + (index+1); + if (drawNames) + label = roi.getName(); + if (label==null) + return; + FontMetrics metrics = g.getFontMetrics(); + int w = metrics.stringWidth(label); + x = x + width/2 - w/2; + y = y + height/2 + Math.max(size/2,6); + int h = metrics.getAscent() + metrics.getDescent(); + int xoffset=0, yoffset=0; + if (pointRoi) { + xoffset = 6 + pointSize; + yoffset = h - 6 + pointSize; + } + if (bgColor!=null) { + int h2 = h; + if (font!=null && font.getSize()>14) + h2 = (int)(h2*0.8); + g.setColor(bgColor); + g.fillRoundRect(x-1+xoffset, y-h2+2+yoffset, w+1, h2-2, 5, 5); + } + if (labelRects!=null && index1.0) + w1 = (int)(w1/aspectRatio); + int h1 = (int)(w1*aspectRatio); + if (w1<4) w1 = 4; + if (h1<4) h1 = 4; + int w2 = (int)(w1*((double)srcRect.width/imageWidth)); + int h2 = (int)(h1*((double)srcRect.height/imageHeight)); + if (w2<1) w2 = 1; + if (h2<1) h2 = 1; + int x2 = (int)(w1*((double)srcRect.x/imageWidth)); + int y2 = (int)(h1*((double)srcRect.y/imageHeight)); + if (zoomIndicatorColor==null) + zoomIndicatorColor = new Color(128, 128, 255); + g.setColor(zoomIndicatorColor); + ((Graphics2D)g).setStroke(Roi.onePixelWide); + g.drawRect(x1, y1, w1, h1); + if (w2*h2<=200 || w2<10 || h2<10) + g.fillRect(x1+x2, y1+y2, w2, h2); + else + g.drawRect(x1+x2, y1+y2, w2, h2); + } + + // Use double buffer to reduce flicker when drawing complex ROIs. + // Author: Erik Meijering + void paintDoubleBuffered(Graphics g) { + final int srcRectWidthMag = (int)(srcRect.width*magnification+0.5); + final int srcRectHeightMag = (int)(srcRect.height*magnification+0.5); + if (offScreenImage==null || offScreenWidth!=srcRectWidthMag || offScreenHeight!=srcRectHeightMag) { + offScreenImage = createImage(srcRectWidthMag, srcRectHeightMag); + offScreenWidth = srcRectWidthMag; + offScreenHeight = srcRectHeightMag; + } + Roi roi = imp.getRoi(); + try { + if (imageUpdated) { + imageUpdated = false; + imp.updateImage(); + } + Graphics offScreenGraphics = offScreenImage.getGraphics(); + setInterpolation(offScreenGraphics, Prefs.interpolateScaledImages); + Image img = imp.getImage(); + if (img!=null) + offScreenGraphics.drawImage(img, 0, 0, srcRectWidthMag, srcRectHeightMag, + srcRect.x, srcRect.y, srcRect.x+srcRect.width, srcRect.y+srcRect.height, null); + Overlay overlay = imp.getOverlay(); + if (overlay!=null) + drawOverlay(overlay, offScreenGraphics); + if (showAllOverlay!=null) + drawOverlay(showAllOverlay, offScreenGraphics); + if (roi!=null) + drawRoi(roi, offScreenGraphics); + if (srcRect.widthfirstFrame+1000) { + firstFrame=System.currentTimeMillis(); + fps = frames; + frames=0; + } + g.setColor(Color.white); + g.fillRect(10, 12, 50, 15); + g.setColor(Color.black); + g.drawString((int)(fps+0.5) + " fps", 10, 25); + } + + public Dimension getPreferredSize() { + return new Dimension(dstWidth, dstHeight); + } + + /** Returns the current cursor location in image coordinates. */ + public Point getCursorLoc() { + return new Point(xMouse, yMouse); + } + + /** Returns 'true' if the cursor is over this image. */ + public boolean cursorOverImage() { + return !mouseExited; + } + + /** Returns the mouse event modifiers. */ + public int getModifiers() { + return flags; + } + + /** Returns the ImagePlus object that is associated with this ImageCanvas. */ + public ImagePlus getImage() { + return imp; + } + + /** Sets the cursor based on the current tool and cursor location. */ + public void setCursor(int sx, int sy, int ox, int oy) { + xMouse = ox; + yMouse = oy; + mouseExited = false; + Roi roi = imp.getRoi(); + ImageWindow win = imp.getWindow(); + overOverlayLabel = false; + if (win==null) + return; + if (IJ.spaceBarDown()) { + setCursor(handCursor); + return; + } + int id = Toolbar.getToolId(); + switch (id) { + case Toolbar.MAGNIFIER: + setCursor(moveCursor); + break; + case Toolbar.HAND: + setCursor(handCursor); + break; + default: //selection tool + PlugInTool tool = Toolbar.getPlugInTool(); + boolean arrowTool = roi!=null && (roi instanceof Arrow) && tool!=null && "Arrow Tool".equals(tool.getToolName()); + if ((id>=Toolbar.CUSTOM1) && !arrowTool) { + if (Prefs.usePointerCursor) + setCursor(defaultCursor); + else + setCursor(crosshairCursor); + } else if (roi!=null && roi.getState()!=roi.CONSTRUCTING && roi.isHandle(sx, sy)>=0) { + setCursor(handCursor); + } else if ((imp.getOverlay()!=null||showAllOverlay!=null) && overOverlayLabel(sx,sy,ox,oy) && (roi==null||roi.getState()!=roi.CONSTRUCTING)) { + overOverlayLabel = true; + setCursor(handCursor); + } else if (Prefs.usePointerCursor || (roi!=null && roi.getState()!=roi.CONSTRUCTING && roi.contains(ox, oy))) + setCursor(defaultCursor); + else + setCursor(crosshairCursor); + } + } + + private boolean overOverlayLabel(int sx, int sy, int ox, int oy) { + Overlay o = showAllOverlay; + if (o==null) + o = imp.getOverlay(); + if (o==null || !o.isSelectable() || !o.isDraggable()|| !o.getDrawLabels() || labelRects==null) + return false; + for (int i=o.size()-1; i>=0; i--) { + if (labelRects!=null&&labelRects[i]!=null&&labelRects[i].contains(sx,sy)) { + Roi roi = imp.getRoi(); + if (roi==null || !roi.contains(ox,oy)) + return true; + else + return false; + } + } + return false; + } + + /**Converts a screen x-coordinate to an offscreen x-coordinate (nearest pixel center).*/ + public int offScreenX(int sx) { + return srcRect.x + (int)(sx/magnification); + } + + /**Converts a screen y-coordinate to an offscreen y-coordinate (nearest pixel center).*/ + public int offScreenY(int sy) { + return srcRect.y + (int)(sy/magnification); + } + + /**Converts a screen x-coordinate to an offscreen x-coordinate (Roi coordinate of nearest pixel border).*/ + public int offScreenX2(int sx) { + return srcRect.x + (int)Math.round(sx/magnification); + } + + /**Converts a screen y-coordinate to an offscreen y-coordinate (Roi coordinate of nearest pixel border).*/ + public int offScreenY2(int sy) { + return srcRect.y + (int)Math.round(sy/magnification); + } + + /**Converts a screen x-coordinate to a floating-point offscreen x-coordinate.*/ + public double offScreenXD(int sx) { + return srcRect.x + sx/magnification; + } + + /**Converts a screen y-coordinate to a floating-point offscreen y-coordinate.*/ + public double offScreenYD(int sy) { + return srcRect.y + sy/magnification; + + } + + /**Converts an offscreen x-coordinate to a screen x-coordinate.*/ + public int screenX(int ox) { + return (int)((ox-srcRect.x)*magnification); + } + + /**Converts an offscreen y-coordinate to a screen y-coordinate.*/ + public int screenY(int oy) { + return (int)((oy-srcRect.y)*magnification); + } + + /**Converts a floating-point offscreen x-coordinate to a screen x-coordinate.*/ + public int screenXD(double ox) { + return (int)((ox-srcRect.x)*magnification); + } + + /**Converts a floating-point offscreen x-coordinate to a screen x-coordinate.*/ + public int screenYD(double oy) { + return (int)((oy-srcRect.y)*magnification); + } + + public double getMagnification() { + return magnification; + } + + public void setMagnification(double magnification) { + setMagnification2(magnification); + } + + void setMagnification2(double magnification) { + if (magnification>32.0) + magnification = 32.0; + if (magnificationdstWidth||height>dstHeight)&&win!=null&&win.maxBounds!=null&&width!=win.maxBounds.width-10) { + if (resetMaxBoundsCount!=0) + resetMaxBounds(); // Works around problem that prevented window from being larger than maximized size + resetMaxBoundsCount++; + } + if (scaleToFit || IJ.altKeyDown()) + {fitToWindow(); return;} + if (width>imageWidth*magnification) + width = (int)(imageWidth*magnification); + if (height>imageHeight*magnification) + height = (int)(imageHeight*magnification); + Dimension size = getSize(); + if (srcRect.widthimageWidth) + srcRect.x = imageWidth-srcRect.width; + if ((srcRect.y+srcRect.height)>imageHeight) + srcRect.y = imageHeight-srcRect.height; + repaint(); + } + //IJ.log("resizeCanvas2: "+srcRect+" "+dstWidth+" "+dstHeight+" "+width+" "+height); + } + + public void fitToWindow() { + ImageWindow win = imp.getWindow(); + if (win==null) return; + Rectangle bounds = win.getBounds(); + Insets insets = win.getInsets(); + int sliderHeight = win.getSliderHeight(); + double xmag = (double)(bounds.width-(insets.left+insets.right+ImageWindow.HGAP*2))/srcRect.width; + double ymag = (double)(bounds.height-(ImageWindow.VGAP*2+insets.top+insets.bottom+sliderHeight))/srcRect.height; + setMagnification(Math.min(xmag, ymag)); + int width=(int)(imageWidth*magnification); + int height=(int)(imageHeight*magnification); + if (width==dstWidth&&height==dstHeight) return; + srcRect=new Rectangle(0,0,imageWidth, imageHeight); + setSize(width, height); + getParent().doLayout(); + } + + void setMaxBounds() { + if (maxBoundsReset) { + maxBoundsReset = false; + ImageWindow win = imp.getWindow(); + if (win!=null && !IJ.isLinux() && win.maxBounds!=null) { + win.setMaximizedBounds(win.maxBounds); + win.setMaxBoundsTime = System.currentTimeMillis(); + } + } + } + + void resetMaxBounds() { + ImageWindow win = imp.getWindow(); + if (win!=null && (System.currentTimeMillis()-win.setMaxBoundsTime)>500L) { + win.setMaximizedBounds(win.maxWindowBounds); + maxBoundsReset = true; + } + } + + private static final double[] zoomLevels = { + 1/72.0, 1/48.0, 1/32.0, 1/24.0, 1/16.0, 1/12.0, + 1/8.0, 1/6.0, 1/4.0, 1/3.0, 1/2.0, 0.75, 1.0, 1.5, + 2.0, 3.0, 4.0, 6.0, 8.0, 12.0, 16.0, 24.0, 32.0 }; + + public static double getLowerZoomLevel(double currentMag) { + double newMag = zoomLevels[0]; + for (int i=0; i=0; i--) { + if (zoomLevels[i]>currentMag) + newMag = zoomLevels[i]; + else + break; + } + return newMag; + } + + /** Zooms in by making the window bigger. If it can't be made bigger, then makes + the source rectangle (srcRect) smaller and centers it on the position in the + image where the cursor was when zooming has started. + Note that sx and sy are screen coordinates. */ + public void zoomIn(int sx, int sy) { + if (magnification>=32) return; + scaleToFit = false; + boolean mouseMoved = sqr(sx-lastZoomSX) + sqr(sy-lastZoomSY) > MAX_MOUSEMOVE_ZOOM*MAX_MOUSEMOVE_ZOOM; + lastZoomSX = sx; + lastZoomSY = sy; + if (mouseMoved || zoomTargetOX<0) { + boolean cursorInside = sx >= 0 && sy >= 0 && sx < dstWidth && sy < dstHeight; + zoomTargetOX = offScreenX(cursorInside ? sx : dstWidth/2); //where to zoom, offscreen (image) coordinates + zoomTargetOY = offScreenY(cursorInside ? sy : dstHeight/2); + } + double newMag = getHigherZoomLevel(magnification); + int newWidth = (int)(imageWidth*newMag); + int newHeight = (int)(imageHeight*newMag); + Dimension newSize = canEnlarge(newWidth, newHeight); + if (newSize!=null) { + setSize(newSize.width, newSize.height); + if (newSize.width!=newWidth || newSize.height!=newHeight) + adjustSourceRect(newMag, zoomTargetOX, zoomTargetOY); + else + setMagnification(newMag); + imp.getWindow().pack(); + } else // can't enlarge window + adjustSourceRect(newMag, zoomTargetOX, zoomTargetOY); + repaint(); + if (srcRect.widthimageWidth) r.x = imageWidth-w; + if (r.y+h>imageHeight) r.y = imageHeight-h; + srcRect = r; + setMagnification(newMag); + //IJ.log("adjustSourceRect2: "+srcRect+" "+dstWidth+" "+dstHeight); + } + + /** Returns the size to which the window can be enlarged, or null if it can't be enlarged. + * newWidth, newHeight is the size needed for showing the full image + * at the magnification needed */ + protected Dimension canEnlarge(int newWidth, int newHeight) { + if (IJ.altKeyDown()) + return null; + ImageWindow win = imp.getWindow(); + if (win==null) return null; + Rectangle r1 = win.getBounds(); + Insets insets = win.getInsets(); + Point loc = getLocation(); + if (loc.x>insets.left+5 || loc.y>insets.top+5) { + r1.width = newWidth+insets.left+insets.right+ImageWindow.HGAP*2; + r1.height = newHeight+insets.top+insets.bottom+ImageWindow.VGAP*2+win.getSliderHeight(); + } else { + r1.width = r1.width - dstWidth + newWidth; + r1.height = r1.height - dstHeight + newHeight; + } + Rectangle max = GUI.getMaxWindowBounds(win); + boolean fitsHorizontally = r1.x+r1.width MAX_MOUSEMOVE_ZOOM*MAX_MOUSEMOVE_ZOOM; + lastZoomSX = sx; + lastZoomSY = sy; + if (mouseMoved || zoomTargetOX<0) { + boolean cursorInside = sx >= 0 && sy >= 0 && sx < dstWidth && sy < dstHeight; + zoomTargetOX = offScreenX(cursorInside ? sx : dstWidth/2); //where to zoom, offscreen (image) coordinates + zoomTargetOY = offScreenY(cursorInside ? sy : dstHeight/2); + } + double oldMag = magnification; + double newMag = getLowerZoomLevel(magnification); + double srcRatio = (double)srcRect.width/srcRect.height; + double imageRatio = (double)imageWidth/imageHeight; + double initialMag = imp.getWindow().getInitialMagnification(); + if (Math.abs(srcRatio-imageRatio)>0.05) { + double scale = oldMag/newMag; + int newSrcWidth = (int)Math.round(srcRect.width*scale); + int newSrcHeight = (int)Math.round(srcRect.height*scale); + if (newSrcWidth>imageWidth) newSrcWidth=imageWidth; + if (newSrcHeight>imageHeight) newSrcHeight=imageHeight; + int newSrcX = srcRect.x - (newSrcWidth - srcRect.width)/2; + int newSrcY = srcRect.y - (newSrcHeight - srcRect.height)/2; + if (newSrcX + newSrcWidth > imageWidth) newSrcX = imageWidth - newSrcWidth; + if (newSrcY + newSrcHeight > imageHeight) newSrcY = imageHeight - newSrcHeight; + if (newSrcX<0) newSrcX = 0; + if (newSrcY<0) newSrcY = 0; + srcRect = new Rectangle(newSrcX, newSrcY, newSrcWidth, newSrcHeight); + //IJ.log(newMag+" "+srcRect+" "+dstWidth+" "+dstHeight); + int newDstWidth = (int)(srcRect.width*newMag); + int newDstHeight = (int)(srcRect.height*newMag); + setMagnification(newMag); + setMaxBounds(); + //IJ.log(newDstWidth+" "+dstWidth+" "+newDstHeight+" "+dstHeight); + if (newDstWidthdstWidth) { + int w = (int)Math.round(dstWidth/newMag); + if (w*newMagimageWidth) r.x = imageWidth-w; + if (r.y+h>imageHeight) r.y = imageHeight-h; + srcRect = r; + setMagnification(newMag); + } else { + srcRect = new Rectangle(0, 0, imageWidth, imageHeight); + setSize((int)(imageWidth*newMag), (int)(imageHeight*newMag)); + setMagnification(newMag); + imp.getWindow().pack(); + } + setMaxBounds(); + repaint(); + } + + int sqr(int x) { + return x*x; + } + + /** Implements the Image/Zoom/Original Scale command. */ + public void unzoom() { + double imag = imp.getWindow().getInitialMagnification(); + if (magnification==imag) + return; + srcRect = new Rectangle(0, 0, imageWidth, imageHeight); + ImageWindow win = imp.getWindow(); + setSize((int)(imageWidth*imag), (int)(imageHeight*imag)); + setMagnification(imag); + setMaxBounds(); + win.pack(); + setMaxBounds(); + repaint(); + } + + /** Implements the Image/Zoom/View 100% command. */ + public void zoom100Percent() { + if (magnification==1.0) + return; + double imag = imp.getWindow().getInitialMagnification(); + if (magnification!=imag) + unzoom(); + if (magnification==1.0) + return; + if (magnification<1.0) { + while (magnification<1.0) + zoomIn(imageWidth/2, imageHeight/2); + } else if (magnification>1.0) { + while (magnification>1.0) + zoomOut(imageWidth/2, imageHeight/2); + } else + return; + int x=xMouse, y=yMouse; + if (mouseExited) { + x = imageWidth/2; + y = imageHeight/2; + } + int sx = screenX(x); + int sy = screenY(y); + adjustSourceRect(1.0, sx, sy); + repaint(); + } + + protected void scroll(int sx, int sy) { + int ox = xSrcStart + (int)(sx/magnification); //convert to offscreen coordinates + int oy = ySrcStart + (int)(sy/magnification); + //IJ.log("scroll: "+ox+" "+oy+" "+xMouseStart+" "+yMouseStart); + int newx = xSrcStart + (xMouseStart-ox); + int newy = ySrcStart + (yMouseStart-oy); + if (newx<0) newx = 0; + if (newy<0) newy = 0; + if ((newx+srcRect.width)>imageWidth) newx = imageWidth-srcRect.width; + if ((newy+srcRect.height)>imageHeight) newy = imageHeight-srcRect.height; + srcRect.x = newx; + srcRect.y = newy; + //IJ.log(sx+" "+sy+" "+newx+" "+newy+" "+srcRect); + imp.draw(); + Thread.yield(); + } + + Color getColor(int index){ + IndexColorModel cm = (IndexColorModel)imp.getProcessor().getColorModel(); + return new Color(cm.getRGB(index)); + } + + /** Sets the foreground drawing color (or background color if + 'setBackground' is true) to the color of the pixel at (ox,oy). */ + public void setDrawingColor(int ox, int oy, boolean setBackground) { + //IJ.log("setDrawingColor: "+setBackground+this); + int type = imp.getType(); + int[] v = imp.getPixel(ox, oy); + switch (type) { + case ImagePlus.GRAY8: { + if (setBackground) + setBackgroundColor(getColor(v[0])); + else + setForegroundColor(getColor(v[0])); + break; + } + case ImagePlus.GRAY16: case ImagePlus.GRAY32: { + double min = imp.getProcessor().getMin(); + double max = imp.getProcessor().getMax(); + double value = (type==ImagePlus.GRAY32)?Float.intBitsToFloat(v[0]):v[0]; + int index = (int)(255.0*((value-min)/(max-min))); + if (index<0) index = 0; + if (index>255) index = 255; + if (setBackground) + setBackgroundColor(getColor(index)); + else + setForegroundColor(getColor(index)); + break; + } + case ImagePlus.COLOR_RGB: case ImagePlus.COLOR_256: { + Color c = new Color(v[0], v[1], v[2]); + if (setBackground) + setBackgroundColor(c); + else + setForegroundColor(c); + break; + } + } + Color c; + if (setBackground) + c = Toolbar.getBackgroundColor(); + else { + c = Toolbar.getForegroundColor(); + imp.setColor(c); + } + IJ.showStatus("("+c.getRed()+", "+c.getGreen()+", "+c.getBlue()+")"); + } + + private void setForegroundColor(Color c) { + Toolbar.setForegroundColor(c); + if (Recorder.record) + Recorder.record("setForegroundColor", c.getRed(), c.getGreen(), c.getBlue()); + } + + private void setBackgroundColor(Color c) { + Toolbar.setBackgroundColor(c); + if (Recorder.record) + Recorder.record("setBackgroundColor", c.getRed(), c.getGreen(), c.getBlue()); + } + + public void mousePressed(final MouseEvent e) { + showCursorStatus = true; + int toolID = Toolbar.getToolId(); + ImageWindow win = imp.getWindow(); + if (win!=null && win.running2 && toolID!=Toolbar.MAGNIFIER) { + if (win instanceof StackWindow) + ((StackWindow)win).setAnimate(false); + else + win.running2 = false; + return; + } + + int x = e.getX(); + int y = e.getY(); + flags = e.getModifiers(); + if (toolID!=Toolbar.MAGNIFIER && (e.isPopupTrigger()||(!IJ.isMacintosh()&&(flags&Event.META_MASK)!=0))) { + handlePopupMenu(e); + return; + } + + int ox = offScreenX(x); + int oy = offScreenY(y); + xMouse = ox; yMouse = oy; + if (IJ.spaceBarDown()) { + // temporarily switch to "hand" tool of space bar down + setupScroll(ox, oy); + return; + } + + if (overOverlayLabel && (imp.getOverlay()!=null||showAllOverlay!=null)) { + if (activateOverlayRoi(ox, oy)) + return; + } + + if ((System.currentTimeMillis()-mousePressedTime)<300L && !drawingTool()) { + if (activateOverlayRoi(ox,oy)) + return; + } + + mousePressedX = ox; + mousePressedY = oy; + mousePressedTime = System.currentTimeMillis(); + + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) { + tool.mousePressed(imp, e); + if (e.isConsumed()) return; + } + if (customRoi && imp.getOverlay()!=null) + return; + + if (toolID>=Toolbar.CUSTOM1) { + if (tool!=null && "Arrow Tool".equals(tool.getToolName())) + handleRoiMouseDown(e); + else + Toolbar.getInstance().runMacroTool(toolID); + return; + } + + final Roi roi1 = imp.getRoi(); + final int size1 = roi1!=null?roi1.size():0; + final Rectangle r1 = roi1!=null?roi1.getBounds():null; + + switch (toolID) { + case Toolbar.MAGNIFIER: + if (IJ.shiftKeyDown()) + zoomToSelection(ox, oy); + else if ((flags & (Event.ALT_MASK|Event.META_MASK|Event.CTRL_MASK))!=0) { + zoomOut(x, y); + if (getMagnification()<1.0) + imp.repaintWindow(); + } else { + zoomIn(x, y); + if (getMagnification()<=1.0) + imp.repaintWindow(); + } + break; + case Toolbar.HAND: + setupScroll(ox, oy); + break; + case Toolbar.DROPPER: + setDrawingColor(ox, oy, IJ.altKeyDown()); + break; + case Toolbar.WAND: + double tolerance = WandToolOptions.getTolerance(); + Roi roi = imp.getRoi(); + if (roi!=null && (tolerance==0.0||imp.isThreshold()) && roi.contains(ox, oy)) { + Rectangle r = roi.getBounds(); + if (r.width==imageWidth && r.height==imageHeight) + imp.deleteRoi(); + else if (!e.isAltDown()) { + handleRoiMouseDown(e); + return; + } + } + if (roi!=null) { + int handle = roi.isHandle(x, y); + if (handle>=0) { + roi.mouseDownInHandle(handle, x, y); + return; + } + } + if (!imp.okToDeleteRoi()) + break; + setRoiModState(e, roi, -1); + String mode = WandToolOptions.getMode(); + if (Prefs.smoothWand) + mode = mode + " smooth"; + int npoints = IJ.doWand(imp, ox, oy, tolerance, mode); + if (Recorder.record && npoints>0) { + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.doWand(imp, "+ox+", "+oy+", "+tolerance+", \""+mode+"\");"); + else { + if (tolerance==0.0 && mode.equals("Legacy")) + Recorder.record("doWand", ox, oy); + else + Recorder.recordString("doWand("+ox+", "+oy+", "+tolerance+", \""+mode+"\");\n"); + } + } + break; + case Toolbar.OVAL: + if (Toolbar.getBrushSize()>0) + new RoiBrush(); + else + handleRoiMouseDown(e); + break; + default: //selection tool + handleRoiMouseDown(e); + } + + if (longClickDelay>0) { + if (pressTimer==null) + pressTimer = new java.util.Timer(); + final Point cursorLoc = getCursorLoc(); + pressTimer.schedule(new TimerTask() { + public void run() { + if (pressTimer != null) { + pressTimer.cancel(); + pressTimer = null; + } + Roi roi2 = imp.getRoi(); + int size2 = roi2!=null?roi2.size():0; + Rectangle r2 = roi2!=null?roi2.getBounds():null; + boolean empty = r2!=null&&r2.width==0&&r2.height==0; + int state = roi2!=null?roi2.getState():-1; + boolean unchanged = state!=Roi.MOVING_HANDLE && r1!=null && r2!=null && r2.x==r1.x + && r2.y==r1.y && r2.width==r1.width && r2.height==r1.height && size2==size1 + && !(size2>1&&state==Roi.CONSTRUCTING); + boolean cursorMoved = !getCursorLoc().equals(cursorLoc); + //IJ.log(size2+" "+empty+" "+unchanged+" "+state+" "+roi1+" "+roi2); + if ((roi1==null && (size2<=1||empty)) || unchanged) { + if (roi1==null) imp.deleteRoi(); + if (!cursorMoved && Toolbar.getToolId()!=Toolbar.HAND) + handlePopupMenu(e); + } + } + }, longClickDelay); + } + + } + + + + private boolean drawingTool() { + int id = Toolbar.getToolId(); + return id==Toolbar.POLYLINE || id==Toolbar.FREELINE || id>=Toolbar.CUSTOM1; + } + + void zoomToSelection(int x, int y) { + IJ.setKeyUp(IJ.ALL_KEYS); + String macro = + "args = split(getArgument);\n"+ + "x1=parseInt(args[0]); y1=parseInt(args[1]); flags=20;\n"+ + "while (flags&20!=0) {\n"+ + "getCursorLoc(x2, y2, z, flags);\n"+ + "if (x2>=x1) x=x1; else x=x2;\n"+ + "if (y2>=y1) y=y1; else y=y2;\n"+ + "makeRectangle(x, y, abs(x2-x1), abs(y2-y1));\n"+ + "wait(10);\n"+ + "}\n"+ + "run('To Selection');\n"; + new MacroRunner(macro, x+" "+y); + } + + protected void setupScroll(int ox, int oy) { + xMouseStart = ox; + yMouseStart = oy; + xSrcStart = srcRect.x; + ySrcStart = srcRect.y; + } + + protected void handlePopupMenu(MouseEvent e) { + if (disablePopupMenu) return; + if (IJ.debugMode) IJ.log("show popup: " + (e.isPopupTrigger()?"true":"false")); + int sx = e.getX(); + int sy = e.getY(); + int ox = offScreenX(sx); + int oy = offScreenY(sy); + Roi roi = imp.getRoi(); + if (roi!=null && (roi.getType()==Roi.POLYGON || roi.getType()==Roi.POLYLINE || roi.getType()==Roi.ANGLE) + && roi.getState()==roi.CONSTRUCTING) { + roi.handleMouseUp(sx, sy); // simulate double-click to finalize + roi.handleMouseUp(sx, sy); // polygon or polyline selection + return; + } + if (roi!=null && !(e.isAltDown()||e.isShiftDown())) { // show ROI popup? + if (roi.contains(ox,oy)) { + if (roiPopupMenu==null) + addRoiPopupMenu(); + if (IJ.isMacOSX()) IJ.wait(10); + roiPopupMenu.show(this, sx, sy); + return; + } + } + PopupMenu popup = Menus.getPopupMenu(); + if (popup!=null) { + add(popup); + if (IJ.isMacOSX()) IJ.wait(10); + popup.show(this, sx, sy); + } + } + + public void mouseExited(MouseEvent e) { + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) { + tool.mouseExited(imp, e); + if (e.isConsumed()) return; + } + ImageWindow win = imp.getWindow(); + if (win!=null) + setCursor(defaultCursor); + IJ.showStatus(""); + mouseExited = true; + } + + public void mouseDragged(MouseEvent e) { + int x = e.getX(); + int y = e.getY(); + xMouse = offScreenX(x); + yMouse = offScreenY(y); + flags = e.getModifiers(); + mousePressedX = mousePressedY = -1; + //IJ.log("mouseDragged: "+flags); + if (flags==0) // workaround for Mac OS 9 bug + flags = InputEvent.BUTTON1_MASK; + if (Toolbar.getToolId()==Toolbar.HAND || IJ.spaceBarDown()) + scroll(x, y); + else { + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) { + tool.mouseDragged(imp, e); + if (e.isConsumed()) return; + } + IJ.setInputEvent(e); + Roi roi = imp.getRoi(); + if (roi != null) + roi.handleMouseDrag(x, y, flags); + } + } + + protected void handleRoiMouseDown(MouseEvent e) { + int sx = e.getX(); + int sy = e.getY(); + int ox = offScreenX(sx); + int oy = offScreenY(sy); + Roi roi = imp.getRoi(); + int tool = Toolbar.getToolId(); + + int handle = roi!=null?roi.isHandle(sx, sy):-1; + boolean multiPointMode = roi!=null && (roi instanceof PointRoi) && handle==-1 + && tool==Toolbar.POINT && Toolbar.getMultiPointMode(); + if (multiPointMode) { + double oxd = roi.offScreenXD(sx); + double oyd = roi.offScreenYD(sy); + if (e.isShiftDown() && !IJ.isMacro()) { + FloatPolygon points = roi.getFloatPolygon(); + if (points.npoints>0) { + double x0 = points.xpoints[0]; + double y0 = points.ypoints[0]; + double slope = Math.abs((oxd-x0)/(oyd-y0)); + if (slope>=1.0) + oyd = points.ypoints[0]; + else + oxd = points.xpoints[0]; + } + } + ((PointRoi)roi).addUserPoint(imp, oxd, oyd); + imp.setRoi(roi); + return; + } + + if (roi!=null && (roi instanceof PointRoi)) { + int npoints = ((PolygonRoi)roi).getNCoordinates(); + if (npoints>1 && handle==-1 && !IJ.altKeyDown() && !(tool==Toolbar.POINT && !Toolbar.getMultiPointMode()&&IJ.shiftKeyDown())) { + String msg = "Type shift-a (Edit>Selection>Select None) to delete\npoints. Use multi-point tool to add points."; + GenericDialog gd=new GenericDialog("Point Selection"); + gd.addMessage(msg); + gd.addHelp(PointToolOptions.help); + gd.hideCancelButton(); + gd.showDialog(); + return; + } + } + + setRoiModState(e, roi, handle); + if (roi!=null) { + if (handle>=0) { + roi.mouseDownInHandle(handle, sx, sy); + return; + } + Rectangle r = roi.getBounds(); + int type = roi.getType(); + if (type==Roi.RECTANGLE && r.width==imp.getWidth() && r.height==imp.getHeight() + && roi.getPasteMode()==Roi.NOT_PASTING && !(roi instanceof ImageRoi)) { + imp.deleteRoi(); + return; + } + if (roi.contains(ox, oy)) { + if (roi.modState==Roi.NO_MODS) + roi.handleMouseDown(sx, sy); + else { + imp.deleteRoi(); + imp.createNewRoi(sx,sy); + } + return; + } + boolean segmentedTool = tool==Toolbar.POLYGON || tool==Toolbar.POLYLINE || tool==Toolbar.ANGLE; + if (segmentedTool && (type==Roi.POLYGON || type==Roi.POLYLINE || type==Roi.ANGLE) + && roi.getState()==roi.CONSTRUCTING) + return; + if (segmentedTool&& !(IJ.shiftKeyDown()||IJ.altKeyDown())) { + imp.deleteRoi(); + return; + } + } + imp.createNewRoi(sx,sy); + } + + void setRoiModState(MouseEvent e, Roi roi, int handle) { + if (roi==null || (handle>=0 && roi.modState==Roi.NO_MODS)) + return; + if (roi.state==Roi.CONSTRUCTING) + return; + int tool = Toolbar.getToolId(); + if (tool>Toolbar.FREEROI && tool!=Toolbar.WAND && tool!=Toolbar.POINT) + {roi.modState = Roi.NO_MODS; return;} + if (e.isShiftDown()) + roi.modState = Roi.ADD_TO_ROI; + else if (e.isAltDown()) + roi.modState = Roi.SUBTRACT_FROM_ROI; + else + roi.modState = Roi.NO_MODS; + //IJ.log("setRoiModState: "+roi.modState+" "+ roi.state); + } + + /** Disable/enable popup menu. */ + public void disablePopupMenu(boolean status) { + disablePopupMenu = status; + } + + public void setShowAllList(Overlay showAllList) { + this.showAllOverlay = showAllList; + labelRects = null; + } + + public Overlay getShowAllList() { + return showAllOverlay; + } + + /** Obsolete */ + public void setShowAllROIs(boolean showAllROIs) { + RoiManager rm = RoiManager.getInstance(); + if (rm!=null) + rm.runCommand(showAllROIs?"show all":"show none"); + } + + /** Obsolete */ + public boolean getShowAllROIs() { + return getShowAllList()!=null; + } + + /** Obsolete */ + public static Color getShowAllColor() { + if (showAllColor!=null && showAllColor.getRGB()==0xff80ffff) + showAllColor = Color.cyan; + return showAllColor; + } + + /** Obsolete */ + public static void setShowAllColor(Color c) { + if (c==null) return; + showAllColor = c; + labelColor = null; + } + + /** Experimental */ + public static void setCursor(Cursor cursor, int type) { + crosshairCursor = cursor; + } + + /** Use ImagePlus.setOverlay(ij.gui.Overlay). */ + public void setOverlay(Overlay overlay) { + imp.setOverlay(overlay); + } + + /** Use ImagePlus.getOverlay(). */ + public Overlay getOverlay() { + return imp.getOverlay(); + } + + /** + * @deprecated + * replaced by ImagePlus.setOverlay(ij.gui.Overlay) + */ + public void setDisplayList(Vector list) { + if (list!=null) { + Overlay list2 = new Overlay(); + list2.setVector(list); + imp.setOverlay(list2); + } else + imp.setOverlay(null); + Overlay overlay = imp.getOverlay(); + if (overlay!=null) + overlay.drawLabels(overlay.size()>0&&overlay.get(0).getStrokeColor()==null); + else + customRoi = false; + repaint(); + } + + /** + * @deprecated + * replaced by ImagePlus.setOverlay(Shape, Color, BasicStroke) + */ + public void setDisplayList(Shape shape, Color color, BasicStroke stroke) { + imp.setOverlay(shape, color, stroke); + } + + /** + * @deprecated + * replaced by ImagePlus.setOverlay(Roi, Color, int, Color) + */ + public void setDisplayList(Roi roi, Color color) { + roi.setStrokeColor(color); + Overlay list = new Overlay(); + list.add(roi); + imp.setOverlay(list); + } + + /** + * @deprecated + * replaced by ImagePlus.getOverlay() + */ + public Vector getDisplayList() { + Overlay overlay = imp.getOverlay(); + if (overlay==null) + return null; + Vector displayList = new Vector(); + for (int i=0; i250L && !drawingTool()) { // long press + if (activateOverlayRoi(ox,oy)) + return; + } + + } + + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) { + tool.mouseReleased(imp, e); + if (e.isConsumed()) return; + } + flags = e.getModifiers(); + flags &= ~InputEvent.BUTTON1_MASK; // make sure button 1 bit is not set + flags &= ~InputEvent.BUTTON2_MASK; // make sure button 2 bit is not set + flags &= ~InputEvent.BUTTON3_MASK; // make sure button 3 bit is not set + Roi roi = imp.getRoi(); + if (roi != null) { + Rectangle r = roi.getBounds(); + int type = roi.getType(); + if ((r.width==0 || r.height==0) + && !(type==Roi.POLYGON||type==Roi.POLYLINE||type==Roi.ANGLE||type==Roi.LINE) + && !(roi instanceof TextRoi) + && roi.getState()==roi.CONSTRUCTING + && type!=roi.POINT) + imp.deleteRoi(); + else + roi.handleMouseUp(e.getX(), e.getY()); + } + } + + private boolean activateOverlayRoi(int ox, int oy) { + int currentImage = -1; + int stackSize = imp.getStackSize(); + if (stackSize>1) + currentImage = imp.getCurrentSlice(); + int channel=0, slice=0, frame=0; + boolean hyperstack = imp.isHyperStack(); + if (hyperstack) { + channel = imp.getChannel(); + slice = imp.getSlice(); + frame = imp.getFrame(); + } + Overlay o = showAllOverlay; + if (o==null) + o = imp.getOverlay(); + if (o==null || !o.isSelectable()) + return false; + boolean roiManagerShowAllMode = o==showAllOverlay && !Prefs.showAllSliceOnly; + boolean labels = o.getDrawLabels(); + int sx = screenX(ox); + int sy = screenY(oy); + for (int i=o.size()-1; i>=0; i--) { + Roi roi = o.get(i); + if (roi==null) + continue; + //IJ.log(".isAltDown: "+roi.contains(ox, oy)); + boolean containsMousePoint = false; + if (roi instanceof Line) { //grab line roi near its center + double grabLineWidth = 1.1 + 5./magnification; + containsMousePoint = (((Line)roi).getFloatPolygon(grabLineWidth)).contains(ox, oy); + } else + containsMousePoint = roi.contains(ox, oy); + if (containsMousePoint || (labels&&labelRects!=null&&labelRects[i]!=null&&labelRects[i].contains(sx,sy))) { + if (hyperstack && roi.getPosition()==0) { + int c = roi.getCPosition(); + int z = roi.getZPosition(); + int t = roi.getTPosition(); + if (!((c==0||c==channel)&&(z==0||z==slice)&&(t==0||t==frame) || roiManagerShowAllMode)) + continue; + } else { + int position = stackSize>1?roi.getPosition():0; + if (!(position==0||position==currentImage||roiManagerShowAllMode)) + continue; + } + if (!IJ.altKeyDown() && roi.getType()==Roi.COMPOSITE + && roi.getBounds().width==imp.getWidth() && roi.getBounds().height==imp.getHeight()) + return false; + if (Toolbar.getToolId()==Toolbar.OVAL && Toolbar.getBrushSize()>0) + Toolbar.getInstance().setTool(Toolbar.RECTANGLE); + roi.setImage(null); + imp.setRoi(roi); + //roi.handleMouseDown(sx, sy); + roiManagerSelect(roi, false); + ResultsTable.selectRow(roi); + return true; + } + } + return false; + } + + public boolean roiManagerSelect(Roi roi, boolean delete) { + RoiManager rm=RoiManager.getInstance(); + if (rm==null) + return false; + int index = rm.getRoiIndex(roi); + if (index<0) + return false; + if (delete) { + rm.select(imp, index); + rm.runCommand("delete"); + } else + rm.selectAndMakeVisible(imp, index); + return true; + } + + public void mouseMoved(MouseEvent e) { + //if (ij==null) return; + int sx = e.getX(); + int sy = e.getY(); + int ox = offScreenX(sx); + int oy = offScreenY(sy); + flags = e.getModifiers(); + setCursor(sx, sy, ox, oy); + mousePressedX = mousePressedY = -1; + IJ.setInputEvent(e); + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) { + tool.mouseMoved(imp, e); + if (e.isConsumed()) return; + } + Roi roi = imp.getRoi(); + int type = roi!=null?roi.getType():-1; + if (type>0 && (type==Roi.POLYGON||type==Roi.POLYLINE||type==Roi.ANGLE||type==Roi.LINE) + && roi.getState()==roi.CONSTRUCTING) + roi.mouseMoved(e); + else { + if (ox144) + showCursorStatus = true; + if (win!=null&&showCursorStatus) + win.mouseMoved(ox, oy); + } else + IJ.showStatus(""); + } + } + + public void mouseEntered(MouseEvent e) { + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) + tool.mouseEntered(imp, e); + } + + public void mouseClicked(MouseEvent e) { + PlugInTool tool = Toolbar.getPlugInTool(); + if (tool!=null) + tool.mouseClicked(imp, e); + } + + public void setScaleToFit(boolean scaleToFit) { + this.scaleToFit = scaleToFit; + } + + public boolean getScaleToFit() { + return scaleToFit; + } + + public boolean hideZoomIndicator(boolean hide) { + boolean hidden = this.hideZoomIndicator; + if (!(srcRect.width0) dim.height += vgap; + dim.height += d.height; + } + Insets insets = target.getInsets(); + dim.width += insets.left + insets.right + hgap*2; + dim.height += insets.top + insets.bottom + vgap*2; + return dim; + } + + /** Returns the minimum dimensions for this layout. */ + public Dimension minimumLayoutSize(Container target) { + return preferredLayoutSize(target); + } + + /** Determines whether to ignore the width of non-image components when calculating + * the preferred width (default false, i.e. the maximum of the widths of all components is used). + * When true, components that do not fit the window will be truncated at the right. + * The width of the 0th component (the ImageCanvas) is always taken into account. */ + public void ignoreNonImageWidths(boolean ignoreNonImageWidths) { + this.ignoreNonImageWidths = ignoreNonImageWidths; + } + + /** Centers the elements in the specified column, if there is any slack.*/ + private void moveComponents(Container target, int x, int y, int width, int height, int nmembers) { + int x2 = 0; + y += height / 2; + for (int i=0; i60) + x2 = x + (width - d.width)/2; + m.setLocation(x2, y); + y += vgap + d.height; + } + } + + /** Lays out the container and calls ImageCanvas.resizeCanvas() + to adjust the image canvas size as needed. */ + public void layoutContainer(Container target) { + Insets insets = target.getInsets(); + int nmembers = target.getComponentCount(); + Dimension d; + int extraHeight = 0; + for (int i=1; i 0) y += vgap; + y += d.height; + if (i==0 || !ignoreNonImageWidths) + colw = Math.max(colw, d.width); + } + moveComponents(target, x, insets.top + vgap, colw, maxheight - y, nmembers); + } + +} diff --git a/src/ij/gui/ImagePanel.java b/src/ij/gui/ImagePanel.java new file mode 100644 index 0000000..628e575 --- /dev/null +++ b/src/ij/gui/ImagePanel.java @@ -0,0 +1,28 @@ +package ij.gui; +import java.awt.*; +import ij.*; + +/** This class is used by GenericDialog to add images to dialogs. */ +public class ImagePanel extends Panel { + private ImagePlus img; + private int width, height; + + ImagePanel(ImagePlus img) { + this.img = img; + width = img.getWidth(); + height = img.getHeight(); + } + + public Dimension getPreferredSize() { + return new Dimension(width, height); + } + + public Dimension getMinimumSize() { + return new Dimension(width, height); + } + + public void paint(Graphics g) { + g.drawImage(img.getProcessor().createImage(), 0, 0, null); + } + +} diff --git a/src/ij/gui/ImageRoi.java b/src/ij/gui/ImageRoi.java new file mode 100644 index 0000000..024366d --- /dev/null +++ b/src/ij/gui/ImageRoi.java @@ -0,0 +1,150 @@ +package ij.gui; +import ij.ImagePlus; +import ij.process.*; +import ij.io.FileSaver; +import java.awt.*; +import java.awt.image.*; + +/** An ImageRoi is an Roi that overlays an image. +* @see ij.ImagePlus#setOverlay(ij.gui.Overlay) +*/ +public class ImageRoi extends Roi { + private Image img; + private Composite composite; + private double opacity = 1.0; + private double angle = 0.0; + private boolean zeroTransparent; + private ImageProcessor ip; + + /** Creates a new ImageRoi from a BufferedImage.*/ + public ImageRoi(int x, int y, BufferedImage bi) { + super(x, y, bi.getWidth(), bi.getHeight()); + img = bi; + setStrokeColor(Color.black); + } + + /** Creates a new ImageRoi from a ImageProcessor.*/ + public ImageRoi(int x, int y, ImageProcessor ip) { + super(x, y, ip.getWidth(), ip.getHeight()); + img = ip.createImage(); + this.ip = ip; + setStrokeColor(Color.black); + } + + public void draw(Graphics g) { + Graphics2D g2d = (Graphics2D)g; + double mag = getMagnification(); + int sx2 = screenX(x+width); + int sy2 = screenY(y+height); + Composite saveComposite = null; + if (composite!=null) { + saveComposite = g2d.getComposite(); + g2d.setComposite(composite); + } + Image img2 = img; + if (angle!=0.0) { + ImageProcessor ip = new ColorProcessor(img); + ip.setInterpolate(true); + ip.setBackgroundValue(0.0); + ip.rotate(angle); + if (zeroTransparent) + ip = makeZeroTransparent(ip, true); + img2 = ip.createImage(); + } + g.drawImage(img2, screenX(x), screenY(y), sx2, sy2, 0, 0, img.getWidth(null), img.getHeight(null), null); + if (composite!=null) g2d.setComposite(saveComposite); + if (isActiveOverlayRoi() && !overlay) + super.draw(g); + } + + /** Sets the composite mode. */ + public void setComposite(Composite composite) { + this.composite = composite; + } + + /** Sets the composite mode using the specified opacity (alpha), in the + range 0.0-1.0, where 0.0 is fully transparent and 1.0 is fully opaque. */ + public void setOpacity(double opacity) { + if (opacity<0.0) opacity = 0.0; + if (opacity>1.0) opacity = 1.0; + this.opacity = opacity; + if (opacity!=1.0) + composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float)opacity); + else + composite = null; + } + + /** Returns a serialized version of the image. */ + public byte[] getSerializedImage() { + ImagePlus imp = new ImagePlus("",img); + return new FileSaver(imp).serialize(); + } + + /** Returns the current opacity. */ + public double getOpacity() { + return opacity; + } + + public void rotate(double angle) { + this.angle += angle; + } + + public void setAngle(double angle) { + this.angle = angle; + } + + public void setZeroTransparent(boolean zeroTransparent) { + if (this.zeroTransparent!=zeroTransparent) { + ip = makeZeroTransparent(new ColorProcessor(img), zeroTransparent); + img = ip.createImage(); + } + this.zeroTransparent = zeroTransparent; + } + + public boolean getZeroTransparent() { + return zeroTransparent; + } + + private ImageProcessor makeZeroTransparent(ImageProcessor ip, boolean transparent) { + if (transparent) { + ip.setColorModel(new DirectColorModel(32,0x00ff0000,0x0000ff00,0x000000ff,0xff000000)); + for (int x=0; x1) + ip.set(x, y, ip.get(x,y)|0xff000000); // set alpha bits + else + ip.set(x, y, ip.get(x,y)&0xffffff); // clear alpha bits + } + } + } + return ip; + } + + public synchronized Object clone() { + ImageRoi roi2 = (ImageRoi)super.clone(); + ImagePlus imp = new ImagePlus("", img); + roi2.setProcessor(imp.getProcessor()); + roi2.setOpacity(getOpacity()); + roi2.zeroTransparent = !zeroTransparent; + roi2.setZeroTransparent(zeroTransparent); + return roi2; + } + + public ImageProcessor getProcessor() { + if (ip!=null) + return ip; + else { + ip = new ColorProcessor(img); + return ip; + } + } + + public void setProcessor(ImageProcessor ip) { + img = ip.createImage(); + this.ip = ip; + width = ip.getWidth(); + height = ip.getHeight(); + } + +} \ No newline at end of file diff --git a/src/ij/gui/ImageWindow.java b/src/ij/gui/ImageWindow.java new file mode 100644 index 0000000..bb1942a --- /dev/null +++ b/src/ij/gui/ImageWindow.java @@ -0,0 +1,763 @@ +package ij.gui; +import java.awt.*; +import java.awt.image.*; +import java.util.Properties; +import java.awt.event.*; +import ij.*; +import ij.process.*; +import ij.io.*; +import ij.measure.*; +import ij.plugin.frame.*; +import ij.plugin.PointToolOptions; +import ij.macro.Interpreter; +import ij.util.*; + +/** A frame for displaying images. */ +public class ImageWindow extends Frame implements FocusListener, WindowListener, WindowStateListener, MouseWheelListener { + + public static final int MIN_WIDTH = 128; + public static final int MIN_HEIGHT = 32; + public static final int HGAP = 5; + public static final int VGAP = 5; + public static final String LOC_KEY = "image.loc"; + + protected ImagePlus imp; + protected ImageJ ij; + protected ImageCanvas ic; + private double initialMagnification = 1; + private int newWidth, newHeight; + protected boolean closed; + private boolean newCanvas; + private boolean unzoomWhenMinimizing = true; + Rectangle maxWindowBounds; // largest possible window on this screen + Rectangle maxBounds; // Size of this window after it is maximized + long setMaxBoundsTime; + private int sliderHeight; + + private static final int XINC = 12; + private static final int YINC = 16; + private final double SCALE = Prefs.getGuiScale(); + private int TEXT_GAP = 11; + private static int xbase = -1; + private static int ybase; + private static int xloc; + private static int yloc; + private static int count; + private static boolean centerOnScreen; + private static Point nextLocation; + public static long setMenuBarTime; + private int textGap = centerOnScreen?0:TEXT_GAP; + private Point initialLoc; + private int screenHeight, screenWidth; + + + /** This variable is set false if the user presses the escape key or closes the window. */ + public boolean running; + + /** This variable is set false if the user clicks in this + window, presses the escape key, or closes the window. */ + public boolean running2; + + public ImageWindow(String title) { + super(title); + } + + public ImageWindow(ImagePlus imp) { + this(imp, null); + } + + public ImageWindow(ImagePlus imp, ImageCanvas ic) { + super(imp.getTitle()); + if (SCALE>1.0) { + TEXT_GAP = (int)(TEXT_GAP*SCALE); + textGap = centerOnScreen?0:TEXT_GAP; + } + if (Prefs.blackCanvas && getClass().getName().equals("ij.gui.ImageWindow")) { + setForeground(Color.white); + setBackground(Color.black); + } else { + setForeground(Color.black); + if (IJ.isLinux()) + setBackground(ImageJ.backgroundColor); + else + setBackground(Color.white); + } + boolean openAsHyperStack = imp.getOpenAsHyperStack(); + ij = IJ.getInstance(); + this.imp = imp; + if (ic==null) { + ic = (this instanceof PlotWindow) ? new PlotCanvas(imp) : new ImageCanvas(imp); + newCanvas=true; + } + this.ic = ic; + ImageWindow previousWindow = imp.getWindow(); + setLayout(new ImageLayout(ic)); + add(ic); + addFocusListener(this); + addWindowListener(this); + addWindowStateListener(this); + addKeyListener(ij); + setFocusTraversalKeysEnabled(false); + if (!(this instanceof StackWindow)) + addMouseWheelListener(this); + setResizable(true); + if (!(this instanceof HistogramWindow&&IJ.isMacro()&&Interpreter.isBatchMode())) { + WindowManager.addWindow(this); + imp.setWindow(this); + } + if (previousWindow!=null) { + if (newCanvas) + setLocationAndSize(false); + else + ic.update(previousWindow.getCanvas()); + Point loc = previousWindow.getLocation(); + setLocation(loc.x, loc.y); + if (!(this instanceof StackWindow || this instanceof PlotWindow)) { //layout now unless components will be added later + pack(); + if (IJ.isMacro()) + imp.setDeactivated(); //prepare for waitTillActivated (imp may have been activated before if it gets a new Window now) + show(); + } + if (ic.getMagnification()!=0.0) + imp.setTitle(imp.getTitle()); + boolean unlocked = imp.lockSilently(); + boolean changes = imp.changes; + imp.changes = false; + previousWindow.close(); + imp.changes = changes; + if (unlocked) + imp.unlock(); + if (this.imp!=null) + this.imp.setOpenAsHyperStack(openAsHyperStack); + WindowManager.setCurrentWindow(this); + } else { + setLocationAndSize(false); + if (ij!=null && !IJ.isMacintosh()) { + Image img = ij.getIconImage(); + if (img!=null) try { + setIconImage(img); + } catch (Exception e) {} + } + if (nextLocation!=null) + setLocation(nextLocation); + else if (centerOnScreen) + GUI.center(this); + nextLocation = null; + centerOnScreen = false; + if (Interpreter.isBatchMode() || (IJ.getInstance()==null&&this instanceof HistogramWindow)) { + WindowManager.setTempCurrentImage(imp); + Interpreter.addBatchModeImage(imp); + } else { + if (IJ.isMacro()) + imp.setDeactivated(); //prepare for waitTillActivated (imp may have been activated previously and gets a new Window now) + show(); + } + } + } + + private void setLocationAndSize(boolean updating) { + if (imp==null) + return; + int width = imp.getWidth(); + int height = imp.getHeight(); + + // load prefernces file location + Point loc = Prefs.getLocation(LOC_KEY); + Rectangle bounds = null; + if (loc!=null) { + bounds = GUI.getMaxWindowBounds(loc); + if (bounds!=null && (loc.x>bounds.x+bounds.width/3||loc.y>bounds.y+bounds.height/3) + && (loc.x+width>bounds.x+bounds.width||loc.y+height>bounds.y+bounds.height)) { + loc = null; + bounds = null; + } + } + // if loc not valid, use screen bounds of visible window (this) or of main window (ij) if not visible yet (updating == false) + Rectangle maxWindow = bounds!=null?bounds:GUI.getMaxWindowBounds(updating?this: ij); + + if (WindowManager.getWindowCount()<=1) + xbase = -1; + if (width>maxWindow.width/2 && xbase>maxWindow.x+5+XINC*6) + xbase = -1; + if (xbase==-1) { + count = 0; + if (loc!=null) { + xbase = loc.x; + ybase = loc.y; + } else if (ij!=null) { + Rectangle ijBounds = ij.getBounds(); + if (ijBounds.y-maxWindow.xmaxWindow.x+maxWindow.width) { + xbase = maxWindow.x+maxWindow.width - width - 10; + if (xbasescreenWidth || ybase+height*mag>=screenHeight) { + double mag2 = ImageCanvas.getLowerZoomLevel(mag); + if (mag2==mag) break; + mag = mag2; + } + } + + if (mag<1.0) { + initialMagnification = mag; + ic.setSize((int)(width*mag), (int)(height*mag)); + } + ic.setMagnification(mag); + if (y+height*mag>screenHeight) + y = ybase; + if (Prefs.open100Percent && ic.getMagnification()<1.0) { + while(ic.getMagnification()<1.0) + ic.zoomIn(0, 0); + setSize(Math.min(width, screenWidth-x), Math.min(height, screenHeight-y)); + validate(); + } else + pack(); + if (!updating) { + setLocation(x, y); + initialLoc = new Point(x,y); + } + } + + Rectangle getMaxWindow(int xloc, int yloc) { + return GUI.getMaxWindowBounds(new Point(xloc, yloc)); + } + + public double getInitialMagnification() { + return initialMagnification; + } + + /** Override Container getInsets() to make room for some text above the image. */ + public Insets getInsets() { + Insets insets = super.getInsets(); + if (imp==null) + return insets; + double mag = ic.getMagnification(); + int extraWidth = (int)((MIN_WIDTH - imp.getWidth()*mag)/2.0); + if (extraWidth<0) extraWidth = 0; + int extraHeight = (int)((MIN_HEIGHT - imp.getHeight()*mag)/2.0); + if (extraHeight<0) extraHeight = 0; + insets = new Insets(insets.top+textGap+extraHeight, insets.left+extraWidth, insets.bottom+extraHeight, insets.right+extraWidth); + return insets; + } + + /** Draws the subtitle. */ + public void drawInfo(Graphics g) { + if (imp==null) + return; + if (textGap!=0) { + Insets insets = super.getInsets(); + Color savec = null; + if (imp.isComposite()) { + CompositeImage ci = (CompositeImage)imp; + if (ci.getMode()==IJ.COMPOSITE) { + savec = g.getColor(); + Color c = ci.getChannelColor(); + if (Color.green.equals(c)) + c = new Color(0,180,0); + g.setColor(c); + } + } + Java2.setAntialiasedText(g, true); + if (SCALE>1.0) { + Font font = new Font("SansSerif", Font.PLAIN, (int)(12*SCALE)); + g.setFont(font); + } + g.drawString(createSubtitle(), insets.left+5, insets.top+TEXT_GAP); + if (savec!=null) + g.setColor(savec); + } + } + + /** Creates the subtitle. */ + public String createSubtitle() { + String s=""; + if (imp==null) + return s; + int stackSize = imp.getStackSize(); + if (stackSize>1) { + ImageStack stack = imp.getStack(); + int currentSlice = imp.getCurrentSlice(); + s += currentSlice+"/"+stackSize; + String label = stack.getShortSliceLabel(currentSlice); + if (label!=null && label.length()>0) { + if (imp.isHyperStack()) label = label.replace(';', ' '); + s += " (" + label + ")"; + } + if ((this instanceof StackWindow) && running2) { + return s; + } + s += "; "; + } else { + String label = imp.getProp("Slice_Label"); + if (label==null && imp.isStack()) + label = imp.getStack().getSliceLabel(1); + if (label!=null && label.length()>0) { + int newline = label.indexOf('\n'); + if (newline>0) + label = label.substring(0, newline); + int len = label.length(); + if (len>4 && label.charAt(len-4)=='.' && !Character.isDigit(label.charAt(len-1))) + label = label.substring(0,len-4); + if (label.length()>60) + label = label.substring(0, 60)+"..."; + s = "\""+label + "\"; "; + } + } + int type = imp.getType(); + Calibration cal = imp.getCalibration(); + if (cal.scaled()) { + boolean unitsMatch = cal.getXUnit().equals(cal.getYUnit()); + double cwidth = imp.getWidth()*cal.pixelWidth; + double cheight = imp.getHeight()*cal.pixelHeight; + int digits = Tools.getDecimalPlaces(cwidth, cheight); + if (digits>2) digits=2; + if (unitsMatch) { + s += IJ.d2s(cwidth,digits) + "x" + IJ.d2s(cheight,digits) + + " " + cal.getUnits() + " (" + imp.getWidth() + "x" + imp.getHeight() + "); "; + } else { + s += d2s(cwidth) + " " + cal.getXUnit() + " x " + + d2s(cheight) + " " + cal.getYUnit() + + " (" + imp.getWidth() + "x" + imp.getHeight() + "); "; + } + } else + s += imp.getWidth() + "x" + imp.getHeight() + " pixels; "; + switch (type) { + case ImagePlus.GRAY8: + case ImagePlus.COLOR_256: + s += "8-bit"; + break; + case ImagePlus.GRAY16: + s += "16-bit"; + break; + case ImagePlus.GRAY32: + s += "32-bit"; + break; + case ImagePlus.COLOR_RGB: + s += imp.isRGB() ? "RGB" : "32-bit (int)"; + break; + } + if (imp.isInvertedLut()) + s += " (inverting LUT)"; + return s+"; "+getImageSize(imp); + } + + public static String getImageSize(ImagePlus imp) { + if (imp==null) + return null; + double size = imp.getSizeInBytes()/1024.0; + String s2=null, s3=null; + if (size<1024.0) + {s2=IJ.d2s(size,0); s3="K";} + else if (size<10000.0) + {s2=IJ.d2s(size/1024.0,1); s3="MB";} + else if (size<1048576.0) + {s2=IJ.d2s(Math.round(size/1024.0),0); s3="MB";} + else + {s2=IJ.d2s(size/1048576.0,1); s3="GB";} + if (s2.endsWith(".0")) s2 = s2.substring(0, s2.length()-2); + return s2+s3; + } + + private String d2s(double n) { + int digits = Tools.getDecimalPlaces(n); + if (digits>2) digits=2; + return IJ.d2s(n,digits); + } + + public void paint(Graphics g) { + drawInfo(g); + Rectangle r = ic.getBounds(); + int extraWidth = MIN_WIDTH - r.width; + int extraHeight = MIN_HEIGHT - r.height; + if (extraWidth<=0 && extraHeight<=0 && !Prefs.noBorder && !IJ.isLinux()) + g.drawRect(r.x-1, r.y-1, r.width+1, r.height+1); + } + + /** Removes this window from the window list and disposes of it. + Returns false if the user cancels the "save changes" dialog. */ + public boolean close() { + boolean isRunning = running || running2; + running = running2 = false; + if (imp==null) return true; + boolean virtual = imp.getStackSize()>1 && imp.getStack().isVirtual(); + if (isRunning) IJ.wait(500); + if (imp==null) return true; + boolean changes = imp.changes; + Roi roi = imp.getRoi(); + if (roi!=null && (roi instanceof PointRoi) && ((PointRoi)roi).promptBeforeDeleting()) + changes = true; + if (ij==null || ij.quittingViaMacro() || IJ.getApplet()!=null || Interpreter.isBatchMode() || IJ.macroRunning() || virtual) + changes = false; + if (changes) { + String msg; + String name = imp.getTitle(); + if (name.length()>22) + msg = "Save changes to\n" + "\"" + name + "\"?"; + else + msg = "Save changes to \"" + name + "\"?"; + if (imp.isLocked()) + msg += "\nWARNING: This image is locked.\nProbably, processing is unfinished (slow or still previewing)."; + toFront(); + YesNoCancelDialog d = new YesNoCancelDialog(this, "ImageJ", msg); + if (d.cancelPressed()) + return false; + else if (d.yesPressed()) { + FileSaver fs = new FileSaver(imp); + if (!fs.save()) return false; + } + } + closed = true; + if (WindowManager.getWindowCount()==0) { + xloc = 0; + yloc = 0; + } + WindowManager.removeWindow(this); + if (ij!=null && ij.quitting()) // this may help avoid thread deadlocks + return true; + Rectangle bounds = getBounds(); + if (initialLoc!=null && !bounds.equals(initialLoc) && !IJ.isMacro() + && bounds.y0) + sw.removeScrollbars(); + else if (stackSize>1 && nScrollbars==0) + sw.addScrollbars(imp); + } + pack(); + repaint(); + maxBounds = getMaximumBounds(); + setMaximizedBounds(maxBounds); + setMaxBoundsTime = System.currentTimeMillis(); + } + + public ImageCanvas getCanvas() { + return ic; + } + + + static ImagePlus getClipboard() { + return ImagePlus.getClipboard(); + } + + public Rectangle getMaximumBounds() { + Rectangle maxWindow = GUI.getMaxWindowBounds(this); + if (imp==null) + return maxWindow; + double width = imp.getWidth(); + double height = imp.getHeight(); + double iAspectRatio = width/height; + maxWindowBounds = maxWindow; + if (iAspectRatio/((double)maxWindow.width/maxWindow.height)>0.75) { + maxWindow.y += 22; // uncover ImageJ menu bar + maxWindow.height -= 22; + } + Dimension extraSize = getExtraSize(); + double maxWidth = maxWindow.width-extraSize.width; + double maxHeight = maxWindow.height-extraSize.height; + double mAspectRatio = maxWidth/maxHeight; + int wWidth, wHeight; + double mag; + if (iAspectRatio>=mAspectRatio) { + mag = maxWidth/width; + wWidth = maxWindow.width; + wHeight = (int)(height*mag+extraSize.height); + } else { + mag = maxHeight/height; + wHeight = maxWindow.height; + wWidth = (int)(width*mag+extraSize.width); + } + int xloc = (int)(maxWidth-wWidth)/2; + if (xlocwidth) srcRect.x = width-srcRect.width; + } else { + srcRect.y += rotation*amount*Math.max(height/200, 1); + if (srcRect.y<0) srcRect.y = 0; + if (srcRect.y+srcRect.height>height) srcRect.y = height-srcRect.height; + } + if (srcRect.x!=xstart || srcRect.y!=ystart) + ic.repaint(); + } + + /** Copies the current ROI to the clipboard. The entire + image is copied if there is no ROI. */ + public void copy(boolean cut) { + imp.copy(cut); + } + + + public void paste() { + imp.paste(); + } + + /** This method is called by ImageCanvas.mouseMoved(MouseEvent). + @see ij.gui.ImageCanvas#mouseMoved + */ + public void mouseMoved(int x, int y) { + imp.mouseMoved(x, y); + } + + public String toString() { + return imp!=null?imp.getTitle():""; + } + + /** Causes the next image to be opened to be centered on the screen + and displayed without informational text above the image. */ + public static void centerNextImage() { + centerOnScreen = true; + } + + /** Causes the next image to be displayed at the specified location. */ + public static void setNextLocation(Point loc) { + nextLocation = loc; + } + + /** Causes the next image to be displayed at the specified location. */ + public static void setNextLocation(int x, int y) { + nextLocation = new Point(x, y); + } + + /** Moves and resizes this window. Changes the + magnification so the image fills the window. */ + public void setLocationAndSize(int x, int y, int width, int height) { + setBounds(x, y, width, height); + getCanvas().fitToWindow(); + initialLoc = null; + pack(); + } + + @Override + public void setLocation(int x, int y) { + super.setLocation(x, y); + initialLoc = null; + } + + public void setSliderHeight(int height) { + sliderHeight = height; + } + + public int getSliderHeight() { + return sliderHeight; + } + + public static void setImageJMenuBar(ImageWindow win) { + ImageJ ij = IJ.getInstance(); + boolean setMenuBar = true; + ImagePlus imp = win.getImagePlus(); + if (imp!=null) + setMenuBar = imp.setIJMenuBar(); + MenuBar mb = Menus.getMenuBar(); + if (mb!=null && mb==win.getMenuBar()) + setMenuBar = false; + setMenuBarTime = 0L; + if (setMenuBar && ij!=null && !ij.quitting() && !Interpreter.nonBatchMacroRunning()) { + IJ.wait(10); // may be needed for Java 1.4 on OS X + long t0 = System.currentTimeMillis(); + win.setMenuBar(mb); + long time = System.currentTimeMillis()-t0; + setMenuBarTime = time; + Menus.setMenuBarCount++; + if (IJ.debugMode) IJ.log("setMenuBar: "+time+"ms ("+Menus.setMenuBarCount+")"); + if (time>2000L) + Prefs.setIJMenuBar = false; + } + if (imp!=null) imp.setIJMenuBar(true); + } + +} //class ImageWindow diff --git a/src/ij/gui/Line.java b/src/ij/gui/Line.java new file mode 100644 index 0000000..b4c601f --- /dev/null +++ b/src/ij/gui/Line.java @@ -0,0 +1,738 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.Straightener; +import ij.plugin.frame.Recorder; +import ij.plugin.CalibrationBar; +import java.awt.*; +import java.awt.image.*; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.awt.event.*; +import java.awt.geom.*; + +/** This class represents a straight line selection. */ +public class Line extends Roi { + public int x1, y1, x2, y2; // the line end points as integer coordinates, for compatibility only + public double x1d, y1d, x2d, y2d; // the line using sub-pixel coordinates + protected double x1R, y1R, x2R, y2R; // the line, relative to base of subpixel bounding rect 'bounds' + protected double startxd, startyd; + static boolean widthChanged; + private boolean dragged; + private int mouseUpCount; + + /** Creates a new straight line selection using the specified + starting and ending offscreen integer coordinates. */ + public Line(int ox1, int oy1, int ox2, int oy2) { + this((double)ox1, (double)oy1, (double)ox2, (double)oy2); + } + + /** Creates a new straight line selection using the specified + starting and ending offscreen double coordinates. */ + public Line(double ox1, double oy1, double ox2, double oy2) { + super((int)(ox1+0.5), (int)(oy1+0.5), 0, 0); + type = LINE; + updateCoordinates(ox1, oy1, ox2, oy2); + if (!(this instanceof Arrow) && lineWidth>1) + updateWideLine(lineWidth); + updateClipRect(); + oldX=x; oldY=y; oldWidth=width; oldHeight=height; + state = NORMAL; + } + + /** Creates a new straight line selection using the specified + starting and ending offscreen coordinates. */ + public static Line create(double x1, double y1, double x2, double y2) { + return new Line(x1, y1, x2, y2); + } + + /** Starts the process of creating a new user-generated straight line + selection. 'sx' and 'sy' are screen coordinates that specify + the start of the line. The user will determine the end of the line + interactively using rubber banding. */ + public Line(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + type = LINE; + startxd = offScreenXD(sx); + startyd = offScreenYD(sy); + if (!magnificationForSubPixel()) { + startxd = Math.round(startxd); + startyd = Math.round(startyd); + } + updateCoordinates(startxd, startyd, startxd, startyd); + if (!(this instanceof Arrow) && lineWidth>1) + updateWideLine(lineWidth); + } + + /** + * @deprecated + * replaced by Line(int, int, int, int) + */ + public Line(int ox1, int oy1, int ox2, int oy2, ImagePlus imp) { + this(ox1, oy1, ox2, oy2); + setImage(imp); + } + + protected void grow(int sx, int sy) { //mouseDragged + drawLine(sx, sy); + dragged = true; + } + + public void mouseMoved(MouseEvent e) { + drawLine(e.getX(), e.getY()); + } + + protected void handleMouseUp(int screenX, int screenY) { + mouseUpCount++; + if (Prefs.enhancedLineTool && mouseUpCount==1 && !dragged) + return; + state = NORMAL; + if (imp==null) return; + imp.draw(clipX-5, clipY-5, clipWidth+10, clipHeight+10); + if (Recorder.record) { + String method = (this instanceof Arrow)?"makeArrow":"makeLine"; + Recorder.record(method, x1, y1, x2, y2); + } + if (getLength()==0.0) + imp.deleteRoi(); + } + + protected void drawLine(int sx, int sy) { + double xend = offScreenXD(sx); + double yend = offScreenYD(sy); + if (xend<0.0) xend=0.0; if (yend<0.0) yend=0.0; + if (xend>xMax) xend=xMax; if (yend>yMax) yend=yMax; + double xstart=getXBase()+x1R, ystart=getYBase()+y1R; + if (constrain) { + int i=0; + double dy = Math.abs(yend-ystart); + double dx = Math.abs(xend-xstart); + double comp = dy / dx; + for (;i ystart) { + yend = ystart + dx*PI_MULT[i]; + } else { + yend = ystart - dx*PI_MULT[i]; + } + } else { + xend = xstart; + } + } + if (!magnificationForSubPixel() || IJ.controlKeyDown()) { //during creation, CTRL enforces integer coordinates + xstart=Math.round(xstart); ystart=Math.round(ystart); + xend=Math.round(xend); yend=Math.round(yend); + } + updateCoordinates(xstart, ystart, xend, yend); + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX=x; oldY=y; + oldWidth=width; oldHeight=height; + } + + /** Used for angle searches in line ROI creation: tan = y/x for angle limits 1/2*45 degrees, and 3/2*45 deg */ + private static final double[] PI_SEARCH = {Math.tan(Math.PI/8), Math.tan((3*Math.PI)/8)}; + private static final double[] PI_MULT = {0, 1}; // y/x for horizontal (0 degrees) and 45 deg + + void move(int sx, int sy) { + int xNew = offScreenX(sx); + int yNew = offScreenY(sy); + x += xNew - startxd; + y += yNew - startyd; + clipboard=null; + startxd = xNew; + startyd = yNew; + updateClipRect(); + if (ignoreClipRect) + imp.draw(); + else + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; + oldY = y; + oldWidth = width; + oldHeight=height; + } + + protected void moveHandle(int sx, int sy) { + if (constrain && activeHandle == 2) { // constrain translation in 90deg steps + int dx = sx - previousSX; + int dy = sy - previousSY; + if (Math.abs(dx) > Math.abs(dy)) + dy = 0; + else + dx = 0; + sx = previousSX + dx; + sy = previousSY + dy; + } + double ox = offScreenXD(sx); + double oy = offScreenYD(sy); + double x1d=getXBase()+x1R, y1d=getYBase()+y1R; + double x2d=getXBase()+x2R, y2d=getYBase()+y2R; + double length = Math.sqrt(sqr(x2d-x1d) + sqr(y2d-y1d)); + switch (activeHandle) { + case 0: + double dx = ox-x1d; + double dy = oy-y1d; + x1d=ox; + y1d=oy; + if(center){ + x2d -= dx; + y2d -= dy; + } + if (aspect){ + double ratio = length/(Math.sqrt(sqr(x2d-x1d) + sqr(y2d-y1d))); + double xcd = x1d+(x2d-x1d)/2; + double ycd = y1d+(y2d-y1d)/2; + + if(center){ + x1d=xcd-ratio*(xcd-x1d); + x2d=xcd+ratio*(x2d-xcd); + y1d=ycd-ratio*(ycd-y1d); + y2d=ycd+ratio*(y2d-ycd); + } else { + x1d=x2d-ratio*(x2d-x1d); + y1d=y2d-ratio*(y2d-y1d); + } + + } + break; + case 1: + dx = ox-x2d; + dy = oy-y2d; + x2d=ox; + y2d=oy; + if(center){ + x1d -= dx; + y1d -= dy; + } + if(aspect){ + double ratio = length/(Math.sqrt((x2d-x1d)*(x2d-x1d) + (y2d-y1d)*(y2d-y1d))); + double xcd = x1d+(x2d-x1d)/2; + double ycd = y1d+(y2d-y1d)/2; + + if(center){ + x1d=xcd-ratio*(xcd-x1d); + x2d=xcd+ratio*(x2d-xcd); + y1d=ycd-ratio*(ycd-y1d); + y2d=ycd+ratio*(y2d-ycd); + } else { + x2d=x1d+ratio*(x2d-x1d); + y2d=y1d+ratio*(y2d-y1d); + } + + } + break; + case 2: + dx = ox-(x1d+(x2d-x1d)/2); + dy = oy-(y1d+(y2d-y1d)/2); + x1d+=dx; y1d+=dy; x2d+=dx; y2d+=dy; + break; + } + if (constrain) { + double dx = Math.abs(x1d-x2d); + double dy = Math.abs(y1d-y2d); + double xcd = Math.min(x1d,x2d)+dx/2; + double ycd = Math.min(y1d,y2d)+dy/2; + + //double ratio = length/(Math.sqrt((x2d-x1d)*(x2d-x1d) + (y2d-y1d)*(y2d-y1d))); + if (activeHandle==0) { + if (dx>=dy) { + if(aspect){ + if(x2d>x1d) x1d=x2d-length; + else x1d=x2d+length; + } + y1d = y2d; + if(center) { + y1d=y2d=ycd; + if(aspect){ + if(xcd>x1d) { + x1d=xcd-length/2; + x2d=xcd+length/2; + } + else{ + x1d=xcd+length/2; + x2d=xcd-length/2; + } + } + } + } else { + if(aspect){ + if(y2d>y1d) y1d=y2d-length; + else y1d=y2d+length; + } + x1d = x2d; + if(center){ + x1d=x2d=xcd; + if(aspect){ + if(ycd>y1d) { + y1d=ycd-length/2; + y2d=ycd+length/2; + } + else{ + y1d=ycd+length/2; + y2d=ycd-length/2; + } + } + } + } + } else if (activeHandle==1) { + if (dx>=dy) { + if(aspect){ + if(x1d>x2d) x2d=x1d-length; + else x2d=x1d+length; + } + y2d= y1d; + if(center){ + y1d=y2d=ycd; + if(aspect){ + if(xcd>x1d) { + x1d=xcd-length/2; + x2d=xcd+length/2; + } + else{ + x1d=xcd+length/2; + x2d=xcd-length/2; + } + } + } + } else { + if(aspect){ + if(y1d>y2d) y2d=y1d-length; + else y2d=y1d+length; + } + x2d = x1d; + if(center){ + x1d=x2d=xcd; + if(aspect){ + if(ycd>y1d) { + y1d=ycd-length/2; + y2d=ycd+length/2; + } + else{ + y1d=ycd+length/2; + y2d=ycd-length/2; + } + } + } + } + } + } + if (!magnificationForSubPixel()) { + x1d = Math.round(x1d); y1d = Math.round(y1d); + x2d = Math.round(x2d); y2d = Math.round(y2d); + } + updateCoordinates(x1d, y1d, x2d, y2d); + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; + oldY = y; + oldWidth = width; + oldHeight = height; + } + + protected void mouseDownInHandle(int handle, int sx, int sy) { + super.mouseDownInHandle(handle, sx, sy); //sets state, activeHandle, previousSX&Y + if (getStrokeWidth()<=3) + ic.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR)); + } + + /** Sets the x1d, y1d, x2d, y2d line end points, + * the (legacy) integer coordinates of the end points x1, y1, x2, y2 + * the 'bounds' subpixel rectangle of the Roi superclass (spanned by the end points), + * the int x, y, width, height integer bounds of the superclass (these enclose + * the 'draw' area for 1 pixel width), and + * the coordinates x1R, y1R, x2R, y2R relative to the base x, y of the 'bounds' */ + void updateCoordinates(double x1d, double y1d, double x2d, double y2d) { + this.x1d = x1d; this.y1d = y1d; + this.x2d = x2d; this.y2d = y2d; + Rectangle2D.Double bounds = this.bounds; //local variable (this.bounds may become null asynchronously upon nudge) + if (bounds == null) bounds = new Rectangle2D.Double(); + bounds.x = Math.min(x1d, x2d); + bounds.y = Math.min(y1d, y2d); + bounds.width = Math.abs(x2d - x1d); + bounds.height = Math.abs(y2d - y1d); + setIntBounds(bounds); //sets x, y, width, height + x1R = x1d - bounds.x; y1R = y1d - bounds.y; + x2R = x2d - bounds.x; y2R = y2d - bounds.y; + x1=(int)x1d; y1=(int)y1d; x2=(int)x2d; y2=(int)y2d; + this.bounds = bounds; + } + + /** Draws this line on the image. */ + public void draw(Graphics g) { + Color color = strokeColor!=null? strokeColor:ROIColor; + boolean isActiveOverlayRoi = !overlay && isActiveOverlayRoi(); + mag = getMagnification(); + if (isActiveOverlayRoi) { + if (color==Color.cyan) + color = Color.magenta; + else + color = Color.cyan; + } + g.setColor(color); + x1d=getXBase()+x1R; y1d=getYBase()+y1R; x2d=getXBase()+x2R; y2d=getYBase()+y2R; + x1=(int)x1d; y1=(int)y1d; x2=(int)x2d; y2=(int)y2d; + int sx1 = screenXD(x1d); + int sy1 = screenYD(y1d); + int sx2 = screenXD(x2d); + int sy2 = screenYD(y2d); + int sx3 = sx1 + (sx2-sx1)/2; + int sy3 = sy1 + (sy2-sy1)/2; + Graphics2D g2d = (Graphics2D)g; + setRenderingHint(g2d); + boolean cbar = overlay && mag<1.0 && Math.abs(getStrokeWidth()-CalibrationBar.STROKE_WIDTH)<0.0001; + if (stroke!=null && !isActiveOverlayRoi && !cbar) + g2d.setStroke(getScaledStroke()); + else if (cbar) + g2d.setStroke(onePixelWide); + if (wideLine && !isActiveOverlayRoi && !cbar) { + double dx = sx2 - sx1; + double dy = sy2 - sy1; + double len = length(dx, dy); + dx *= 0.5*mag/len; //half-pixel extension, corresponding to getFloatPolygon or convertLineToArea + dy *= 0.5*mag/len; + g2d.draw(new Line2D.Double(sx1-dx, sy1-dy, sx2+dx, sy2+dy)); + } else + g.drawLine(sx1, sy1, sx2, sy2); + if (wideLine && !overlay) { + g2d.setStroke(onePixelWide); + g.setColor(getColor()); + g.drawLine(sx1, sy1, sx2, sy2); + } + if (!overlay) { + handleColor = strokeColor!=null?strokeColor:ROIColor; + drawHandle(g, sx1, sy1); + handleColor=Color.white; + drawHandle(g, sx2, sy2); + drawHandle(g, sx3, sy3); + } + if (state!=NORMAL) + showStatus(); + if (updateFullWindow) + {updateFullWindow = false; imp.draw();} + } + + public void showStatus() { + IJ.showStatus(imp.getLocationAsString((int)Math.round(x2d),(int)Math.round(y2d))+ + ", angle=" + IJ.d2s(getAngle()) + ", length=" + IJ.d2s(getLength())); + } + + public double getAngle() { + return getFloatAngle(x1d, y1d, x2d, y2d); + } + + /** Returns the length of this line. */ + public double getLength() { + if (imp==null || IJ.altKeyDown()) + return getRawLength(); + else { + Calibration cal = imp.getCalibration(); + return Math.sqrt(sqr((x2d-x1d)*cal.pixelWidth) + sqr((y2d-y1d)*cal.pixelHeight)); + } + } + + /** Returns the length of this line in pixels. */ + public double getRawLength() { + return Math.sqrt(sqr(x2d-x1d)+sqr(y2d-y1d)); + } + + /** Returns the pixel values along this line. + * The line roi must have an associated ImagePlus */ + public double[] getPixels() { + double[] profile; + if (getStrokeWidth()<=1) { + ImageProcessor ip = imp.getProcessor(); + profile = ip.getLine(x1d, y1d, x2d, y2d); + } else { + ImageProcessor ip2 = (new Straightener()).rotateLine(imp,(int)getStrokeWidth()); + if (ip2==null) return new double[0]; + int width = ip2.getWidth(); + int height = ip2.getHeight(); + if (ip2 instanceof FloatProcessor) + return ProfilePlot.getColumnAverageProfile(new Rectangle(0,0,width,height),ip2); + profile = new double[width]; + double[] aLine; + ip2.setInterpolate(false); + for (int y=0; y1) { + if ((x==x1&&y==y1) || (x==x2&&y==y2)) + return true; + else + return getPolygon().contains(x,y); + } else + return false; + } + + protected void handleMouseDown(int sx, int sy) { + super.handleMouseDown(sx, sy); + startxd = ic.offScreenXD(sx); + startyd = ic.offScreenYD(sy); + } + + /** Returns a handle number if the specified screen coordinates are + inside or near a handle, otherwise returns -1. */ + public int isHandle(int sx, int sy) { + int size = HANDLE_SIZE+5; + if (getStrokeWidth()>1) size += (int)Math.log(getStrokeWidth()); + int halfSize = size/2; + int sx1 = screenXD(getXBase()+x1R) - halfSize; + int sy1 = screenYD(getYBase()+y1R) - halfSize; + int sx2 = screenXD(getXBase()+x2R) - halfSize; + int sy2 = screenYD(getYBase()+y2R) - halfSize; + int sx3 = sx1 + (sx2-sx1)/2-1; + int sy3 = sy1 + (sy2-sy1)/2-1; + if (sx>=sx1&&sx<=sx1+size&&sy>=sy1&&sy<=sy1+size) return 0; + if (sx>=sx2&&sx<=sx2+size&&sy>=sy2&&sy<=sy2+size) return 1; + if (sx>=sx3&&sx<=sx3+size+2&&sy>=sy3&&sy<=sy3+size+2) return 2; + return -1; + } + + public static int getWidth() { + return lineWidth; + } + + public static void setWidth(int w) { + if (w<1) w = 1; + int max = 500; + if (w>max) { + ImagePlus imp2 = WindowManager.getCurrentImage(); + if (imp2!=null) { + max = Math.max(max, imp2.getWidth()); + max = Math.max(max, imp2.getHeight()); + } + if (w>max) w = max; + } + lineWidth = w; + widthChanged = true; + } + + public void setStrokeWidth(float width) { + super.setStrokeWidth(width); + if (getStrokeColor()==Roi.getColor()) + wideLine = true; + } + + protected int clipRectMargin() { + return 4; + } + + /** Nudge end point of line by one pixel. */ + public void nudgeCorner(int key) { + if (ic==null) return; + double inc = 1.0/ic.getMagnification(); + switch(key) { + case KeyEvent.VK_UP: y2R-=inc; break; + case KeyEvent.VK_DOWN: y2R+=inc; break; + case KeyEvent.VK_LEFT: x2R-=inc; break; + case KeyEvent.VK_RIGHT: x2R+=inc; break; + } + grow(screenXD(x+x2R), screenYD(y+y2R)); + notifyListeners(RoiListener.MOVED); + showStatus(); + } + + /** Always returns true. */ + public boolean subPixelResolution() { + return true; + } + + public void setLocation(int x, int y) { + setLocation((double)x, (double)y); + } + + /** Sets the x coordinate of the leftmost and y coordinate of the topmost end point */ + public void setLocation(double x, double y) { + updateCoordinates(x+x1R, y+y1R, x+x2R, y+y2R); + } + + public FloatPolygon getRotationCenter() { + double xcenter = x1d + (x2d-x1d)/2.0; + double ycenter = y1d + (y2d-y1d)/2.0; + FloatPolygon p = new FloatPolygon(); + p.addPoint(xcenter,ycenter); + return p; + } + + /** + * Dedicated point iterator for thin lines. + * The iterator is based on (an improved version of) the algorithm used by + * the original method {@code ImageProcessor.getLine(double, double, double, double)}. + * Improvements are (a) that the endpoint is drawn too and (b) every line + * point is visited only once, duplicates are skipped. + * + * Author: Wilhelm Burger (04/2017) + */ + public static class PointIterator implements Iterator { + private double x1, y1; + private final int n; + private final double xinc, yinc; + private double x, y; + private int u, v; + private int u_prev, v_prev; + private int i; + + public PointIterator(Line line) { + this(line.x1d, line.y1d, line.x2d, line.y2d); + } + + public PointIterator(double x1, double y1, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + double dx = x2 - x1; + double dy = y2 - y1; + this.n = (int) Math.ceil(Math.sqrt(dx * dx + dy * dy)); + this.xinc = dx / n; + this.yinc = dy / n; + x = x1; + y = y1; + u = (int) Math.round(x - 0.5); + v = (int) Math.round(y - 0.5); + u_prev = Integer.MIN_VALUE; + v_prev = Integer.MIN_VALUE; + i = 0; + } + + @Override + public boolean hasNext() { + return i <= n; // needs to be '<=' to include last segment (point)! + } + + @Override + public Point next() { + if (i > n) throw new NoSuchElementException(); + Point p = new Point(u, v); // the current (next) point + moveToNext(); + return p; + } + + // move to next point by skipping duplicate points + private void moveToNext() { + do { + i = i + 1; + x = x1 + i * xinc; + y = y1 + i * yinc; + u_prev = u; + v_prev = v; + u = (int) Math.round(x - 0.5); + v = (int) Math.round(y - 0.5); + } while (i <= n && u == u_prev && v == v_prev); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public Iterator iterator() { + if (getStrokeWidth() <= 1.0) + return new PointIterator(this); // use the specific thin-line iterator + else + return super.iterator(); // fall back on Roi's iterator + } + +} diff --git a/src/ij/gui/MessageDialog.java b/src/ij/gui/MessageDialog.java new file mode 100644 index 0000000..7d8b942 --- /dev/null +++ b/src/ij/gui/MessageDialog.java @@ -0,0 +1,85 @@ +package ij.gui; +import ij.*; +import java.awt.*; +import java.awt.event.*; + +/** A modal dialog box that displays information. Based on the + InfoDialogclass from "Java in a Nutshell" by David Flanagan. */ +public class MessageDialog extends Dialog implements ActionListener, KeyListener, WindowListener { + protected Button button; + protected MultiLineLabel label; + private boolean escapePressed; + + public MessageDialog(Frame parent, String title, String message) { + super(parent, title, true); + setLayout(new BorderLayout()); + if (message==null) message = ""; + Font font = null; + double scale = Prefs.getGuiScale(); + if (scale>1.0) { + font = getFont(); + if (font!=null) + font = font.deriveFont((float)(font.getSize()*scale)); + else + font = new Font("SansSerif", Font.PLAIN, (int)(12*scale)); + setFont(font); + } + label = new MultiLineLabel(message); + if (font!=null) + label.setFont(font); + else if (!IJ.isLinux()) + label.setFont(new Font("SansSerif", Font.PLAIN, 14)); + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.CENTER, 15, 15)); + panel.add(label); + add("Center", panel); + button = new Button(" OK "); + button.addActionListener(this); + button.addKeyListener(this); + panel = new Panel(); + panel.setLayout(new FlowLayout()); + panel.add(button); + add("South", panel); + if (ij.IJ.isMacintosh()) + setResizable(false); + pack(); + GUI.centerOnImageJScreen(this); + addWindowListener(this); + show(); + } + + public void actionPerformed(ActionEvent e) { + dispose(); + } + + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyDown(keyCode); + escapePressed = keyCode==KeyEvent.VK_ESCAPE; + if (keyCode==KeyEvent.VK_ENTER || escapePressed) + dispose(); + } + + public void keyReleased(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyUp(keyCode); + } + + public void keyTyped(KeyEvent e) {} + + public void windowClosing(WindowEvent e) { + dispose(); + } + + public boolean escapePressed() { + return escapePressed; + } + + public void windowActivated(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + +} diff --git a/src/ij/gui/MultiLineLabel.java b/src/ij/gui/MultiLineLabel.java new file mode 100644 index 0000000..054b3ad --- /dev/null +++ b/src/ij/gui/MultiLineLabel.java @@ -0,0 +1,108 @@ +package ij.gui; +import java.awt.*; +import java.util.*; + +/**Custom component for displaying multiple lines. Based on + MultiLineLabel class from "Java in a Nutshell" by David Flanagan.*/ +public class MultiLineLabel extends Canvas { + String[] lines; + int num_lines; + int margin_width = 6; + int margin_height = 6; + int line_height; + int line_ascent; + int[] line_widths; + int min_width, max_width; + + // Breaks the specified label up into an array of lines. + public MultiLineLabel(String label) { + init(label); + } + + public MultiLineLabel(String label, int minimumWidth) { + init(label); + min_width = minimumWidth; + } + + private void init(String text) { + StringTokenizer t = new StringTokenizer(text, "\n"); + num_lines = t.countTokens(); + lines = new String[num_lines]; + line_widths = new int[num_lines]; + for (int i=0; i max_width) max_width = line_widths[i]; + } + } + + + public void setText(String text) { + init(text); + measure(); + repaint(); + } + + public void setFont(Font f) { + super.setFont(f); + measure(); + repaint(); + } + + + // This method is invoked after our Canvas is first created + // but before it can actually be displayed. After we've + // invoked our superclass's addNotify() method, we have font + // metrics and can successfully call measure() to figure out + // how big the label is. + public void addNotify() { + super.addNotify(); + measure(); + } + + + // Called by a layout manager when it wants to + // know how big we'd like to be. + public Dimension getPreferredSize() { + return new Dimension(Math.max(min_width, max_width + 2*margin_width), + num_lines * line_height + 2*margin_height); + } + + + // Called when the layout manager wants to know + // the bare minimum amount of space we need to get by. + public Dimension getMinimumSize() { + return new Dimension(Math.max(min_width, max_width), num_lines * line_height); + } + + // Draws the label + public void paint(Graphics g) { + int x, y; + Dimension d = this.getSize(); + if (!ij.IJ.isLinux()) setAntialiasedText(g); + y = line_ascent + (d.height - num_lines * line_height)/2; + for(int i = 0; i < num_lines; i++, y += line_height) { + x = margin_width; + g.drawString(lines[i], x, y); + } + } + + void setAntialiasedText(Graphics g) { + Graphics2D g2d = (Graphics2D)g; + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + } + +} diff --git a/src/ij/gui/NewImage.java b/src/ij/gui/NewImage.java new file mode 100644 index 0000000..e14e15c --- /dev/null +++ b/src/ij/gui/NewImage.java @@ -0,0 +1,466 @@ +package ij.gui; + +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.awt.event.*; +import java.util.*; +import ij.*; +import ij.process.*; + +/** New image dialog box plus several static utility methods for creating images.*/ +public class NewImage { + + public static final int GRAY8=0, GRAY16=1, GRAY32=2, RGB=3; + public static final int FILL_BLACK=1, FILL_RAMP=2, FILL_NOISE=3, FILL_RANDOM=3, + FILL_WHITE=4, CHECK_AVAILABLE_MEMORY=8, SIGNED_INT=16; + private static final int OLD_FILL_WHITE=0; + + static final String TYPE = "new.type"; + static final String FILL = "new.fill"; + static final String WIDTH = "new.width"; + static final String HEIGHT = "new.height"; + static final String SLICES = "new.slices"; + + private static String name = "Untitled"; + private static int staticWidth = Prefs.getInt(WIDTH, 512); + private static int staticHeight = Prefs.getInt(HEIGHT, 512); + private static int staticSlices = Prefs.getInt(SLICES, 1); + private static int staticType = Prefs.getInt(TYPE, GRAY8); + private static int staticFillWith = Prefs.getInt(FILL, FILL_BLACK); + private static String[] types = {"8-bit", "16-bit", "32-bit", "RGB"}; + private static String[] fill = {"White", "Black", "Ramp", "Noise"}; + private int gwidth, gheight, gslices, gtype, gfill; + + public NewImage() { + openImage(); + } + + static boolean createStack(ImagePlus imp, ImageProcessor ip, int nSlices, int type, int options) { + int fill = getFill(options); + int width = imp.getWidth(); + int height = imp.getHeight(); + long bytesPerPixel = 1; + if (type==GRAY16) bytesPerPixel = 2; + else if (type==GRAY32||type==RGB) bytesPerPixel = 4; + long size = (long)width*height*nSlices*bytesPerPixel; + int sizeThreshold = fill==FILL_NOISE?10:250; + boolean bigStack = size/(1024*1024)>=sizeThreshold; + String size2 = size/(1024*1024)+"MB ("+width+"x"+height+"x"+nSlices+")"; + if ((options&CHECK_AVAILABLE_MEMORY)!=0) { + long max = IJ.maxMemory(); // - 100*1024*1024; + if (max>0) { + long inUse = IJ.currentMemory(); + long available = max - inUse; + if (size>available) + System.gc(); + inUse = IJ.currentMemory(); + available = max-inUse; + if (size>available) { + IJ.error("Insufficient Memory", "There is not enough free memory to allocate a \n" + + size2+" stack.\n \n" + + "Memory available: "+available/(1024*1024)+"MB\n" + + "Memory in use: "+IJ.freeMemory()+"\n \n" + + "More information can be found in the \"Memory\"\n" + + "sections of the ImageJ installation notes at\n" + + "\""+IJ.URL+"/docs/install/\"."); + return false; + } + } + } + ImageStack stack = imp.createEmptyStack(); + boolean signedInt = (options&SIGNED_INT)!=0; + if (type==RGB && signedInt) + stack.setOptions("32-bit int"); + int inc = nSlices/40; + if (inc<1) inc = 1; + if (bigStack) + IJ.showStatus("Allocating "+size2+". Press 'Esc' to abort."); + IJ.resetEscape(); + try { + stack.addSlice(null, ip); + for (int i=2; i<=nSlices; i++) { + if ((i%inc)==0 && bigStack) + IJ.showProgress(i, nSlices); + Object pixels2 = null; + switch (type) { + case GRAY8: pixels2 = new byte[width*height]; + if (fill==FILL_NOISE) + fillNoiseByte(new ByteProcessor(width,height,(byte[])pixels2)); + break; + case GRAY16: pixels2 = new short[width*height]; + if (fill==FILL_NOISE) + fillNoiseShort(new ShortProcessor(width,height,(short[])pixels2,null)); + break; + case GRAY32: pixels2 = new float[width*height]; + if (fill==FILL_NOISE) + fillNoiseFloat(new FloatProcessor(width,height,(float[])pixels2,null)); + break; + case RGB: pixels2 = new int[width*height]; + if (fill==FILL_NOISE) { + if (signedInt) + fillNoiseInt(new IntProcessor(width,height,(int[])pixels2)); + else + fillNoiseRGB(new ColorProcessor(width,height,(int[])pixels2), false); + } + break; + } + if (signedInt && (fill==FILL_WHITE||fill==FILL_RAMP) || ((type==RGB)&&(fill!=FILL_NOISE))) + System.arraycopy(ip.getPixels(), 0, pixels2, 0, width*height); + stack.addSlice(null, pixels2); + if (IJ.escapePressed()) {IJ.beep(); break;}; + } + } + catch(OutOfMemoryError e) { + IJ.outOfMemory(imp.getTitle()); + stack.trim(); + } + IJ.showStatus(""); + if (bigStack) + IJ.showProgress(nSlices, nSlices); + if (stack.size()>1) + imp.setStack(null, stack); + return true; + } + + static int getFill(int options) { + int fill = options&7; + if (fill==OLD_FILL_WHITE) + fill = FILL_WHITE; + if (fill==7||fill==6||fill==5) + fill = FILL_BLACK; + return fill; + } + + public static ImagePlus createByteImage(String title, int width, int height, int slices, int options) { + int fill = getFill(options); + int size = getSize(width, height); + if (size<0) return null; + byte[] pixels = new byte[size]; + ImageProcessor ip = new ByteProcessor(width, height, pixels, null); + switch (fill) { + case FILL_WHITE: + for (int i=0; i1) { + boolean ok = createStack(imp, ip, slices, GRAY8, options); + if (!ok) imp = null; + } + return imp; + } + + private static void fillNoiseByte(ImageProcessor ip) { + ip.add(127); + ip.noise(31); + } + + public static ImagePlus createRGBImage(String title, int width, int height, int slices, int options) { + int fill = getFill(options); + int size = getSize(width, height); + if (size<0) return null; + int[] pixels = new int[size]; + ColorProcessor ip = new ColorProcessor(width, height, pixels); + switch (fill) { + case FILL_WHITE: + for (int i=0; i1) { + boolean ok = createStack(imp, ip, slices, RGB, options); + if (!ok) imp = null; + } + return imp; + } + + public static ImagePlus createIntImage(String title, int width, int height, int slices, int options) { + int fill = getFill(options); + int size = getSize(width, height); + if (size<0) return null; + int[] pixels = new int[size]; + IntProcessor ip = new IntProcessor(width, height, pixels); + switch (fill) { + case FILL_RAMP: + int[] ramp = new int[width]; + double inc = ((double)Integer.MAX_VALUE - (double)Integer.MIN_VALUE)/width; + for (int i=0; i1) { + boolean ok = createStack(imp, ip, slices, RGB, options); + if (!ok) imp = null; + } + return imp; + } + + private static void fillNoiseRGB(ColorProcessor ip, boolean sp) { + int width = ip.getWidth(); + int height = ip.getHeight(); + ByteProcessor rr = new ByteProcessor(width, height); + ByteProcessor gg = new ByteProcessor(width, height); + ByteProcessor bb = new ByteProcessor(width, height); + if (sp) IJ.showProgress(0.0); + rr.add(127); if (sp) IJ.showProgress(0.05); + gg.add(127); if (sp) IJ.showProgress(0.10); + bb.add(127); if (sp) IJ.showProgress(0.15); + rr.noise(31); if (sp) IJ.showProgress(0.40); + gg.noise(31); if (sp) IJ.showProgress(0.65); + bb.noise(31); if (sp) IJ.showProgress(0.90); + if (sp) IJ.showProgress(1.0); + ip.setChannel(1,rr); ip.setChannel(2,gg); ip.setChannel(3,bb); + } + + private static void fillNoiseInt(ImageProcessor ip) { + Random rnd = new Random(); + int n = ip.getPixelCount(); + double std =((double)Integer.MAX_VALUE - (double)Integer.MIN_VALUE)*0.12; + for (int i=0; i1) { + boolean ok = createStack(imp, ip, slices, GRAY16, options); + if (!ok) imp = null; + } + imp.getProcessor().setMinAndMax(0, 65535); // default display range + return imp; + } + + private static void fillNoiseShort(ImageProcessor ip) { + ip.add(32767); + ip.noise(7940); + } + + /** + * @deprecated + * Short images are always unsigned. + */ + public static ImagePlus createUnsignedShortImage(String title, int width, int height, int slices, int options) { + return createShortImage(title, width, height, slices, options); + } + + public static ImagePlus createFloatImage(String title, int width, int height, int slices, int options) { + int fill = getFill(options); + int size = getSize(width, height); + if (size<0) return null; + float[] pixels = new float[size]; + ImageProcessor ip = new FloatProcessor(width, height, pixels, null); + switch (fill) { + case FILL_WHITE: case FILL_BLACK: + break; + case FILL_RAMP: + float[] ramp = new float[width]; + for (int i=0; i1) { + boolean ok = createStack(imp, ip, slices, GRAY32, options); + if (!ok) imp = null; + } + if (fill!=FILL_NOISE) + imp.getProcessor().setMinAndMax(0.0, 1.0); // default display range + return imp; + } + + private static void fillNoiseFloat(ImageProcessor ip) { + ip.noise(1); + } + + private static int getSize(int width, int height) { + long size = (long)width*height; + if (size>Integer.MAX_VALUE) { + IJ.error("Image is too large. ImageJ does not support\nsingle images larger than 2 gigapixels."); + return -1; + } else + return (int)size; + } + + public static void open(String title, int width, int height, int nSlices, int type, int options) { + int bitDepth = 8; + if (type==GRAY16) bitDepth = 16; + else if (type==GRAY32) bitDepth = 32; + else if (type==RGB) bitDepth = 24; + long startTime = System.currentTimeMillis(); + ImagePlus imp = createImage(title, width, height, nSlices, bitDepth, options); + if (imp!=null) { + WindowManager.checkForDuplicateName = true; + imp.show(); + IJ.showStatus(IJ.d2s(((System.currentTimeMillis()-startTime)/1000.0),2)+" seconds"); + } + } + + public static ImagePlus createImage(String title, int width, int height, int nSlices, int bitDepth, int options) { + ImagePlus imp = null; + switch (bitDepth) { + case 8: imp = createByteImage(title, width, height, nSlices, options); break; + case 16: imp = createShortImage(title, width, height, nSlices, options); break; + case 32: imp = createFloatImage(title, width, height, nSlices, options); break; + case 24: + if ((options&SIGNED_INT)!=0) + imp = createIntImage(title, width, height, nSlices, options); + else + imp = createRGBImage(title, width, height, nSlices, options); + break; + default: throw new IllegalArgumentException("Invalid bitDepth: "+bitDepth); + } + return imp; + } + + boolean showDialog() { + if (staticTypeRGB) + staticType = GRAY8; + if (staticFillWithFILL_NOISE) + staticFillWith = FILL_WHITE; + GenericDialog gd = new GenericDialog("New Image..."); + gd.addStringField("Name:", name, 12); + gd.addChoice("Type:", types, types[staticType]); + gd.addChoice("Fill with:", fill, fill[staticFillWith]); + gd.addNumericField("Width:", staticWidth, 0, 5, "pixels"); + gd.addNumericField("Height:", staticHeight, 0, 5, "pixels"); + gd.addNumericField("Slices:", staticSlices, 0, 5, ""); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + name = gd.getNextString(); + String s = gd.getNextChoice(); + if (s.startsWith("8")) + gtype = GRAY8; + else if (s.startsWith("16")) + gtype = GRAY16; + else if (s.endsWith("RGB") || s.endsWith("rgb")) + gtype = RGB; + else + gtype = GRAY32; + gfill = gd.getNextChoiceIndex(); + gwidth = (int)gd.getNextNumber(); + gheight = (int)gd.getNextNumber(); + gslices = (int)gd.getNextNumber(); + if (gslices<1) gslices = 1; + if (gwidth<1 || gheight<1) { + IJ.error("New Image", "Width and height must be >0"); + return false; + } else { + if (!IJ.isMacro()) { + staticWidth = gwidth; + staticHeight = gheight; + staticSlices = gslices; + staticType = gtype; + staticFillWith = gfill; + } + return true; + } + } + + void openImage() { + if (!showDialog()) + return; + try { + open(name, gwidth, gheight, gslices, gtype, gfill); + } catch(OutOfMemoryError e) { + IJ.outOfMemory("New Image..."); + } + } + + /** Called when ImageJ quits. */ + public static void savePreferences(Properties prefs) { + prefs.put(TYPE, Integer.toString(staticType)); + prefs.put(FILL, Integer.toString(staticFillWith)); + prefs.put(WIDTH, Integer.toString(staticWidth)); + prefs.put(HEIGHT, Integer.toString(staticHeight)); + prefs.put(SLICES, Integer.toString(staticSlices)); + } + +} diff --git a/src/ij/gui/NonBlockingGenericDialog.java b/src/ij/gui/NonBlockingGenericDialog.java new file mode 100644 index 0000000..55dc6b3 --- /dev/null +++ b/src/ij/gui/NonBlockingGenericDialog.java @@ -0,0 +1,102 @@ +package ij.gui; +import ij.*; +import java.awt.event.*; +import java.awt.EventQueue; +import java.awt.GraphicsEnvironment; +import java.awt.Frame; + +/** This is an extension of GenericDialog that is non-modal. + * @author Johannes Schindelin + */ +public class NonBlockingGenericDialog extends GenericDialog { + ImagePlus imp; //when non-null, this dialog gets closed when the image is closed + WindowListener windowListener; //checking for whether the associated window gets closed + + public NonBlockingGenericDialog(String title) { + super(title, null); + setModal(false); + IJ.protectStatusBar(false); + instance = this; + } + + public synchronized void showDialog() { + super.showDialog(); + if (isMacro()) + return; + if (!IJ.macroRunning()) { // add to Window menu on event dispatch thread + final NonBlockingGenericDialog thisDialog = this; + EventQueue.invokeLater(new Runnable() { + public void run() { + WindowManager.addWindow(thisDialog); + } + }); + } + if (imp != null) { + ImageWindow win = imp.getWindow(); + if (win != null) { //when the associated image closes, also close the dialog + final NonBlockingGenericDialog gd = this; + windowListener = new WindowAdapter() { + public void windowClosed(WindowEvent e) { + cancelDialogAndClose(); + } + }; + win.addWindowListener(windowListener); + } + } + try { + wait(); + } catch (InterruptedException e) { } + } + + /** Gets called if the associated image window is closed */ + private void cancelDialogAndClose() { + super.windowClosing(null); // sets wasCanceled=true and does dispose() + } + + public synchronized void actionPerformed(ActionEvent e) { + super.actionPerformed(e); + if (!isVisible()) + notify(); + } + + public synchronized void keyPressed(KeyEvent e) { + super.keyPressed(e); + if (wasOKed() || wasCanceled()) + notify(); + } + + public synchronized void windowClosing(WindowEvent e) { + super.windowClosing(e); + if (wasOKed() || wasCanceled()) + notify(); + } + + public void dispose() { + super.dispose(); + WindowManager.removeWindow(this); + if (imp != null) { + ImageWindow win = imp.getWindow(); + if (win != null && windowListener != null) + win.removeWindowListener(windowListener); + } + } + + /** Obsolete, replaced by GUI.newNonBlockingDialog(String,ImagePlus). */ + public static GenericDialog newDialog(String title, ImagePlus imp) { + return GUI.newNonBlockingDialog(title, imp); + } + + /** Obsolete, replaced by GUI.newNonBlockingDialog(String). */ + public static GenericDialog newDialog(String title) { + return GUI.newNonBlockingDialog(title); + } + + /** Put the dialog into the foreground when the image we work on gets into the foreground */ + @Override + public void windowActivated(WindowEvent e) { + if ((e.getWindow() instanceof ImageWindow) && e.getOppositeWindow()!=this) + toFront(); + WindowManager.setWindow(this); + } + +} diff --git a/src/ij/gui/OvalRoi.java b/src/ij/gui/OvalRoi.java new file mode 100644 index 0000000..7923313 --- /dev/null +++ b/src/ij/gui/OvalRoi.java @@ -0,0 +1,430 @@ +package ij.gui; +import java.awt.*; +import java.awt.image.*; +import java.awt.geom.*; +import ij.*; +import ij.process.*; +import ij.measure.Calibration; + +/** Oval region of interest */ +public class OvalRoi extends Roi { + + /** Creates an OvalRoi.*/ + public OvalRoi(int x, int y, int width, int height) { + super(x, y, width, height); + type = OVAL; + } + + /** Creates an OvalRoi using double arguments.*/ + public OvalRoi(double x, double y, double width, double height) { + super(x, y, width, height); + type = OVAL; + } + + /** Creates an OvalRoi. */ + public static OvalRoi create(double x, double y, double width, double height) { + return new OvalRoi(x, y, width, height); + } + + /** Starts the process of creating a user-defined OvalRoi. */ + public OvalRoi(int x, int y, ImagePlus imp) { + super(x, y, imp); + type = OVAL; + } + + /** @deprecated */ + public OvalRoi(int x, int y, int width, int height, ImagePlus imp) { + this(x, y, width, height); + setImage(imp); + } + + /** Feret (caliper width) values, see ij.gui.Roi.getFeretValues(). + * The superclass method of calculating this via the convex hull is less accurate for the MinFeret + * because it does not get the exact minor axis. */ + public double[] getFeretValues() { + double[] a = new double[FERET_ARRAYSIZE]; + double pw=1.0, ph=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } + boolean highAspect = ph*height > pw*width; + a[0] = highAspect ? height*ph : width*pw; // (max)Feret + a[1] = highAspect ? 90.0 : 0.0; // (max)Feret angle + a[2] = highAspect ? width*pw : height*ph; // MinFeret + a[3] = (x + (highAspect ? 0.5*width : 0)) * pw; //FeretX scaled + a[4] = (y + (highAspect ? height : 0.5*height)) * ph;//FeretY scaled + int i = FERET_ARRAY_POINTOFFSET; + a[i++] = x + (highAspect ? 0.5*width : 0); //MaxFeret start + a[i++] = y + (highAspect ? height : 0.5*height); + a[i++] = x + (highAspect ? 0.5*width : width); //MaxFeret end + a[i++] = y + (highAspect ? 0 : 0.5*height); + a[i++] = x + (highAspect ? 0 : 0.5*width); //MinFeret start + a[i++] = y + (highAspect ? 0.5*height : height); + a[i++] = x + (highAspect ? width : 0.5*width); //MinFeret end + a[i++] = y + (highAspect ? 0.5*height : 0); + return a; + } + + protected void moveHandle(int sx, int sy) { + double asp; + if (clipboard!=null) return; + int ox = offScreenX(sx); + int oy = offScreenY(sy); + //IJ.log("moveHandle: "+activeHandle+" "+ox+" "+oy); + int x1=x, y1=y, x2=x+width, y2=y+height, xc=x+width/2, yc=y+height/2; + int w2 = (int)(0.14645*width); + int h2 = (int)(0.14645*height); + if (width > 7 && height > 7) { + asp = (double)width/(double)height; + asp_bk = asp; + } else + asp = asp_bk; + switch (activeHandle) { + case 0: x=ox-w2; y=oy-h2; break; + case 1: y=oy; break; + case 2: x2=ox+w2; y=oy-h2; break; + case 3: x2=ox; break; + case 4: x2=ox+w2; y2=oy+h2; break; + case 5: y2=oy; break; + case 6: x=ox-w2; y2=oy+h2; break; + case 7: x=ox; break; + } + //if (x<0) x=0; if (y<0) y=0; + if (x=x2) { + width=1; + x=x2=xc; + } + if (y>=y2) { + height=1; + y=y2=yc; + } + + } + + if (constrain) { + if (activeHandle==1 || activeHandle==5) width=height; + else height=width; + + if (x>=x2) { + width=1; + x=x2=xc; + } + if (y>=y2) { + height=1; + y=y2=yc; + } + switch(activeHandle){ + case 0: + x=x2-width; + y=y2-height; + break; + case 1: + x=xc-width/2; + y=y2-height; + break; + case 2: + y=y2-height; + break; + case 3: + y=yc-height/2; + break; + case 5: + x=xc-width/2; + break; + case 6: + x=x2-width; + break; + case 7: + y=yc-height/2; + x=x2-width; + break; + } + if (center){ + x=xc-width/2; + y=yc-height/2; + } + } + + if (aspect && !constrain) { + if (activeHandle==1 || activeHandle==5) width=(int)Math.rint((double)height*asp); + else height=(int)Math.rint((double)width/asp); + + switch (activeHandle) { + case 0: + x=x2-width; + y=y2-height; + break; + case 1: + x=xc-width/2; + y=y2-height; + break; + case 2: + y=y2-height; + break; + case 3: + y=yc-height/2; + break; + case 5: + x=xc-width/2; + break; + case 6: + x=x2-width; + break; + case 7: + y=yc-height/2; + x=x2-width; + break; + } + if (center) { + x=xc-width/2; + y=yc-height/2; + } + // Attempt to preserve aspect ratio when roi very small: + if (width<8) { + if (width<1) width = 1; + height=(int)Math.rint((double)width/asp_bk); + } + if (height<8) { + if (height<1) height =1; + width=(int)Math.rint((double)height*asp_bk); + } + } + + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX=x; oldY=y; + oldWidth=width; oldHeight=height; + cachedMask = null; + bounds = null; + } + + public void draw(Graphics g) { + Color color = strokeColor!=null? strokeColor:ROIColor; + if (fillColor!=null) color = fillColor; + g.setColor(color); + mag = getMagnification(); + int sw = (int)(width*mag); + int sh = (int)(height*mag); + int sx1 = screenX(x); + int sy1 = screenY(y); + if (subPixelResolution() && bounds!=null) { + sw = (int)(bounds.width*mag); + sh = (int)(bounds.height*mag); + sx1 = screenXD(bounds.x); + sy1 = screenYD(bounds.y); + } + int sw2 = (int)(0.14645*width*mag); + int sh2 = (int)(0.14645*height*mag); + int sx2 = sx1+sw/2; + int sy2 = sy1+sh/2; + int sx3 = sx1+sw; + int sy3 = sy1+sh; + Graphics2D g2d = (Graphics2D)g; + if (stroke!=null) + g2d.setStroke(getScaledStroke()); + setRenderingHint(g2d); + if (fillColor!=null) { + if (!overlay && isActiveOverlayRoi()) { + g.setColor(Color.cyan); + g.drawOval(sx1, sy1, sw, sh); + } else + g.fillOval(sx1, sy1, sw, sh); + } else + g.drawOval(sx1, sy1, sw, sh); + if (clipboard==null && !overlay) { + drawHandle(g, sx1+sw2, sy1+sh2); + drawHandle(g, sx3-sw2, sy1+sh2); + drawHandle(g, sx3-sw2, sy3-sh2); + drawHandle(g, sx1+sw2, sy3-sh2); + drawHandle(g, sx2, sy1); + drawHandle(g, sx3, sy2); + drawHandle(g, sx2, sy3); + drawHandle(g, sx1, sy2); + } + drawPreviousRoi(g); + if (updateFullWindow) + {updateFullWindow = false; imp.draw();} + if (state!=NORMAL) showStatus(); + } + + /** Draws an outline of this OvalRoi on the image. */ + public void drawPixels(ImageProcessor ip) { + Polygon p = getPolygon(); + if (p.npoints>0) { + int saveWidth = ip.getLineWidth(); + if (getStrokeWidth()>1f) + ip.setLineWidth((int)Math.round(getStrokeWidth())); + ip.drawPolygon(p); + ip.setLineWidth(saveWidth); + } + if (Line.getWidth()>1 || getStrokeWidth()>1) + updateFullWindow = true; + } + + /** Returns this OvalRoi as a Polygon that outlines the mask, in image pixel coordinates. */ + public Polygon getPolygon() { + return getPolygon(true); + } + + /** Returns this OvalRoi as a Polygon that outlines the mask. + * @param absoluteCoordinates determines whether to use image pixel coordinates + * instead of coordinates relative to roi origin. */ + Polygon getPolygon(boolean absoluteCoordinates) { + ImageProcessor mask = getMask(); + Wand wand = new Wand(mask); + wand.autoOutline(width/2,height/2, 255, 255); + if (absoluteCoordinates) + for (int i=0; i=sx1+sw2&&sx<=sx1+sw2+size&&sy>=sy1+sh2&&sy<=sy1+sh2+size) return 0; + if (sx>=sx2&&sx<=sx2+size&&sy>=sy1&&sy<=sy1+size) return 1; + if (sx>=sx3-sw2&&sx<=sx3-sw2+size&&sy>=sy1+sh2&&sy<=sy1+sh2+size) return 2; + if (sx>=sx3&&sx<=sx3+size&&sy>=sy2&&sy<=sy2+size) return 3; + if (sx>=sx3-sw2&&sx<=sx3-sw2+size&&sy>=sy3-sh2&&sy<=sy3-sh2+size) return 4; + if (sx>=sx2&&sx<=sx2+size&&sy>=sy3&&sy<=sy3+size) return 5; + if (sx>=sx1+sw2&&sx<=sx1+sw2+size&&sy>=sy3-sh2&&sy<=sy3-sh2+size) return 6; + if (sx>=sx1&&sx<=sx1+size&&sy>=sy2&&sy<=sy2+size) return 7; + return -1; + } + + public ImageProcessor getMask() { + ImageProcessor mask = cachedMask; + if (mask!=null && mask.getPixels()!=null && mask.getWidth()==width && mask.getHeight()==height) + return mask; + mask = new ByteProcessor(width, height); + double a=width/2.0, b=height/2.0; + double a2=a*a, b2=b*b; + a -= 0.5; b -= 0.5; + double xx, yy; + int offset; + byte[] pixels = (byte[])mask.getPixels(); + for (int y=0; y { + private Vector list; + private boolean label; + private boolean drawNames; + private boolean drawBackgrounds; + private Color labelColor; + private Font labelFont; + private boolean scalableLabels; + private boolean isCalibrationBar; + private boolean selectable = true; + private boolean draggable = true; + + /** Constructs an empty Overlay. */ + public Overlay() { + list = new Vector(); + } + + /** Constructs an Overlay and adds the specified ROI. */ + public Overlay(Roi roi) { + list = new Vector(); + if (roi!=null) + list.add(roi); + } + + /** Adds an ROI to this Overlay. */ + public void add(Roi roi) { + if (roi!=null) + list.add(roi); + } + + /** Adds an ROI to this Overlay using the specified name. */ + public void add(Roi roi, String name) { + roi.setName(name); + add(roi); + } + + /** Adds an ROI to this Overlay. */ + public void addElement(Roi roi) { + if (roi!=null) + list.add(roi); + } + + /** Replaces the ROI at the specified index. */ + public void set(Roi roi, int index) { + if (index<0 || index>=list.size()) + throw new IllegalArgumentException("set: index out of range"); + if (roi!=null) + list.set(index, roi); + } + + /** Removes the ROI with the specified index from this Overlay. */ + public void remove(int index) { + if (index>=0) + list.remove(index); + } + + /** Removes the specified ROI from this Overlay. */ + public void remove(Roi roi) { + list.remove(roi); + } + + /** Removes all ROIs that have the specified name. */ + public void remove(String name) { + if (name==null) return; + for (int i=size()-1; i>=0; i--) { + if (name.equals(get(i).getName())) + remove(i); + } + } + + /** Removes all the ROIs in this Overlay. */ + public void clear() { + list.clear(); + } + + /** Returns the ROI with the specified index or null if the index is invalid. */ + public Roi get(int index) { + try { + return (Roi)list.get(index); + } catch(Exception e) { + return null; + } + } + + /** Returns the ROI with the specified name or null if not found. */ + public Roi get(String name) { + int index = getIndex(name); + if (index==-1) + return null; + else + return get(index); + } + + /** Returns the index of the ROI with the specified name, or -1 if not found. */ + public int getIndex(String name) { + if (name==null) return -1; + Roi[] rois = toArray(); + for (int i=rois.length-1; i>=0; i--) { + if (name.equals(rois[i].getName())) + return i; + } + return -1; + } + + /** Returns the index of the last ROI that contains the point (x,y) + or null if no ROI contains the point. */ + public int indexAt(int x, int y) { + Roi[] rois = toArray(); + for (int i=rois.length-1; i>=0; i--) { + if (contains(rois[i],x,y)) + return i; + } + return -1; + } + + private boolean contains(Roi roi, int x, int y) { + if (roi==null) return false; + if (roi instanceof Line) + return (((Line)roi).getFloatPolygon(10)).contains(x,y); + else + return roi.contains(x,y); + } + + /** Returns 'true' if this Overlay contains the specified ROI. */ + public boolean contains(Roi roi) { + return list.contains(roi); + } + + /** Returns the number of ROIs in this Overlay. */ + public int size() { + return list.size(); + } + + /** Returns on array containing the ROIs in this Overlay. */ + public Roi[] toArray() { + Roi[] array = new Roi[list.size()]; + return (Roi[])list.toArray(array); + } + + /** Returns on array containing the ROIs with the specified indexes. */ + public Roi[] toArray(int[] indexes) { + ArrayList rois = new ArrayList(); + for (int i=0; i=0 && indexes[i]0?bounds.x:0; + int dy = bounds.y>0?bounds.y:0; + if (dx>0 || dy>0) + overlay2.translate(-dx, -dy); + return overlay2; + } + + /** Removes ROIs having positions outside of the + * interval defined by firstSlice and lastSlice. + * Marcel Boeglin, September 2013 + */ + public void crop(int firstSlice, int lastSlice) { + for (int i=size()-1; i>=0; i--) { + Roi roi = get(i); + int position = roi.getPosition(); + if (position>0) { + if (positionlastSlice) + remove(i); + else + roi.setPosition(position-firstSlice+1); + } + } + } + + /** Removes ROIs having a C, Z or T coordinate outside the volume + * defined by firstC, lastC, firstZ, lastZ, firstT and lastT. + * Marcel Boeglin, September 2013 + */ + public void crop(int firstC, int lastC, int firstZ, int lastZ, int firstT, int lastT) { + int nc = lastC-firstC+1, nz = lastZ-firstZ+1, nt = lastT-firstT+1; + boolean toCStack = nz==1 && nt==1; + boolean toZStack = nt==1 && nc==1; + boolean toTStack = nc==1 && nz==1; + Roi roi; + int c, z, t, c2, z2, t2; + for (int i=size()-1; i>=0; i--) { + roi = get(i); + c = roi.getCPosition(); + z = roi.getZPosition(); + t = roi.getTPosition(); + c2 = c-firstC+1; + z2 = z-firstZ+1; + t2 = t-firstT+1; + if (toCStack) + roi.setPosition(c2); + else if (toZStack) + roi.setPosition(z2); + else if (toTStack) + roi.setPosition(t2); + else + roi.setPosition(c2, z2, t2); + if ((c2<1||c2>nc) && c>0 || (z2<1||z2>nz) && z>0 || (t2<1||t2>nt) && t>0) + remove(i); + } + } + + /** Returns the bounds of this overlay. */ + /* + public Rectangle getBounds() { + if (size()==0) + return new Rectangle(0,0,0,0); + int xmin = Integer.MAX_VALUE; + int xmax = -Integer.MAX_VALUE; + int ymin = Integer.MAX_VALUE; + int ymax = -Integer.MAX_VALUE; + Roi[] rois = toArray(); + for (int i=0; ixmax) xmax = r.x+r.width; + if (r.y+r.height>ymax) ymax = r.y+r.height; + } + return new Rectangle(xmin, ymin, xmax-xmin, ymax-ymin); + } + */ + + /* Returns the Roi that results from XORing all the ROIs + * in this overlay that have an index in the array ‘indexes’. + */ + public Roi xor(int[] indexes) { + return Roi.xor(toArray(indexes)); + } + + /** Returns a new Overlay that has the same properties as this one. */ + public Overlay create() { + Overlay overlay2 = new Overlay(); + overlay2.drawLabels(label); + overlay2.drawNames(drawNames); + overlay2.drawBackgrounds(drawBackgrounds); + overlay2.setLabelColor(labelColor); + overlay2.setLabelFont(labelFont, scalableLabels); + overlay2.setIsCalibrationBar(isCalibrationBar); + overlay2.selectable(selectable); + overlay2.setDraggable(draggable); + return overlay2; + } + + /** Returns a clone of this Overlay. */ + public Overlay duplicate() { + Roi[] rois = toArray(); + Overlay overlay2 = create(); + for (int i=0; i v) {list = v;} + + Vector getVector() {return list;} + + /** Set 'false' to prevent ROIs in this overlay from being activated + by clicking on their labels or by a long clicking. */ + public void selectable(boolean selectable) { + this.selectable = selectable; + } + + /** Returns 'true' if ROIs in this overlay can be activated + by clicking on their labels or by a long press. */ + public boolean isSelectable() { + return selectable; + } + + /** Set 'false' to prevent ROIs in this overlay from being dragged by their labels. */ + public void setDraggable(boolean draggable) { + this.draggable = draggable; + } + + /** Returns 'true' if ROIs in this overlay can be dragged by their labels. */ + public boolean isDraggable() { + return draggable; + } + + public boolean scalableLabels() { + return scalableLabels; + } + + public String toString() { + return "Overlay[size="+size()+" "+(scalableLabels?"scale":"")+" "+Colors.colorToString(getLabelColor())+"]"; + } + + /** Updates overlays created by the particle analyzer + after rows are deleted from the Results table. */ + public static void updateTableOverlay(ImagePlus imp, int first, int last, int tableSize) { + if (imp==null) + return; + Overlay overlay = imp.getOverlay(); + if (overlay==null) + return; + if (overlay.size()!=tableSize) + return; + if (first<0) + first = 0; + if (last>tableSize-1) + last = tableSize-1; + if (first>last) + return; + String name1 = overlay.get(0).getName(); + String name2 = overlay.get(overlay.size()-1).getName(); + if (!"1".equals(name1) || !(""+tableSize).equals(name2)) + return; + int count = last-first+1; + if (overlay.size()==count && !IJ.isMacro()) { + if (count==1 || IJ.showMessageWithCancel("ImageJ", "Delete "+overlay.size()+" element overlay? ")) + imp.setOverlay(null); + return; + } + for (int i=0; i iterator() { + final Overlay overlay = this; + + Iterator it = new Iterator() { + private int index = -1; + + /** Returns 'true' if next element exists. */ + @Override + public boolean hasNext() { + if (index+12 + private static final int LEGEND_PADDING = 4; //pixels around legend text etc + private static final int LEGEND_LINELENGTH = 20; //length of lines in legend + private static final int USUALLY_ENLARGE = 1, ALWAYS_ENLARGE = 2; //enlargeRange settings + private static final double RELATIVE_ARROWHEAD_SIZE = 0.2; //arrow heads have 1/5 of vector length + private static final int MIN_ARROWHEAD_LENGTH = 3; + private static final int MAX_ARROWHEAD_LENGTH = 20; + private static final String MULTIPLY_SYMBOL = "\u00B7"; //middot, default multiplication symbol for scientific notation. Use setOptions("msymbol=\\u00d7") for '×' + + PlotProperties pp = new PlotProperties(); //size, range, formatting etc, for easy serialization + PlotProperties ppSnapshot; //copy for reverting + Vector allPlotObjects = new Vector(); //all curves, labels etc., also serialized for saving/reading + Vector allPlotObjectsSnapshot; //copy for reverting + private PlotVirtualStack stack; + /** For high-resolution plots, everything will be scaled with this number. Otherwise, must be 1.0. + * (creating margins, saving PlotProperties etc only supports scale=1.0) */ + float scale = 1.0f; + Rectangle frame = null; //the clip frame, do not use for image scale + //The following are the margin sizes actually used. They are modified for font size and also scaled for high-resolution plots + int leftMargin = LEFT_MARGIN, rightMargin = RIGHT_MARGIN, topMargin = TOP_MARGIN, bottomMargin = BOTTOM_MARGIN; + int frameWidth; //width corresponding to plot range; frame.width is larger by 1 + int frameHeight; //height corresponding to plot range; frame.height is larger by 1 + int preferredPlotWidth = PlotWindow.plotWidth; //default size of plot frame (not taking 'High-Resolution' scale factor into account) + int preferredPlotHeight = PlotWindow.plotHeight; + + double xMin = Double.NaN, xMax, yMin, yMax; //current plot range, logarithm if log axis + double[] currentMinMax = new double[]{Double.NaN, 0, Double.NaN, 0}; //current plot range, xMin, xMax, yMin, yMax (values, not logarithm if log axis) + double[] defaultMinMax = new double[]{Double.NaN, 0, Double.NaN, 0}; //default plot range + double[] savedMinMax = new double[]{Double.NaN, 0, Double.NaN, 0}; //keeps previous range for revert + int[] enlargeRange; // whether to enlarge the range slightly to avoid values at the border (0=off, USUALLY_ENLARGE, ALWAYS_ENLARGE) + boolean logXAxis, logYAxis; // whether to really use log axis (never for small relative range) + //for passing on what should be kept when 'live' plotting (PlotMaker), but note that 'COPY_EXTRA_OBJECTS' is also on for live plotting: + int templateFlags = COPY_SIZE | COPY_LABELS | COPY_AXIS_STYLE | COPY_CONTENTS_STYLE | COPY_LEGEND; + private int dsize = PlotWindow.getDefaultFontSize(); + Font defaultFont = FontUtil.getFont("Arial",Font.PLAIN,dsize); //default font for labels, axis, etc. + Font currentFont = defaultFont; // font as changed by setFont or setFontSize, must never be null + private double xScale, yScale; // pixels per data unit + private int xBasePxl, yBasePxl; // pixel coordinates corresponding to 0 + private int maxIntervals = 12; // maximum number of intervals between ticks or grid lines + private int tickLength = 7; // length of major ticks + private int minorTickLength = 3; // length of minor ticks + private Color gridColor = new Color(0xc0c0c0); // light gray + private ImageProcessor ip; + private ImagePlus imp; // if we have an ImagePlus, updateAndDraw on changes + private String title; + private boolean invertedLut; // grayscale plots only, set in Edit>Options>Appearance + private boolean plotDrawn; + PlotMaker plotMaker; // for PlotMaker interface, handled by PlotWindow + private Color currentColor; // for next objects added + private Color currentColor2; // 2nd color for next object added (e.g. line for CONNECTED_CIRCLES) + float currentLineWidth; + private int currentJustification = LEFT; + private boolean ignoreForce2Grid; // after explicit setting of range (limits), ignore 'FORCE2GRID' flags + //private boolean snapToMinorGrid; // snap to grid when zooming to selection + private static double SEPARATED_BAR_WIDTH=0.5; // for plots with separate bars (e.g. categories), fraction of space, 0.1-1.0 + double[] steps; // x & y interval between numbers, major ticks & grid lines, remembered for redrawing the grid + private int objectToReplace = -1; // index in allPlotObjects, for replace + private Point2D.Double textLoc; // remembers position of previous addLabel call (replaces text if at the same position) + private int textIndex; // remembers index of previous addLabel call (for replacing if at the same position) + + /** Constructs a new Plot with the default options. + * Use add(shape,xvalues,yvalues) to add curves. + * @param title the window title + * @param xLabel the x-axis label; see setXYLabels for seting categories on an axis via the label + * @param yLabel the y-axis label; see setXYLabels for seting categories on an axis via the label + * @see #add(String,double[],double[]) + * @see #add(String,double[]) + */ + public Plot(String title, String xLabel, String yLabel) { + this(title, xLabel, yLabel, (float[])null, (float[])null, getDefaultFlags()); + } + + /** Obsolete, replaced by "new Plot(title,xLabel,yLabel); add(shape,x,y);". + * @deprecated + */ + public Plot(String title, String xLabel, String yLabel, float[] x, float[] y) { + this(title, xLabel, yLabel, x, y, getDefaultFlags()); + } + + /** Obsolete, replaced by "new Plot(title,xLabel,yLabel); add(shape,x,y);". + * @deprecated + */ + public Plot(String title, String xLabel, String yLabel, double[] x, double[] y) { + this(title, xLabel, yLabel, x!=null?Tools.toFloat(x):null, y!=null?Tools.toFloat(y):null, getDefaultFlags()); + } + + /** This version of the constructor has a 'flags' argument for + controlling whether ticks, grid, etc. are present and whether + the axes are logarithmic */ + public Plot(String title, String xLabel, String yLabel, int flags) { + this(title, xLabel, yLabel, (float[])null, (float[])null, flags); + } + + /** Obsolete, replaced by "new Plot(title,xLabel,yLabel,flags); add(shape,x,y);". + * @deprecated + */ + public Plot(String title, String xLabel, String yLabel, float[] xValues, float[] yValues, int flags) { + this.title = title; + pp.axisFlags = flags; + setXYLabels(xLabel, yLabel); + if (yValues != null && yValues.length>0) { + addPoints(xValues, yValues, /*yErrorBars=*/null, LINE, /*label=*/null); + allPlotObjects.get(0).flags = PlotObject.CONSTRUCTOR_DATA; + } + } + + /** Obsolete, replaced by "new Plot(title,xLabel,yLabel,flags); add(shape,x,y);". + * @deprecated + */ + public Plot(String title, String xLabel, String yLabel, double[] x, double[] y, int flags) { + this(title, xLabel, yLabel, x!=null?Tools.toFloat(x):null, y!=null?Tools.toFloat(y):null, flags); + } + + /** Constructs a new plot from an InputStream and closes the stream. If the ImagePlus is + * non-null, its title and ImageProcessor are used, but the image displayed is not modified. + */ + public Plot(ImagePlus imp, InputStream is) throws IOException, ClassNotFoundException { + ObjectInputStream in = new ObjectInputStream(is); + pp = (PlotProperties)in.readObject(); + allPlotObjects = (Vector)in.readObject(); + in.close(); + if (pp.xLabel.type==8) { + pp.xLabel.updateType(); //convert old (pre-1.52i) type codes for the PlotObjects + pp.yLabel.updateType(); + pp.frame.updateType(); + if (pp.legend != null) pp.legend.updateType(); + for (PlotObject plotObject : allPlotObjects) + plotObject.updateType(); + } + + defaultMinMax = pp.rangeMinMax; + currentFont = nonNullFont(pp.frame.getFont(), currentFont); // best guess in case we want to add a legend + getProcessor(); //prepares scale, calibration etc, but does not plot it yet + this.title = imp != null ? imp.getTitle() : "Untitled Plot"; + if (imp != null) { + this.imp = imp; + ip = imp.getProcessor(); + imp.setIgnoreGlobalCalibration(true); + adjustCalibration(imp.getCalibration()); + imp.setProperty(PROPERTY_KEY, this); + } + } + + /** Obsolete, replaced by "new Plot(title,xLabel,yLabel); add(shape,x,y);". + * @deprecated + */ + public Plot(String dummy, String title, String xLabel, String yLabel, float[] x, float[] y) { + this(title, xLabel, yLabel, x, y, getDefaultFlags()); + } + + /** Writes this plot into an OutputStream containing (1) the serialized PlotProperties and + * (2) the serialized Vector of all 'added' PlotObjects. The stream is NOT closed. + * The plot should have been drawn already. + */ + // Conversion to Streams can be also used to clone plots (not a shallow clone), but this is rather slow. + // Sample code: + // try { + // final PipedOutputStream pos = new PipedOutputStream(); + // final PipedInputStream pis = new PipedInputStream(pos); + // new Thread(new Runnable() { + // final public void run() { + // try { + // Plot p = new Plot(null, pis); + // pis.close(); + // pos.close(); + // p.show(); + // } catch(Exception e) {IJ.handleException(e);}; + // } + // }, "threadMakingPlotFromStream").start(); + // toStream(pos); + // } catch(Exception e) {IJ.handleException(e);} + void toStream(OutputStream os) throws IOException { + //prepare + for (PlotObject plotObject : pp.getAllPlotObjects()) //make sure all fonts are set properly + if (plotObject != null) + plotObject.setFont(nonNullFont(plotObject.getFont(), currentFont)); + pp.rangeMinMax = currentMinMax; + //write + ObjectOutputStream out = new ObjectOutputStream(os); + out.writeObject(pp); + out.writeObject(allPlotObjects); + } + + /** Writes this plot into a byte array containing (1) the serialized PlotProperties and + * (2) the serialized Vector of all 'added' PlotObjects. + * The plot should have been drawn already. Returns null on error (which should never happen). */ + public byte[] toByteArray() { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + toStream(bos); + bos.close(); + return bos.toByteArray(); + } catch (Exception e) { + IJ.handleException(e); + return null; + } + } + + /** Returns the title of the image showing the plot (if any) or title of the plot */ + public String getTitle() { + return imp == null ? title : imp.getTitle(); + } + + /** Sets the x-axis and y-axis range. Saves the new limits as default (so the 'R' field sets the limits to these). + * Updates the image if existing. + * Accepts NaN values to indicate auto-range. */ + public void setLimits(double xMin, double xMax, double yMin, double yMax) { + setLimitsNoUpdate(xMin, xMax, yMin, yMax); + makeLimitsDefault(); + ignoreForce2Grid = true; + if (plotDrawn) + setLimitsToDefaults(true); + } + + /** Sets the x-axis and y-axis range. Accepts NaN values to indicate auto range. + * Does not update the image and leaves the default limits (for reset via the 'R' field) untouched. */ + void setLimitsNoUpdate(double xMin, double xMax, double yMin, double yMax) { + boolean containsNaN = (Double.isNaN(xMin + xMax + yMin + yMax)); + if (containsNaN && getNumPlotObjects(PlotObject.XY_DATA|PlotObject.ARROWS, false)==0)//can't apply auto-range without data + return; + double[] range = {xMin, xMax, yMin, yMax}; + if (containsNaN) { //auto range for at least one limit + double[] extrema = getMinAndMax(true, ALL_AXES_RANGE); + boolean[] auto = new boolean[range.length]; + for (int i = 0; i < range.length; i++) + if (Double.isNaN(range[i])) { + auto[i] = true; + range[i] = extrema[i]; + } + for (int a = 0; aOptions>Plots. + * @see #setFrameSize(int,int) + * @see #setWindowSize(int,int) + */ + public void setSize(int width, int height) { + if (ip != null && width == ip.getWidth() && height == ip.getHeight()) + return; + Dimension minSize = getMinimumSize(); + pp.width = Math.max(width, minSize.width); + pp.height = Math.max(height, minSize.height); + scale = 1.0f; + ip = null; + if (plotDrawn) updateImage(); + } + + /** The size of the plot including borders with axis labels etc., in pixels */ + public Dimension getSize() { + if (ip == null) + getBlankProcessor(); + return new Dimension(ip.getWidth(), ip.getHeight()); + } + + /** Sets the plot frame size in (unscaled) pixels. This size does not include the + * borders with the axis labels. Also sets the scale to 1.0. + * This frame size in pixels divided by the data range defines the image scale. + * This method does not check for the minimum size MIN_FRAMEWIDTH, MIN_FRAMEHEIGHT. + * Note that the black frame will have an outer size that is one pixel larger + * (when plotted with a linewidth of one pixel). + * @see #setWindowSize(int,int) + */ + public void setFrameSize(int width, int height) { + if (pp.width <= 0) { //plot not drawn yet? Just remember as preferred size + preferredPlotWidth = width; + preferredPlotHeight = height; + scale = 1.0f; + } else { + makeMarginValues(); + width += leftMargin+rightMargin; + height += topMargin+bottomMargin; + setSize(width, height); + } + } + + /** Sets the plot window size in pixels. + * @see #setFrameSize(int,int) + */ + public void setWindowSize(int width, int height) { + scale = 1.0f; + makeMarginValues(); + int titleBarHeight = 22; + int infoHeight = 11; + double scale = Prefs.getGuiScale(); + if (scale>1.0) + infoHeight = (int)(infoHeight*scale); + int buttonPanelHeight = 45; + if (pp.width <= 0) { //plot not drawn yet? + int extraWidth = leftMargin+rightMargin+ImageWindow.HGAP*2; + int extraHeight = topMargin+bottomMargin+titleBarHeight+infoHeight+buttonPanelHeight; + if (extraWidthplot may be null; then the call has no effect. */ + public void useTemplate(Plot plot) { + useTemplate(plot, templateFlags); + } + + /** Adjusts the format (style) with another plot as a template. Flags determine what to + * copy from the template; these can be X_RANGE, Y_RANGE, COPY_SIZE, COPY_LABELS, COPY_AXIS_STYLE, + * COPY_CONTENTS_STYLE (hidden items are ignored), and COPY_LEGEND. + * plot may be null; then the call has no effect. */ + public void useTemplate(Plot plot, int templateFlags) { + if (plot == null) return; + this.defaultFont = plot.defaultFont; + this.currentFont = plot.currentFont; + this.currentLineWidth = plot.currentLineWidth; + this.currentColor = plot.currentColor; + if ((templateFlags & COPY_AXIS_STYLE) != 0) { + this.pp.axisFlags = plot.pp.axisFlags; + this.pp.frame = plot.pp.frame.deepClone(); + } + if ((templateFlags & COPY_LABELS) != 0) { + this.pp.xLabel.label = plot.pp.xLabel.label; + this.pp.yLabel.label = plot.pp.yLabel.label; + this.pp.xLabel.setFont(plot.pp.xLabel.getFont()); + this.pp.yLabel.setFont(plot.pp.yLabel.getFont()); + } + for (int i=0; i>(i/2)&0x1) != 0) { + currentMinMax[i] = plot.currentMinMax[i]; + if (!plotDrawn) defaultMinMax[i] = plot.currentMinMax[i]; + } + if ((templateFlags & COPY_LEGEND) != 0 && plot.pp.legend != null) + this.pp.legend = plot.pp.legend.deepClone(); + if ((templateFlags & (COPY_LEGEND | COPY_CONTENTS_STYLE)) != 0) { + int plotPObjectIndex = 0; //points to PlotObjects of the templatePlot + int plotPObjectsSize = plot.allPlotObjects.size(); + for (PlotObject plotObject : allPlotObjects) { + if (plotObject.type == PlotObject.XY_DATA && !plotObject.hasFlag(PlotObject.HIDDEN)) { + while(plotPObjectIndex=plotPObjectsSize) break; + if ((templateFlags & COPY_LEGEND) != 0) + plotObject.label = plot.allPlotObjects.get(plotPObjectIndex).label; + if ((templateFlags & COPY_CONTENTS_STYLE) != 0) + setPlotObjectStyle(plotObject, getPlotObjectStyle(plot.allPlotObjects.get(plotPObjectIndex))); + plotPObjectIndex++; + } + } + } + if ((templateFlags & COPY_SIZE) != 0) + setSize(plot.pp.width, plot.pp.height); + + if ((templateFlags & COPY_EXTRA_OBJECTS) != 0) + for (int p = allPlotObjects.size(); p < plot.allPlotObjects.size(); p++) + allPlotObjects.add(plot.allPlotObjects.get(p)); + this.templateFlags = templateFlags; + } + + /** Sets the scale. Everything, including labels, line thicknesses, etc will be scaled by this factor. + * Also multiplies the plot size by this value. Used for 'Create high-resolution plot'. + * Should be called before creating the plot. + * Note that plots with a scale different from 1.0 must not be shown in a PlotWindow, but only as + * simple image in a normal ImageWindow. */ + public void setScale(float scale) { + this.scale = scale; + if (scale > 20f) scale = 20f; + if (scale < 0.7f) scale = 0.7f; + pp.width = sc(pp.width); + pp.height = sc(pp.height); + plotDrawn = false; + } + + /** Sets the labels of the x and y axes. 'xLabel', 'yLabel' may be null. + * If a label has the form {txt1,txt2,txt3}, the corresponding axis will be labeled + * not by numbers but rather with the texts "txt1", "txt2" ... instead of 0, 1, ... + * In this special case, there will be no label for the axis on the plot. + * Call update() thereafter to make the change visible (if it is shown already). */ + public void setXYLabels(String xLabel, String yLabel) { + pp.xLabel.label = xLabel!=null ? xLabel : ""; + pp.yLabel.label = yLabel!=null ? yLabel : ""; + } + + /** Sets the maximum number of intervals in a plot. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setMaxIntervals(int intervals) { + maxIntervals = intervals; + } + + /** Sets the length of the major tick in pixels. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setTickLength(int tickLength) { + tickLength = tickLength; + } + + /** Sets the length of the minor tick in pixels. */ + public void setMinorTickLength(int minorTickLength) { + minorTickLength = minorTickLength; + } + + /** Sets the flags that control the axes format. + * Does not modify the flags for logarithmic axes on/off and the FORCE2GRID flags. + * Call update() thereafter to make the change visible (if it is shown already). */ + public void setFormatFlags(int flags) { + int unchangedFlags = X_LOG_NUMBERS | Y_LOG_NUMBERS | X_FORCE2GRID | Y_FORCE2GRID; + flags = flags & (~unchangedFlags); //remove flags that should not be affected + pp.axisFlags = (pp.axisFlags & unchangedFlags) | flags; + } + + /** Returns the flags that control the axes */ + public int getFlags() { + return pp.axisFlags; + } + + /** Sets the X Axis format to Log or Linear. + * Call update() thereafter to make the change visible (if it is shown already). */ + public void setAxisXLog(boolean axisXLog) { + pp.axisFlags = axisXLog ? pp.axisFlags | X_LOG_NUMBERS : pp.axisFlags & (~X_LOG_NUMBERS); + } + + /** Sets the Y Axis format to Log or Linear. + * Call update() thereafter to make the change visible (if it is shown already). */ + public void setAxisYLog(boolean axisYLog) { + pp.axisFlags = axisYLog ? pp.axisFlags | Y_LOG_NUMBERS : pp.axisFlags & (~Y_LOG_NUMBERS); + } + + /** Sets whether to show major ticks at the x axis. + * Call update() thereafter to make the change visible (if the image is shown already). */ + + public void setXTicks(boolean xTicks) { + pp.axisFlags = xTicks ? pp.axisFlags | X_TICKS : pp.axisFlags & (~X_TICKS); + } + + /** Sets whether to show major ticks at the y axis. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setYTicks(boolean yTicks) { + pp.axisFlags = yTicks ? pp.axisFlags | Y_TICKS : pp.axisFlags & (~Y_TICKS); + } + + /** Sets whether to show minor ticks on the x axis (if linear). Also sets major ticks if true and no grid is set. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setXMinorTicks(boolean xMinorTicks) { + pp.axisFlags = xMinorTicks ? pp.axisFlags | X_MINOR_TICKS : pp.axisFlags & (~X_MINOR_TICKS); + if (xMinorTicks && !hasFlag(X_GRID)) + pp.axisFlags |= X_TICKS; + } + + /** Sets whether to show minor ticks on the y axis (if linear). Also sets major ticks if true and no grid is set. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setYMinorTicks(boolean yMinorTicks) { + pp.axisFlags = yMinorTicks ? pp.axisFlags | Y_MINOR_TICKS : pp.axisFlags & (~Y_MINOR_TICKS); + if (yMinorTicks && !hasFlag(Y_GRID)) + pp.axisFlags |= Y_TICKS; + } + + /** Sets the properties of the axes. Call update() thereafter to make the change visible + * (if the image is shown already). */ + public void setAxes(boolean xLog, boolean yLog, boolean xTicks, boolean yTicks, boolean xMinorTicks, boolean yMinorTicks, + int tickLenght, int minorTickLenght) { + setAxisXLog (xLog); + setAxisYLog (yLog); + setXMinorTicks (xMinorTicks); + setYMinorTicks (yMinorTicks); + setXTicks (xTicks); + setYTicks (yTicks); + setTickLength (tickLenght); + setMinorTickLength(minorTickLenght); + } + + /** Sets log scale in x. Call update() thereafter to make the change visible + * (if the image is shown already). */ + + public void setLogScaleX() { + setAxisXLog(true); + } + + public void setLogScaleY() { + setAxisYLog(true); + } + + /** The default flags, taking PlotWindow.noGridLines, PlotWindow.noTicks into account */ + public static int getDefaultFlags() { + int defaultFlags = 0; + if (!PlotWindow.noGridLines) //note that log ticks are also needed because the range may span less than a decade, then no grid is visible + defaultFlags |= X_GRID | Y_GRID | X_NUMBERS | Y_NUMBERS | X_LOG_TICKS | Y_LOG_TICKS; + if (!PlotWindow.noTicks) + defaultFlags |= X_TICKS | Y_TICKS | X_MINOR_TICKS | Y_MINOR_TICKS | X_NUMBERS | Y_NUMBERS | X_LOG_TICKS | Y_LOG_TICKS; + return defaultFlags; + } + + /** Adds a curve or set of points to this plot, where 'type' is + * "line", "connected circle", "filled", "bar", "separated bar", "circle", "box", "triangle", "diamond", "cross", + * "x" or "dot". Run Help>Examples>JavaScript>Graph Types to see examples. + * If 'type' is in the form "code: ", the macro given is executed to draw the symbol; + * macro variables 'x' and 'y' are the pixel coordinates of the point, 'xval' and 'yval' are the plot data + * and 'i' is the index of the data point (starting with 0 for the first point in the array). + * The drawing including line thickness, font size, etc. be scaled by scale factor 's' (to make high-resolution plots work). + * Example: "code: setFont('sanserif',12*s,'bold anti');drawString(d2s(yval,1),x-14*s,y-4*s);" + * writes the y value for each point above the point. + */ + public void add(String type, double[] xvalues, double[] yvalues) { + int iShape = toShape(type); + addPoints(Tools.toFloat(xvalues), Tools.toFloat(yvalues), null, iShape, iShape==CUSTOM?type.substring(5, type.length()):null); + } + + /** Replaces the specified plot object (curve or set of points). + Equivalent to add() if there are no plot objects. */ + public void replace(int index, String type, double[] xvalues, double[] yvalues) { + if (index>=0 && index0?index:-1; + add(type, xvalues, yvalues); + } + } + + /** Adds a curve, set of points or error bars to this plot, where 'type' is + * "line", "connected circle", "filled", "bar", "separated bar", "circle", "box", + * "triangle", "diamond", "cross", "x", "dot", "error bars" or "xerror bars". + */ + public void add(String type, double[] yvalues) { + int iShape = toShape(type); + if (iShape==-1) + addErrorBars(yvalues); + else if (iShape==-2) + addHorizontalErrorBars(yvalues); + else + addPoints(null, Tools.toFloat(yvalues), null, iShape, iShape==CUSTOM?type.substring(5, type.length()):null); + } + + /** Adds a set of points to the plot or adds a curve if shape is set to LINE. + * @param xValues the x coordinates, or null. If null, integers starting at 0 will be used for x. + * @param yValues the y coordinates (must not be null) + * @param yErrorBars error bars in y, may be null + * @param shape CIRCLE, X, BOX, TRIANGLE, CROSS, DIAMOND, DOT, LINE, CONNECTED_CIRCLES + * @param label Label for this curve or set of points, used for a legend and for listing the plots + */ + public void addPoints(float[] xValues, float[] yValues, float[] yErrorBars, int shape, String label) { + if (xValues==null || xValues.length==0) { + xValues = new float[yValues.length]; + for (int i=0; i=0) + allPlotObjects.set(objectToReplace, new PlotObject(xValues, yValues, yErrorBars, shape, currentLineWidth, currentColor, currentColor2, label)); + else + allPlotObjects.add(new PlotObject(xValues, yValues, yErrorBars, shape, currentLineWidth, currentColor, currentColor2, label)); + objectToReplace = -1; + if (plotDrawn) updateImage(); + } + + /** Adds a set of points to the plot or adds a curve if shape is set to LINE. + * @param x the x coordinates + * @param y the y coordinates + * @param shape CIRCLE, X, BOX, TRIANGLE, CROSS, DIAMOND, DOT, LINE, CONNECTED_CIRCLES + */ + public void addPoints(float[] x, float[] y, int shape) { + addPoints(x, y, null, shape, null); + } + + /** Adds a set of points to the plot using double arrays. */ + public void addPoints(double[] x, double[] y, int shape) { + addPoints(Tools.toFloat(x), Tools.toFloat(y), shape); + } + + /** Returns the number for a given plot symbol shape, -1 for xError and -2 for yError (all case-insensitive) */ + public static int toShape(String str) { + str = str.toLowerCase(Locale.US); + int shape = Plot.CIRCLE; + if (str.contains("curve") || str.contains("line")) + shape = Plot.LINE; + else if (str.contains("connected")) + shape = Plot.CONNECTED_CIRCLES; + else if (str.contains("filled")) + shape = Plot.FILLED; + else if (str.contains("circle")) + shape = Plot.CIRCLE; + else if (str.contains("box")) + shape = Plot.BOX; + else if (str.contains("triangle")) + shape = Plot.TRIANGLE; + else if (str.contains("cross") || str.contains("+")) + shape = Plot.CROSS; + else if (str.contains("diamond")) + shape = Plot.DIAMOND; + else if (str.contains("dot")) + shape = Plot.DOT; + else if (str.contains("xerror")) + shape = -2; + else if (str.contains("error")) + shape = -1; + else if (str.contains("x")) + shape = Plot.X; + else if (str.contains("separate")) + shape = Plot.SEPARATED_BAR; + else if (str.contains("bar")) + shape = Plot.BAR; + if (str.startsWith("code:")) + shape = CUSTOM; + return shape; + } + + /** Adds a set of points to the plot using double ArrayLists. + * Must be called before the plot is displayed. */ + public void addPoints(ArrayList x, ArrayList y, int shape) { + addPoints(getDoubleFromArrayList(x), getDoubleFromArrayList(y), shape); + } + + /** Adds a set of points to the plot or adds a curve if shape is set to LINE. + * @param x the x-coodinates + * @param y the y-coodinates + * @param errorBars half-lengths of the vertical error bars, may be null + * @param shape CIRCLE, X, BOX, TRIANGLE, CROSS, DIAMOND, DOT or LINE + */ + public void addPoints(double[] x, double[] y, double[] errorBars, int shape) { + addPoints(Tools.toFloat(x), Tools.toFloat(y), Tools.toFloat(errorBars), shape, null); + } + + /** Adds a set of points to the plot using double ArrayLists. + * Must be called before the plot is displayed. */ + public void addPoints(ArrayList x, ArrayList y, ArrayList errorBars, int shape) { + addPoints(getDoubleFromArrayList(x), getDoubleFromArrayList(y), getDoubleFromArrayList(errorBars), shape); + } + + public double[] getDoubleFromArrayList(ArrayList list) { + if (list == null) return null; + double[] targ = new double[list.size()]; + for (int i = 0; i < list.size(); i++) + targ[i] = ((Double) list.get(i)).doubleValue(); + return targ; + } + + /** Adds a set of points that will be drawn as ARROWs. + * @param x1 the x-coodinates of the beginning of the arrow + * @param y1 the y-coodinates of the beginning of the arrow + * @param x2 the x-coodinates of the end of the arrow + * @param y2 the y-coodinates of the end of the arrow + */ + public void drawVectors(double[] x1, double[] y1, double[] x2, double[] y2) { + allPlotObjects.add(new PlotObject(Tools.toFloat(x1), Tools.toFloat(y1), + Tools.toFloat(x2), Tools.toFloat(y2), currentLineWidth, currentColor)); + } + + /** + * Adds a set of 'shapes' such as boxes and whiskers + * + * @param shapeType e.g. "boxes width=20" + * @param floatCoords eg[6][3] holding 1 Xval + 5 Yvals for 3 boxes + */ + public void drawShapes(String shapeType, ArrayList floatCoords) { + allPlotObjects.add(new PlotObject(shapeType, floatCoords, currentLineWidth, currentColor, currentColor2)); + } + + public static double calculateDistance(int x1, int y1, int x2, int y2) { + return java.lang.Math.sqrt((x2 - x1)*(double)(x2 - x1) + (y2 - y1)*(double)(y2 - y1)); + } + + /** Adds a set of vectors to the plot using double ArrayLists. + * Does not support logarithmic axes. + * Must be called before the plot is displayed. */ + public void drawVectors(ArrayList x1, ArrayList y1, ArrayList x2, ArrayList y2) { + drawVectors(getDoubleFromArrayList(x1), getDoubleFromArrayList(y1), getDoubleFromArrayList(x2), getDoubleFromArrayList(y2)); + } + + /** Adds vertical error bars to the last data passed to the plot (via the constructor or addPoints). */ + public void addErrorBars(float[] errorBars) { + PlotObject mainObject = getLastCurveObject(); + if (mainObject != null) + mainObject.yEValues = errorBars; + else throw new RuntimeException("Plot can't add y error bars without data"); + } + + /** Adds vertical error bars to the last data passed to the plot (via the constructor or addPoints). */ + public void addErrorBars(double[] errorBars) { + addErrorBars(Tools.toFloat(errorBars)); + } + + /** Adds horizontal error bars to the last data passed to the plot (via the constructor or addPoints). */ + public void addHorizontalErrorBars(float[] xErrorBars) { + PlotObject mainObject = getLastCurveObject(); + if (mainObject != null) + mainObject.xEValues = xErrorBars; + else throw new RuntimeException("Plot can't add x error bars without data"); + } + + /** Adds horizontal error bars to the last data passed to the plot (via the constructor or addPoints). */ + public void addHorizontalErrorBars(double[] xErrorBars) { + addHorizontalErrorBars(Tools.toFloat(xErrorBars)); + } + + /** Draws text at the specified location, where (0,0) + * is the upper left corner of the the plot frame and (1,1) is + * the lower right corner. Uses the justification specified by setJustification(). + * When called with the same position as the previous addLabel call, the text of that previous call is replaced */ + public void addLabel(double x, double y, String label) { + if (textLoc!=null && x==textLoc.getX() && y==textLoc.getY()) + allPlotObjects.set(textIndex, new PlotObject(label, x, y, currentJustification, currentFont, currentColor, PlotObject.NORMALIZED_LABEL)); + else { + allPlotObjects.add(new PlotObject(label, x, y, currentJustification, currentFont, currentColor, PlotObject.NORMALIZED_LABEL)); + textLoc = new Point2D.Double(x,y); + textIndex = allPlotObjects.size()-1; + } + } + + /* Draws text at the specified location, using the coordinate system defined + * by setLimits() and the justification specified by setJustification(). */ + public void addText(String label, double x, double y) { + allPlotObjects.add(new PlotObject(label, x, y, currentJustification, currentFont, currentColor, PlotObject.LABEL)); + } + + /** Adds an automatically positioned legend, where 'labels' can be a tab-delimited or + newline-delimited list of curve or point labels in the sequence these data were added. + Hidden data sets are ignored. + If 'labels' is null or empty, the labels of the data set previously (if any) are used. + To modify the legend's style, call 'setFont' and 'setLineWidth' before 'addLegend'. */ + public void addLegend(String labels) { + addLegend(labels, "auto"); + } + + /** Adds a legend at the position given in 'options', where 'labels' can be tab-delimited or + newline-delimited list of curve or point labels in the sequence these data were added. + Hidden data sets are ignored. + If 'labels' is null or empty, the labels of the data set previously (if any) are used. + To modify the legend's style, call 'setFont' and 'setLineWidth' before 'addLegend'. */ + public void addLegend(String labels, String options) { + int flags = 0; + if (options!=null) { + options = options.toLowerCase(); + if (options.contains("top-left")) + flags |= Plot.TOP_LEFT; + else if (options.contains("top-right")) + flags |= Plot.TOP_RIGHT; + else if (options.contains("bottom-left")) + flags |= Plot.BOTTOM_LEFT; + else if (options.contains("bottom-right")) + flags |= Plot.BOTTOM_RIGHT; + else if (!options.contains("off") && !options.contains("no")) + flags |= Plot.AUTO_POSITION; + if (options.contains("bottom-to-top")) + flags |= Plot.LEGEND_BOTTOM_UP; + if (options.contains("transparent")) + flags |= Plot.LEGEND_TRANSPARENT; + } + setLegend(labels, flags); + } + + /** Adds a legend. The legend will be always drawn last (on top of everything). + * To modify the legend's style, call 'setFont' and 'setLineWidth' before 'addLegend' + * @param labels labels of the points or curves in the sequence of the data were added, tab-delimited or linefeed-delimited. + * The labels of the datasets will be set to these values. If null or not given, the labels set + * previously (if any) will be used. + * Hidden data sets are ignored. + * @param flags Bitwise or of position (AUTO_POSITION, TOP_LEFT etc.), LEGEND_TRANSPARENT, and LEGEND_BOTTOM_UP if desired. + * Updates the image (if it is shown already). */ + public void setLegend(String labels, int flags) { + if (labels != null && labels.length()>0) { + String[] allLabels = labels.split("[\n\t]"); + int iPart = 0; + for (PlotObject plotObject : allPlotObjects) + if (plotObject.type == PlotObject.XY_DATA && !plotObject.hasFlag(PlotObject.HIDDEN)) + if (iPart < allLabels.length) { + String label = allLabels[iPart++]; + if (label!=null && label.length()>0) + plotObject.label = label; + } + } + pp.legend = new PlotObject(currentLineWidth == 0 ? 1 : currentLineWidth, + currentFont, currentColor == null ? Color.black : currentColor, flags); + if (plotDrawn) updateImage(); + } + + /** Sets the label for the plot object nuber 'index' in the sequence they were added. + * With index=-1, sets the label for the last object added. + * For x/y data, the label is used for the legend and as header in getResultsTableWithLabels. + * For Text/Label objects, it affects the label shown (but the plot is not redisplayed). */ + public void setLabel(int index, String label) { + if (index < 0) index = allPlotObjects.size() + index; + allPlotObjects.get(index).label = label; + } + + /** Removes NaNs from the xValues and yValues arrays of all plot objects. */ + public void removeNaNs() { + for (PlotObject plotObj : allPlotObjects){ + if(plotObj != null && plotObj.xValues!= null && plotObj.yValues != null ){ + int oldSize = plotObj.xValues.length; + float[] xVals = new float[oldSize]; + float[] yVals = new float[oldSize]; + int newSize = 0; + for (int kk = 0; kk < oldSize; kk++) { + if (!Float.isNaN(plotObj.xValues[kk] + plotObj.yValues[kk])) { + xVals[newSize] = plotObj.xValues[kk]; + yVals[newSize] = plotObj.yValues[kk]; + newSize++; + } + } + if (newSize < oldSize) { + plotObj.xValues = new float[newSize]; + plotObj.yValues = new float[newSize]; + System.arraycopy(xVals, 0, plotObj.xValues, 0, newSize); + System.arraycopy(yVals, 0, plotObj.yValues, 0, newSize); + } + } + } + } + + /** Returns an array of the available curve types ("Line", "Bar", "Circle", etc). */ + public String[] getTypes() { + return SORTED_SHAPES; + } + + /** Sets the justification used by addLabel(), where justification + * is Plot.LEFT, Plot.CENTER or Plot.RIGHT. Default is LEFT. */ + public void setJustification(int justification) { + currentJustification = justification; + } + + /** Changes the drawing color for the next objects that will be added to the plot. + * For selecting the color of the curve passed with the constructor, + * use setColor before draw. + * The frame and labels are always drawn in black. */ + public void setColor(Color c) { + currentColor = c; + currentColor2 = null; + } + + public void setColor(String color) { + setColor(Colors.decode(color, Color.black)); + } + + /** Changes the drawing color for the next objects that will be added to the plot. + * It also sets secondary color: This is the color of the line for CONNECTED_CIRCLES, + * and the color for filling open symbols (CIRCLE, BOX, TRIANGLE). + * Set it to null or use the one-argument call setColor(color) to disable filling. + * For selecting the color of the curve passed with the constructor, + * use setColor before draw. + * The frame and labels are always drawn in black. */ + public void setColor(Color c, Color c2) { + currentColor = c; + currentColor2 = c2; + } + + /** Sets the drawing colors for the next objects that will be added to the plot. */ + public void setColor(String c1, String c2) { + setColor(Colors.decode(c1, Color.black), Colors.decode(c2, null)); + } + + /** Set the plot frame background color. */ + public void setBackgroundColor(Color c) { + pp.frame.color2 = c; + } + + /** Set the plot frame background color. */ + public void setBackgroundColor(String c) { + setBackgroundColor(Colors.decode(c,Color.white)); + } + + /** Changes the line width for the next objects that will be added to the plot. */ + public void setLineWidth(int lineWidth) { + currentLineWidth = lineWidth > 0 ? lineWidth : 0.01f; + } + + /** Changes the line width for the next objects that will be added to the plot. + * After all objects have been added, set the line width to the width desired + * for the frame around the plot (max. 3) */ + public void setLineWidth(float lineWidth) { + currentLineWidth = lineWidth > 0.01 ? lineWidth : 0.01f; + } + + /** Draws a line using the coordinate system defined by setLimits(). */ + public void drawLine(double x1, double y1, double x2, double y2) { + allPlotObjects.add(new PlotObject(x1, y1, x2, y2, currentLineWidth, 0, currentColor, PlotObject.LINE)); + } + + /** Draws a line using a normalized 0-1, 0-1 coordinate system, + * with (0,0) at the top left and (1,1) at the lower right corner. + * This is the same coordinate system used by addLabel(x,y,label). + */ + public void drawNormalizedLine(double x1, double y1, double x2, double y2) { + allPlotObjects.add(new PlotObject(x1, y1, x2, y2, currentLineWidth, 0, currentColor, PlotObject.NORMALIZED_LINE)); + } + + /** Draws a line using the coordinate system defined by setLimits(). */ + public void drawDottedLine(double x1, double y1, double x2, double y2, int step) { + allPlotObjects.add(new PlotObject(x1, y1, x2, y2, currentLineWidth, step, currentColor, PlotObject.DOTTED_LINE)); + } + + /** Sets the font size for all following addLabel() etc. operations. The currently set font when + * displaying the plot determines the font of all labels & numbers. + * After the plot has been shown, sets the font for the numbers and the legend (if present). + * If the plot is hown already, call update() thereafter to make the change visible. */ + public void setFontSize(int size) { + setFont(-1, (float)size); + } + + /** Sets the font for all following addLabel() etc. operations. The currently set font when + * displaying the plot determines the font of all labels & numbers. + * After the plot has been shown, sets the font for the numbers and the legend (if present); + * use setFont(char, Font) to set these fonts individually. + * If the plot is hown already, call update() thereafter to make the change visible. */ + public void setFont(Font font) { + if (font == null) font = defaultFont; + currentFont = font; + if (plotDrawn) { + pp.frame.setFont(font); + if (pp.legend != null) + pp.legend.setFont(font); + } + } + + /** Sets the font size and style for all following addLabel() etc. operations. This leaves + * the font name and style of the previously used fonts unchanged. The currently set font + * when displaying the plot determines the font of the numbers at the axes. + * That font also sets the default label font size, which may be overridden by + * setAxisLabelFontSize or setXLabelFont, setYLabelFont. + * After the plot has been shown, sets the font for the numbers and the legend (if present); + * use setFont(char, Font) to set these fonts individually. + * Styles are defined in the Font class, e.g. Font.PLAIN, Font.BOLD. + * Set style to -1 to leave the style unchanged. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setFont(int style, float size) { + if (size < 9) size = 9f; + if (size > 36) size = 36f; + Font previousFont = nonNullFont(pp.frame.getFont(), currentFont); + if (style < 0) style = previousFont.getStyle(); + setFont(previousFont.deriveFont(style, size)); + } + + /** Sets the x and y label font size and style. Styles are defined + * in the Font class, e.g. Font.PLAIN, Font.BOLD. + * Set style to -1 to leave the style unchanged. + * Call update() thereafter to make the change visible (if the image is shown already). */ + public void setAxisLabelFont(int style, float size) { + if (size < 9) size = 9f; + if (size > 33) size = 33f; + pp.xLabel.setFont(nonNullFont(pp.xLabel.getFont(), currentFont)); + pp.yLabel.setFont(nonNullFont(pp.yLabel.getFont(), currentFont)); + setXLabelFont(pp.xLabel.getFont().deriveFont(style < 0 ? pp.xLabel.getFont().getStyle() : style, size)); + setYLabelFont(pp.xLabel.getFont().deriveFont(style < 0 ? pp.yLabel.getFont().getStyle() : style, size)); + } + + /** Sets the xLabelFont; must not be null. If this method is not used, the last setFont + * of setFontSize call before displaying the plot determines the font, or if neither + * was called, the font size of the Plot Options is used. */ + public void setXLabelFont(Font font) { + pp.xLabel.setFont(font); + } + + /** Sets the yLabelFont; must not be null. */ + public void setYLabelFont(Font font) { + pp.yLabel.setFont(font); + } + + /** Determines whether to use antialiased text (default true) */ + public void setAntialiasedText(boolean antialiasedText) { + pp.antialiasedText = antialiasedText; + } + + /** Returns the font currently used (e.g. for the next 'addLabel') */ + public Font getCurrentFont() { + return currentFont != null ? currentFont : defaultFont; + } + + /** Returns the default font for the plot */ + public Font getDefaultFont() { + return defaultFont; + } + + /** Gets the font for xLabel ('x'), yLabel('y'), numbers ('f' for 'frame') or the legend ('l'). + * Returns null if the given PlotObject does not exist or its font is null */ + public Font getFont(char c) { + PlotObject plotObject = pp.getPlotObject(c); + if (plotObject != null) + return plotObject.getFont(); + else + return null; + } + + /** Sets the font for xLabel ('x'), yLabel('y'), numbers ('f' for 'frame') or the legend ('l') */ + public void setFont(char c, Font font) { + PlotObject plotObject = pp.getPlotObject(c); + if (plotObject != null) + plotObject.setFont(font); + } + + /** Gets the label String of the xLabel ('x'), yLabel('y') or the legend ('l'). + * Returns null if the given PlotObject does not exist or its label is null */ + public String getLabel(char c) { + PlotObject plotObject = pp.getPlotObject(c); + if (plotObject != null) + return plotObject.label; + else + return null; + } + + /** Gets the flags of the xLabel ('x'), yLabel('y') or the legend ('l'). + * Returns -1 if the given PlotObject does not exist */ + public int getObjectFlags(char c) { + PlotObject plotObject = pp.getPlotObject(c); + if (plotObject != null) + return plotObject.flags; + else + return -1; + } + + /** Get the x coordinates of the data set passed with the constructor (if not null) + * or otherwise of the data set of the first 'addPoints'. Returns null if neither exists */ + public float[] getXValues() { + PlotObject p = getMainCurveObject(); + return p==null ? null : p.xValues; + } + + /** Get the y coordinates of the data set passed with the constructor (if not null) + * or otherwise of the data set of the first 'addPoints'. Returns null if neither exists */ + public float[] getYValues() { + PlotObject p = getMainCurveObject(); + return p==null ? null : p.yValues; + } + + /** Get the data of the n-th Plot Object containing xy data in the sequence they were added + * (Other Plot Objects such as labels, arrows, lines, shapes and hidden PlotObjects are not counted). + * The array returned has elements [0] x data, [1] y data, [2] x error bars, [3] y error bars. + * If no error bars are given, the corresponding arrays are null. + * Returns null if there is no Plot Object with xy data with this index. + * @see #getDataObjectDesignations() + **/ + public float[][] getDataObjectArrays(int index) { + int i = 0; + for (PlotObject plotObject : allPlotObjects) { + if (plotObject.type != PlotObject.XY_DATA || plotObject.hasFlag(PlotObject.HIDDEN)) continue; + if (index == i) + return new float[][] {plotObject.xValues, plotObject.yValues, plotObject.xEValues, plotObject.yEValues}; + i++; + } + return null; + } + + /** Gets an array with human-readable designations of the PlotObjects (curves, labels, ...) + * in the sequence they were added (the object passed with the constructor is first, + * even though it is plotted last). Hidden PlotObjects are included. **/ + public String[] getPlotObjectDesignations() { + return getPlotObjectDesignations(-1, true); + } + + /** Gets an array with human-readable designations of the PlotObjects containing xy data + * in the sequence they were added. Other Plot Objects such as labels, arrows, lines, + * shapes and hidden PlotObjects are not counted. + * (the object passed with the constructor is first, even though it is plotted last). */ + public String[] getDataObjectDesignations() { + return getPlotObjectDesignations(PlotObject.XY_DATA, false); + } + + /** Returns the number of PlotObjects (curves, labels, ...) passed with the constructor or added by 'add' or 'draw' methods. + * Legend, frame and axes (though internally PlotObjects) are not included */ + public int getNumPlotObjects() { + return allPlotObjects.size(); + } + + /** Returns the number of PlotObjects fitting the mask. + * Legend, frame and axes (though internally PlotObjects) are not included */ + int getNumPlotObjects(int mask, boolean includeHidden) { + int nObjects = 0; + for (PlotObject plotObject : allPlotObjects) + if ((plotObject.type & mask) != 0 && (includeHidden || !plotObject.hasFlag(PlotObject.HIDDEN))) + nObjects++; + return nObjects; + } + + /** Gets an array with human-readable designations of the PlotObjects with types fitting the mask + * (i.e., 'mask' should be a bitwise or of the types desired) */ + String[] getPlotObjectDesignations(int mask, boolean includeHidden) { + int nObjects = getNumPlotObjects(mask, includeHidden); + String[] names = new String[nObjects]; + if (names.length == 0) return names; + int iData = 1, iArrow = 1, iLine = 1, iText = 1, iBox = 1, iShape = 1; //Human readable counters of each object type + int i = 0; + for (PlotObject plotObject : allPlotObjects) { + int type = plotObject.type; + if ((type & mask) == 0 || (!includeHidden && plotObject.hasFlag(PlotObject.HIDDEN))) continue; + String label = plotObject.label; + switch (type) { + case PlotObject.XY_DATA: + names[i] = "Data Set "+iData+": "+(plotObject.label != null ? + plotObject.label : "(" + plotObject.yValues.length + " data points)"); + iData++; + break; + case PlotObject.ARROWS: + names[i] = "Arrow Set "+iArrow+" ("+ plotObject.xValues.length + ")"; + iArrow++; + break; + case PlotObject.LINE: case PlotObject.NORMALIZED_LINE: case PlotObject.DOTTED_LINE: + String detail = ""; + if (type == PlotObject.DOTTED_LINE) detail = "dotted "; + if (plotObject.x ==plotObject.xEnd) detail += "vertical"; + else if (plotObject.y ==plotObject.yEnd) detail += "horizontal"; + if (detail.length()>0) detail = " ("+detail.trim()+")"; + names[i] = "Straight Line "+iLine+detail; + iLine++; + break; + case PlotObject.LABEL: case PlotObject.NORMALIZED_LABEL: + String text = plotObject.label.replaceAll("\n"," "); + if (text.length()>45) text = text.substring(0, 40)+"..."; + names[i] = "Text "+iText+": \""+text+'"'; + iText++; + break; + case PlotObject.SHAPES: + String s = plotObject.shapeType; + String[] words = s.split(" "); + names[i] = "Shapes (" + words[0] +") " + iShape; + iShape++; + break; + } + i++; + } + return names; + } + + /** Add the i-th PlotObject (in the sequence how they were added, including hidden ones) + * from another plot to this one. PlotObjects here refers to curves, arrows, labels etc. + * (not legend, axes and frame, though implemented as PlotObjects) + * Use 'update' to update the plot thereafter. + * @return Index of the plotObject added in the sequence they were added */ + public int addObjectFromPlot(Plot plot, int i) { + PlotObject plotObject = plot.getPlotObjectDeepClone(i); + plotObject.unsetFlag(PlotObject.CONSTRUCTOR_DATA); + allPlotObjects.add(plotObject); + int index = allPlotObjects.size() - 1; + return index; + } + + /** Get the style of the i-th PlotObject (curve, label, ...) in the sequence + * they were added (including hidden ones), as String with comma delimiters: + * Main Color, Secondary Color (or "none"), Line Width [, Symbol shape for XY_DATA] [,hidden] + * PlotObjects here refers to curves, arrows, labels etc. + * (not legend, exes and frame, though implemented as PlotObjects) */ + public String getPlotObjectStyle(int i) { + return getPlotObjectStyle(allPlotObjects.get(i)); + } + + String getPlotObjectStyle(PlotObject plotObject) { + String styleString = Colors.colorToString(plotObject.color) + "," + + Colors.colorToString(plotObject.color2) + "," + + plotObject.lineWidth; + if (plotObject.type == PlotObject.XY_DATA) + styleString += ","+SHAPE_NAMES[plotObject.shape]; + if (plotObject.hasFlag(PlotObject.HIDDEN)) + styleString += ",hidden"; + return styleString; + } + + /** Get the label the i-th PlotObject (in the sequence how they were added, including hidden ones). + * Returns null if no label. PlotObjects here refers to curves, arrows, labels etc. + * (not legend, exes and frame, though implemented as PlotObjects) */ + public String getPlotObjectLabel(int i) { + return allPlotObjects.get(i).label; + } + + /** Set the label the i-th PlotObject (in the sequence how they were added, including hidden ones) + * PlotObjects here refers to curves, arrows, labels etc. + * (not legend, exes and frame, though implemented as PlotObjects) */ + public void setPlotObjectLabel(int i, String label) { + allPlotObjects.get(i).label = label; + } + + /** Sets the style of the specified PlotObject (curve, label, etc.) from a + * comma-delimited string ("color1,color2,lineWidth[,symbol][,hidden]"), + * where "color2" can be "none" and "symbol" and "hidden" are optional. + * PlotObjects here refers to curves, arrows, labels etc. + * (not legend, exes and frame, though implemented as PlotObjects) */ + public void setStyle(int index, String style) { + if (index<0 || index>=allPlotObjects.size()) + throw new IllegalArgumentException("Index out of range"); + setPlotObjectStyle(allPlotObjects.get(index), style); + } + + public void setPlotObjectStyle(int i, String styleString) { + setStyle(i, styleString); + } + + void setPlotObjectStyle(PlotObject plotObject, String styleString) { + String[] items = styleString.split(","); + int nItems = items.length; + if (items[nItems-1].indexOf("hidden") >= 0) { + plotObject.setFlag(PlotObject.HIDDEN); + nItems = items.length - 1; + } else + plotObject.unsetFlag(PlotObject.HIDDEN); + plotObject.color = Colors.decode(items[0].trim(), plotObject.color); + plotObject.color2 = Colors.decode(items[1].trim(), null); + float lineWidth = plotObject.lineWidth; + if (items.length >= 3) try { + plotObject.lineWidth = Float.parseFloat(items[2].trim()); + } catch (NumberFormatException e) {}; + if (items.length >= 4 && plotObject.shape!=CUSTOM) + plotObject.shape = toShape(items[3].trim()); + updateImage(); + return; + } + + /** Returns the index of the first plot object with x,y data (points, line) or arrows + * with all data equal to those given. Returns or -1 is no such plot object exists. + * The array 'values' should contain the x, y, x error bar, yerror bar data. The 'values' array may have any size; + * only the data given are compared (e.g. for an array with length 2, there is no check for erro bars). + * Used when adding data from a table not to suggest the same data twice. */ + public int getPlotObjectIndex(float[][] values) { + return getPlotObjectIndex(PlotObject.XY_DATA|PlotObject.ARROWS, values); + } + + /** Returns the index of the first plot object fitting the type mask and with all data equal to those given. + * ('mask' should be a bitwise or of the types desired) + * Returns or -1 is no such plot object exists. + * The array 'values' should contain the x, y, x error bar, yerror bar data. The 'values' array may have any size; + * only the data given are compared (e.g. for an array with length 2, there is no check for erro bars). + * Used when adding data from a table not to suggest the same data twice. */ + int getPlotObjectIndex(int typeMask, float[][] values) { + for (int i=0; i(allPlotObjects.size()); + copyPlotObjectsVector(allPlotObjects, allPlotObjectsSnapshot); + } + + /** Restores the plot contents (not including axis formats etc) from the snapshot + * previously created by savePlotObjects(). See also killPlotObjectsSnapshot + * Use 'update' to update the plot thereafter. */ + public void restorePlotObjects() { + if (allPlotObjectsSnapshot != null) + copyPlotObjectsVector(allPlotObjectsSnapshot, allPlotObjects); + } + + /** Deletes the snapshot of the plot contents to make space */ + public void killPlotObjectsSnapshot() { + allPlotObjectsSnapshot = null; + } + + /** Creates a snapshot of the plot properties (formatting, range etc., not PlotObjects such as data and corresponding curves etc.), + * for later undo by restorePlotProperties. See also killPlotPropertiesSnapshot */ + public void savePlotPlotProperties() { + pp.rangeMinMax = currentMinMax; + ppSnapshot = pp.deepClone(); + } + + /** Restores the plot properties (formatting, range etc., not PlotObjects such as data and corresponding curves etc.) + * from a snapshot previously created by savePlotPlotProperties. See also killPlotPropertiesSnapshot. + * Use 'update' to update the plot thereafter. */ + public void restorePlotProperties() { + pp = ppSnapshot.deepClone(); + System.arraycopy(pp.rangeMinMax, 0, currentMinMax, 0, Math.min(pp.rangeMinMax.length, currentMinMax.length)); + } + + /** Deletes the snapshot of the plot properties to make space */ + public void killPlotPropertiesSnapshot() { + ppSnapshot = null; + } + + private void copyPlotObjectsVector(Vector src, Vectordest) { + if (dest.size() > 0) dest.removeAllElements(); + for (PlotObject plotObject : src) + dest.add(plotObject.deepClone()); + } + + PlotObject getPlotObjectDeepClone(int i) { + return allPlotObjects.get(i).deepClone(); + } + + /** Sets the plot range to the initial value determined from minima&maxima or given by setLimits. + * Updates the image if existing and updateImg is true */ + public void setLimitsToDefaults(boolean updateImg) { + saveMinMax(); + System.arraycopy(defaultMinMax, 0, currentMinMax, 0, defaultMinMax.length); + if (plotDrawn && updateImg) updateImage(); + } + + /** Sets the plot range to encompass all data. Updates the image if existing and updateImg is true. */ + public void setLimitsToFit(boolean updateImg) { + saveMinMax(); + currentMinMax = getMinAndMax(true, ALL_AXES_RANGE); + if (Double.isNaN(defaultMinMax[0]) && Double.isNaN(defaultMinMax[2])) //no range at all so far + System.arraycopy(currentMinMax, 0, defaultMinMax, 0, Math.min(currentMinMax.length, defaultMinMax.length)); + + enlargeRange(currentMinMax); //avoid points exactly at the border + //System.arraycopy(currentMinMax, 0, defaultMinMax, 0, currentMinMax.length); + if (plotDrawn && updateImg) updateImage(); + } + + /** reverts plot range to previous values and updates the image */ + public void setPreviousMinMax() { + if (Double.isNaN(savedMinMax[0])) return; //no saved values yet + double[] swap = new double[currentMinMax.length]; + System.arraycopy(currentMinMax, 0, swap, 0, currentMinMax.length); + System.arraycopy(savedMinMax, 0, currentMinMax, 0, currentMinMax.length); + System.arraycopy(swap, 0, savedMinMax, 0, currentMinMax.length); + updateImage(); + } + + /** Draws the plot (if not done before) in an ImageProcessor and returns the ImageProcessor with the plot. */ + public ImageProcessor getProcessor() { + draw(); + return ip; + } + + /** Returns the plot as an ImagePlus. + * If an ImagePlus for this plot already exists, displays the plot in that ImagePlus and returns it. */ + public ImagePlus getImagePlus() { + if (stack != null) { + if (imp != null) + return imp; + else { + imp = new ImagePlus(title, stack); + adjustCalibration(imp.getCalibration()); + return imp; + } + } + if (plotDrawn) + updateImage(); + else + draw(); + if (imp != null) { + if (imp.getProcessor() != ip) + imp.setProcessor(ip); + return imp; + } else { + ImagePlus imp = new ImagePlus(title, ip); + setImagePlus(imp); + return imp; + } + } + + /** Sets the ImagePlus where the plot will be displayed. If the ImagePlus is not + * known otherwise (e.g. from getImagePlus), this is needed for changes such as + * zooming in to work correctly. It also sets the calibration of the ImagePlus. + * The ImagePlus is not displayed or updated unless its ImageProcessor is + * no that of the current Plot (then it gets this ImageProcessor). + * Does nothing if imp is unchanged and has the ImageProcessor of this plot. + * 'imp' may be null to disconnect the plot from its ImagePlus. + * Does nothing for Plot Stacks. */ + public void setImagePlus(ImagePlus imp) { + if (imp != null && imp == this.imp && imp.getProcessor() == ip) + return; + if (stack != null) + return; + if (this.imp != null) + this.imp.setProperty(PROPERTY_KEY, null); + this.imp = imp; + if (imp != null) { + imp.setIgnoreGlobalCalibration(true); + adjustCalibration(imp.getCalibration()); + imp.setProperty(PROPERTY_KEY, this); + if (ip != null && imp.getProcessor() != ip) + imp.setProcessor(ip); + } + } + + /** Adjusts a Calibration object to fit the current axes. + * For log axes, the calibration refers to the base-10 logarithm of the value */ + public void adjustCalibration(Calibration cal) { + if (xMin == xMax) //tiff images can't handle infinity in scale, see TiffEncoder.writeScale + xScale = 1e6; + if (yMin == yMax) + yScale = 1e6; + cal.xOrigin = xBasePxl-xMin*xScale; + cal.pixelWidth = 1.0/Math.abs(xScale); //Calibration must not have negative pixel size + cal.yOrigin = yBasePxl+yMin*yScale; + cal.pixelHeight = 1.0/Math.abs(yScale); + cal.setInvertY(yScale >= 0); + cal.setXUnit(" "); // avoid 'pixels' for scaled units + if (xMin == xMax) + xScale = Double.POSITIVE_INFINITY; + if (yMin == yMax) + yScale = Double.POSITIVE_INFINITY; + } + + /** Displays the plot in a PlotWindow. + * Plot stacks are shown in a StackWindow, not in a PlotWindow; + * in this case the return value is null (use getImagePlus().getWindow() instead). + * Also returns null in BatchMode. Note that the PlotWindow might get closed + * immediately if its 'listValues' and 'autoClose' flags are set. + * @see #update() + */ + public PlotWindow show() { + PlotVirtualStack stack = getStack(); + if (stack!=null) { + getImagePlus().show(); + return null; + } + if ((IJ.macroRunning() && IJ.getInstance()==null) || Interpreter.isBatchMode()) { + imp = getImagePlus(); + imp.setPlot(this); + WindowManager.setTempCurrentImage(imp); + if (getMainCurveObject() != null) { + imp.setProperty("XValues", getXValues()); // Allows values to be retrieved by + imp.setProperty("YValues", getYValues()); // by Plot.getValues() macro function + } + Interpreter.addBatchModeImage(imp); + return null; + } + if (imp != null) { + Window win = imp.getWindow(); + if (win instanceof PlotWindow && win.isVisible()) { + updateImage(); // show in existing window + return (PlotWindow)win; + } else + setImagePlus(null); + } + PlotWindow pw = new PlotWindow(this); //note: this may set imp to null if pw has listValues and autoClose are set + if (IJ.isMacro() && imp!=null) // wait for plot to be displayed + IJ.selectWindow(imp.getID()); + return pw; + } + + /** + * Appends the current plot to a virtual stack and resets allPlotObjects + * for next slice + * N. Vischer + */ + public void addToStack() { + if (stack==null) + stack = new PlotVirtualStack(getSize().width,getSize().height); + draw(); + stack.addPlot(this); + IJ.showStatus("addToPlotStack: "+stack.size()); + allPlotObjects.clear(); + textLoc = null; + } + + public void appendToStack() { addToStack(); } + + /** Returns the virtual stack created by addToStack(). */ + public PlotVirtualStack getStack() { + IJ.showStatus(""); + return stack; + } + + /** Draws the plot specified for the first time. Does nothing if the plot has been drawn already. + * Call getProcessor to retrieve the ImageProcessor with it. + * Does no action with respect to the ImagePlus (if any) */ + public void draw() { + //IJ.log("draw(); plotDrawn="+plotDrawn); + if (plotDrawn) return; + getInitialMinAndMax(); + pp.frame.setFont(nonNullFont(pp.frame.getFont(), currentFont)); //make sure we have a number font for calculating the margins + getBlankProcessor(); + drawContents(ip); + } + + /** Freezes or unfreezes the plot. In the frozen state, the plot cannot be resized or updated, + * and the Plot class does no modifications to the ImageProcessor. + * Changes are recorded nevertheless and become effective with setFrozen(false). */ + public void setFrozen(boolean frozen) { + pp.isFrozen = frozen; + if (!pp.isFrozen) { // unfreeze operations ... + if (imp != null && ip != null) { + ImageCanvas ic = imp.getCanvas(); + if (ic instanceof PlotCanvas) { + ((PlotCanvas)ic).resetMagnification(); + imp.setTitle(imp.getTitle()); //update magnification in title + } + Undo.setup(Undo.TRANSFORM, imp); + } + updateImage(); + ImageWindow win = imp == null ? null : imp.getWindow(); + if (win != null) win.updateImage(imp); //show any changes made during the frozen state + } + } + + public boolean isFrozen() { + return pp.isFrozen; + } + + /** Draws the plot again, ignored if the plot has not been drawn before or the plot is frozen. */ + public void update() { + updateImage(); + } + + /** Draws the plot again, ignored if the plot has not been drawn before or the plot is frozen. + * If the ImagePlus exist, updates it and its calibration. */ + public void updateImage() { + if (!plotDrawn || pp.isFrozen) return; + getBlankProcessor(); + drawContents(ip); + if (imp == null || stack != null) return; + adjustCalibration(imp.getCalibration()); + imp.updateAndDraw(); + if (ip != imp.getProcessor()) + imp.setProcessor(ip); + } + + /** Returns the rectangle where the data are plotted. + * This rectangle includes the black outline frame at the top and left, but not at the bottom + * and right (when the frame is plotted with 1 pxl width). + * The image scale is its width or height in pixels divided by the data range in x or y. */ + public Rectangle getDrawingFrame() { + if (frame == null) + getBlankProcessor(); //setup frame if not done yet + return new Rectangle(frame.x, frame.y, frameWidth, frameHeight); + } + + /** Creates a new high-resolution plot by scaling it and displays that plot if showIt is true. + * title may be null, then a default title is used. */ + public ImagePlus makeHighResolution(String title, float scale, boolean antialiasedText, boolean showIt) { + Plot hiresPlot = null; + try { + hiresPlot = (Plot)clone(); //shallow clone, thus arrays&objects are not cloned, but they will be used only now + } catch (Exception e) {return null;} + hiresPlot.ip = null; + hiresPlot.imp = null; + hiresPlot.pp = pp.clone(); + if (!plotDrawn) hiresPlot.getInitialMinAndMax(); + hiresPlot.setScale(scale); + hiresPlot.setAntialiasedText(antialiasedText); + hiresPlot.defaultMinMax = currentMinMax.clone(); + ImageProcessor hiresIp = hiresPlot.getProcessor(); + if (title == null || title.length() == 0) + title = getTitle()+"_HiRes"; + title = WindowManager.makeUniqueName(title); + ImagePlus hiresImp = new ImagePlus(title, hiresIp); + Calibration cal = hiresImp.getCalibration(); + hiresPlot.adjustCalibration(cal); + if (showIt) { + hiresImp.setIgnoreGlobalCalibration(true); + hiresImp.show(); + } + hiresPlot.dispose(); //after drawing, we don't need the plot of the high-resolution image any more + return hiresImp; + } + + /** Releases the ImageProcessor and ImagePlus associated with the plot. + * May help garbage collection because some garbage collectors + * are said to be inefficient with circular references. */ + public void dispose() { + if (imp != null) + imp.setProperty(PROPERTY_KEY, null); + imp = null; + ip = null; + } + + /** Converts pixels to calibrated coordinates. In contrast to the image calibration, also + * works with log axes and inverted x axes */ + public double descaleX(int x) { + if (xMin == xMax) return xMin; + double xv = (x-xBasePxl)/xScale + xMin; + if (logXAxis) xv = Math.pow(10, xv); + return xv; + } + + /** Converts pixels to calibrated coordinates. In contrast to the image calibration, also + * works with log axes */ + public double descaleY(int y) { + if (yMin == yMax) return yMin; + double yv = (yBasePxl-y)/yScale +yMin; + if (logYAxis) yv = Math.pow(10, yv); + return yv; + } + + + /** Converts calibrated coordinates to pixel coordinates. In contrast to the image calibration, also + * works with log x axis and inverted x axis */ + public double scaleXtoPxl(double x) { + if (xMin == xMax) { + if (x==xMin) return xBasePxl; + else return x>xMin ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY; + } + if (logXAxis) + return xBasePxl+(Math.log10(x)-xMin)*xScale; + else + return xBasePxl+(x-xMin)*xScale; + } + + /** Converts calibrated coordinates to pixel coordinates. In contrast to the image calibration, also + * works with log y axis */ + public double scaleYtoPxl(double y) { + if (yMin == yMax) { + if (y==xMin) return yBasePxl; + else return y>yMin ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY; + } + if (logYAxis) + return yBasePxl-(Math.log10(y)-yMin)*yScale; + else + return yBasePxl-(y-yMin)*yScale; + } + + /** Calibrated coordinates to integer pixel coordinates */ + private int scaleX(double x) { + if (xMin == xMax) { + if (x==xMin) return xBasePxl; + else return x>xMin ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + if (logXAxis) + return xBasePxl+(int)Math.round((Math.log10(x)-xMin)*xScale); + else + return xBasePxl+(int)Math.round((x-xMin)*xScale); + } + + /** Converts calibrated coordinates to pixel coordinates. In contrast to the image calibration, also + * works with log axes */ + private int scaleY(double y) { + if (yMin == yMax) { + if (y==yMin) return yBasePxl; + else return y>yMin ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + if (logYAxis) + return yBasePxl-(int)Math.round((Math.log10(y)-yMin)*yScale); + else + return yBasePxl-(int)Math.round((y-yMin)*yScale); + } + + /** Converts calibrated coordinates to pixel coordinates. In contrast to the image calibration, also + * works with log axes and inverted x axes. Returns a large number instead NaN for log x axis and zero or negative x */ + private int scaleXWithOverflow(double x) { + if (!logXAxis || x>0) + return scaleX(x); + else + return xScale > 0 ? -1000000 : 1000000; + } + + /** Converts calibrated coordinates to pixel coordinates. In contrast to the image calibration, also + * works with log axes and inverted x axes. Returns a large number instead NaN for log y axis and zero or negative y */ + private int scaleYWithOverflow(double y) { + if (!logYAxis || y>0) + return scaleY(y); + else + return yScale > 0 ? 1000000 : -1000000; + } + + /** Scales a value of the original plot for a high-resolution plot. Returns an integer number of pixels >=1 */ + int sc(float length) { + int pixels = (int)(length*scale + 0.5); + if (pixels < 1) pixels = 1; + return pixels; + } + + /** Scales a font of the original plot for a high-resolution plot. */ + Font scFont(Font font) { + float size = font.getSize2D(); + return scale==1 ? font : font.deriveFont(size*scale); + } + + /** Returns whether the plot requires color (not grayscale) */ + boolean isColored() { + for (PlotObject plotObject : allPlotObjects) + if (isColored(plotObject.color) || isColored(plotObject.color2)) + return true; + for (PlotObject plotObject : pp.getAllPlotObjects()) + if (plotObject != null && (isColored(plotObject.color) || isColored(plotObject.color2))) + return true; + return false; + } + + /** Whether a color is non-grayscale, which requires color (not grayscale) for the plot */ + boolean isColored(Color c) { + if (c == null) return false; + return c.getRed() != c.getGreen() || c.getGreen() != c.getBlue(); + } + + /** Draws the plot contents (all PlotObjects and the frame and legend), without axes etc. */ + void drawContents(ImageProcessor ip) { + makeRangeGetSteps(); + ip.setColor(Color.black); + ip.setLineWidth(sc(1)); + float lineWidth = 1; + Color color = Color.black; + Font font = defaultFont; + + // draw all the plot objects in the sequence they were added, except for the one of the constructor + for (PlotObject plotObject : allPlotObjects) + if (!plotObject.hasFlag(PlotObject.CONSTRUCTOR_DATA)) { + //properties lineWidth, Font, Color set for one object remain for the next object unless changed + if (plotObject.lineWidth > 0) + lineWidth = plotObject.lineWidth; + else + plotObject.lineWidth = lineWidth; + if (plotObject.color != null) + color = plotObject.color; + else + plotObject.color = color; + if (plotObject.getFont() != null) + font = plotObject.getFont(); + else + plotObject.setFont(font); + //IJ.log("type="+plotObject.type+" color="+plotObject.color); + drawPlotObject(plotObject, ip); + } + + // draw the line passed with the constructor last, using the settings present when calling 'draw' + if (allPlotObjects.size()>0 && allPlotObjects.get(0).hasFlag(PlotObject.CONSTRUCTOR_DATA)) { + PlotObject mainPlotObject = allPlotObjects.get(0); + if (mainPlotObject.lineWidth == 0) + mainPlotObject.lineWidth = currentLineWidth == 0 ? 1 : currentLineWidth; + lineWidth = mainPlotObject.lineWidth; + if (mainPlotObject.color == null) + mainPlotObject.color = currentColor == null ? Color.black : currentColor; + drawPlotObject(mainPlotObject, ip); + } else { + if (currentLineWidth > 0) lineWidth = currentLineWidth; //linewidth when drawing determines frame linewidth + } + + // finally draw the frame & legend + if (!plotDrawn && pp.frame.lineWidth==DEFAULT_FRAME_LINE_WIDTH) { //when modifying PlotObjects styles later, don't change the frame line width any more + pp.frame.lineWidth = lineWidth; + if (pp.frame.lineWidth == 0) pp.frame.lineWidth = 1; + if (pp.frame.lineWidth > 3) pp.frame.lineWidth = 3; + } + ip.setLineWidth(sc(pp.frame.lineWidth)); + ip.setColor(pp.frame.color); + int x2 = frame.x + frame.width - 1; + int y2 = frame.y + frame.height - 1; + ip.moveTo(frame.x, frame.y); // draw the frame. Can't use ip.drawRect because it is inconsistent for different lineWidths + ip.lineTo(x2, frame.y); + ip.lineTo(x2, y2); + ip.lineTo(frame.x, y2); + ip.lineTo(frame.x, frame.y); + if (pp.legend != null && (pp.legend.flags & LEGEND_POSITION_MASK) != 0) + drawPlotObject(pp.legend, ip); + + plotDrawn = true; + } + + /** Creates the processor if not existing, clears the background and prepares + * it for plotting. Also called by the PlotWindow class to prepare the window. */ + ImageProcessor getBlankProcessor() { + makeMarginValues(); + //IJ.log("Plot.getBlankPr preferredH="+preferredPlotHeight+" pp.h="+pp.height); + if (pp.width <= 0 || pp.height <= 0) { + pp.width = sc(preferredPlotWidth) + leftMargin + rightMargin; + pp.height = sc(preferredPlotHeight) + topMargin + bottomMargin; + } + frameWidth = pp.width - (leftMargin + rightMargin); + frameHeight = pp.height - (topMargin + bottomMargin); + boolean isColored = isColored(); //color, not grayscale required? + if (ip == null || pp.width != ip.getWidth() || pp.height != ip.getHeight() || (isColored != (ip instanceof ColorProcessor))) { + if (isColored) { + ip = new ColorProcessor(pp.width, pp.height); + } else { + ip = new ByteProcessor(pp.width, pp.height); + invertedLut = Prefs.useInvertingLut && !Interpreter.isBatchMode() && IJ.getInstance()!=null; + if (invertedLut) ip.invertLut(); + } + if (imp != null && stack == null) + imp.setProcessor(ip); + } + if (ip instanceof ColorProcessor) + Arrays.fill((int[])(ip.getPixels()), 0xffffff); + else + Arrays.fill((byte[])(ip.getPixels()), invertedLut ? (byte)0 : (byte)0xff); + + ip.setFont(scFont(defaultFont)); + ip.setLineWidth(sc(1)); + ip.setAntialiasedText(pp.antialiasedText); + frame = new Rectangle(leftMargin, topMargin, frameWidth+1, frameHeight+1); + if (pp.frame.color2 != null) { //background color + ip.setColor(pp.frame.color2); + ip.setRoi(frame); + ip.fill(); + ip.resetRoi(); + } + ip.setColor(Color.black); + return ip; + } + + /** Calculates the margin sizes and sets the class variables accordingly */ + void makeMarginValues() { + Font font = nonNullFont(pp.frame.getFont(), currentFont); + float marginScale = 0.1f + 0.9f*font.getSize2D()/12f; + if (marginScale < 0.7f) marginScale = 0.7f; + if (marginScale > 2f) marginScale = 2f; + int addHspace = (int)Tools.getNumberFromList(pp.frame.options, "addhspace="); //user-defined extra space + int addVspace = (int)Tools.getNumberFromList(pp.frame.options, "addvspace="); + leftMargin = sc(LEFT_MARGIN*marginScale + addHspace); + rightMargin = sc(RIGHT_MARGIN*marginScale + addHspace); + topMargin = sc(TOP_MARGIN*marginScale + addVspace); + bottomMargin = sc(BOTTOM_MARGIN*marginScale + 2 + addVspace); + if(pp != null && pp.xLabel != null && pp.xLabel.getFont() != null){ + float numberSize = font.getSize2D(); + float labelSize = pp.xLabel.getFont().getSize2D(); + float extraHeight = 1.5f *(labelSize - numberSize); + if(extraHeight > 0){ + bottomMargin += sc(extraHeight); + leftMargin += sc(extraHeight); + } + } + } + + /** Calculate the actual range, major step interval and set variables for data <-> pixels scaling */ + double[] makeRangeGetSteps() { + steps = new double[2]; + logXAxis = hasFlag(X_LOG_NUMBERS); + logYAxis = hasFlag(Y_LOG_NUMBERS); + + for (int i=0; i MIN_LOG_RATIO || 1./rangeRatio > MIN_LOG_RATIO) || + !(currentMinMax[i] > 10*Float.MIN_VALUE) || !(currentMinMax[i+1] > 10*Float.MIN_VALUE)) + logAxis = false; + } + //for log axes, temporarily work on the logarithm + if (logAxis) { + currentMinMax[i] = Math.log10(currentMinMax[i]); + currentMinMax[i+1] = Math.log10(currentMinMax[i+1]); + } + // calculate grid or major tick interval + if ((i==0 && !simpleXAxis()) || (i==2 && !simpleYAxis())) { + int minGridspacing = i==0 ? MIN_X_GRIDSPACING : MIN_Y_GRIDSPACING; + int frameSize = i==0 ? frameWidth : frameHeight; + double step = Tools.getNumberFromList(pp.frame.options, i==0 ? "xinterval=" : "yinterval="); //user-defined interval + if (!Double.isNaN(step)) { + int nSteps = (int)(Math.floor(currentMinMax[i+1]/step+1e-10) - Math.ceil(currentMinMax[i]/step-1e-10)); + if (nSteps < 1) step = Double.NaN; //user-suppied interval too large, less than two numbers would be shown + if ((i==0 && nSteps*sc(minGridspacing)*0.5 > frameSize) || i!=0 && nSteps*sc(pp.frame.getFont().getSize()) > frameSize) + step = Double.NaN; //user-suppied interval too small, too many numbers would be shown + } + if (Double.isNaN(step)) { //automatic interval + step = Math.abs((currentMinMax[i+1] - currentMinMax[i]) * + Math.max(1.0/maxIntervals, (float)sc(minGridspacing)/frameSize+(maxIntervals>12 ? 0.02 : 0.06))); //the smallest allowable step + step = niceNumber(step); + } + if (logAxis && step < 1) + step = 1; + steps[i/2] = step; + //modify limits to grid or minor ticks if desired + boolean force2grid = hasFlag(i==0 ? X_FORCE2GRID : Y_FORCE2GRID) && !ignoreForce2Grid; + if (force2grid) { + int i1 = (int)Math.floor(Math.min(currentMinMax[i],currentMinMax[i+1])/step+1.e-10); + int i2 = (int)Math.ceil (Math.max(currentMinMax[i],currentMinMax[i+1])/step-1.e-10); + if (currentMinMax[i+1] > currentMinMax[i]) { // care about inverted axes with max= 0.999) { //don't snap on log axis if minor ticks are not full decades + // currentMinMax[i] = stepForSnap * Math.round(currentMinMax[i]/stepForSnap); + // currentMinMax[i+1] = stepForSnap * Math.round(currentMinMax[i+1]/stepForSnap); + // } + } + } + if (i==0) { + xMin = currentMinMax[i]; + xMax = currentMinMax[i+1]; + logXAxis = logAxis; + } else { + yMin = currentMinMax[i]; + yMax = currentMinMax[i+1]; + logYAxis = logAxis; + } + if (logAxis) { + currentMinMax[i] = Math.pow(10, currentMinMax[i]); + currentMinMax[i+1] = Math.pow(10, currentMinMax[i+1]); + } + } + //snapToMinorGrid = false; + ignoreForce2Grid = false; + + // calculate what we need to convert the data to screen pixels + xBasePxl = leftMargin; + yBasePxl = topMargin + frameHeight; + xScale = frameWidth/(xMax-xMin); + if (!(xMax-xMin!=0.0)) //if range==0 (all data the same), or NaN shift zero level so one can see the curve + xBasePxl += sc(10); + yScale = frameHeight/(yMax-yMin); + if (!(yMax-yMin!=0.0)) + yBasePxl -= sc(10); + //IJ.log("x,yScale="+(float)xScale+","+(float)yScale+" xMin,max="+(float)xMin+","+(float)xMax+" yMin.max="+(float)yMin+","+(float)yMax); + + drawAxesTicksGridNumbers(steps); + return steps; + } + + public void redrawGrid(){ + if (ip != null) { + ip.setColor(Color.black); + drawAxesTicksGridNumbers(steps); + ip.setColor(Color.black); + } + } + + /** Gets the initial plot limits (i.e., x&y ranges). For compatibility with previous versions of ImageJ, + * only the first PlotObject (with numeric data) is used to determine the limits. */ + void getInitialMinAndMax() { + int axisRangeFlags = 0; + if (Double.isNaN(defaultMinMax[0])) axisRangeFlags |= X_RANGE; + if (Double.isNaN(defaultMinMax[2])) axisRangeFlags |= Y_RANGE; + if (axisRangeFlags != 0) { + defaultMinMax = getMinAndMax(false, axisRangeFlags); + enlargeRange(defaultMinMax); + } + setLimitsToDefaults(false); //use the range values to start with, but don't draw yet + } + + /** Gets the minimum and maximum values from the first XY_DATA or ARROWS plotObject or all such plotObjects; + * axisRangeFlags determine for which axis to calculate the min&max (X_RANGE for x axis, Y_RANGE for y axis); + * for the other axes the limit is taken from defaultMinMax + * Array elements returned are xMin, xMax, yMin, yMax. Also sets enlargeRange to tell which limits should be enlarged + * beyond the minimum or maximum of the data */ + double[] getMinAndMax(boolean allObjects, int axisRangeFlags) { + boolean invertedXAxis = currentMinMax[1] < currentMinMax[0]; + boolean invertedYAxis = currentMinMax[3] < currentMinMax[2]; + double xSign = invertedXAxis ? -1 : 1; + double ySign = invertedYAxis ? -1 : 1; + double[] allMinMax = new double[]{xSign*Double.MAX_VALUE, -xSign*Double.MAX_VALUE, ySign*Double.MAX_VALUE, -ySign*Double.MAX_VALUE}; + for (int i=0; i>i/2) & 1)==0) //keep default min & max for this axis + allMinMax[i] = defaultMinMax[i]; + enlargeRange = new int[allMinMax.length]; + for (PlotObject plotObject : allPlotObjects) { + if ((plotObject.type == PlotObject.XY_DATA || plotObject.type == PlotObject.ARROWS) && !plotObject.hasFlag(PlotObject.HIDDEN)) { + getMinAndMax(allMinMax, enlargeRange, plotObject, axisRangeFlags); + if (!allObjects) break; + } + } + if ((axisRangeFlags & X_RANGE) != 0) { + String[] xCats = labelsInBraces('x'); // if we have categories at the axis, make some space for this text + if (xCats != null) { + allMinMax[0] = Math.min(allMinMax[0], -0.5); + allMinMax[1] = Math.min(allMinMax[1], xCats.length+0.5); + } + } + if ((axisRangeFlags & Y_RANGE) != 0) { + String[] yCats = labelsInBraces('y'); + if (yCats != null) { + allMinMax[2] = Math.min(allMinMax[2], -0.5); + allMinMax[3] = Math.min(allMinMax[3], yCats.length+0.5); + } + } + if (allMinMax[0]==Double.MAX_VALUE && allMinMax[1]==-Double.MAX_VALUE) { // no x values at all? keep previous + allMinMax[0] = defaultMinMax[0]; + allMinMax[1] = defaultMinMax[1]; + } + if (allMinMax[2]==Double.MAX_VALUE && allMinMax[3]==-Double.MAX_VALUE) { // no y values at all? keep previous + allMinMax[2] = defaultMinMax[2]; + allMinMax[3] = defaultMinMax[3]; + } + return allMinMax; + } + + /** Enlarges the current minimum and maximum ranges to include the data range of the last plotObject added, + * if it is an XY_DATA or ARROWS plotObject. + * Does not set the new limits as default, does not redraw the plot. */ + void fitRangeToLastPlotObject() { + if (allPlotObjects.size() < 1) return; + PlotObject plotObject = allPlotObjects.lastElement(); + if (Double.isNaN(currentMinMax[0]) || Double.isNaN(currentMinMax[2])) { // no range determined yet? + setLimitsToFit(false); + } else { //we have min&max already, just extend the range if necessary + enlargeRange = new int[currentMinMax.length]; + getMinAndMax(currentMinMax, enlargeRange, plotObject, ALL_AXES_RANGE); + enlargeRange(currentMinMax); + } + } + + /** Gets the minimum and maximum values from an XY_DATA or ARROWS plotObject; + * axisRangeFlags determine for which axis (X_RANGE for x axis, Y_RANGE for y axis) + * The minimum modifies allMinAndMax[0] (x), allMinAndMax[2] (y); the maximum modifies [1], [3]. + * If allMinAndMax values are modified, the corresponding enlargeRange array elements are also set */ + void getMinAndMax(double[] allMinAndMax, int[] enlargeRange, PlotObject plotObject, int axisRangeFlags) { + boolean invertedXAxis = currentMinMax[1] < currentMinMax[0]; + boolean invertedYAxis = currentMinMax[3] < currentMinMax[2]; + if (plotObject.type == PlotObject.XY_DATA) { + if ((axisRangeFlags & X_RANGE) != 0) { + int suggestedEnlarge = 0; + if (!(plotObject.shape == LINE || plotObject.shape == FILLED) || plotObject.yEValues != null) + suggestedEnlarge = ALWAYS_ENLARGE; //enlarge to make space at the obrders (we don't try to keep x=0 at the frame border) + getMinAndMax(allMinAndMax, enlargeRange, suggestedEnlarge, 0, plotObject.xValues, plotObject.xEValues, invertedXAxis); + if ((plotObject.shape == BAR || plotObject.shape == SEPARATED_BAR)&& plotObject.xValues.length > 1) { + int n = plotObject.xValues.length; + allMinAndMax[0] -= 0.5 * Math.abs(plotObject.xValues[1] - plotObject.xValues[0]); + allMinAndMax[1] += 0.5 * Math.abs(plotObject.xValues[n - 1] - plotObject.xValues[n - 2]); + } + } + if ((axisRangeFlags & Y_RANGE) != 0) { + int suggestedEnlarge = 0; + if (plotObject.shape==DOT || plotObject.xEValues != null) //these can't be seen if merging with the frame + suggestedEnlarge = ALWAYS_ENLARGE; + else if (!(plotObject.shape == LINE || plotObject.shape == FILLED)) + suggestedEnlarge = USUALLY_ENLARGE; + getMinAndMax(allMinAndMax, enlargeRange, suggestedEnlarge, 2, plotObject.yValues, plotObject.yEValues, invertedYAxis); + if ((plotObject.shape == BAR || plotObject.shape == SEPARATED_BAR) && + (allMinAndMax[2] > 0 && allMinAndMax[3]/allMinAndMax[2] >= 2) && !logYAxis) + allMinAndMax[2] = 0; // for bar plots, y min = 0 unless values differ less than a factor of 2 + } + } else if (plotObject.type == PlotObject.ARROWS) { + if ((axisRangeFlags & X_RANGE) != 0) { + getMinAndMax(allMinAndMax, enlargeRange, ALWAYS_ENLARGE, 0, plotObject.xValues, null, invertedXAxis); + getMinAndMax(allMinAndMax, enlargeRange, ALWAYS_ENLARGE, 0, plotObject.xEValues, null, invertedXAxis); + } + if ((axisRangeFlags & Y_RANGE) != 0) { + getMinAndMax(allMinAndMax, enlargeRange, ALWAYS_ENLARGE, 2, plotObject.yValues, null, invertedYAxis); + getMinAndMax(allMinAndMax, enlargeRange, ALWAYS_ENLARGE, 2, plotObject.yEValues, null, invertedYAxis); + } + } + } + + /** Gets the minimum and maximum values for a dataset (one direction, x or y), + * taking error bars (if not null) into account. + * The minimum modifies allMinAndMax[axisIndex] the maximum modifies allMinAndMax[axisIndex+1]. + * Also cares about whether the range should be enlarged to avoid hiding markers at the borders: + * suggestedEnlarge is 0 for lines or a suggestion for the data type; if the allMinAndMax is + * range is extended, the corresponding enlargeRange item is set accordingly */ + void getMinAndMax(double[] allMinAndMax, int[] enlargeRange, int suggestedEnlarge, + int axisIndex, float[] data, float[] errorBars, boolean invertedAxis) { + int nMinEqual = 0, nMaxEqual = 0; + int minIndex = invertedAxis ? axisIndex+1 : axisIndex; // index of 'min' value in allMinAndMax, enlargeRange + int maxIndex = invertedAxis ? axisIndex : axisIndex+1; + for (int i=0; i0 && i allMinAndMax[maxIndex]) { + allMinAndMax[maxIndex] = v2; + nMaxEqual = 1; + enlargeRange[maxIndex] = suggestedEnlarge; + if (suggestedEnlarge == 0 && ((i>0 && i10%) at min or max? Add extra space at borders ('usually', i.e. unless limit is zero) + if (enlargeRange[minIndex] == 0 && nMinEqual > 2 && nMinEqual*10 > data.length) + enlargeRange[minIndex] = USUALLY_ENLARGE; + if (enlargeRange[maxIndex] == 0 && nMaxEqual > 2 && nMaxEqual*10 > data.length) + enlargeRange[maxIndex] = USUALLY_ENLARGE; + //all data at min or max? Always add space to avoid hiding the line behind the frame + if (nMinEqual == data.length) + enlargeRange[minIndex] = ALWAYS_ENLARGE; + if (nMaxEqual == data.length) + enlargeRange[maxIndex] = ALWAYS_ENLARGE; + //same min or max as for current data set found already previously, but not asking yet for added space at borders? + if (nMinEqual>0 && enlargeRange[minIndex]0 && enlargeRange[maxIndex] frame.x && x < frame.x + frame.width; + boolean insideY = y > frame.y && y < frame.y + frame.height; + if (!insideX && !insideY) { + insideX = true; + insideY = true; + x = frame.x + frame.width / 2; + y = frame.y + frame.height / 2; + } + int leftPart = x - frame.x; + int rightPart = frame.x + frame.width - x; + int highPart = y - frame.y; + int lowPart = frame.y + frame.height - y; + + if (insideX) { + currentMinMax[0] = descaleX((int) (x - leftPart / zoomFactor)); + currentMinMax[1] = descaleX((int) (x + rightPart / zoomFactor)); + } + if (insideY) { + currentMinMax[2] = descaleY((int) (y + lowPart / zoomFactor)); + currentMinMax[3] = descaleY((int) (y - highPart / zoomFactor)); + } + updateImage(); + if (wasLogX != logXAxis ){//log-lin was automatically changed + int changedX = (int) scaleXtoPxl(plotX); + int left = changedX - leftPart; + int right = changedX + rightPart; + currentMinMax[0] = descaleX(left); + currentMinMax[1] = descaleX(right); + updateImage(); + } + if (wasLogY != logYAxis){//log-lin was automatically changed + int changedY = (int) scaleYtoPxl(plotY); + int bottom = changedY + lowPart; + int top = changedY + highPart; + currentMinMax[2] = descaleY(bottom); + currentMinMax[3] = descaleY(top); + updateImage(); + } + } + + /** Moves the plot range by a given number of pixels and updates the image */ + void scroll(int dx, int dy) { + if (logXAxis) { + currentMinMax[0] /= Math.pow(10, dx/xScale); + currentMinMax[1] /= Math.pow(10, dx/xScale); + } else { + currentMinMax[0] -= dx/xScale; + currentMinMax[1] -= dx/xScale; + } + if (logYAxis) { + currentMinMax[2] *= Math.pow(10, dy/yScale); + currentMinMax[3] *= Math.pow(10, dy/yScale); + } else { + currentMinMax[2] += dy/yScale; + currentMinMax[3] += dy/yScale; + } + updateImage(); + } + + /** Whether to draw simple axes without ticks, grid and numbers only for min, max*/ + private boolean simpleXAxis() { + return !hasFlag(X_TICKS | X_MINOR_TICKS | X_LOG_TICKS | X_GRID | X_NUMBERS); + } + + private boolean simpleYAxis() { + return !hasFlag(Y_TICKS | Y_MINOR_TICKS | Y_LOG_TICKS | Y_GRID | Y_NUMBERS); + } + + /** Draws ticks, grid and axis label for each tick/grid line. + * The grid or major tick spacing in each direction is given by steps */ + void drawAxesTicksGridNumbers(double[] steps) { + + if (ip==null) + return; + String[] xCats = labelsInBraces('x'); // create categories for the axes (if any) + String[] yCats = labelsInBraces('y'); + String multiplySymbol = getMultiplySymbol(); // for scientific notation + Font scFont = scFont(pp.frame.getFont()); + Font scFontMedium = scFont.deriveFont(scFont.getSize2D()*10f/12f); //for axis numbers if full size does not fit + Font scFontSmall = scFont.deriveFont(scFont.getSize2D()*9f/12f); //for subscripts + ip.setFont(scFont); + FontMetrics fm = ip.getFontMetrics(); + int fontAscent = fm.getAscent(); + ip.setJustification(LEFT); + // --- A l o n g X A x i s + int yOfXAxisNumbers = topMargin + frameHeight + fm.getHeight()*5/4 + sc(2); + if (hasFlag(X_NUMBERS | (logXAxis ? (X_TICKS | X_MINOR_TICKS) : X_LOG_TICKS) + X_GRID)) { + Font baseFont = scFont; + boolean majorTicks = logXAxis ? hasFlag(X_LOG_TICKS) : hasFlag(X_TICKS); + boolean minorTicks = hasFlag(X_MINOR_TICKS); + minorTicks = minorTicks && (xCats == null); + double step = steps[0]; + int i1 = (int)Math.ceil (Math.min(xMin, xMax)/step-1.e-10); + int i2 = (int)Math.floor(Math.max(xMin, xMax)/step+1.e-10); + int suggestedDigits = (int)Tools.getNumberFromList(pp.frame.options, "xdecimals="); //is not given, NaN cast to 0 + int digits = getDigits(xMin, xMax, step, 7, suggestedDigits); + int y1 = topMargin; + int y2 = topMargin + frameHeight; + if (xMin==xMax) { + if (hasFlag(X_NUMBERS)) { + String s = IJ.d2s(xMin,getDigits(xMin, 0.001*xMin, 5, suggestedDigits)); + int y = yBasePxl; + ip.drawString(s, xBasePxl-ip.getStringWidth(s)/2, yOfXAxisNumbers); + } + } else { + if (hasFlag(X_NUMBERS)) { + int w1 = ip.getStringWidth(IJ.d2s(currentMinMax[0], logXAxis ? -1 : digits)); + int w2 = ip.getStringWidth(IJ.d2s(currentMinMax[1], logXAxis ? -1 : digits)); + int wMax = Math.max(w1,w2); + if (wMax > Math.abs(step*xScale)-sc(8)) { + baseFont = scFontMedium; //small font if there is not enough space for the numbers + ip.setFont(baseFont); + } + } + + for (int i=0; i<=(i2-i1); i++) { + double v = (i+i1)*step; + int x = (int)Math.round((v - xMin)*xScale) + leftMargin; + + if (xCats!= null) { + int index = (int) v; + double remainder = Math.abs(v - Math.round(v)); + if(index >= 0 && index < xCats.length && remainder < 1e-9){ + String s = xCats[index]; + String[] parts = s.split("\n"); + int w = 0; + for(int jj = 0; jj < parts.length; jj++) + w = Math.max(w, ip.getStringWidth(parts[jj])); + + ip.drawString(s, x-w/2, yOfXAxisNumbers); + //ip.drawString(s, x-ip.getStringWidth(s)/2, yOfXAxisNumbers); + } + continue; + } + + if (hasFlag(X_GRID)) { + ip.setColor(gridColor); + ip.drawLine(x, y1, x, y2); + ip.setColor(Color.black); + } + if (majorTicks) { + ip.drawLine(x, y1, x, y1+sc(tickLength)); + ip.drawLine(x, y2, x, y2-sc(tickLength)); + } + if (hasFlag(X_NUMBERS)) { + if (logXAxis || digits<0) { + drawExpString(logXAxis ? Math.pow(10,v) : v, logXAxis ? -1 : -digits, + x, yOfXAxisNumbers-fontAscent/2, CENTER, fontAscent, baseFont, scFontSmall, multiplySymbol); + } else { + String s = IJ.d2s(v,digits); + ip.drawString(s, x-ip.getStringWidth(s)/2, yOfXAxisNumbers); + } + } + } + boolean haveMinorLogNumbers = i2-i1 < 2; //nunbers on log minor ticks only if < 2 decades + if (minorTicks && (!logXAxis || step > 1.1)) { //'standard' log minor ticks only for full decades + double mstep = niceNumber(step*0.19); //non-log: 4 or 5 minor ticks per major tick + double minorPerMajor = step/mstep; + if (Math.abs(minorPerMajor-Math.round(minorPerMajor)) > 1e-10) //major steps are not an integer multiple of minor steps? (e.g. user step 90 deg) + mstep = step/4; + if (logXAxis && mstep < 1) mstep = 1; + i1 = (int)Math.ceil (Math.min(xMin,xMax)/mstep-1.e-10); + i2 = (int)Math.floor(Math.max(xMin,xMax)/mstep+1.e-10); + for (int i=i1; i<=i2; i++) { + double v = i*mstep; + int x = (int)Math.round((v - xMin)*xScale) + leftMargin; + ip.drawLine(x, y1, x, y1+sc(minorTickLength)); + ip.drawLine(x, y2, x, y2-sc(minorTickLength)); + } + } else if (logXAxis && majorTicks && Math.abs(xScale)>sc(MIN_X_GRIDSPACING)) { //minor ticks for log + int minorNumberLimit = haveMinorLogNumbers ? (int)(0.12*Math.abs(xScale)/(fm.charWidth('0')+sc(2))) : 0; //more numbers on minor ticks when zoomed in + i1 = (int)Math.floor(Math.min(xMin,xMax)-1.e-10); + i2 = (int)Math.ceil (Math.max(xMin,xMax)+1.e-10); + for (int i=i1; i<=i2; i++) { + for (int m=2; m<10; m++) { + double v = i+Math.log10(m); + if (v > Math.min(xMin,xMax) && v < Math.max(xMin,xMax)) { + int x = (int)Math.round((v - xMin)*xScale) + leftMargin; + ip.drawLine(x, y1, x, y1+sc(minorTickLength)); + ip.drawLine(x, y2, x, y2-sc(minorTickLength)); + if (m<=minorNumberLimit) + drawExpString(Math.pow(10,v), 0, x, yOfXAxisNumbers-fontAscent/2, CENTER, + fontAscent, baseFont, scFontSmall, multiplySymbol); + } + } + } + } + } + } + // --- A l o n g Y A x i s + ip.setFont(scFont); + int maxNumWidth = 0; + int xNumberRight = leftMargin-sc(2)-ip.getStringWidth("0")/2; + Rectangle rect = ip.getStringBounds("0169"); + int yNumberOffset = -rect.y-rect.height/2; + if (hasFlag(Y_NUMBERS | (logYAxis ? (Y_TICKS | Y_MINOR_TICKS) : Y_LOG_TICKS) + Y_GRID)) { + ip.setJustification(RIGHT); + Font baseFont = scFont; + boolean majorTicks = logYAxis ? hasFlag(Y_LOG_TICKS) : hasFlag(Y_TICKS); + boolean minorTicks = logYAxis ? hasFlag(Y_LOG_TICKS) : hasFlag(Y_MINOR_TICKS); + minorTicks = minorTicks && (yCats == null); + double step = steps[1]; + int i1 = (int)Math.ceil (Math.min(yMin, yMax)/step-1.e-10); + int i2 = (int)Math.floor(Math.max(yMin, yMax)/step+1.e-10); + int suggestedDigits = (int)Tools.getNumberFromList(pp.frame.options, "ydecimals="); //is not given, NaN cast to 0 + int digits = getDigits(yMin, yMax, step, 5, suggestedDigits); + int x1 = leftMargin; + int x2 = leftMargin + frameWidth; + if (yMin==yMax) { + if (hasFlag(Y_NUMBERS)) { + String s = IJ.d2s(yMin,getDigits(yMin, 0.001*yMin, 5, suggestedDigits)); + maxNumWidth = ip.getStringWidth(s); + int y = yBasePxl; + ip.drawString(s, xNumberRight, y+fontAscent/2+sc(1)); + } + } else { + int digitsForWidth = logYAxis ? -1 : digits; + if (digitsForWidth < 0) { + digitsForWidth--; //"1.0*10^5" etc. needs more space than 1.0*5, simulate by adding one decimal + xNumberRight += sc(1)+ip.getStringWidth("0")/4; + } + String str1 = IJ.d2s(currentMinMax[2], digitsForWidth); + String str2 = IJ.d2s(currentMinMax[3], digitsForWidth); + if (digitsForWidth < 0) { + str1 = str1.replaceFirst("E",multiplySymbol); + str2 = str2.replaceFirst("E",multiplySymbol); + } + int w1 = ip.getStringWidth(str1); + int w2 = ip.getStringWidth(str2); + int wMax = Math.max(w1,w2); + if (hasFlag(Y_NUMBERS)) { + if (wMax > xNumberRight - sc(4) - (pp.yLabel.label.length()>0 ? fm.getHeight() : 0)) { + baseFont = scFontMedium; //small font if there is not enough space for the numbers + ip.setFont(baseFont); + } + } + //IJ.log(IJ.d2s(currentMinMax[2],digits)+": w="+w1+"; "+IJ.d2s(currentMinMax[3],digits)+": w="+w2+baseFont+" Space="+(leftMargin-sc(4+5)-fm.getHeight())); + for (int i=i1; i<=i2; i++) { + double v = step==0 ? yMin : i*step; + int y = topMargin + frameHeight - (int)Math.round((v - yMin)*yScale); + + if (yCats != null){ + int index = (int) v; + double remainder = Math.abs(v - Math.round(v)); + if(index >= 0 && index < yCats.length && remainder < 1e-9){ + String s = yCats[index]; + int multiLineOffset = 0; // multi-line cat labels + for(int jj = 0; jj < s.length(); jj++) + if(s.charAt(jj) == '\n') + multiLineOffset -= rect.height/2; + + ip.drawString(s, xNumberRight, y+yNumberOffset+ multiLineOffset); + } + continue; + } + + if (hasFlag(Y_GRID)) { + ip.setColor(gridColor); + ip.drawLine(x1, y, x2, y); + ip.setColor(Color.black); + } + if (majorTicks) { + ip.drawLine(x1, y, x1+sc(tickLength), y); + ip.drawLine(x2, y, x2-sc(tickLength), y); + } + if (hasFlag(Y_NUMBERS)) { + int w = 0; + if (logYAxis || digits<0) { + w = drawExpString(logYAxis ? Math.pow(10,v) : v, logYAxis ? -1 : -digits, + xNumberRight, y, RIGHT, fontAscent, baseFont, scFontSmall, multiplySymbol); + } else { + String s = IJ.d2s(v,digits); + w = ip.getStringWidth(s); + ip.drawString(s, xNumberRight, y+yNumberOffset); + } + if (w > maxNumWidth) maxNumWidth = w; + } + } + boolean haveMinorLogNumbers = i2-i1 < 2; //numbers on log minor ticks only if < 2 decades + if (minorTicks && (!logYAxis || step > 1.1)) { //'standard' log minor ticks only for full decades + double mstep = niceNumber(step*0.19); //non-log: 4 or 5 minor ticks per major tick + double minorPerMajor = step/mstep; + if (Math.abs(minorPerMajor-Math.round(minorPerMajor)) > 1e-10) //major steps are not an integer multiple of minor steps? (e.g. user step 90 deg) + mstep = step/4; + if (logYAxis && step < 1) mstep = 1; + i1 = (int)Math.ceil (Math.min(yMin,yMax)/mstep-1.e-10); + i2 = (int)Math.floor(Math.max(yMin,yMax)/mstep+1.e-10); + for (int i=i1; i<=i2; i++) { + double v = i*mstep; + int y = topMargin + frameHeight - (int)Math.round((v - yMin)*yScale); + ip.drawLine(x1, y, x1+sc(minorTickLength), y); + ip.drawLine(x2, y, x2-sc(minorTickLength), y); + } + } + if (logYAxis && majorTicks && Math.abs(yScale)>sc(MIN_X_GRIDSPACING)) { //minor ticks for log within the decade + int minorNumberLimit = haveMinorLogNumbers ? (int)(0.4*Math.abs(yScale)/fm.getHeight()) : 0; //more numbers on minor ticks when zoomed in + i1 = (int)Math.floor(Math.min(yMin,yMax)-1.e-10); + i2 = (int)Math.ceil(Math.max(yMin,yMax)+1.e-10); + for (int i=i1; i<=i2; i++) { + for (int m=2; m<10; m++) { + double v = i+Math.log10(m); + if (v > Math.min(yMin,yMax) && v < Math.max(yMin,yMax)) { + int y = topMargin + frameHeight - (int)Math.round((v - yMin)*yScale); + ip.drawLine(x1, y, x1+sc(minorTickLength), y); + ip.drawLine(x2, y, x2-sc(minorTickLength), y); + if (m<=minorNumberLimit) { + int w = drawExpString(Math.pow(10,v), 0, xNumberRight, y, RIGHT, + fontAscent, baseFont, scFontSmall, multiplySymbol); + if (w > maxNumWidth) maxNumWidth = w; + } + } + } + } + } + } + } + // --- Write min&max of range if simple style without any axis format flags + ip.setFont(scFont); + ip.setJustification(LEFT); + String xLabelToDraw = pp.xLabel.label; + String yLabelToDraw = pp.yLabel.label; + if (simpleYAxis()) { // y-axis min&max + int digits = getDigits(yMin, yMax, 0.001*(yMax-yMin), 6, 0); + String s = IJ.d2s(yMax, digits); + int sw = ip.getStringWidth(s); + if ((sw+sc(4)) > leftMargin) + ip.drawString(s, sc(4), topMargin-sc(4)); + else + ip.drawString(s, leftMargin-ip.getStringWidth(s)-sc(4), topMargin+10); + s = IJ.d2s(yMin, digits); + sw = ip.getStringWidth(s); + if ((sw+4)>leftMargin) + ip.drawString(s, sc(4), topMargin+frame.height); + else + ip.drawString(s, leftMargin-ip.getStringWidth(s)-sc(4), topMargin+frame.height); + if (logYAxis) yLabelToDraw += " (LOG)"; + } + int y = yOfXAxisNumbers; + if (simpleXAxis()) { // x-axis min&max + int digits = getDigits(xMin, xMax, 0.001*(xMax-xMin), 7, 0); + ip.drawString(IJ.d2s(xMin,digits), leftMargin, y); + String s = IJ.d2s(xMax,digits); + ip.drawString(s, leftMargin + frame.width-ip.getStringWidth(s)+6, y); + y -= fm.getHeight(); + if (logXAxis) xLabelToDraw += " (LOG)"; + } else + y += sc(1); + // --- Write x and y axis text labels + if (xCats == null) { + ip.setFont(pp.xLabel.getFont() == null ? scFont : scFont(pp.xLabel.getFont())); + ImageProcessor xLabel = stringToPixels(xLabelToDraw); + if(xLabel != null){ + int xpos = leftMargin+(frame.width-xLabel.getWidth())/2; + int ypos = y + scFont.getSize()/3;//topMargin + frame.height + bottomMargin-xLabel.getHeight(); + ip.insert(xLabel, xpos, ypos); + } + } + if (yCats == null) { + ip.setFont(pp.yLabel.getFont() == null ? scFont : scFont(pp.yLabel.getFont())); + ImageProcessor yLabel = stringToPixels(yLabelToDraw); + if(yLabel != null){ + yLabel = yLabel.rotateLeft(); + int xRightOfYLabel = xNumberRight - maxNumWidth - sc(2); + int xpos = xRightOfYLabel - yLabel.getWidth() - sc(2); + int ypos = topMargin + (frame.height -yLabel.getHeight())/2; + ip.insert(yLabel, xpos, ypos); + } + } + } + + /** Returns the array of categories from an axis label in the form {cat1,cat2,cat3}, or null if not this form + * @param labelCode can be 'x' or 'y', for the x or y axis label*/ + String[] labelsInBraces(char labelCode) { + String s = getLabel(labelCode); + if (s.startsWith("{") && s.endsWith("}")) { + String inBraces = s.substring(1, s.length() - 1); + String[] catLabels = inBraces.split(","); + return catLabels; + } else { + return null; + } + } + + /** Returns the smallest "nice" number >= v. "Nice" numbers are .. 0.5, 1, 2, 5, 10, 20 ... */ + double niceNumber(double v) { + double base = Math.pow(10,Math.floor(Math.log10(v)-1.e-6)); + if (v > 5.0000001*base) return 10*base; + else if (v > 2.0000001*base) return 5*base; + else return 2*base; + } + + /** draw something like 1.2 10^-9; returns the width of the string drawn. + * 'Digits' should be >=0 for drawing the mantissa (=1.38 in this example), negative to draw only 10^exponent + * Currently only supports center justification and right justification (y of center line) + * Fonts baseFont, smallFont should be scaled already + * Returns the width of the String */ + int drawExpString(double value, int digits, int x, int y, int justification, + int fontAscent, Font baseFont, Font smallFont, String multiplySymbol) { + String base = "10"; + String exponent = null; + String s = IJ.d2s(value, digits<=0 ? -1 : -digits); + if (Tools.parseDouble(s) == 0) s = "0"; //don't write 0 as 0*10^0 + int ePos = s.indexOf('E'); + if (ePos < 0) + base = s; //can't have exponential format, e.g. NaN + else { + if (digits>=0) { + base = s.substring(0,ePos); + if (digits == 0) + base = Integer.toString((int)Math.round(Tools.parseDouble(base))); + base += multiplySymbol+"10"; + } + exponent = s.substring(ePos+1); + } + //IJ.log(s+" -> "+base+"^"+exponent+" maxAsc="+fontAscent+" font="+baseFont); + ip.setJustification(RIGHT); + int width = ip.getStringWidth(base); + if (exponent != null) { + ip.setFont(smallFont); + int wExponent = ip.getStringWidth(exponent); + width += wExponent; + if (justification == CENTER) x += width/2; + ip.drawString(exponent, x, y+fontAscent*3/10); + x -= wExponent; + ip.setFont(baseFont); + } + ip.drawString(base, x, y+fontAscent*7/10); + return width; + } + + /** Returns the user-supplied (via setOptions) or default multiplication symbol (middot) */ + String getMultiplySymbol() { + String multiplySymbol = Tools.getStringFromList(pp.frame.options, "msymbol="); + if (multiplySymbol==null) + multiplySymbol = Tools.getStringFromList(pp.frame.options, "multiplysymbol="); + return multiplySymbol != null ? multiplySymbol : MULTIPLY_SYMBOL; + } + + //Returns a pixelMap containting labelStr. + //Uses font of current ImageProcessor. + //Returns null for empty or blank-only strings + //Supports !!subscript!! and ^^superscript^^ + ByteProcessor stringToPixels(String labelStr) { + Font bigFont = ip.getFont(); + Rectangle rect = ip.getStringBounds(labelStr); + int ww = rect.width * 2; + int hh = rect.height * 3;//enough space, will be cropped later + int y0 = rect.height * 2;//base line + if (ww <= 0 || hh <= 0) { + return null; + } + ByteProcessor box = new ByteProcessor(ww, hh); + box.setColor(Color.WHITE); + //box.setColor(Color.LIGHT_GRAY); //make box visible for test + box.fill(); + box.setColor(Color.black); + box.setAntialiasedText(pp.antialiasedText); + if (invertedLut) { + box.invertLut(); + } + box.setFont(bigFont); + + FontMetrics fm = box.getFontMetrics(); + int ascent = fm.getAscent(); + int offSub = ascent / 6; + int offSuper = -ascent / 2; + Font smallFont = bigFont.deriveFont((float) (bigFont.getSize() * 0.7)); + + Rectangle bigBounds = box.getStringBounds(labelStr); + boolean doParse = (labelStr.indexOf("^^") >= 0 || labelStr.indexOf("!!") >= 0); + doParse = doParse && (labelStr.indexOf("^^^") < 0 && labelStr.indexOf("!!!") < 0); + if (!doParse) { + box.drawString(labelStr, 0, y0); + Rectangle cropRect = new Rectangle(bigBounds); + cropRect.y += y0; + box.setRoi(cropRect); + ImageProcessor boxI = box.crop(); + box = boxI.convertToByteProcessor(); + return box; + } + + if (labelStr.endsWith("^^") || labelStr.endsWith("!!")) { + labelStr = labelStr.substring(0, labelStr.length() - 2); + } + if (labelStr.startsWith("^^") || labelStr.startsWith("!!")) { + labelStr = " " + labelStr; + } + + box.setFont(smallFont); + Rectangle smallBounds = box.getStringBounds(labelStr); + box.setFont(bigFont); + int upperBound = y0 + smallBounds.y + offSuper; + int lowerBound = y0 + smallBounds.y + smallBounds.height + offSub; + + int h = fm.getHeight(); + int len = labelStr.length(); + int[] tags = new int[len]; + int nTags = 0; + + for (int jj = 0; jj < len - 2; jj++) {//get positions where font size changes + if (labelStr.substring(jj, jj + 2).equals("^^")) { + tags[nTags++] = jj; + } + if (labelStr.substring(jj, jj + 2).equals("!!")) { + tags[nTags++] = -jj; + } + } + tags[nTags++] = len; + tags = Arrays.copyOf(tags, nTags); + + int leftIndex = 0; + int xRight = 0; + int y2 = y0; + + boolean subscript = labelStr.startsWith("!!"); + for (int pp = 0; pp < tags.length; pp++) {//draw all text fragments + int rightIndex = tags[pp]; + rightIndex = Math.abs(rightIndex); + String part = labelStr.substring(leftIndex, rightIndex); + boolean small = pp % 2 == 1;//toggle odd/even + if (small) { + box.setFont(smallFont); + if (subscript) { + y2 = y0 + offSub; + } else {//superscript: + y2 = y0 + offSuper; + } + } else { + box.setFont(bigFont); + y2 = y0; + } + xRight++; + int partWidth = box.getStringWidth(part); + box.drawString(part, xRight, y2); + leftIndex = rightIndex + 2; + subscript = tags[pp] < 0;//negative positions = subscript + xRight += partWidth; + } + xRight += h / 4; + Rectangle cropRect = new Rectangle(0, upperBound, xRight, lowerBound - upperBound); + box.setRoi(cropRect); + ImageProcessor boxI = box.crop(); + box = boxI.convertToByteProcessor(); + return box; + } + + /** Returns the number of digits to display the number n with resolution 'resolution'; + * (if n is integer and small enough to display without scientific notation, + * no decimals are needed, irrespective of 'resolution') + * Scientific notation is used for more than 'maxDigits' (must be >=3), and indicated + * by a negative return value, or if suggestedDigits is negative + * Returns 'suggestedDigits' if not 0 and compatible with the resolution; negative values of + * 'suggestedDigits' switch to scientific notation. */ + static int getDigits(double n, double resolution, int maxDigits, int suggestedDigits) { + if (n==Math.round(n) && Math.abs(n) < Math.pow(10,maxDigits-1)-1) //integers and not too big + return suggestedDigits; + else + return getDigits2(n, resolution, maxDigits, suggestedDigits); + } + + /** Number of digits to display the range between n1 and n2 with resolution 'resolution'; + * Scientific notation is used for more than 'maxDigits' (must be >=3), and indicated + * by a negative return value + * Returns 'suggestedDigits' if not 0 and compatible with the resolution; negative values of + * 'suggestedDigits' switch to sceintific notation. */ + static int getDigits(double n1, double n2, double resolution, int maxDigits, int suggestedDigits) { + if (n1==0 && n2==0) return suggestedDigits; + return getDigits2(Math.max(Math.abs(n1),Math.abs(n2)), resolution, maxDigits, suggestedDigits); + } + + static int getDigits2(double n, double resolution, int maxDigits, int suggestedDigits) { + if (Double.isNaN(n) || Double.isInfinite(n)) + return 0; //no scientific notation + int log10ofN = (int)Math.floor(Math.log10(Math.abs(n))+1e-7); + int digits = resolution != 0 ? + -(int)Math.floor(Math.log10(Math.abs(resolution))+1e-7) : + Math.max(0, -log10ofN+maxDigits-2); + int sciDigits = -Math.max((log10ofN+digits),1); + //IJ.log("n="+(float)n+"digitsRaw="+digits+" log10ofN="+log10ofN+" sciDigits="+sciDigits); + if ((digits < -2 && log10ofN >= maxDigits) || suggestedDigits < 0) + digits = sciDigits; //scientific notation for large numbers or if desired via suggestedDigits (plot.setOptions) + else if (digits < 0) + digits = 0; + else if (digits > maxDigits-1 && log10ofN < -2) + digits = sciDigits; // scientific notation for small numbers + return digits < 0 ? Math.min(sciDigits, suggestedDigits) : Math.max(digits, suggestedDigits); + } + + static boolean isInteger(double n) { + return n==Math.round(n); + } + + private void drawPlotObject(PlotObject plotObject, ImageProcessor ip) { + //IJ.log("DRAWING type="+plotObject.type+" lineWidth="+plotObject.lineWidth+" shape="+plotObject.shape); + if (plotObject.hasFlag(PlotObject.HIDDEN)) return; + ip.setColor(plotObject.color); + ip.setLineWidth(sc(plotObject.lineWidth)); + int type = plotObject.type; + switch (type) { + case PlotObject.XY_DATA: + ip.setClipRect(frame); + int nPoints = Math.min(plotObject.xValues.length, plotObject.yValues.length); + + if (plotObject.shape==BAR || plotObject.shape==SEPARATED_BAR) + drawBarChart(plotObject); // (separated) bars + + if (plotObject.shape == FILLED) { // filling below line + ip.setColor(plotObject.color2 != null ? plotObject.color2 : plotObject.color); + drawFloatPolyLineFilled(ip, plotObject.xValues, plotObject.yValues, nPoints); + } + ip.setColor(plotObject.color); + ip.setLineWidth(sc(plotObject.lineWidth)); + + if (plotObject.yEValues != null) // error bars in front of bars and fill area below the line, but behind lines and marker symbols + drawVerticalErrorBars(plotObject.xValues, plotObject.yValues, plotObject.yEValues); + if (plotObject.xEValues != null) + drawHorizontalErrorBars(plotObject.xValues, plotObject.yValues, plotObject.xEValues); + + if (plotObject.hasFilledMarker()) { // fill markers with secondary color + int markSize = plotObject.getMarkerSize(); + ip.setColor(plotObject.color2); + ip.setLineWidth(1); + for (int i=0; i0) && (!logYAxis || plotObject.yValues[i]>0) + && !Double.isNaN(plotObject.xValues[i]) && !Double.isNaN(plotObject.yValues[i])) + fillShape(plotObject.shape, scaleX(plotObject.xValues[i]), scaleY(plotObject.yValues[i]), markSize); + ip.setColor(plotObject.color); + ip.setLineWidth(sc(plotObject.lineWidth)); + } + if (plotObject.hasCurve()) { // draw the lines between the points + if (plotObject.shape == CONNECTED_CIRCLES) + ip.setColor(plotObject.color2 == null ? Color.black : plotObject.color2); + drawFloatPolyline(ip, plotObject.xValues, plotObject.yValues, nPoints); + ip.setColor(plotObject.color); + } + if (plotObject.hasMarker()) { // draw the marker symbols + int markSize = plotObject.getMarkerSize(); + ip.setColor(plotObject.color); + Font saveFont = ip.getFont(); + for (int i=0; i0) && (!logYAxis || plotObject.yValues[i]>0) + && !Double.isNaN(plotObject.xValues[i]) && !Double.isNaN(plotObject.yValues[i])) + drawShape(plotObject, scaleX(plotObject.xValues[i]), scaleY(plotObject.yValues[i]), markSize, i); + } + if (plotObject.shape==CUSTOM) + ip.setFont(saveFont); + } + ip.setClipRect(null); + break; + case PlotObject.ARROWS: + ip.setClipRect(frame); + for (int i=0; i sc(MAX_ARROWHEAD_LENGTH)) arrowHeadLength = sc(MAX_ARROWHEAD_LENGTH); + if (arrowHeadLength < sc(MIN_ARROWHEAD_LENGTH)) arrowHeadLength = sc(MIN_ARROWHEAD_LENGTH); + drawArrow(xt1, yt1, xt2, yt2, arrowHeadLength); + } + } + ip.setClipRect(null); + break; + + case PlotObject.SHAPES: + int iBoxWidth = 20; + ip.setClipRect(frame); + String shType = plotObject.shapeType.toLowerCase(); + if (shType.contains("rectangles")) { + int nShapes = plotObject.shapeData.size(); + + for (int i = 0; i < nShapes; i++) { + float[] corners = (float[])(plotObject.shapeData.get(i)); + int x1 = scaleX(corners[0]); + int y1 = scaleY(corners[1]); + int x2 = scaleX(corners[2]); + int y2 = scaleY(corners[3]); + + ip.setLineWidth(sc(plotObject.lineWidth)); + int left = Math.min(x1, x2); + int right = Math.max(x1, x2); + int top = Math.min(y1, y2); + int bottom = Math.max(y1, y2); + + Rectangle r1 = new Rectangle(left, top, right-left, bottom - top); + Rectangle cBox = frame.intersection(r1); + if (plotObject.color2 != null) { + ip.setColor(plotObject.color2); + ip.fillRect(cBox.x, cBox.y, cBox.width, cBox.height); + } + ip.setColor(plotObject.color); + ip.drawRect(cBox.x, cBox.y, cBox.width, cBox.height); + } + ip.setClipRect(null); + break; + } + if (shType.equals("redraw_grid")) { + ip.setLineWidth(sc(1)); + redrawGrid(); + ip.setClipRect(null); + break; + } + if (shType.contains("boxes")) { + + String[] parts = Tools.split(shType); + for (int jj = 0; jj < parts.length; jj++) { + String[] pairs = parts[jj].split("="); + if ((pairs.length == 2) && pairs[0].equals("width")) { + iBoxWidth = Integer.parseInt(pairs[1]); + } + } + boolean horizontal = shType.contains("boxesx"); + int nShapes = plotObject.shapeData.size(); + int halfWidth = Math.round(sc(iBoxWidth / 2)); + for (int i = 0; i < nShapes; i++) { + + float[] coords = (float[])(plotObject.shapeData.get(i)); + + if (!horizontal) { + + int x = scaleX(coords[0]); + int y1 = scaleY(coords[1]); + int y2 = scaleY(coords[2]); + int y3 = scaleY(coords[3]); + int y4 = scaleY(coords[4]); + int y5 = scaleY(coords[5]); + ip.setLineWidth(sc(plotObject.lineWidth)); + + Rectangle r1 = new Rectangle(x - halfWidth, y4, halfWidth * 2, y2 - y4); + Rectangle cBox = frame.intersection(r1); + if (y1 != y2 || y4 != y5)//otherwise omit whiskers + { + ip.drawLine(x, y1, x, y5);//whiskers + } + if (plotObject.color2 != null) { + ip.setColor(plotObject.color2); + ip.fillRect(cBox.x, cBox.y, cBox.width, cBox.height); + } + ip.setColor(plotObject.color); + ip.drawRect(cBox.x, cBox.y, cBox.width, cBox.height); + ip.setClipRect(frame); + ip.drawLine(x - halfWidth, y3, x + halfWidth - 1, y3); + } + + if (horizontal) { + + int y = scaleY(coords[0]); + int x1 = scaleX(coords[1]); + int x2 = scaleX(coords[2]); + int x3 = scaleX(coords[3]); + int x4 = scaleX(coords[4]); + int x5 = scaleX(coords[5]); + ip.setLineWidth(sc(plotObject.lineWidth)); + if(x1 !=x2 || x4 != x5)//otherwise omit whiskers + ip.drawLine(x1, y, x5, y);//whiskers + Rectangle r1 = new Rectangle(x2, y - halfWidth, x4 - x2, halfWidth * 2); + Rectangle cBox = frame.intersection(r1); + if (plotObject.color2 != null) { + ip.setColor(plotObject.color2); + ip.fillRect(cBox.x, cBox.y, cBox.width, cBox.height); + } + ip.setColor(plotObject.color); + ip.drawRect(cBox.x, cBox.y, cBox.width, cBox.height); + ip.setClipRect(frame); + ip.drawLine(x3, y - halfWidth, x3, y + halfWidth - 1); + } + } + ip.setClipRect(null); + break; + } + case PlotObject.LINE: + ip.setClipRect(frame); + ip.drawLine(scaleX(plotObject.x), scaleY(plotObject.y), scaleX(plotObject.xEnd), scaleY(plotObject.yEnd)); + ip.setClipRect(null); + break; + case PlotObject.NORMALIZED_LINE: + ip.setClipRect(frame); + int ix1 = leftMargin + (int)(plotObject.x*frameWidth); + int iy1 = topMargin + (int)(plotObject.y*frameHeight); + int ix2 = leftMargin + (int)(plotObject.xEnd*frameWidth); + int iy2 = topMargin + (int)(plotObject.yEnd*frameHeight); + ip.drawLine(ix1, iy1, ix2, iy2); + ip.setClipRect(null); + break; + case PlotObject.DOTTED_LINE: + ip.setClipRect(frame); + ix1 = scaleX(plotObject.x); + iy1 = scaleY(plotObject.y); + ix2 = scaleX(plotObject.xEnd); + iy2 = scaleY(plotObject.yEnd); + double length = calculateDistance(ix1, ix2, iy1, iy2) + 0.1; + int n = (int)(length/plotObject.step); + for (int i = 0; i<=n; i++) + ip.drawDot(ix1 + (int)Math.round((ix2-ix1)*(double)i/n), iy1 + (int)Math.round((iy2-iy1)*(double)i/n)); + ip.setClipRect(null); + break; + case PlotObject.LABEL: + case PlotObject.NORMALIZED_LABEL: + ip.setJustification(plotObject.justification); + if (plotObject.getFont() != null) + ip.setFont(scFont(plotObject.getFont())); + int xt = type==PlotObject.LABEL ? scaleX(plotObject.x) : leftMargin + (int)(plotObject.x*frameWidth); + int yt = type==PlotObject.LABEL ? scaleY(plotObject.y) : topMargin + (int)(plotObject.y*frameHeight); + ip.drawString(plotObject.label, xt, yt); + break; + case PlotObject.LEGEND: + drawLegend(plotObject, ip); + break; + } + } + + /** Draw a bar at each point */ + void drawBarChart(PlotObject plotObject) { + int n = Math.min(plotObject.xValues.length, plotObject.yValues.length); + String[] xCats = labelsInBraces('x'); // do we have categories at the x axis instead of numbers? + boolean separatedBars = plotObject.shape == SEPARATED_BAR || xCats != null; + int halfBarWidthInPixels = n <= 1 ? Math.max(1, frameWidth/2-2) : 0; + if (separatedBars && n > 1) + halfBarWidthInPixels = Math.max(1, (int)Math.round(Math.abs + (0.5*(plotObject.xValues[n-1] - plotObject.xValues[0])/(n-1) * xScale * SEPARATED_BAR_WIDTH))); + int y0 = scaleYWithOverflow(0); + boolean yZeroInFrame = !logYAxis && yBasePxl>frame.y && yBasePxl 0 ? 0.5f*(plotObject.xValues[i-1]+plotObject.xValues[i]) : + 1.5f*plotObject.xValues[i] - 0.5f*plotObject.xValues[i+1]); + right = scaleX(i < n-1 ? 0.5f*(plotObject.xValues[i]+plotObject.xValues[i+1]) : + 1.5f*plotObject.xValues[i] - 0.5f*plotObject.xValues[i-1]); + } else { + int x = scaleX(plotObject.xValues[i]); + left = x - halfBarWidthInPixels; //separated bars or n<=1 : fixed bar width + right = x + halfBarWidthInPixels; + } + if (left < frame.x) left = frame.x; + if (left > frame.x+frame.width) left = frame.x+frame.width; + if (right < frame.x) right = frame.x; + if (right > frame.x+frame.width) right = frame.x+frame.width; + int y = scaleYWithOverflow(plotObject.yValues[i]); + if (plotObject.color2 != null) { + ip.setColor(plotObject.color2); + for (int x2 = Math.min(left,right); x2 <= Math.max(left,right); x2++) + ip.drawLine(x2, y0, x2, y); //cant use ip.fillRect (ignores the clipRect), so we it fill line by line + } + ip.setColor(plotObject.color); + ip.setLineWidth(sc(plotObject.lineWidth)); + if (separatedBars) { + ip.drawLine(left, y0, left, y); //up + ip.drawLine(left, y, right, y); //right + ip.drawLine(right, y, right, y0); //down + if (yZeroInFrame) + ip.drawLine(left, y0, right, y0);//baseline + } else { + ip.drawLine(left, prevY, left, y); //up or down + ip.drawLine(left, y, right, y); //right + if (i == n - 1) + ip.drawLine(right, y, right, y0);//last down + prevY = y; + } + } + } + + /** Draw the symbol for the data point number 'pointIndex' (pointIndex < 0 when drawing the legend) */ + void drawShape(PlotObject plotObject, int x, int y, int size, int pointIndex) { + int shape = plotObject.shape; + if (shape == DIAMOND) size = (int)(size*1.21); + int xbase = x-sc(size/2); + int ybase = y-sc(size/2); + int xend = x+sc(size/2); + int yend = y+sc(size/2); + if (ip==null) + return; + switch(shape) { + case X: + ip.drawLine(xbase,ybase,xend,yend); + ip.drawLine(xend,ybase,xbase,yend); + break; + case BOX: + ip.drawLine(xbase,ybase,xend,ybase); + ip.drawLine(xend,ybase,xend,yend); + ip.drawLine(xend,yend,xbase,yend); + ip.drawLine(xbase,yend,xbase,ybase); + break; + case TRIANGLE: + ip.drawLine(x,ybase-sc(1),xend+sc(1),yend); //height must be odd, otherwise rounding leads to asymmetric shape + ip.drawLine(x,ybase-sc(1),xbase-sc(1),yend); + ip.drawLine(xend+sc(1),yend,xbase-sc(1),yend); + break; + case CROSS: + ip.drawLine(xbase,y,xend,y); + ip.drawLine(x,ybase,x,yend); + break; + case DIAMOND: + ip.drawLine(xbase,y,x,ybase); + ip.drawLine(x,ybase,xend,y); + ip.drawLine(xend,y,x,yend); + ip.drawLine(x,yend,xbase,y); + break; + case DOT: + ip.drawDot(x, y); //uses current line width + break; + case CUSTOM: + if (plotObject.macroCode==null || frame==null) + break; + if (x=frame.x+frame.width || y>=frame.y+frame.height) + break; + ImagePlus imp = new ImagePlus("", ip); + WindowManager.setTempCurrentImage(imp); + StringBuilder sb = new StringBuilder(140+plotObject.macroCode.length()); + sb.append("x="); sb.append(x); + sb.append(";y="); sb.append(y); + sb.append(";setColor('"); + sb.append(Tools.c2hex(plotObject.color)); + sb.append("');s="); sb.append(sc(1)); + boolean drawingLegend = pointIndex < 0; + double xVal = 0; + double yVal = 0; + if (!drawingLegend) { + xVal = plotObject.xValues[pointIndex]; + yVal = plotObject.yValues[pointIndex]; + } + sb.append(";i="); sb.append(drawingLegend ? 0 : pointIndex); + sb.append(";xval=" + xVal); + sb.append(";yval=" + yVal); + sb.append(";"); + sb.append(plotObject.macroCode); + if (!drawingLegend ||!sb.toString().contains("d2s") ) {// a graphical symbol won't contain "d2s" .. + String rtn = IJ.runMacro(sb.toString());//.. so it can go to the legend + if ("[aborted]".equals(rtn)) + plotObject.macroCode = null; + } + WindowManager.setTempCurrentImage(null); + break; + default: // CIRCLE, CONNECTED_CIRCLES: 5x5 oval approximated by 5x5 square without corners + if (sc(size) < 5.01) { + ip.drawLine(x-1, y-2, x+1, y-2); + ip.drawLine(x-1, y+2, x+1, y+2); + ip.drawLine(x+2, y+1, x+2, y-1); + ip.drawLine(x-2, y+1, x-2, y-1); + } else { + int r = sc(0.5f*size-0.5f); + ip.drawOval(x-r, y-r, 2*r, 2*r); + } + break; + } + } + + /** Fill the area of the symbols for data points (except for shape=DOT) + * Note that ip.fill, ip.fillOval etc. can't be used here: they do not care about the clip rectangle */ + void fillShape(int shape, int x0, int y0, int size) { + if (shape == DIAMOND) size = (int)(size*1.21); + int r = sc(size/2)-1; + switch(shape) { + case BOX: + for (int dy=-r; dy<=r; dy++) + for (int dx=-r; dx<=r; dx++) + ip.drawDot(x0+dx, y0+dy); + break; + case TRIANGLE: + int ybase = y0 - r - sc(1); + int yend = y0 + r; + double halfWidth = sc(size/2)+sc(1)-1; + double hwStep = halfWidth/(yend-ybase+1); + for (int y=yend; y>=ybase; y--, halfWidth -= hwStep) { + int dx = (int)(Math.round(halfWidth)); + for (int x=x0-dx; x<=x0+dx; x++) + ip.drawDot(x,y); + } + break; + case DIAMOND: + ybase = y0 - r - sc(1); + yend = y0 + r; + halfWidth = sc(size/2)+sc(1)-1; + hwStep = halfWidth/(yend-ybase+1); + for (int y=yend; y>=ybase; y--) { + int dx = (int)(Math.round(halfWidth-(hwStep+1)*Math.abs(y-y0))); + for (int x=x0-dx; x<=x0+dx; x++) + ip.drawDot(x,y); + } + break; + case CIRCLE: case CONNECTED_CIRCLES: + int rsquare = (r+1)*(r+1); + for (int dy=-r; dy<=r; dy++) + for (int dx=-r; dx<=r; dx++) + if (dx*dx + dy*dy <= rsquare) + ip.drawDot(x0+dx, y0+dy); + break; + } + } + + /** Adds an arrow from position 1 to 2 given in pixels; 'size' is the length of the arrowhead + * @deprecated Use as a public method is not supported any more because it is incompatible with rescaling */ + @Deprecated + public void drawArrow(int x1, int y1, int x2, int y2, double size) { + double dx = x2 - x1; + double dy = y2 - y1; + double ra = Math.sqrt(dx * dx + dy * dy); + dx /= ra; + dy /= ra; + int x3 = (int) Math.round(x2 - dx * size); //arrow base + int y3 = (int) Math.round(y2 - dy * size); + double r = 0.3 * size; + int x4 = (int) Math.round(x3 + dy * r); + int y4 = (int) Math.round(y3 - dx * r); + int x5 = (int) Math.round(x3 - dy * r); + int y5 = (int) Math.round(y3 + dx * r); + ip.moveTo(x1, y1); ip.lineTo(x2, y2); + ip.moveTo(x4, y4); ip.lineTo(x2, y2); ip.lineTo(x5, y5); + } + + private void drawVerticalErrorBars(float[] x, float[] y, float[] e) { + int nPoints = Math.min(Math.min(x.length, y.length), e.length); + for (int i=0; i0))) continue; + int x0 = scaleX(x[i]); + int yPlus = scaleYWithOverflow(y[i] + e[i]); + int yMinus = scaleYWithOverflow(y[i] - e[i]); + ip.moveTo(x0,yMinus); + ip.lineTo(x0, yPlus); + } + } + + private void drawHorizontalErrorBars(float[] x, float[] y, float[] e) { + int nPoints = Math.min(Math.min(x.length, y.length), e.length); + float[] xpoints = new float[2]; + float[] ypoints = new float[2]; + for (int i=0; i0))) continue; + int y0 = scaleY(y[i]); + int xPlus = scaleXWithOverflow(x[i] + e[i]); + int xMinus = scaleXWithOverflow(x[i] - e[i]); + ip.moveTo(xMinus,y0); + ip.lineTo(xPlus, y0); + } + } + + /** Draw a polygon line; NaN values interrupt it. */ + void drawFloatPolyline(ImageProcessor ip, float[] x, float[] y, int n) { + if (x==null || x.length==0) return; + int x1, y1; + boolean isNaN1; + int x2 = scaleX(x[0]); + int y2 = scaleY(y[0]); + boolean isNaN2 = Float.isNaN(x[0]) || Float.isNaN(y[0]) || (logXAxis && x[0]<=0) || (logYAxis && y[0]<=0);; + for (int i=1; i= frame.x+frame.width && right >= frame.x+frame.width) continue; + if (left < frame.x) left = frame.x; + if (left >= frame.x+frame.width) left = frame.x+frame.width-1; + if (right < frame.x) right = frame.x; + if (right >= frame.x+frame.width) right = frame.x+frame.width-1; + if (left != right) { + for (int xi = Math.min(left,right); xi <= Math.max(left,right); xi++) { + int yi = (int)Math.round(y1 + (double)(y2 - y1)*(double)(xi - x1)/(double)(x2 - x1)); + /* double yMin = Math.min(yF[i-1], yF[i]); + double yMax = Math.max(yF[i-1], yF[i]); + if (y < yMin) y = yMin; // dont extrapolate (in case rounding to pixels falls outside [xi, xi+1] interval) + if (y > yMax) y = yMax;*/ + ip.drawLine(xi, y0, xi, yi); + } + } else { + ip.drawLine(left, y0, left, y2); + } + } + } + + /** Returns only indexed and sorted plot objects, if at least one label is indexed like "1__MyLabel" */ + Vector getIndexedPlotObjects(){ + boolean withIndex = false; + int len = allPlotObjects.size(); + String[] labels = new String[len]; + Vector indexedObjects = new Vector(); + for(int jj = 0; jj < len; jj++){ + PlotObject plotObject = allPlotObjects.get(jj); + labels[jj] = ""; + if (plotObject.type == PlotObject.XY_DATA && !plotObject.hasFlag(PlotObject.HIDDEN) && plotObject.label != null) { + String label = plotObject.label; + if(label.indexOf("__") >=0 && label.indexOf("__") <= 2){ + labels[jj]= plotObject.label; + withIndex = true; + } + } + } + int[] ranks = Tools.rank(labels); + for(int jj = 0; jj < len; jj++){ + if(labels[ranks[jj]] != ""){ + int index = ranks[jj]; + indexedObjects.add(allPlotObjects.get(index)); + } + } + if(!withIndex) + return null; + return indexedObjects; + } + + /** Draw the legend */ + void drawLegend(PlotObject legendObject, ImageProcessor ip) { + ip.setFont(scFont(legendObject.getFont())); + int nLabels = 0; + int maxStringWidth = 0; + float maxLineThickness = 0; + Vector usedPlotObjects = allPlotObjects; + Vector indexedObjects = getIndexedPlotObjects(); + if(indexedObjects != null) + usedPlotObjects= indexedObjects; + + for (PlotObject plotObject : usedPlotObjects) + if (plotObject.type == PlotObject.XY_DATA && !plotObject.hasFlag(PlotObject.HIDDEN) && plotObject.label != null) { //label exists: was set now or previously + nLabels++; + String label = plotObject.label; + if (indexedObjects != null) + label = label.substring(label.indexOf("__") + 2); + int w = ip.getStringWidth(label); + if (w > maxStringWidth) maxStringWidth = w; + if (plotObject.lineWidth > maxLineThickness) maxLineThickness = plotObject.lineWidth; + } + if (nLabels == 0) return; + if (pp.antialiasedText && scale > 1) //fix incorrect width of large fonts + maxStringWidth = (int)((1 + 0.004*scale) * maxStringWidth); + int frameThickness = sc(legendObject.lineWidth > 0 ? legendObject.lineWidth : 1); + FontMetrics fm = ip.getFontMetrics(); + ip.setJustification(LEFT); + int lineHeight = fm.getHeight(); + int height = nLabels*lineHeight + 2*sc(LEGEND_PADDING); + int width = maxStringWidth + sc(3*LEGEND_PADDING + LEGEND_LINELENGTH + maxLineThickness); + int positionCode = legendObject.flags & LEGEND_POSITION_MASK; + if (positionCode == AUTO_POSITION) + positionCode = autoLegendPosition(width, height, frameThickness); + Rectangle rect = legendRect(positionCode, width, height, frameThickness); + int x0 = rect.x; + int y0 = rect.y; + + ip.setColor(Color.white); + ip.setLineWidth(1); + if (!legendObject.hasFlag(LEGEND_TRANSPARENT)) { + ip.setRoi(x0, y0, width, height); + ip.fill(); + } else if (hasFlag(X_GRID | Y_GRID)) { //erase grid + int grid = ip instanceof ColorProcessor ? (gridColor.getRGB() & 0xffffff) : ip.getBestIndex(gridColor); + for (int y=y0; y=0) + label = label.substring(start+2); + } + ip.drawString(label, xText, y+ lineHeight/2); + y += bottomUp ? -lineHeight : lineHeight; + } + } + + /** The legend area; positionCode should be TOP_LEFT, TOP_RIGHT, etc. */ + Rectangle legendRect(int positionCode, int width, int height, int frameThickness) { + boolean leftPosition = positionCode == TOP_LEFT || positionCode == BOTTOM_LEFT; + boolean topPosition = positionCode == TOP_LEFT || positionCode == TOP_RIGHT; + int x0 = (leftPosition) ? + leftMargin + sc(2*LEGEND_PADDING) + frameThickness/2 : + leftMargin + frameWidth - width - sc(2*LEGEND_PADDING) - frameThickness/2; + int y0 = (topPosition) ? + topMargin + sc(LEGEND_PADDING) + frameThickness/2 : + topMargin + frameHeight - height - sc(LEGEND_PADDING) + frameThickness/2; + if (hasFlag(Y_TICKS)) + x0 += (leftPosition ? 1 : -1) * sc(tickLength - LEGEND_PADDING); + if (hasFlag(X_TICKS)) + y0 += (topPosition ? 1 : -1) * sc(tickLength - LEGEND_PADDING/2); + return new Rectangle(x0, y0, width, height); + } + + /** The position code of the legend position where the smallest amount of foreground pixels is covered */ + int autoLegendPosition(int width, int height, int frameThickness) { + int background = ip instanceof ColorProcessor ? (0xffffff) : (ip.isInvertedLut() ? 0 : 0xff); + int grid = ip instanceof ColorProcessor ? (gridColor.getRGB() & 0xffffff) : ip.getBestIndex(gridColor); + int bestPosition = 0; + int minCoveredPixels = Integer.MAX_VALUE; + for (int positionCode : new int[]{TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT}) { + Rectangle rect = legendRect(positionCode, width, height, frameThickness); + int coveredPixels = 0; + for (int y = rect.y - frameThickness/2; y <= rect.y + rect.height + frameThickness/2; y++) + for (int x = rect.x - frameThickness/2; x <= rect.x + rect.width + frameThickness/2; x++) { + int pixel = ip.getPixel(x, y) & 0xffffff; + if (pixel != background && pixel != grid) + coveredPixels ++; + } + if (coveredPixels < minCoveredPixels) { + minCoveredPixels = coveredPixels; + bestPosition = positionCode; + } + } + return bestPosition; + } + + /** Returns the x, y coordinates at the cursor position or the nearest point as a String */ + String getCoordinates(int x, int y) { + if (frame==null) return ""; + String text = ""; + if (!frame.contains(x, y)) + return text; + double xv = descaleX(x); // cursor location + double yv = descaleY(y); + boolean yIsValue = false; + if (!hasMultiplePlots()) { + PlotObject p = getMainCurveObject(); // display x and f(x) instead of cursor y + if (p != null) { + double bestDx = Double.MAX_VALUE; + double xBest = 0, yBest = 0; + for (int i=0; i=0; i--) { + if (allPlotObjects.get(i).type == PlotObject.XY_DATA) + return allPlotObjects.get(i); + } + return null; + } + + /** returns whether there are several plots so that one cannot give a single y value for a given x value */ + private boolean hasMultiplePlots() { + int nPlots = 0; + for (PlotObject plotObject : allPlotObjects) { + if (plotObject.type == PlotObject.ARROWS) + return true; + else if (plotObject.type == PlotObject.XY_DATA) { + nPlots ++; + if (nPlots > 1) return true; + } + } + return nPlots > 1; + } + + public void setPlotMaker(PlotMaker plotMaker) { + this.plotMaker = plotMaker; + } + + PlotMaker getPlotMaker() { + return plotMaker; + } + + /** Returns the labels of the (non-hidden) datasets as linefeed-delimited String. + * If the label is not set, a blank line is added. */ + String getDataLabels() { + String labels = ""; + boolean first = true; + for (PlotObject plotObject : allPlotObjects) + if (plotObject.type == PlotObject.XY_DATA && !plotObject.hasFlag(PlotObject.HIDDEN)) { + if (first) + first = false; + else + labels += '\n'; + if (plotObject.label != null) labels += plotObject.label; + } + return labels; + } + + /** Creates a ResultsTable with the plot data. Returns an empty table if no data. */ + public ResultsTable getResultsTable() { + return getResultsTable(true); + } + + /** Creates a ResultsTable with the data of the plot. Returns null if no data. + * Does not write the first x column if writeFirstXColumn is false. + * When all columns are the same length, x columns equal to the first x column are + * not written, independent of writeFirstXColumn. + * Column headings are "X", "Y", "X1", "Y1", etc, irrespective of any labels of the data sets + */ + public ResultsTable getResultsTable(boolean writeFirstXColumn) { + return getResultsTable(writeFirstXColumn, false); + } + + /** Creates a ResultsTable with the data of the plot. Returns null if no data. + * When all columns are the same length, x columns equal to the first x column are + * not written, independent of writeFirstXColumn. + * When the data sets have labels, they are used for column headings + */ + public ResultsTable getResultsTableWithLabels() { + return getResultsTable(true, true); + } + + /** Creates a ResultsTable with the data of the plot. Returns null if no data. + * Does not write the first x column if writeFirstXColumn is false. + * When all columns are the same length, x columns equal to the first x column are + * not written, independent of writeFirstXColumn. + * When the data sets have labels and useLabels is true, they are used for column headings, + * otherwise columns are named X, Y, X1, Y1, ... */ + ResultsTable getResultsTable(boolean writeFirstXColumn, boolean useLabels) { + ResultsTable rt = new ResultsTable(); + // find the longest x-value data set and count the data sets + int nDataSets = 0; + int tableLength = 0; + for (PlotObject plotObject : allPlotObjects) + if (plotObject.xValues != null) { + nDataSets++; + tableLength = Math.max(tableLength, plotObject.xValues.length); + } + if (nDataSets == 0) + return null; + // enter columns one by one to lists of data and headings + ArrayList headings = new ArrayList(2*nDataSets); + ArrayList data = new ArrayList(2*nDataSets); + int dataSetNumber = 0; + int arrowsNumber = 0; + PlotObject firstXYobject = null; + boolean allSameLength = true; + for (PlotObject plotObject : allPlotObjects) { + if (plotObject.type==PlotObject.XY_DATA) { + if (firstXYobject != null && firstXYobject.xValues.length!=plotObject.xValues.length) { + allSameLength = false; + break; + } + if (firstXYobject==null) + firstXYobject = plotObject; + } + } + firstXYobject = null; + for (PlotObject plotObject : allPlotObjects) { + if (plotObject.type==PlotObject.XY_DATA) { + boolean sameX = firstXYobject!=null && Arrays.equals(firstXYobject.xValues, plotObject.xValues) && allSameLength; + boolean sameXY = sameX && Arrays.equals(firstXYobject.yValues, plotObject.yValues); //ignore duplicates (e.g. Markers plus Curve) + boolean writeX = firstXYobject==null ? writeFirstXColumn : !sameX; + addToLists(headings, data, plotObject, dataSetNumber, writeX, /*writeY=*/!sameXY, /*multipleSets=*/nDataSets>1, useLabels); + if (firstXYobject == null) + firstXYobject = plotObject; + dataSetNumber++; + } else if (plotObject.type==PlotObject.ARROWS) { + addToLists(headings, data, plotObject, arrowsNumber, /*writeX=*/true, /*writeY=*/true, /*multipleSets=*/nDataSets>1, /*useLabels=*/false); + arrowsNumber++; + } + } + // populate the ResultsTable + int nColumns = headings.size(); + for (int line=0; line headings, ArrayListdata, PlotObject plotObject, + int dataSetNumber, boolean writeX, boolean writeY, boolean multipleSets, boolean useLabels) { + String plotObjectLabel = useLabels ? replaceSpacesEtc(plotObject.label) : null; + if (writeX) { + String label = null; // column header for x column + if (plotObject.type!=PlotObject.ARROWS) { + String plotXLabel = getLabel('x'); + if (dataSetNumber==0 && plotXLabel!=null) { // use x axis label for 1st dataset if permitted + if (useLabels) + label = replaceSpacesEtc(plotXLabel); + else if (plotXLabel.startsWith(" ") && plotXLabel.endsWith(" ")) // legacy: always use axis label for 1st data if spaces at start&end + label = plotXLabel.substring(1,plotXLabel.length()-1); + } else if (plotObjectLabel != null && dataSetNumber>0) + label = "X_"+plotObjectLabel; // use "X_" + dataset label + if (label != null && headings.contains(label)) + label = null; // avoid duplicate labels (not possible in ResultsTable) + } + if (label == null) { // create default label if no specific label yet + label = plotObject.type == PlotObject.ARROWS ? "XStart" : "X"; + if (multipleSets) label += dataSetNumber; + } + headings.add(label); + data.add(plotObject.xValues); + } + if (writeY) { + String label = null;; // column header for y column + if (plotObject.type!=PlotObject.ARROWS) { + String plotYLabel = getLabel('y'); + if (dataSetNumber==0 && plotYLabel!=null) { + if (useLabels && plotObjectLabel == null) // use y axis label for 1st dataset if no data set label + label = replaceSpacesEtc(plotYLabel); + else if (plotYLabel.startsWith(" ") && plotYLabel.endsWith(" ")) // legacy: always use axis label for 1st data if spaces at start&end + label = plotYLabel.substring(1,plotYLabel.length()-1); + } + if (plotObjectLabel != null) + label = plotObjectLabel; + if (label != null && headings.contains(label)) + label = null; // avoid duplicate labels (not possible in ResultsTable) + } + if (label == null) { // create default label if no specific label yet + label = plotObject.type == PlotObject.ARROWS ? "YStart" : "Y"; + if (multipleSets) label += dataSetNumber; + } + headings.add(label); + data.add(plotObject.yValues); + } + if (plotObject.xEValues != null) { + String label = plotObject.type == PlotObject.ARROWS ? "XEnd" : "XERR"; + if (multipleSets) label += dataSetNumber; + headings.add(label); + data.add(plotObject.xEValues); + } + if (plotObject.yEValues != null) { + String label = plotObject.type == PlotObject.ARROWS ? "YEnd" : "ERR"; + if (multipleSets) label += dataSetNumber; + headings.add(label); + data.add(plotObject.yEValues); + } + } + + /** Convert a string to a label suitable for a ResultsTable without whitespace, quotes or commas, + * to avoid problems when saving and reading the table. Returns null if an empty string or null. */ + static String replaceSpacesEtc(String s) { + if (s == null) return null; + s = s.trim().replaceAll("[\\s,]", "_").replace("\"","''"); + if (s.length() == 0) return null; + return s; + } + + /** get the number of digits for writing a column to the results table or the clipboard */ + static int getPrecision(float[] values) { + int setDigits = Analyzer.getPrecision(); + int measurements = Analyzer.getMeasurements(); + boolean scientificNotation = (measurements&Measurements.SCIENTIFIC_NOTATION)!=0; + if (scientificNotation) { + if (setDigits max) max = values[i]; + } + } + if (allInteger) + return 0; + int digits = (max - min) > 0 ? getDigits(min, max, MIN_FLOAT_PRECISION*(max-min), 15, 0) : + getDigits(max, MIN_FLOAT_PRECISION*Math.abs(max), 15, 0); + if (setDigits>Math.abs(digits)) + digits = setDigits * (digits < 0 ? -1 : 1); //use scientific notation if needed + return digits; + } + + /** Whether a given flag 'what' is set */ + boolean hasFlag(int what) { + return (pp.axisFlags&what) != 0; + } + + /* Obsolete, replaced by add(shape,x,y). */ + public void addPoints(String dummy, float[] x, float[] y, int shape) { + addPoints(x, y, shape); + } + + /** Plots a histogram from an array using auto-binning. + * @param values array containing the population + * N.Vischer + */ + public void addHistogram(double[] values) { + addHistogram(values, 0, 0); + } + + /** Plots a histogram from an array using the specified bin width. + * @param values array containing the population + * @param binWidth set zero for auto-binning + * N.Vischer + */ + public void addHistogram(double[] values, double binWidth) { + addHistogram(values, binWidth, 0); + } + + /** Plots a histogram of the value distribution (bin counts) from an array + * @param values array containing the values for the population + * @param binWidth set zero for auto-binning + * @param binCenter any x value can be the center of a bin + * N.Vischer + */ + public void addHistogram(double[] values, double binWidth, double binCenter) { + int len = values.length; + double min = Double.POSITIVE_INFINITY; + double max = Double.NEGATIVE_INFINITY; + double[] cleanVals = new double[len]; + int count = 0; + double sum = 0, sum2 = 0; + for (int i = 0; i < len; i++) { + double val = values[i]; + if (!Double.isNaN(val)) { + cleanVals[count++] = val; + sum += val; + sum2 += val * val; + if (val < min) + min = val; + if (val > max) + max = val; + } + } + if (binWidth <= 0) {//autobinning + double stdDev = Math.sqrt(((count * sum2 - sum * sum) / count) / count);//not count - 1 + // use Scott's method (1979 Biometrika, 66:605-610) for optimal binning: 3.49*sd*N^-1/3 + binWidth = 3.49 * stdDev * (Math.pow(count, -1.0 / 3)); + + } + double modCenter = binCenter % binWidth; + double modMin = min % binWidth; + double diff = modMin - modCenter; + double firstBin = min-diff; + while(firstBin - binWidth * 0.499 > min) + firstBin -= binWidth; + int nBins = (int) ((max - firstBin)/binWidth); + double lastBin = firstBin + nBins * binWidth; + while(lastBin + binWidth * 0.499 < max) + lastBin += binWidth; + nBins = (int) Math.round((lastBin - firstBin)/binWidth) + 1; + if (nBins == 1) + nBins = 2; + if (nBins > 9999) { + IJ.error("max bins > 9999"); + return; + } + double[] histo = new double[nBins]; + double[] xValues = new double[nBins]; + for (int i = 0; i < nBins; i++) + xValues[i] = firstBin + i * binWidth; + for (int i = 0; i < count; i++) { + double val = cleanVals[i]; + double indexD = (val - firstBin) / binWidth; + int index = (int) Math.round(indexD); + if (index < 0 || index >= nBins) { + IJ.error("index out of range"); + return; + } else + histo[index]++; + } + add("bar", xValues, histo); + } + + /* Obsolete, replaced by add("error bars",errorBars). */ + public void addErrorBars(String dummy, float[] errorBars) { + addErrorBars(errorBars); + } + + /* Obsolete; replaced by setFont(). */ + public void changeFont(Font font) { + setFont(font); + } + +} + +/** This class contains the properties of the plot, such as size, format, range, etc, except for the data+format (plot contents). + * To enable reading serialized PlotObjects of plots created with previous versions of ImageJ, + * the variable names MUST NEVER be changed! Also any additions should be made after careful thought, + * since they have to be kept for all future versions. */ +class PlotProperties implements Cloneable, Serializable { + /** The serialVersionUID should not be modified, otherwise saved plots won't be readable any more */ + static final long serialVersionUID = 1L; + // + PlotObject frame = new PlotObject(Plot.DEFAULT_FRAME_LINE_WIDTH); //the frame, including background color and axis numbering + PlotObject xLabel = new PlotObject(PlotObject.AXIS_LABEL); //the x axis label (string & font) + PlotObject yLabel = new PlotObject(PlotObject.AXIS_LABEL); //the x axis label (string & font) + PlotObject legend; //the legend (if any) + int width = 0; //canvas width (note: when stored, this must fit the image) + int height = 0; + int axisFlags; //these define axis layout + double[] rangeMinMax; //currentMinMax when writing, sets defaultMinMax when reading + boolean antialiasedText = true; + boolean isFrozen; //modifications (size, range, contents) don't update the ImageProcessor + + /** Returns an array of all PlotObjects defined as PlotProperties. Note that some may be null */ + PlotObject[] getAllPlotObjects() { + return new PlotObject[]{frame, xLabel, xLabel, legend}; + } + + /** Returns the PlotObject for xLabel ('x'), yLabel('y'), frame ('f'; includes number font) or the legend ('l'). */ + PlotObject getPlotObject(char c) { + switch(c) { + case 'x': return xLabel; + case 'y': return yLabel; + case 'f': return frame; + case 'l': return legend; + default: return null; + } + } + + /** A shallow clone that does not duplicate arrays or objects */ + public PlotProperties clone() { + try { + return (PlotProperties)(super.clone()); + } catch (CloneNotSupportedException e) { + return null; + } + } + + /** A deep clone; it also duplicates arrays and pPlotObjects */ + public PlotProperties deepClone() { + PlotProperties pp2 = clone(); //shallow clone + if (frame != null) pp2.frame = frame.deepClone(); + if (xLabel != null) pp2.xLabel = xLabel.deepClone(); + if (yLabel != null) pp2.yLabel = yLabel.deepClone(); + if (legend != null) pp2.legend = legend.deepClone(); + if (rangeMinMax != null) pp2.rangeMinMax = rangeMinMax.clone(); + return pp2; + } + +} // class PlotProperties + +/** This class contains the data and properties for displaying a curve, a set of arrows, a line or a label in a plot, + * as well as the legend, axis labels, and frame (including background and fonts of axis numbering). + * Note that all properties such as lineWidths and Fonts have to be scaled up for high-resolution plots. + * This class allows serialization for writing into tiff files. + * To enable reading serialized PlotObjects of plots created with previous versions of ImageJ, + * the variable names MUST NEVER be changed! Also any additions should be made after careful thought, + * since they have to be kept for all future versions. */ +class PlotObject implements Cloneable, Serializable { + /** The serialVersionUID should not be modified, otherwise saved plots won't be readable any more */ + static final long serialVersionUID = 1L; + /** Constants for the type of objects. These are powers of two so one can use them as masks */ + public final static int XY_DATA = 1, ARROWS = 2, LINE = 4, NORMALIZED_LINE = 8, DOTTED_LINE = 16, + LABEL = 32, NORMALIZED_LABEL = 64, LEGEND = 128, AXIS_LABEL = 256, FRAME = 512, SHAPES = 1024; + /** mask for recovering font style from the flags */ + final static int FONT_STYLE_MASK = 0x0f; + /** flag for the data set passed with the constructor. Note that 0 to 0x0f are reserved for fonts modifiers, 0x010-0x800 are reserved for legend modifiers */ + public final static int CONSTRUCTOR_DATA = 0x1000; + /** flag for hiding a PlotObject */ + public final static int HIDDEN = 0x2000; + /** Type of the object; XY_DATA stands for curve or markers, can be also ARROWS ... SHAPES */ + public int type = XY_DATA; + /** bitwise combination of flags, or the position of a legend */ + public int flags; + /** Options, currently only for FRAME, see Plot.setOptions */ + public String options; + /** The x and y data arrays and the error bars (if non-null). These arrays also serve as x0, y0, x1, y1 + * arrays for plotting arrays of arrows */ + public float[] xValues, yValues, xEValues, yEValues; + /** For SHAPES: For boxplots with whiskers ('boxes'), elements of the ArrayList are float[6] with x and all 5 y values + * (for 'boxesx', y and all 5 x values), for 'rectangles', float[4] with x1, y1, x2, y2. */ + public ArrayList shapeData; + /** For SHAPES only, shape type & options. Currently implemented: 'boxes', 'boxesx' (box plots with whiskers), 'rectangles', 'redraw_grid' */ + public String shapeType; //e.g. "boxes width=20" + /** Type of the points, such as Plot.LINE, Plot.CROSS etc. (for type = XY_DATA) */ + public int shape; + /** The line width in pixels for 'normal' plots (for high-resolution plots, to be multiplied by a scale factor) */ + public float lineWidth; + /** The color of the object, must not be null */ + public Color color; + /** The secondary color (for filling closed symbols and for the line of CIRCLES_AND_LINE, may be null for unfilled/default */ + public Color color2; + /** Labels and lines: Position (NORMALIZED objects: relative units 0...1). */ + public double x, y; + /** Lines only: End position */ + public double xEnd, yEnd; + /** Dotted lines only: step */ + public int step; + /** A label for the y data of the curve, a text to draw, or the text of a legend (tab-delimited lines) */ + public String label; + /** Labels only: Justification can be Plot.LEFT, Plot.CENTER or Plot.RIGHT */ + public int justification; + /** Macro code for drawing symbols */ + public String macroCode; + /** Text objects (labels, legend, axis labels) only: the font; maybe null for default. This is not serialized (transient) */ + private transient Font font; + /** String for representation of the font family (for Serialization); may be null for default. Font style is in flags, font size in fontSize. */ + private String fontFamily; + /** Font size (for Serialization), for 'normal' plots (for high-resolution plots, to be multiplied by a scale factor) */ + private float fontSize; + + + /** Generic constructor */ + PlotObject(int type) { + this.type = type; + } + + /** Constructor for XY_DATA, i.e., a curve or set of points */ + PlotObject(float[] xValues, float[] yValues, float[] yErrorBars, int shape, float lineWidth, Color color, Color color2, String yLabel) { + this.type = XY_DATA; + this.xValues = xValues; + this.yValues = yValues; + this.yEValues = yErrorBars; + this.shape = shape; + this.lineWidth = lineWidth; + this.color = color; + this.color2 = color2; + this.label = yLabel; + if (shape==Plot.CUSTOM) + this.macroCode = yLabel; + } + + /** Constructor for a set of arrows */ + PlotObject(float[] x1, float[] y1, float[] x2, float[] y2, float lineWidth, Color color) { + this.type = ARROWS; + this.xValues = x1; + this.yValues = y1; + this.xEValues = x2; + this.yEValues = y2; + this.lineWidth = lineWidth; + this.color = color; + } + + /** Constructor for a set of shapes */ + PlotObject(String shapeType, ArrayList shapeData, float lineWidth, Color color, Color color2) { + this.type = SHAPES; + this.shapeData = shapeData; + this.shapeType = shapeType; + this.lineWidth = lineWidth; + this.color = color; + this.color2 = color2; + } + + /** Constructor for a line */ + PlotObject(double x, double y, double xEnd, double yEnd, float lineWidth, int step, Color color, int type) { + this.type = type; + this.x = x; + this.y = y; + this.xEnd = xEnd; + this.yEnd = yEnd; + this.lineWidth = lineWidth; + this.step = step; + this.color = color; + } + + /** Constructor for a label or NORMALIZED_LABEL */ + PlotObject(String label, double x, double y, int justification, Font font, Color color, int type) { + this.type = type; + this.label = label; + this.x = x; + this.y = y; + this.justification = justification; + setFont(font); + this.color = color; + } + + /** Constructor for the legend. flags is bitwise or of TOP_LEFT etc. and LEGEND_TRANSPARENT if desired. + * Note that the labels in the legend are those of the data plotObjects */ + PlotObject(float lineWidth, Font font, Color color, int flags) { + this.type = LEGEND; + this.lineWidth = lineWidth; + setFont(font); + this.color = color; + this.flags = flags; + } + + /** Constructor for the frame, including axis numbers. In the current version, the primary color (line color) is always black */ + PlotObject(float lineWidth) { + this.type = FRAME; + this.color = Color.black; + this.lineWidth = lineWidth; + } + + /** Whether a given flag 'what' is set */ + boolean hasFlag(int what) { + return (flags&what) != 0; + } + + /** Sets a given flag */ + void setFlag(int what) { + flags |= what; + } + + /** Unsets a given flag */ + void unsetFlag(int what) { + flags = flags & (~what); + } + + /** Whether an XY_DATA object has a curve to draw */ + boolean hasCurve() { + return type == XY_DATA && (shape == Plot.LINE || shape == Plot.CONNECTED_CIRCLES || shape == Plot.FILLED); + } + + /** Whether an XY_DATA object has markers to draw */ + boolean hasMarker() { + return type == XY_DATA && (shape == Plot.CIRCLE || shape == Plot.X || shape == Plot.BOX || shape == Plot.TRIANGLE + || shape == Plot.CROSS || shape == Plot.DIAMOND || shape == Plot.DOT || shape == Plot.CONNECTED_CIRCLES + || shape == Plot.CUSTOM); + } + + /** Whether an XY_DATA object has markers that can be filled */ + boolean hasFilledMarker() { + return type == XY_DATA && color2 != null && (shape == Plot.CIRCLE || shape == Plot.BOX || shape == Plot.TRIANGLE || + shape == Plot.DIAMOND || shape == Plot.CONNECTED_CIRCLES); + } + + /** Size of the markers for an XY_DATA object with markers */ + int getMarkerSize () { + return lineWidth<=1 ? 5 : 7; + } + + /** Sets the font. Also writes font properties for serialization. */ + void setFont(Font font) { + if (font == this.font) return; + this.font = font; + if (font == null) { + fontFamily = null; + } else { + fontFamily = font.getFamily(); + flags = (flags & ~FONT_STYLE_MASK) | font.getStyle(); + fontSize = font.getSize2D(); + } + } + + /** Returns the font, or null if none specified */ + Font getFont() { + if (font == null && fontFamily != null) //after recovery from serialization, create the font from its description + font = FontUtil.getFont(fontFamily, flags&FONT_STYLE_MASK, fontSize); + return font; + } + + /** Returns all data xValues, yValues, xEValues, yEValues as a float[][] array. Note that future versions may have more data. */ + float[][] getAllDataValues() { + return new float[][] {xValues, yValues, xEValues, yEValues}; + } + + /** A shallow clone that does not duplicate arrays or objects */ + public PlotObject clone() { + try { + return (PlotObject)(super.clone()); + } catch (CloneNotSupportedException e) { + return null; + } + } + + /** A deep clone, which duplicates arrays etc. + * Note that colors & font are not cloned; it is assumed that these wil not be modified but replaced, + * so the clone remains unaffected */ + public PlotObject deepClone() { + PlotObject po2 = clone(); + if (xValues != null) po2.xValues = xValues.clone(); + if (yValues != null) po2.yValues = yValues.clone(); + if (xEValues != null) po2.xEValues = xEValues.clone(); + if (yEValues != null) po2.yEValues = yEValues.clone(); + if (shapeData != null) po2.shapeData = cloneArrayList(shapeData); + return po2; + } + + /** A clone of an array list one level deeper than a shallow clone. + * The clone() method of the objects in the list must be accessible */ + private ArrayList cloneArrayList(ArrayList src) { + ArrayList dest = (ArrayList)(src.clone()); //shallow clone + Class[] noClasses = new Class[0]; + Object[] noObjects = new Object[0]; + for (int i=0; i 1) + super.zoomIn(sx, sy); + else + super.zoomOut(sx, sy); + return; + } + plot.zoom(sx, sy, zoomFactor); + } + + /** Implements the Image/Zoom/Original Scale command. + * Sets the original range of the x, y axes (unless the plot is frozen) */ + public void unzoom() { + if (plot == null || plot.isFrozen()) { + super.unzoom(); + return; + } + resetMagnification(); + plot.setLimitsToDefaults(true); + } + + /** Implements the Image/Zoom/View 100% command: Sets the original frame size as specified + * in Edit/Options/Plots (unless the plot is frozen) */ + public void zoom100Percent() { + if (plot == null || plot.isFrozen()) { + super.zoom100Percent(); + return; + } + resetMagnification(); + plot.setFrameSize(PlotWindow.plotWidth, PlotWindow.plotHeight); + } + + /** Resizes the plot (unless frozen) to fit the window */ + public void fitToWindow() { + if (plot == null || plot.isFrozen()) { + super.fitToWindow(); + return; + } + ImageWindow win = imp.getWindow(); + if (win==null) return; + Rectangle bounds = win.getBounds(); + Dimension extraSize = win.getExtraSize(); + int width = bounds.width-extraSize.width;//(insets.left+insets.right+ImageWindow.HGAP*2); + int height = bounds.height-extraSize.height;//(insets.top+insets.bottom+ImageWindow.VGAP*2); + //IJ.log("fitToWindow "+bounds+"-> w*h="+width+"*"+height); + resizeCanvas(width, height); + getParent().doLayout(); + } + + /** Resizes the canvas when the user resizes the window. To avoid a race condition while creating + * a new window, this is ignored if no window exists or the window has not been activated yet. + */ + void resizeCanvas(int width, int height) { + if (plot == null || plot.isFrozen()) { + super.resizeCanvas(width, height); + return; + } + resetMagnification(); + if (width == oldWidth && height == oldHeight) return; + if (plot == null) return; + ImageWindow win = imp.getWindow(); + if (win==null || !(win instanceof PlotWindow)) return; + if (!win.isVisible()) return; + if (!((PlotWindow)win).wasActivated) return; // window layout not finished yet? + Dimension minSize = plot.getMinimumSize(); + int plotWidth = width < minSize.width ? minSize.width : width; + int plotHeight = height < minSize.height ? minSize.height : height; + plot.setSize(plotWidth, plotHeight); + setSize(width, height); + oldWidth = width; + oldHeight = height; + ((PlotWindow)win).canvasResized(); + } + + /** The image of a PlotCanvas is always shown at 100% magnification unless the plot is frozen */ + public void setMagnification(double magnification) { + if (plot == null || plot.isFrozen()) + super.setMagnification(magnification); + else + resetMagnification(); + } + + /** Scrolling a PlotCanvas is updating the plot, not viewing part of the plot, unless the plot is frozen */ + public void setSourceRect(Rectangle r) { + if (plot.isFrozen()) + super.setSourceRect(r); + else + resetMagnification(); + } + + void resetMagnification() { + magnification = 1.0; + srcRect.x = 0; + srcRect.y = 0; + } + + /** overrides ImageCanvas.setupScroll; if plot is not frozen, scrolling modifies the plot data range */ + protected void setupScroll(int ox, int oy) { + if (plot.isFrozen()) { + super.setupScroll(ox, oy); + return; + } + xMouseStart = ox; + yMouseStart = oy; + xScrolled = 0; + yScrolled = 0; + } + + /** overrides ImageCanvas.scroll; if plot is not frozen, scrolling modifies the plot data range */ + protected void scroll(int sx, int sy) { + if (plot.isFrozen()) { + super.scroll(sx, sy); + return; + } + if (sx == 0 && sy == 0) return; + if (xScrolled == 0 && yScrolled == 0) + plot.saveMinMax(); + int dx = sx - xMouseStart; + int dy = sy - yMouseStart; + plot.scroll(dx-xScrolled, dy-yScrolled); + xScrolled = dx; + yScrolled = dy; + Thread.yield(); + } + + /** overrides ImageCanvas.mouseExited; removes 'range' arrows */ + public void mouseExited(MouseEvent e) { + ImageWindow win = imp.getWindow(); + if (win instanceof PlotWindow) + ((PlotWindow)win).mouseExited(e); + super.mouseExited(e); + } + + /** overrides ImageCanvas.mousePressed: no further processing of clicks on 'range' arrows */ + public void mousePressed(MouseEvent e) { + rangeArrowIndexWhenPressed = getRangeArrowIndex(e); + if (rangeArrowIndexWhenPressed <0) + super.mousePressed(e); + } + + /** Overrides ImageCanvas.mouseReleased, handles clicks on 'range' arrows */ + public void mouseReleased(MouseEvent e) { + if (rangeArrowIndexWhenPressed>=0 && rangeArrowIndexWhenPressed==getRangeArrowIndex(e)) + plot.zoomOnRangeArrow(rangeArrowIndexWhenPressed); + else + super.mouseReleased(e); + } + + /** Returns the index of the arrow for modifying the range when the mouse click was + * at such an arrow, otherwise -1 */ + int getRangeArrowIndex(MouseEvent e) { + ImageWindow win = imp.getWindow(); + int rangeArrowIndex = -1; + if (win instanceof PlotWindow) { + int x = e.getX(); + int y = e.getY(); + rangeArrowIndex = ((PlotWindow)win).getRangeArrowIndex(x, y); + } + return rangeArrowIndex; + } +} diff --git a/src/ij/gui/PlotContentsDialog.java b/src/ij/gui/PlotContentsDialog.java new file mode 100644 index 0000000..c3ccdb0 --- /dev/null +++ b/src/ij/gui/PlotContentsDialog.java @@ -0,0 +1,671 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.measure.CurveFitter; +import ij.measure.Minimizer; +import ij.text.TextWindow; +import ij.measure.ResultsTable; +import ij.plugin.Colors; +import ij.plugin.frame.Recorder; +import ij.util.Tools; +import java.awt.*; +import java.awt.event.*; +import java.util.Arrays; +import java.util.Vector; +import java.util.ArrayList; + +/** This class implements the Plot Window's Data>"Add from Plot", "Add form Table", "Add Fit" and + * "More>Contents Style" dialogs + */ +public class PlotContentsDialog implements DialogListener { + /** Types of dialog (ERROR suppresses the dialog after an invalid call of a constructor) */ + public final static int ERROR=-1, STYLE=0, ADD_FROM_PLOT=1, ADD_FROM_TABLE=2, ADD_FROM_ARRAYS=3, ADD_FIT=4; + /** Dialog headings for each dialogType >= 0 */ + private static final String[] HEADINGS = new String[] {"Plot Contents Style", "Add From Plot", "Plot From Table", "Add Plot Data", "Add Fit"}; + private Plot plot; // the plot we work on + private int dialogType; // determines what to do: ADD_FORM_PLOT, etc. + private double[] savedLimits; // previous plot range, for undo upon cancel + GenericDialog gd; + private int currentObjectIndex = -1; + private Choice objectChoice; + private Choice symbolChoice; + private TextField colorField, color2Field, labelField, widthField; + private Checkbox visibleCheckbox; + private boolean creatingPlot; // for creating plots; dialogType determines source + private Choice plotChoice; // for "Add from Plot" + private Plot[] allPlots; + private String[] allPlotNames; + private static Plot previousPlot; + private static int previousPlotObjectIndex; + private int defaultPlotIndex, defaultObjectIndex; + private int currentPlotNumObjects; + private Choice tableChoice; // for "Add from Table" + final static int N_COLUMNS = 4; // number of data columns that we can have; x, y, xE, yE + private int nColumnsToUse = N_COLUMNS; // user may restrict to 2, for not having error bars + private Choice[] columnChoice = new Choice[N_COLUMNS]; + private final static String[] COLUMN_NAMES = new String[] {"X:", "Y:", "X Error:", "Y Error:"}; + private final static boolean[] COLUMN_ALLOW_NONE = new boolean[] {true, false, true, true}; //y data cannot be null + private ResultsTable[] allTables; + private String[] allTableNames; + private static String previousTableName; + private static int[] previousColumns = new int[]{1, 1, 0, 0}; //must be N_COLUMNS elements + private static int defaultTableIndex; + private static int[] defaultColumnIndex = new int[N_COLUMNS]; + private String[] arrayHeadings; // for "Add from Arrays" + private ArrayList arrayData; + private Choice fitDataChoice; // for "Add Fit" + private Choice fitFunctionChoice; + private Thread fittingThread; + private static String lastFitFunction = CurveFitter.fitList[0]; + private String curveFitterStatusString; + private static String previousColor="blue", previousColor2="#a0a0ff", previousSymbol="Circle"; + private static double previousLineWidth = 1; + private static final String[] PLOT_COLORS = new String[] {"blue", "red", "black", "#00c0ff", "#00e000", "gray", "#c08060", "magenta"}; + + + /** Prepares a new PlotContentsDialog for an existing plot. Use showDialog thereafter. + * @param dialogType may be STYLE (contents style), ADD_FROM_PLOT (add object from other plot), ADD_FROM_TABLE, and ADD_FIT */ + public PlotContentsDialog(Plot plot, int dialogType) { + this.plot = plot; + this.dialogType = plot == null ? ERROR : dialogType; + if (plot != null) currentPlotNumObjects = plot.getNumPlotObjects(); + } + + /** Prepares a new PlotContentsDialog for creating a new plot using data from a ResultsTable. + * Use showDialog thereafter. */ + public PlotContentsDialog(String title, ResultsTable rt) { + creatingPlot = true; + dialogType = ADD_FROM_TABLE; + if (rt == null || !isValid(rt)) { + IJ.error("Cant Create Plot","No (results) table or no data in "+title); + dialogType = ERROR; + } + plot = new Plot("Plot of "+title, "x", "y"); + allTables = new ResultsTable[] {rt}; + allTableNames = new String[] {title}; + } + + /** Prepares a new PlotContentsDialog for creating a new plot using float[] arrays as data. + * Each 'data' array in the ArrayList must have a corresponding element in the 'headings' array. + * 'defaultHeadings' may contain the headings of the items selected initially for x, y, x error and y error, respectively. + * 'defaultHeadings' and each of its entries may be null, and the array may have any length. */ + public PlotContentsDialog(String title, String[] headings, String[] defaultHeadings, ArrayList data) { + creatingPlot = true; + dialogType = ADD_FROM_ARRAYS; + this.arrayHeadings = headings; + this.arrayData = data; + plot = new Plot(title, "x", "y"); + setDefaultColumns(defaultHeadings); + } + + /** Prepares a new PlotContentsDialog for adding data from float[] arrays to a plot. + * Each 'data' array in the ArrayList must have a corresponding element in the 'headings' array. + * 'defaultHeadings' may contain the headings of the items selected initially for x, y, x error and y error, respectively. + * 'defaultHeadings' and each of its entries may be null, and the array may have any length. */ + public PlotContentsDialog(Plot plot, String[] headings, String[] defaultHeadings, ArrayList data) { + dialogType = ADD_FROM_ARRAYS; + this.plot = plot; + this.arrayHeadings = headings; + this.arrayData = data; + setDefaultColumns(defaultHeadings); + } + + /** Avoids showing a selection for the error bars; must be called before showDialog. */ + public void noErrorBars() { + nColumnsToUse = 2; // only x, y columns can be selected + } + + /** Shows the dialog, with a given parent Frame (may be null) */ + public void showDialog(Frame parent) { + if (dialogType == ERROR) return; + if (!creatingPlot) savedLimits = plot.getLimits(); + plot.savePlotObjects(); + String[] designations = plot.getPlotObjectDesignations(); + if (dialogType == STYLE && designations.length==0) { + IJ.error("Empty Plot"); + return; + } else if (dialogType == ADD_FROM_PLOT) { + prepareAddFromPlot(); + if (allPlots.length == 0) return; //should never happen; we have at least the current plot + } else if (dialogType == ADD_FROM_TABLE && !creatingPlot) { + prepareAddFromTable(); + if (allTables.length == 0) return; //should never happen; PlotWindow should not enable if no table + } + if ((dialogType == ADD_FROM_TABLE || dialogType == ADD_FROM_ARRAYS) && !creatingPlot) + suggestColor(); + if (parent == null && plot.getImagePlus() != null) + parent = plot.getImagePlus().getWindow(); + gd = parent == null ? new GenericDialog(HEADINGS[dialogType]) : + new GenericDialog(HEADINGS[dialogType], parent); + IJ.wait(100); //sometimes needed to avoid hanging? + if (dialogType == STYLE) { + gd.addChoice("Item:", designations, designations[0]); + objectChoice = (Choice)(gd.getChoices().get(0)); + currentObjectIndex = objectChoice.getSelectedIndex(); + } else if (dialogType == ADD_FROM_PLOT) { + gd.addChoice("Select Plot:", allPlotNames, allPlotNames[defaultPlotIndex]); + gd.addChoice("Item to Add:", new String[]{""}, ""); // will be set up by makeSourcePlotObjects + Vector choices = gd.getChoices(); + plotChoice = (Choice)(choices.get(0)); + objectChoice = (Choice)(choices.get(1)); + makeSourcePlotObjects(); + } else if (dialogType == ADD_FROM_TABLE) { + gd.addChoice("Select Table:", allTableNames, allTableNames[defaultTableIndex]); + tableChoice = (Choice)(gd.getChoices().get(0)); + if (creatingPlot) tableChoice.setEnabled(false); // we can't select the table, we have only one + } else if (dialogType == ADD_FIT) { + String[] dataSources = plot.getDataObjectDesignations(); + if (dataSources.length == 0) { + IJ.error("No Data For Fitting"); + return; + } + gd.addChoice("Fit Data Set:", dataSources, dataSources[0]); + gd.addChoice("Fit Function:", new String[0], ""); + Vector choices = gd.getChoices(); + fitDataChoice = (Choice)(choices.get(0)); + fitFunctionChoice = (Choice)(choices.get(1)); + if (dataSources.length == 1) + fitDataChoice.setEnabled(false); + for (int i=0; i 0) + Recorder.recordString("Plot.add(\"xerror\", Table.getColumn(\""+ + columnChoice[2].getSelectedItem()+"\", \""+tableChoice.getSelectedItem()+"\"));\n"); + if (columnChoice[3].getSelectedIndex() > 0) + Recorder.recordString("Plot.add(\"error\", Table.getColumn(\""+ + columnChoice[3].getSelectedItem()+"\", \""+tableChoice.getSelectedItem()+"\"));\n"); + } + Recorder.recordString("Plot.setStyle("+currentObjectIndex+", \""+getStyleString()+"\");\n"); + } + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (e==null) + return true; //gets called with e=null upon OK + boolean setStyle = false; + if (dialogType == STYLE) { + int objectIndex = objectChoice.getSelectedIndex(); // no getNextChoice since Choices depend on dialog type + setStyle = (e.getSource() != objectChoice); + if (e.getSource() == objectChoice) { + setDialogStyleFields(objectIndex); + currentObjectIndex = objectIndex; + } else + setStyle = true; + } else if (dialogType == ADD_FROM_PLOT) { + if (e.getSource() == plotChoice) { + makeSourcePlotObjects(); + addObjectFromPlot(); + } else if (e.getSource() == objectChoice) { + addObjectFromPlot(); + } else + setStyle = true; + } else if (dialogType == ADD_FROM_TABLE || dialogType == ADD_FROM_ARRAYS) { + if (e.getSource() == tableChoice) { + makeSourceColumns(); + addObjectFromTable(); + } else { + boolean columnChanged = false; + for (int c=0; c 0 ? label : null); + } + } + + /** Returns the style String for Plot.setPlotObjectStyle from the dialog fields */ + private String getStyleString() { + String color = colorField.getText(); + String color2 = color2Field.getText(); + double width = Tools.parseDouble(widthField.getText()); + String symbol = symbolChoice.getSelectedItem(); + Boolean visible = visibleCheckbox.getState(); + String style = color.trim()+","+color2.trim()+","+(float)width+","+ symbol+(visible?"":"hidden"); + return style; + } + + /** Sets the style fields of the dialog according to the style of the PlotObject having the + * index given. Does nothing with index < 0 */ + private void setDialogStyleFields(int index) { + if (index < 0) return; + Checkbox visibleC = (Checkbox)gd.getCheckboxes().get(0); + String styleString = plot.getPlotObjectStyle(index); + String designation = plot.getPlotObjectDesignations()[index].toLowerCase(); + boolean isData = designation.startsWith("data"); + boolean isText = designation.startsWith("text"); + boolean isBox = designation.startsWith("shapes") && + (designation.contains("boxes") || designation.contains("rectangles")); + boolean isGrid = designation.startsWith("shapes") && designation.contains("redraw_grid"); + + String[] items = styleString.split(","); + colorField.setText(items[0]); + color2Field.setText(items[1]); + widthField.setText(items[2]); + if (items.length >= 4) + symbolChoice.select(items[3]); + labelField.setText(isData ? plot.getPlotObjectLabel(index) : ""); + visibleC.setState(!styleString.contains("hidden")); + + colorField.setEnabled(!isGrid); //grid color is fixed + color2Field.setEnabled(isData || isBox);//only (some) data symbols and boxes have secondary (fill) color + widthField.setEnabled(!isText && !isGrid); //all non-Text types have line width + // visibleC.setEnabled(!isGrid); //allow to hide everything + symbolChoice.setEnabled(isData); //only data have a symbol to choose + labelField.setEnabled(isData); //only data have a label in the legend + } + + + /** Prepare the lists 'allPlots', 'allPlotNames' for the "Add from Plot" dialog. + * Also sets 'defaultPlotIndex', 'defaultObjectIndex' */ + private void prepareAddFromPlot() { + int[] windowIDlist = WindowManager.getIDList(); + ArrayList plotImps = new ArrayList(); + ImagePlus currentPlotImp = null; + for (int windowID : windowIDlist) { + ImagePlus imp = WindowManager.getImage(windowID); + if (imp == null || imp.getWindow() == null) continue; + Plot thePlot = (Plot)(imp.getProperty(Plot.PROPERTY_KEY)); + if (thePlot != null) { + if (thePlot == plot) + currentPlotImp = imp; + else + plotImps.add(imp); + } + } + if (currentPlotImp != null) + plotImps.add(currentPlotImp); // add current plot as the last one (usually not used) + if (plotImps.size() == 0) return; // should never happen; we have at least the current plot + + allPlots = new Plot[plotImps.size()]; + allPlotNames = new String[plotImps.size()]; + defaultPlotIndex = 0; + for (int i=0; i tableWindows = new ArrayList(); + Frame[] windows = WindowManager.getNonImageWindows(); + for (Frame win : windows) { + if (!(win instanceof TextWindow)) continue; + ResultsTable rt = ((TextWindow)win).getResultsTable(); + if (isValid(rt)) + tableWindows.add((TextWindow)win); + } + allTables = new ResultsTable[tableWindows.size()]; + allTableNames = new String[tableWindows.size()]; + defaultTableIndex = 0; + for (int i=0; i 0) { + String[] newHeadings = new String[columnHeadings.length - nBadColumns]; + int i=0; + for (String heading : columnHeadings) //copy 'good' headings + if (heading != null) + newHeadings[i++] = heading; + columnHeadings = newHeadings; + } + } else { // if (dialogType == ADD_FROM_ARRAYS) + columnHeadings = new String[arrayHeadings.length+1]; + System.arraycopy(arrayHeadings, 0, columnHeadings, 1, arrayHeadings.length); + } + columnHeadings[0] = "---"; + for (int c=0; c 0 && previousColumns[c] >= 0) + columnChoice[c].select(Math.min(columnChoice[c].getItemCount()-1, previousColumns[c])); + } + } + + /** For "Add from Table" and "Add from Arrays" adds item to the plot according to the current Choice settings + * and sets the Style fields for it. */ + private void addObjectFromTable() { + float[][] data = getDataArrays(); + if (data[1] == null) return; //no y data? then can't plot + String label = columnChoice[1].getSelectedItem(); //take label from y + int shape = Plot.toShape(symbolChoice.getSelectedItem()); + float lineWidth = (float)(Tools.parseDouble(widthField.getText())); + if (lineWidth > 0) + plot.setLineWidth(lineWidth); + plot.restorePlotObjects(); + if (savedLimits != null) + plot.setLimits(savedLimits); + plot.setColor(colorField.getText(), color2Field.getText()); + plot.addPoints(data[0], data[1], data[3], shape, label); + if (data[2] != null) + plot.addHorizontalErrorBars(data[2]); + if (creatingPlot) { + plot.setXYLabels(data[0]==null ? "x" : columnChoice[0].getSelectedItem(), columnChoice[1].getSelectedItem()); + plot.setLimitsToFit(false); + } else + plot.fitRangeToLastPlotObject(); + currentObjectIndex = plot.getNumPlotObjects()-1; + setDialogStyleFields(currentObjectIndex); + if (dialogType == ADD_FROM_TABLE) + previousTableName = allTableNames[tableChoice.getSelectedIndex()]; + } + + /** For "Add from Table" and "Add from Arrays", tries to set a 'y' data column that has not been plotted yet. */ + private void suggestNewYColumn() { + int nYcolumns = columnChoice[1].getItemCount(); + int currentIndex = columnChoice[1].getSelectedIndex(); + for (int i=0; i= 0) + data[c] = rt.getColumn(index); + previousColumns[c] = columnChoice[c].getSelectedIndex(); + } + } else { //if (dialogType == ADD_FROM_ARRAYS) + for (int c=0; c= 0) + data[c] = arrayData.get(index); + previousColumns[c] = columnChoice[c].getSelectedIndex(); + } + } + return data; + } + + /** Does the curve fit and adds the fit curve to the plot */ + private void addFitCurve() { + plot.restorePlotObjects(); + if (savedLimits != null) + plot.setLimits(savedLimits); + int dataIndex = fitDataChoice.getSelectedIndex(); + float[][] data = plot.getDataObjectArrays(dataIndex); + String fitName = fitFunctionChoice.getSelectedItem(); + int fitType = getIndex(CurveFitter.fitList, fitName); + CurveFitter cf = new CurveFitter(Tools.toDouble(data[0]), Tools.toDouble(data[1])); + cf.doFit(fitType); + String statusString = "Fit: "+Minimizer.STATUS_STRING[cf.getStatus()]; + if (cf.getStatus() == Minimizer.SUCCESS) + statusString += ", sum residuals ^2 = "+(float)(cf.getSumResidualsSqr()); + IJ.showStatus(statusString); + curveFitterStatusString = "Fit for "+plot.getTitle()+": "+fitDataChoice.getSelectedItem()+cf.getResultString(); //will be shown in Log when done + + double[] plotMinMax = plot.getLimits(); + double[] dataMinMax = Tools.getMinMax(data[0]); + double min = Math.min(plotMinMax[0], dataMinMax[0]); + double max = Math.max(plotMinMax[1], dataMinMax[1]); + double plotSpan = Math.abs(plotMinMax[1] - plotMinMax[0]); + double dataSpan = Math.abs(dataMinMax[1] - dataMinMax[0]); + double rangeFactor = Math.max(plotSpan/dataSpan, dataSpan/plotSpan); + if (rangeFactor > 20) rangeFactor = 20; + int nPoints = (int)(1000*rangeFactor); //finer data point spacing if we will want to zoom out or in + float[] xFit = new float[nPoints]; + float[] yFit = new float[nPoints]; + for (int i=0; i=3; + else + return columnHeadingStr.length() >= 1; + + } + + /** For "Add from Arrays", sets default columns from the colums titles */ + private void setDefaultColumns(String[] defaultHeadings) { + if (defaultHeadings == null) return; + for (int i=0; i 190 ? 255 : 255 - (int)(0.4*(255-v)); + } +} diff --git a/src/ij/gui/PlotDialog.java b/src/ij/gui/PlotDialog.java new file mode 100644 index 0000000..2f53362 --- /dev/null +++ b/src/ij/gui/PlotDialog.java @@ -0,0 +1,551 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.plugin.frame.Recorder; +import java.awt.*; +import java.util.Vector; + +/* + * This class contains dialogs for formatting of plots (range, axes, labels, legend, creating a high-resolution plot) + * Adding and formatting of contents (curves, symbols, ...) is in PlotContentsStyleDialog + */ + +public class PlotDialog implements DialogListener { + + /** Types of dialog. Note that 10-14 must be the same as the corresponding PlotWindow.rangeArrow numbers */ + public static final int SET_RANGE = 0, AXIS_OPTIONS = 1, LEGEND = 2, HI_RESOLUTION = 3, TEMPLATE = 4, //5-9 spare + X_LEFT = 10, X_RIGHT = 11, Y_BOTTOM = 12, Y_TOP = 13, X_AXIS = 14, Y_AXIS = 15; + /** Dialog headings for the dialogTypes */ + private static final String[] HEADINGS = new String[] {"Plot Range", "Axis Options", "Add Legend", "High-Resolution Plot", "Use Template", + null, null, null, null, null, // 5-9 spare + "X Left", "X Right", "Y Bottom","Y Top", "X Axis", "Y Axis"}; + /** Positions and corresponding codes for legend position */ + private static final String[] LEGEND_POSITIONS = new String[] {"Auto", "Top-Left", "Top-Right", "Bottom-Left", "Bottom-Right", "No Legend"}; + private static final int[] LEGEND_POSITION_N = new int[] {Plot.AUTO_POSITION, Plot.TOP_LEFT, Plot.TOP_RIGHT, Plot.BOTTOM_LEFT, Plot.BOTTOM_RIGHT, 0}; + /** Template "copy what" flag: dialog texts and corresponding bit masks, in the sequence as they appear in the dialog*/ + private static final String[] TEMPLATE_FLAG_NAMES = new String[] {"X Range", "Y Range", "Axis Style", "Labels", + "Legend", "Contents Style", "Extra Objects (Curves...)", "Window Size"}; + private static final int[] TEMPLATE_FLAGS = new int[] {Plot.X_RANGE, Plot.Y_RANGE, Plot.COPY_AXIS_STYLE, Plot.COPY_LABELS, + Plot.COPY_LEGEND, Plot.COPY_CONTENTS_STYLE, Plot.COPY_EXTRA_OBJECTS, Plot.COPY_SIZE}; + + private Plot plot; + private int dialogType; + private boolean minMaxSaved; //whether plot min&max has been saved for "previous range" + private boolean dialogShowing; //when the dialog is showing, ignore the last call with event null + private Plot[] templatePlots; + + private Checkbox xLogCheckbox, yLogCheckbox; + + //saved dialog options: legend + private static int legendPosNumber = 0; + private static boolean bottomUp; + private static boolean transparentBackground; + //saved dialog options: Axis labels + private static String lastXLabel, lastYLabel; + private static float plotFontSize; + //saved dialog options: High-resolution plot + private static float hiResFactor = 4.0f; + private static boolean hiResAntiAliased = true; + //saved dialog options: Use Template + private static int templateID; + private static int lastTemplateFlags = Plot.COPY_AXIS_STYLE|Plot.COPY_CONTENTS_STYLE; + + /** Constructs a new PlotDialog for a given plot and sets the type of dialog */ + public PlotDialog(Plot plot, int dialogType) { + this.plot = plot; + this.dialogType = dialogType; + } + + /** Asks the user for axis scaling; then replot with new scale on the same ImageProcessor. + * The 'parent' frame may be null */ + public void showDialog(Frame parent) { + if (dialogType == HI_RESOLUTION) { //'make high-resolution plot' dialog has no preview, handled separately + doHighResolutionDialog(parent); + return; + } + plot.savePlotPlotProperties(); + if (dialogType == TEMPLATE) + plot.savePlotObjects(); + + String dialogTitle = dialogType >= X_LEFT && dialogType <= Y_TOP ? + "Set Axis Limit..." : (HEADINGS[dialogType] + "..."); + GenericDialog gd = parent == null ? new GenericDialog(dialogTitle) : + new GenericDialog(dialogTitle, parent); + if (!setupDialog(gd)) return; + gd.addDialogListener(this); + dialogItemChanged(gd, null); //preview immediately + dialogShowing = true; + gd.showDialog(); + if (gd.wasCanceled()) { + plot.restorePlotProperties(); + if (dialogType == TEMPLATE) + plot.restorePlotObjects(); + plot.update(); + } else { + if (Recorder.record) + record(); + String xAxisLabel = plot.getLabel('x'); + if ((dialogType == AXIS_OPTIONS || dialogType == X_AXIS) && xAxisLabel != null && xAxisLabel.length() > 0) + lastXLabel = xAxisLabel; //remember for next time, in case we have none + String yAxisLabel = plot.getLabel('y'); + if ((dialogType == AXIS_OPTIONS || dialogType == Y_AXIS) && yAxisLabel != null && yAxisLabel.length() > 0) + lastYLabel = yAxisLabel; + if (dialogType == SET_RANGE || dialogType == X_AXIS || dialogType == Y_AXIS) + plot.makeLimitsDefault(); + if (dialogType == TEMPLATE) + lastTemplateFlags = plot.templateFlags; + + } + plot.killPlotPropertiesSnapshot(); + if (dialogType == TEMPLATE) + plot.killPlotObjectsSnapshot(); + + ImagePlus imp = plot.getImagePlus(); + ImageWindow win = imp == null ? null : imp.getWindow(); + if (win instanceof PlotWindow) + ((PlotWindow)win).hideRangeArrows(); // arrows etc might be still visible, but the mouse maybe elsewhere + + if (!gd.wasCanceled() && !gd.wasOKed()) { // user has pressed "Set all limits" or "Set Axis Options" button + int newDialogType = (dialogType == SET_RANGE) ? AXIS_OPTIONS : SET_RANGE; + new PlotDialog(plot, newDialogType).showDialog(parent); + } + } + + /** Setting up the dialog fields and initial parameters. The input is read in the dialogItemChanged method, which must + * have exactly the same structure of 'if' blocks and matching 'get' methods for each input field. + * @return false on error */ + private boolean setupDialog(GenericDialog gd) { + double[] currentMinMax = plot.getLimits(); + boolean livePlot = plot.plotMaker != null; + + int xDigits = plot.logXAxis ? -2 : Plot.getDigits(currentMinMax[0], currentMinMax[1], 0.005*Math.abs(currentMinMax[1]-currentMinMax[0]), 6, 0); + if (dialogType == SET_RANGE || dialogType == X_AXIS) { + gd.addNumericField("X_From", currentMinMax[0], xDigits, 6, "*"); + gd.addToSameRow(); + gd.addNumericField("To", currentMinMax[1], xDigits, 6, "*"); + gd.setInsets(0, 20, 0); //top, left, bottom + if (livePlot) + gd.addCheckbox("Fix_X Range While Live", (plot.templateFlags & Plot.X_RANGE) != 0); + gd.addCheckbox("Log_X Axis **", (plot.hasFlag(Plot.X_LOG_NUMBERS))); + xLogCheckbox = lastCheckboxAdded(gd); + enableDisableLogCheckbox(xLogCheckbox, currentMinMax[0], currentMinMax[1]); + } + int yDigits = plot.logYAxis ? -2 : Plot.getDigits(currentMinMax[2], currentMinMax[3], 0.005*Math.abs(currentMinMax[3]-currentMinMax[2]), 6, 0); + if (dialogType == SET_RANGE || dialogType == Y_AXIS) { + gd.setInsets(20, 0, 3); //top, left, bottom + gd.addNumericField("Y_From", currentMinMax[2], yDigits, 6, "*"); + gd.addToSameRow(); + gd.addNumericField("To", currentMinMax[3], yDigits, 6, "*"); + if (livePlot) + gd.addCheckbox("Fix_Y Range While Live", (plot.templateFlags & Plot.Y_RANGE) != 0); + gd.addCheckbox("Log_Y Axis **", (plot.hasFlag(Plot.Y_LOG_NUMBERS))); + yLogCheckbox = lastCheckboxAdded(gd); + enableDisableLogCheckbox(yLogCheckbox, currentMinMax[2], currentMinMax[3]); + } + if (dialogType >= X_LEFT && dialogType <= Y_TOP) { + int digits = dialogType < Y_BOTTOM ? xDigits : yDigits; + gd.addNumericField(HEADINGS[dialogType], currentMinMax[dialogType - X_LEFT], digits, 6, "*"); + } + + if (dialogType == AXIS_OPTIONS || dialogType == X_AXIS || dialogType == Y_AXIS) { + int flags = plot.getFlags(); + final String[] labels = new String[] {" Draw Grid", " Major Ticks", " Minor Ticks", " Ticks if Logarithmic", " Numbers"}; + final int[] xFlags = new int[] {Plot.X_GRID, Plot.X_TICKS, Plot.X_MINOR_TICKS, Plot.X_LOG_TICKS, Plot.X_NUMBERS}; + int rows = xFlags.length; + int columns = dialogType == AXIS_OPTIONS ? 2 : 1; + String[] allLabels = new String[rows*columns]; + boolean[] defaultValues = new boolean[rows*columns]; + String[] headings = dialogType == AXIS_OPTIONS ? new String[]{"X Axis", "Y Axis"} : null; + int i=0; + for (int l=0; l 80) nChars = 80; + //plotXLabel = plotXLabel.replace("\n", "|"); //multiline label currently no supported by Plot class + if (dialogType == AXIS_OPTIONS || dialogType == X_AXIS) + gd.addStringField("X Axis Label", plotXLabel, nChars); + //plotYLabel = plotYLabel.replace("\n", "|"); + if (dialogType == AXIS_OPTIONS || dialogType == Y_AXIS) + gd.addStringField("Y Axis Label", plotYLabel, nChars); + } + if (dialogType == SET_RANGE || dialogType == X_AXIS || dialogType == Y_AXIS) { + Font smallFont = IJ.font10; + gd.setInsets(10, 0, 0); //top, left, bottom + gd.addMessage("* Leave empty for automatic range", smallFont, Color.gray); + gd.setInsets(0, 0, 0); + gd.addMessage("** Requires limits > 0 and max/min > 3", smallFont, Color.gray); + if (dialogType == X_AXIS || dialogType == Y_AXIS) { + gd.setInsets(0, 0, 0); + gd.addMessage(" Label supports !!sub-!! and ^^superscript^^", smallFont, Color.gray); + } + } + + if (dialogType == AXIS_OPTIONS) { + Font plotFont = (plot.currentFont != null) ? plot.currentFont : plot.defaultFont; + Font labelFont = plot.getFont('x'); + if (labelFont == null) labelFont = plotFont; + Font numberFont = plot.getFont('f'); + if (numberFont == null) numberFont = plotFont; + gd.addNumericField("Number Font Size", numberFont.getSize2D(), 1); + gd.addNumericField("Label Font Size", labelFont.getSize2D(), 1); + //gd.setInsets(0, 20, 0); // no extra space + gd.addToSameRow(); + gd.addCheckbox("Bold", labelFont.isBold()); + } + if (dialogType == LEGEND) { + String labels = plot.getDataLabels(); + int nLines = labels.split("\n", -1).length; + Font legendFont = plot.getFont('l'); + if (legendFont == null) legendFont = (plot.currentFont != null) ? plot.currentFont : plot.defaultFont; + int lFlags = plot.getObjectFlags('l'); + if (lFlags != -1) { //if we have a legend already + for (int i=0; i= X_LEFT && dialogType <= Y_TOP) + gd.enableYesNoCancel("OK", "Set All Limits..."); + else if (dialogType == AXIS_OPTIONS) + gd.enableYesNoCancel("OK", "Set Range..."); + else if (dialogType == SET_RANGE) + gd.enableYesNoCancel("OK", "Set Axis Options..."); + return true; + } //setupDialog + + /** This method is called when the user changes something in the dialog. Note that the 'if's for reading + * the fields must be exactly the same as those for setting up the fields in 'setupDialog' (fields must be + * also read in the same sequence). */ + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (dialogShowing && e==null) + return true; //gets called with e=null upon OK; ignore this + boolean livePlot = plot.plotMaker != null; + + if (dialogType == SET_RANGE || dialogType == X_AXIS) { + double[] currentMinMax = plot.getLimits(); + double linXMin = gd.getNextNumber(); + //if (gd.invalidNumber()) + //linXMin = Double.NaN; + double linXMax = gd.getNextNumber(); + //if (gd.invalidNumber()) + //linXMax = Double.NaN; + if (linXMin == linXMax) return false; + if (!minMaxSaved) { + plot.saveMinMax(); //save for 'Previous Range' in plot menu + minMaxSaved = true; + } + if (livePlot) + plot.templateFlags = setFlag(plot.templateFlags, Plot.X_RANGE, gd.getNextBoolean()); + boolean xLog = gd.getNextBoolean(); + plot.setAxisXLog(xLog); + plot.setLimitsNoUpdate(linXMin, linXMax, currentMinMax[2], currentMinMax[3]); + currentMinMax = plot.getLimits(); + enableDisableLogCheckbox(xLogCheckbox, currentMinMax[0], currentMinMax[1]); + } + if (dialogType == SET_RANGE || dialogType == Y_AXIS) { + double[] currentMinMax = plot.getLimits(); + double linYMin = gd.getNextNumber(); + //if (gd.invalidNumber()) + //linYMin = Double.NaN; + double linYMax = gd.getNextNumber(); + //if (gd.invalidNumber()) + //linYMax = Double.NaN; + if (linYMin == linYMax) return false; + if (!minMaxSaved) { + plot.saveMinMax(); //save for 'Previous Range' in plot menu + minMaxSaved = true; + } + + if (livePlot) + plot.templateFlags = setFlag(plot.templateFlags, Plot.Y_RANGE, gd.getNextBoolean()); + boolean yLog = gd.getNextBoolean(); + plot.setAxisYLog(yLog); + plot.setLimitsNoUpdate(currentMinMax[0], currentMinMax[1], linYMin, linYMax); + currentMinMax = plot.getLimits(); + enableDisableLogCheckbox(yLogCheckbox, currentMinMax[2], currentMinMax[3]); + } + if (dialogType >= X_LEFT && dialogType <= Y_TOP) { + double newLimit = gd.getNextNumber(); + double[] minMaxCopy = (double[])(plot.getLimits().clone()); + minMaxCopy[dialogType - X_LEFT] = newLimit; + plot.setLimitsNoUpdate(minMaxCopy[0], minMaxCopy[1], minMaxCopy[2], minMaxCopy[3]); + } + + if (dialogType == AXIS_OPTIONS || dialogType == X_AXIS || dialogType == Y_AXIS) { + final int[] xFlags = new int[] {Plot.X_GRID, Plot.X_TICKS, Plot.X_MINOR_TICKS, Plot.X_LOG_TICKS, Plot.X_NUMBERS}; + int rows = xFlags.length; + int columns = dialogType == AXIS_OPTIONS ? 2 : 1; + int flags = 0; + if (dialogType == X_AXIS) + flags = plot.getFlags() & 0xaaaaaaaa; //keep y flags, i.e., odd bits + if (dialogType == Y_AXIS) + flags = plot.getFlags() & 0x55555555; //keep x flags, i.e., even bits + for (int l=0; l 24) numberFontSize = 24f; + float labelFontSize = (float)gd.getNextNumber(); + if (gd.invalidNumber()) labelFontSize = labelFont.getSize2D(); + boolean axisLabelBold = gd.getNextBoolean(); + plot.setFont('f', numberFont.deriveFont(numberFont.getStyle(), numberFontSize)); + plot.setAxisLabelFont(axisLabelBold ? Font.BOLD : Font.PLAIN, labelFontSize); + Font smallFont = new Font("SansSerif", Font.PLAIN, (int)(10*Prefs.getGuiScale())); + gd.addMessage("Labels support !!sub-!! and ^^superscript^^", smallFont, Color.gray); + } + if (dialogType == LEGEND) { + Font legendFont = plot.getFont('l'); + if (legendFont == null) legendFont = (plot.currentFont != null) ? plot.currentFont : plot.defaultFont; + + String labels = gd.getNextText(); + legendPosNumber = gd.getNextChoiceIndex(); + int lFlags = LEGEND_POSITION_N[legendPosNumber]; + float legendFontSize = (float)gd.getNextNumber(); + transparentBackground = gd.getNextBoolean(); + bottomUp = gd.getNextBoolean(); + if (bottomUp) + lFlags |= Plot.LEGEND_BOTTOM_UP; + if (transparentBackground) + lFlags |= Plot.LEGEND_TRANSPARENT; + plot.setColor(Color.black); + plot.setLineWidth(1); + plot.setLegend(labels, lFlags); + plot.setFont('l', legendFont.deriveFont(legendFont.getStyle(), legendFontSize)); + } + if (dialogType == TEMPLATE) { + Plot templatePlot = templatePlots[gd.getNextChoiceIndex()]; + ImagePlus imp = templatePlot.getImagePlus(); + if (imp != null) templateID = imp.getID(); //remember for next time + int templateFlags = 0; + for (int i=0; i0) //more range checking is done in Plot.setScale + hiResFactor = (float)scale; + hiResAntiAliased = !gd.getNextBoolean(); + final ImagePlus hiresImp = plot.makeHighResolution(title, hiResFactor, hiResAntiAliased, /*showIt=*/true); + /** The following command is needed to have the high-resolution plot as front window. Otherwise, as the + * dialog is owned by the original PlotWindow, the WindowManager will see the original plot as active, + * but the user interface will show the high-res plot as foreground window */ + EventQueue.invokeLater(new Runnable() {public void run() {IJ.selectWindow(hiresImp.getID());}}); + + if (Recorder.record) { + if (Recorder.scriptMode()) { + Recorder.recordCall("plot.makeHighResolution(\""+title+"\","+hiResFactor+","+hiResAntiAliased+",true);"); + } else { + String options = !hiResAntiAliased ? "disable" : ""; + if (options.length() > 0) + options = ",\""+options+"\""; + Recorder.recordString("Plot.makeHighResolution(\""+title+"\","+hiResFactor+options+");\n"); + } + } + } + + /** Disables switching on a checkbox for log range if the axis limits do not allow it. + * The checkbox can be always switched off. */ + void enableDisableLogCheckbox(Checkbox checkbox, double limit1, double limit2) { + boolean logPossible = limit1 > 0 && limit2 > 0 && (limit1 > 3*limit2 || limit2 > 3*limit1); + checkbox.setEnabled(logPossible); + } + + + boolean getFlag(int flags, int bitMask) { + return (flags&bitMask) != 0; + } + + int setFlag(int flags, int bitMask, boolean state) { + flags &= ~bitMask; + if (state) flags |= bitMask; + return flags; + } + + Checkbox lastCheckboxAdded(GenericDialog gd) { + Vector checkboxes = gd.getCheckboxes(); + return (Checkbox)(checkboxes.get(checkboxes.size() - 1)); + } + +} diff --git a/src/ij/gui/PlotMaker.java b/src/ij/gui/PlotMaker.java new file mode 100644 index 0000000..ddaef8a --- /dev/null +++ b/src/ij/gui/PlotMaker.java @@ -0,0 +1,15 @@ +package ij.gui; +import ij.ImagePlus; + +/** Plugins that generate "Live" profile plots (Profiler and ZAxisProfiler) + displayed in PlotWindows implement this interface. */ +public interface PlotMaker { + + /** Returns a profile plot. */ + public Plot getPlot(); + + /** Returns the ImagePlus used to generate the profile plots. */ + public ImagePlus getSourceImage(); + +} + diff --git a/src/ij/gui/PlotVirtualStack.java b/src/ij/gui/PlotVirtualStack.java new file mode 100644 index 0000000..6ef864a --- /dev/null +++ b/src/ij/gui/PlotVirtualStack.java @@ -0,0 +1,85 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import java.util.*; +import java.io.*; + +/** This is a virtual stack of frozen plots. */ +public class PlotVirtualStack extends VirtualStack { + private Vector plots = new Vector(50); + private int bitDepth = 8; + + public PlotVirtualStack(int width, int height) { + super(width, height); + this.bitDepth = bitDepth; + } + + /** Adds a plot to the end of the stack. */ + public void addPlot(Plot plot) { + plots.add(plot.toByteArray()); + if (plot.isColored()) + bitDepth = 24; + } + + /** Returns the pixel array for the specified slice, were 1<=n<=nslices. */ + public Object getPixels(int n) { + ImageProcessor ip = getProcessor(n); + if (ip!=null) + return ip.getPixels(); + else + return null; + } + + /** Returns an ImageProcessor for the specified slice, + were 1<=n<=nslices. Returns null if the stack is empty. */ + public ImageProcessor getProcessor(int n) { + byte[] bytes = (byte[])plots.get(n-1); + if (bytes!=null) { + try { + Plot plot = new Plot(null, new ByteArrayInputStream(bytes)); + ImageProcessor ip = plot.getProcessor(); + if (bitDepth==24) + ip = ip.convertToRGB(); + else if (bitDepth==8) + ip = ip.convertToByte(false); + return ip; + } catch (Exception e) { + IJ.handleException(e); + } + } + return null; + } + + /** Returns the number of slices in this stack. */ + public int getSize() { + return plots.size(); + } + + /** Returns either 24 (RGB) or 8 (grayscale). */ + public int getBitDepth() { + return bitDepth; + } + + public void setBitDepth(int bitDepth) { + this.bitDepth = bitDepth; + } + + public String getSliceLabel(int n) { + return null; + } + + public void setPixels(Object pixels, int n) { + } + + /** Deletes the specified slice, were 1<=n<=nslices. */ + public void deleteSlice(int n) { + if (n<1 || n>plots.size()) + throw new IllegalArgumentException("Argument out of range: "+n); + if (plots.size()<1) + return; + plots.remove(n-1); + } + + +} // PlotVirtualStack + diff --git a/src/ij/gui/PlotWindow.java b/src/ij/gui/PlotWindow.java new file mode 100644 index 0000000..b411f01 --- /dev/null +++ b/src/ij/gui/PlotWindow.java @@ -0,0 +1,917 @@ +package ij.gui; + +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.awt.datatransfer.*; +import java.util.*; +import ij.*; +import ij.process.*; +import ij.util.*; +import ij.text.TextWindow; +import ij.plugin.filter.Analyzer; +import ij.plugin.filter.PlugInFilterRunner; +import ij.measure.*; +import ij.io.SaveDialog; + +/** This class implements the Analyze/Plot Profile command. +* @author Michael Schmid +* @author Wayne Rasband +*/ +public class PlotWindow extends ImageWindow implements ActionListener, ItemListener, + ClipboardOwner, ImageListener, RoiListener, Runnable { + + private static final int WIDTH = 600; + private static final int HEIGHT = 340; + private static final int FONT_SIZE = 14; + private static final String PREFS_WIDTH = "pp.width"; + private static final String PREFS_HEIGHT = "pp.height"; + private static final String PREFS_FONT_SIZE = "pp.fontsize"; + + /** @deprecated */ + public static final int CIRCLE = Plot.CIRCLE; + /** @deprecated */ + public static final int X = Plot.X; + /** @deprecated */ + public static final int BOX = Plot.BOX; + /** @deprecated */ + public static final int TRIANGLE = Plot.TRIANGLE; + /** @deprecated */ + public static final int CROSS = Plot.CROSS; + /** @deprecated */ + public static final int LINE = Plot.LINE; + /** Write first X column when listing or saving. */ + public static boolean saveXValues = true; + /** Automatically close window after saving values. To set, use Edit/Options/Plots. */ + public static boolean autoClose; + /** Display the XY coordinates in a separate window. To set, use Edit/Options/Plots. */ + public static boolean listValues; + /** Interpolate line profiles. To set, use Edit/Options/Plots or setOption("InterpolateLines",boolean). */ + public static boolean interpolate = true; + // default values for new installations; values will be then saved in prefs + private static int defaultFontSize = Prefs.getInt(PREFS_FONT_SIZE, FONT_SIZE); + /** The width of the plot (without frame) in pixels. */ + public static int plotWidth = WIDTH; + /** The height of the plot in pixels. */ + public static int plotHeight = HEIGHT; + /** The plot text size, can be overridden by Plot.setFont, Plot.setFontSize, Plot.setXLabelFont etc. */ + public static int fontSize = defaultFontSize; + /** Have axes with no grid lines. If both noGridLines and noTicks are true, + * only min&max value of the axes are given */ + public static boolean noGridLines; + /** Have axes with no ticks. If both noGridLines and noTicks are true, + * only min&max value of the axes are given */ + public static boolean noTicks; + + private static final String OPTIONS = "pp.options"; + private static final int SAVE_X_VALUES = 1; + private static final int AUTO_CLOSE = 2; + private static final int LIST_VALUES = 4; + private static final int INTERPOLATE = 8; + private static final int NO_GRID_LINES = 16; + private static final int NO_TICKS = 32; + private static String moreButtonLabel = "More "+'\u00bb'; + private static String dataButtonLabel = "Data "+'\u00bb'; + + boolean wasActivated; // true after window has been activated once, needed by PlotCanvas + + private Button list, data, more, live; + private PopupMenu dataPopupMenu, morePopupMenu; + private static final int NUM_MENU_ITEMS = 20; //how many menu items we have in total + private MenuItem[] menuItems = new MenuItem[NUM_MENU_ITEMS]; + private Label statusLabel; + private String userStatusText; + private static String defaultDirectory = null; + private static int options; + private int defaultDigits = -1; + private int markSize = 5; + private static Plot staticPlot; + private Plot plot; + + private PlotMaker plotMaker; + private ImagePlus srcImp; // the source image for live plotting + private Thread bgThread; // thread for plotting (in the background) + private boolean doUpdate; // tells the background thread to update + + private Roi[] rangeArrowRois; // the overlays (arrows etc) for changing the range. Note: #10-15 must correspond to PlotDialog.dialogType! + private boolean rangeArrowsVisible; + private int activeRangeArrow = -1; + private static Color inactiveRangeArrowColor = Color.GRAY; + private static Color inactiveRangeRectColor = new Color(0x20404040, true); //transparent gray + private static Color activeRangeArrowColor = Color.RED; + private static Color activeRangeRectColor = new Color(0x18ff0000, true); //transparent red + + // static initializer + static { + options = Prefs.getInt(OPTIONS, SAVE_X_VALUES); + autoClose = (options&AUTO_CLOSE)!=0; + plotWidth = Prefs.getInt(PREFS_WIDTH, WIDTH); + plotHeight = Prefs.getInt(PREFS_HEIGHT, HEIGHT); + Dimension screen = IJ.getScreenSize(); + if (plotWidth>screen.width && plotHeight>screen.height) { + plotWidth = WIDTH; + plotHeight = HEIGHT; + } + } + + /** + * @deprecated + * replaced by the Plot class. + */ + public PlotWindow(String title, String xLabel, String yLabel, float[] xValues, float[] yValues) { + super(createImage(title, xLabel, yLabel, xValues, yValues)); + plot = staticPlot; + ((PlotCanvas)getCanvas()).setPlot(plot); + } + + /** + * @deprecated + * replaced by the Plot class. + */ + public PlotWindow(String title, String xLabel, String yLabel, double[] xValues, double[] yValues) { + this(title, xLabel, yLabel, Tools.toFloat(xValues), Tools.toFloat(yValues)); + } + + /** Creates a PlotWindow from a given ImagePlus with a Plot object. + * (called when reading an ImagePlus with an associated plot from a file) */ + public PlotWindow(ImagePlus imp, Plot plot) { + super(imp); + ((PlotCanvas)getCanvas()).setPlot(plot); + this.plot = plot; + draw(); + } + + /** Creates a PlotWindow from a Plot object. */ + PlotWindow(Plot plot) { + super(plot.getImagePlus()); + ((PlotCanvas)getCanvas()).setPlot(plot); + this.plot = plot; + draw(); + } + + /** Called by the constructor to generate the image the plot will be drawn on. + This is a static method because constructors cannot call instance methods. */ + static ImagePlus createImage(String title, String xLabel, String yLabel, float[] xValues, float[] yValues) { + staticPlot = new Plot(title, xLabel, yLabel, xValues, yValues); + return new ImagePlus(title, staticPlot.getBlankProcessor()); + } + + /** Sets the x-axis and y-axis range. + * @deprecated use the corresponding method of the Plot class */ + public void setLimits(double xMin, double xMax, double yMin, double yMax) { + plot.setLimits(xMin, xMax, yMin, yMax); + } + + /** Adds a set of points to the plot or adds a curve if shape is set to LINE. + * Note that there are more options available by using the methods of the Plot class instead. + * @param x the x-coodinates + * @param y the y-coodinates + * @param shape Plot.CIRCLE, X, BOX, TRIANGLE, CROSS, LINE etc. + * @deprecated use the corresponding method of the Plot class */ + public void addPoints(float[] x, float[] y, int shape) { + plot.addPoints(x, y, shape); + } + + /** Adds a set of points to the plot using double arrays. + * Must be called before the plot is displayed. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void addPoints(double[] x, double[] y, int shape) { + addPoints(Tools.toFloat(x), Tools.toFloat(y), shape); + } + + /** Adds vertical error bars to the plot. + * Must be called before the plot is displayed. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void addErrorBars(float[] errorBars) { + plot.addErrorBars(errorBars); + } + + /** Draws a label. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void addLabel(double x, double y, String label) { + plot.addLabel(x, y, label); + } + + /** Changes the drawing color. The frame and labels are + * always drawn in black. + * Must be called before the plot is displayed. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void setColor(Color c) { + plot.setColor(c); + } + + /** Changes the line width. + * Must be called before the plot is displayed. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void setLineWidth(int lineWidth) { + plot.setLineWidth(lineWidth); + } + + /** Changes the font. + * Must be called before the plot is displayed. + * Note that there are more options available by using the methods of the Plot class instead. + * @deprecated use the corresponding method of the Plot class */ + public void changeFont(Font font) { + plot.changeFont(font); + } + + /** Displays the plot. */ + public void draw() { + Panel bottomPanel = new Panel(); + int hgap = IJ.isMacOSX()?1:5; + + list = new Button(" List "); + list.addActionListener(this); + bottomPanel.add(list); + bottomPanel.setLayout(new FlowLayout(FlowLayout.RIGHT,hgap,0)); + data = new Button(dataButtonLabel); + data.addActionListener(this); + bottomPanel.add(data); + more = new Button(moreButtonLabel); + more.addActionListener(this); + bottomPanel.add(more); + if (plot!=null && plot.getPlotMaker()!=null) { + live = new Button("Live"); + live.addActionListener(this); + bottomPanel.add(live); + } + statusLabel = new Label(); + statusLabel.setFont(new Font("Monospaced", Font.PLAIN, 12)); + statusLabel.setBackground(new Color(220, 220, 220)); + bottomPanel.add(statusLabel); + add(bottomPanel); + data.add(getDataPopupMenu()); + more.add(getMorePopupMenu()); + plot.draw(); + LayoutManager lm = getLayout(); + if (lm instanceof ImageLayout) + ((ImageLayout)lm).ignoreNonImageWidths(true); //don't expand size to make the panel fit + GUI.scale(bottomPanel); + maximizeCoordinatesLabelWidth(); + pack(); + + ImageProcessor ip = plot.getProcessor(); + boolean ipIsColor = ip instanceof ColorProcessor; + boolean impIsColor = imp.getProcessor() instanceof ColorProcessor; + if (ipIsColor != impIsColor) + imp.setProcessor(null, ip); + else + imp.updateAndDraw(); + if (listValues) + showList(/*useLabels=*/false); + else + ic.requestFocus(); //have focus on the canvas, not the button, so that pressing the space bar allows panning + } + + /** Sets the Plot object shown in this PlotWindow. Does not update the window. */ + public void setPlot(Plot plot) { + this.plot = plot; + ((PlotCanvas)getCanvas()).setPlot(plot); + } + + /** Releases the resources used by this PlotWindow */ + public void dispose() { + if (plot!=null) + plot.dispose(); + disableLivePlot(); + plot = null; + plotMaker = null; + srcImp = null; + super.dispose(); + } + + /** Called when the window is activated (WindowListener) + * Window layout is finished at latest a few millisec after windowActivated, then the + * 'wasActivated' boolean is set to tell the ImageCanvas that resize events should + * lead to resizing the canvas (before, creating the layout can lead to resize events)*/ + public void windowActivated(WindowEvent e) { + super.windowActivated(e); + if (!wasActivated) { + new Thread(new Runnable() { + public void run() { + IJ.wait(50); //sometimes, window layout is done only a few millisec after windowActivated + wasActivated = true; + } + }).start(); + } + } + + /** Called when the canvas is resized */ + void canvasResized() { + if (plot == null) return; + /*Dimension d1 = getExtraSize(); + Dimension d2 = plot.getMinimumSize(); + setMinimumSize(new Dimension(d1.width + d2.width, d1.height + d2.height));*/ + maximizeCoordinatesLabelWidth(); + } + + /** Maximizes the width for the coordinate&status readout field and its parent bottomPanel */ + void maximizeCoordinatesLabelWidth() { + Insets insets = getInsets(); //by default, left & right insets are 0 anyhow + Component parent = statusLabel.getParent(); //the bottomPanel, has insets of 0 + if (!parent.isValid()) parent.validate(); + int cWidth = getWidth() - 2*HGAP - statusLabel.getX() - insets.left - insets.right; + int cHeight = statusLabel.getPreferredSize().height; + statusLabel.setPreferredSize(new Dimension(cWidth, cHeight)); + parent.setSize(getWidth() - 2*HGAP, parent.getHeight()); + } + + /** Shows the text in the coordinate&status readout field at the bottom. + * This text may get temporarily replaced for 'tooltips' (mouse over range arrows etc.). + * Call with a null argument to enable coordinate readout again. */ + public void showStatus(String text) { + userStatusText = text; + if (statusLabel != null) + statusLabel.setText(text == null ? "" : text); + } + + /** Names for popupMenu items. Update NUM_MENU_ITEMS at the top when adding new ones! */ + private static int SAVE=0, COPY=1, COPY_ALL=2, LIST_SIMPLE=3, ADD_FROM_TABLE=4, ADD_FROM_PLOT=5, ADD_FIT=6, //data menu + SET_RANGE=7, PREV_RANGE=8, RESET_RANGE=9, FIT_RANGE=10, //the rest is in the more menu + ZOOM_SELECTION=11, AXIS_OPTIONS=12, LEGEND=13, STYLE=14, TEMPLATE=15, RESET_PLOT=16, + FREEZE=17, HI_RESOLUTION=18, PROFILE_PLOT_OPTIONS=19; + //the following commands are disabled when the plot is frozen + private static int[] DISABLED_WHEN_FROZEN = new int[]{ADD_FROM_TABLE, ADD_FROM_PLOT, ADD_FIT, + SET_RANGE, PREV_RANGE, RESET_RANGE, FIT_RANGE, ZOOM_SELECTION, AXIS_OPTIONS, LEGEND, STYLE, RESET_PLOT}; + + /** Prepares and returns the popupMenu of the Data>> button */ + PopupMenu getDataPopupMenu() { + dataPopupMenu = new PopupMenu(); + GUI.scalePopupMenu(dataPopupMenu); + menuItems[SAVE] = addPopupItem(dataPopupMenu, "Save Data..."); + menuItems[COPY] = addPopupItem(dataPopupMenu, "Copy 1st Data Set"); + menuItems[COPY_ALL] = addPopupItem(dataPopupMenu, "Copy All Data"); + menuItems[LIST_SIMPLE] = addPopupItem(dataPopupMenu, "List (Simple Headings)"); + dataPopupMenu.addSeparator(); + menuItems[ADD_FROM_TABLE] = addPopupItem(dataPopupMenu, "Add from Table..."); + menuItems[ADD_FROM_PLOT] = addPopupItem(dataPopupMenu, "Add from Plot..."); + menuItems[ADD_FIT] = addPopupItem(dataPopupMenu, "Add Fit..."); + return dataPopupMenu; + } + + /** Prepares and returns the popupMenu of the More>> button */ + PopupMenu getMorePopupMenu() { + morePopupMenu = new PopupMenu(); + GUI.scalePopupMenu(morePopupMenu); + menuItems[SET_RANGE] = addPopupItem(morePopupMenu, "Set Range..."); + menuItems[PREV_RANGE] = addPopupItem(morePopupMenu, "Previous Range"); + menuItems[RESET_RANGE] = addPopupItem(morePopupMenu, "Reset Range"); + menuItems[FIT_RANGE] = addPopupItem(morePopupMenu, "Set Range to Fit All"); + menuItems[ZOOM_SELECTION] = addPopupItem(morePopupMenu, "Zoom to Selection"); + morePopupMenu.addSeparator(); + menuItems[AXIS_OPTIONS] = addPopupItem(morePopupMenu, "Axis Options..."); + menuItems[LEGEND] = addPopupItem(morePopupMenu, "Legend..."); + menuItems[STYLE] = addPopupItem(morePopupMenu, "Contents Style..."); + menuItems[TEMPLATE] = addPopupItem(morePopupMenu, "Use Template..."); + menuItems[RESET_PLOT] = addPopupItem(morePopupMenu, "Reset Format"); + menuItems[FREEZE] = addPopupItem(morePopupMenu, "Freeze Plot", true); + menuItems[HI_RESOLUTION] = addPopupItem(morePopupMenu, "High-Resolution Plot..."); + morePopupMenu.addSeparator(); + menuItems[PROFILE_PLOT_OPTIONS] = addPopupItem(morePopupMenu, "Plot Defaults..."); + return morePopupMenu; + } + + MenuItem addPopupItem(PopupMenu popupMenu, String s) { + return addPopupItem(popupMenu, s, false); + } + + MenuItem addPopupItem(PopupMenu popupMenu, String s, boolean isCheckboxItem) { + MenuItem mi = null; + if (isCheckboxItem) { + mi = new CheckboxMenuItem(s); + ((CheckboxMenuItem)mi).addItemListener(this); + } else { + mi = new MenuItem(s); + mi.addActionListener(this); + } + popupMenu.add(mi); + return mi; + } + + /** Called if user has activated a button or popup menu item */ + public void actionPerformed(ActionEvent e) { + try { + Object b = e.getSource(); + if (b==live) + toggleLiveProfiling(); + else if (b==list) + showList(/*useLabels=*/true); + else if (b==data) { + enableDisableMenuItems(); + dataPopupMenu.show((Component)b, 1, 1); + } else if (b==more) { + enableDisableMenuItems(); + morePopupMenu.show((Component)b, 1, 1); + } else if (b==menuItems[SAVE]) + saveAsText(); + else if (b==menuItems[COPY]) + copyToClipboard(false); + else if (b==menuItems[COPY_ALL]) + copyToClipboard(true); + else if (b==menuItems[LIST_SIMPLE]) + showList(/*useLabels=*/false); + else if (b==menuItems[ADD_FROM_TABLE]) + new PlotContentsDialog(plot, PlotContentsDialog.ADD_FROM_TABLE).showDialog(this); + else if (b==menuItems[ADD_FROM_PLOT]) + new PlotContentsDialog(plot, PlotContentsDialog.ADD_FROM_PLOT).showDialog(this); + else if (b==menuItems[ADD_FIT]) + new PlotContentsDialog(plot, PlotContentsDialog.ADD_FIT).showDialog(this); + else if (b==menuItems[ZOOM_SELECTION]) { + if (imp!=null && imp.getRoi()!=null && imp.getRoi().isArea()) + plot.zoomToRect(imp.getRoi().getBounds()); + } else if (b==menuItems[SET_RANGE]) + new PlotDialog(plot, PlotDialog.SET_RANGE).showDialog(this); + else if (b==menuItems[PREV_RANGE]) + plot.setPreviousMinMax(); + else if (b==menuItems[RESET_RANGE]) + plot.setLimitsToDefaults(true); + else if (b==menuItems[FIT_RANGE]) + plot.setLimitsToFit(true); + else if (b==menuItems[AXIS_OPTIONS]) + new PlotDialog(plot, PlotDialog.AXIS_OPTIONS).showDialog(this); + else if (b==menuItems[LEGEND]) + new PlotDialog(plot, PlotDialog.LEGEND).showDialog(this); + else if (b==menuItems[STYLE]) + new PlotContentsDialog(plot, PlotContentsDialog.STYLE).showDialog(this); + else if (b==menuItems[TEMPLATE]) + new PlotDialog(plot, PlotDialog.TEMPLATE).showDialog(this); + else if (b==menuItems[RESET_PLOT]) { + plot.setFont(Font.PLAIN, fontSize); + plot.setAxisLabelFont(Font.PLAIN, fontSize); + plot.setFormatFlags(Plot.getDefaultFlags()); + plot.setFrameSize(plotWidth, plotHeight); //updates the image only when size changed + plot.updateImage(); + } else if (b==menuItems[HI_RESOLUTION]) + new PlotDialog(plot, PlotDialog.HI_RESOLUTION).showDialog(this); + else if (b==menuItems[PROFILE_PLOT_OPTIONS]) + IJ.doCommand("Plots..."); + ic.requestFocus(); //have focus on the canvas, not the button, so that pressing the space bar allows panning + } catch (Exception ex) { IJ.handleException(ex); } + } + + private void enableDisableMenuItems() { + boolean frozen = plot.isFrozen(); //prepare menu according to 'frozen' state of plot + ((CheckboxMenuItem)menuItems[FREEZE]).setState(frozen); + for (int i : DISABLED_WHEN_FROZEN) + menuItems[i].setEnabled(!frozen); + if (!PlotContentsDialog.tableWindowExists()) + menuItems[ADD_FROM_TABLE].setEnabled(false); + if (plot.getDataObjectDesignations().length == 0) + menuItems[ADD_FIT].setEnabled(false); + } + + /** Called if the user activates/deactivates a CheckboxMenuItem */ + public void itemStateChanged(ItemEvent e) { + if (e.getSource()==menuItems[FREEZE]) { + boolean frozen = ((CheckboxMenuItem)menuItems[FREEZE]).getState(); + plot.setFrozen(frozen); + } + } + + /** + * Updates the X and Y values when the mouse is moved and, if appropriate, + * shows/hides the overlay with the triangular buttons for changing the axis + * range limits. + * Overrides mouseMoved() in ImageWindow. + * + * @see ij.gui.ImageWindow#mouseMoved + */ + public void mouseMoved(int x, int y) { + super.mouseMoved(x, y); + if (plot == null) + return; + String statusText = null; //coordinate readout, status or tooltip, will be shown in coordinate&status line + + //arrows and other symbols for modifying the plot range + if (x < plot.leftMargin || y > plot.topMargin + plot.frameHeight) { + if (!rangeArrowsVisible && !plot.isFrozen()) + showRangeArrows(); + if (activeRangeArrow < 0) //mouse is not on one of the symbols, ignore (nothing to display) + {} + else if (activeRangeArrow < 8) //mouse over an arrow: 0,3,4,7 for increase, 1,2,5,6 for decrease + statusText = ((activeRangeArrow+1)&0x02) != 0 ? "Decrease Range" : "Increase Range"; + else if (activeRangeArrow == 8) //it's the 'R' icon + statusText = "Reset Range"; + else if (activeRangeArrow == 9) //it's the 'F' icon + statusText = "Full Range (Fit All)"; + else if (activeRangeArrow >= 10 && + activeRangeArrow < 14) //space between arrow-pairs for single number + statusText = "Set limit..."; + else if (activeRangeArrow >= 14) + statusText = "Axis Range & Options..."; + boolean repaint = false; + if (activeRangeArrow >= 0 && !rangeArrowRois[activeRangeArrow].contains(x, y)) { + rangeArrowRois[activeRangeArrow].setFillColor( + activeRangeArrow < 10 ? inactiveRangeArrowColor : inactiveRangeRectColor); + repaint = true; //de-highlight arrow where cursor has moved out + activeRangeArrow = -1; + } + if (activeRangeArrow < 0) { //no currently highlighted arrow, do we have a new one? + int i = getRangeArrowIndex(x, y); + if (i >= 0) { //we have an arrow or symbol at cursor position + rangeArrowRois[i].setFillColor( + i < 14 ? activeRangeArrowColor : activeRangeRectColor); + activeRangeArrow = i; + repaint = true; + } + } + if (repaint) ic.repaint(); + } else if (rangeArrowsVisible) + hideRangeArrows(); + + if (statusText == null) + statusText = userStatusText != null ? userStatusText : plot.getCoordinates(x, y); + if (statusLabel != null) + statusLabel.setText(statusText); + } + + /** Called by PlotCanvas */ + void mouseExited(MouseEvent e) { + if (rangeArrowsVisible) + hideRangeArrows(); + } + + /** Mouse wheel: zooms when shift or ctrl is pressed, scrolls in x if space bar down, in y otherwise. */ + public synchronized void mouseWheelMoved(MouseWheelEvent e) { + if (plot.isFrozen() || !(ic instanceof PlotCanvas)) { //frozen plots are like normal images + super.mouseWheelMoved(e); + return; + } + int rotation = e.getWheelRotation(); + int amount = e.getScrollAmount(); + if (e.getX() < plot.leftMargin || e.getX() > plot.leftMargin + plot.frameWidth)//n__ + return; + if (e.getY() < plot.topMargin || e.getY() > plot.topMargin + plot.frameHeight) + return; + boolean ctrl = (e.getModifiers()&Event.CTRL_MASK)!=0; + if (amount<1) amount=1; + if (rotation==0) + return; + if (ctrl||IJ.shiftKeyDown()) { + double zoomFactor = rotation<0 ? Math.pow(2, 0.2) : Math.pow(0.5, 0.2); + Point loc = ic.getCursorLoc(); + int x = ic.screenX(loc.x); + int y = ic.screenY(loc.y); + ((PlotCanvas)ic).zoom(x, y, zoomFactor); + } else if (IJ.spaceBarDown()) + plot.scroll(rotation*amount*Math.max(ic.imageWidth/50, 1), 0); + else + plot.scroll(0, rotation*amount*Math.max(ic.imageHeight/50, 1)); + } + + /** + * Creates an overlay with triangular buttons and othr symbols for changing the axis range + * limits and shows it + */ + void showRangeArrows() { + if (imp == null) + return; + hideRangeArrows(); //in case we have old arrows from a different plot size or so + rangeArrowRois = new Roi[4 * 2 + 2 + 4 + 2]; //4 arrows per axis, + 'Reset' and 'Fit All' icons, + 4 numerical input boxes + 2 axes + int i = 0; + int height = imp.getHeight(); + int arrowH = plot.topMargin < 14 ? 6 : 8; //height of arrows and distance between them; base is twice that value + float[] yP = new float[]{height - arrowH / 2, height - 3 * arrowH / 2, height - 5 * arrowH / 2 - 0.1f}; + + for (float x : new float[]{plot.leftMargin, plot.leftMargin + plot.frameWidth}) { //create arrows for x axis + float[] x0 = new float[]{x - arrowH / 2, x - 3 * arrowH / 2 - 0.1f, x - arrowH / 2}; + rangeArrowRois[i++] = new PolygonRoi(x0, yP, 3, Roi.POLYGON); + float[] x1 = new float[]{x + arrowH / 2, x + 3 * arrowH / 2 + 0.1f, x + arrowH / 2}; + rangeArrowRois[i++] = new PolygonRoi(x1, yP, 3, Roi.POLYGON); + } + float[] xP = new float[]{arrowH / 2 - 0.1f, 3 * arrowH / 2, 5 * arrowH / 2 + 0.1f}; + for (float y : new float[]{plot.topMargin + plot.frameHeight, plot.topMargin}) { //create arrows for y axis + float[] y0 = new float[]{y + arrowH / 2, y + 3 * arrowH / 2 + 0.1f, y + arrowH / 2}; + rangeArrowRois[i++] = new PolygonRoi(xP, y0, 3, Roi.POLYGON); + float[] y1 = new float[]{y - arrowH / 2, y - 3 * arrowH / 2 - 0.1f, y - arrowH / 2}; + rangeArrowRois[i++] = new PolygonRoi(xP, y1, 3, Roi.POLYGON); + } + Font theFont = new Font("SansSerif", Font.BOLD, 13); + + TextRoi txtRoi = new TextRoi(1, height - 19, "\u2009R\u2009", theFont); //thin spaces to make roi slightly wider + rangeArrowRois[8] = txtRoi; + TextRoi txtRoi2 = new TextRoi(20, height - 19, "\u2009F\u2009", theFont); + rangeArrowRois[9] = txtRoi2; + + rangeArrowRois[10] = new Roi(plot.leftMargin - arrowH/2 + 1, height - 5 * arrowH / 2, arrowH - 2, arrowH * 2);//numerical box left + rangeArrowRois[11] = new Roi(plot.leftMargin + plot.frameWidth - arrowH/2 + 1, height - 5 * arrowH / 2, arrowH - 2, arrowH * 2);//numerical box right + rangeArrowRois[12] = new Roi(arrowH / 2, plot.topMargin + plot.frameHeight - arrowH/2 + 1, arrowH * 2, arrowH -2);//numerical box bottom + rangeArrowRois[13] = new Roi(arrowH / 2, plot.topMargin - arrowH/2 + 1, arrowH * 2, arrowH - 2 );//numerical box top + + int topMargin = plot.topMargin; + int bottomMargin = topMargin + plot.frameHeight; + int leftMargin = plot.leftMargin; + int rightMargin = plot.leftMargin + plot.frameWidth; + rangeArrowRois[14] = new Roi(leftMargin, bottomMargin+2, // area to click for x axis options + rightMargin - leftMargin + 1, 2*arrowH); + rangeArrowRois[15] = new Roi(leftMargin-2*arrowH-2, topMargin, // area to click for y axis options + 2*arrowH, bottomMargin - topMargin + 1); + + Overlay ovly = imp.getOverlay(); + if (ovly == null) + ovly = new Overlay(); + for (Roi roi : rangeArrowRois) { + if (roi instanceof PolygonRoi) + roi.setFillColor(inactiveRangeArrowColor); + else if (roi instanceof TextRoi) { + roi.setStrokeColor(Color.WHITE); + roi.setFillColor(inactiveRangeArrowColor); + } else + roi.setFillColor(inactiveRangeRectColor); //transparent gray for single number boxes and axis range + ovly.add(roi); + } + imp.setOverlay(ovly); + ic.repaint(); + rangeArrowsVisible = true; + } + + void hideRangeArrows() { + if (imp == null || rangeArrowRois==null) return; + Overlay ovly = imp.getOverlay(); + if (ovly == null) return; + for (Roi roi : rangeArrowRois) + ovly.remove(roi); + ic.repaint(); + rangeArrowsVisible = false; + activeRangeArrow = -1; + } + + /** Returns the index of the range-modifying symbol or axis at the + * cursor position x,y, or -1 of none. + * Index numbers for arrows start with 0 at the 'down' arrow of the + * lower side of the x axis and end with 7 the up arrow at the upper + * side of the y axis. Numbers 8 & 9 are for "Reset Range" and "Fit All"; + * numbers 10-13 for a dialog to set a single limit, and 14-15 for the axis options. */ + + int getRangeArrowIndex(int x, int y) { + if (!rangeArrowsVisible) return -1; + for (int i=0; i0) { + plot.useTemplate(this.plot, this.plot.templateFlags | Plot.COPY_SIZE | Plot.COPY_LABELS | Plot.COPY_AXIS_STYLE | + Plot.COPY_CONTENTS_STYLE | Plot.COPY_LEGEND | Plot.COPY_EXTRA_OBJECTS); + plot.setPlotMaker(plotMaker); + this.plot = plot; + ((PlotCanvas)ic).setPlot(plot); + ImageProcessor ip = plot.getProcessor(); + if (ip!=null && imp!=null) { + imp.setProcessor(null, ip); + plot.setImagePlus(imp); + } + } + synchronized(this) { + if (doUpdate) { + doUpdate = false; //and loop again + } else { + try {wait();} //notify wakes up the thread + catch(InterruptedException e) { //interrupted tells the thread to exit + return; + } + } + } + } + } + + /** Returns the Plot associated with this PlotWindow. */ + public Plot getPlot() { + return plot; + } + + /** Freezes the active plot window, so the image does not get redrawn for zooming, + * setting the range, etc. */ + public static void freeze() { + Window win = WindowManager.getActiveWindow(); + if (win!=null && (win instanceof PlotWindow)) + ((PlotWindow)win).getPlot().setFrozen(true); + } + + public static void setDefaultFontSize(int size) { + if (size < 9) size = 9; + defaultFontSize = size; + } + + public static int getDefaultFontSize() { + return defaultFontSize; + } + +} diff --git a/src/ij/gui/PointRoi.java b/src/ij/gui/PointRoi.java new file mode 100644 index 0000000..2986b31 --- /dev/null +++ b/src/ij/gui/PointRoi.java @@ -0,0 +1,987 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.Colors; +import ij.plugin.PointToolOptions; +import ij.plugin.filter.Analyzer; +import ij.plugin.frame.Recorder; +import ij.util.Java2; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.KeyEvent; +import java.util.*; +import java.awt.geom.*; + +/** This class represents a collection of points that can be associated with counters. + * @see PointProperties.js +*/ +public class PointRoi extends PolygonRoi { + public static final String[] sizes = {"Tiny", "Small", "Medium", "Large", "Extra Large", "XXL", "XXXL"}; + public static final String[] types = {"Hybrid", "Cross", "Dot", "Circle"}; + public static final int HYBRID=0, CROSS=1, CROSSHAIR=1, DOT=2, CIRCLE=3; + private static final String TYPE_KEY = "point.type"; + private static final String SIZE_KEY = "point.size"; + private static final String CROSS_COLOR_KEY = "point.cross.color"; + private static final int TINY=1, SMALL=3, MEDIUM=5, LARGE=7, EXTRA_LARGE=11, XXL=17, XXXL=25; + private static final BasicStroke twoPixelsWide = new BasicStroke(2); + private static final BasicStroke threePixelsWide = new BasicStroke(3); + private static final BasicStroke fivePixelsWide = new BasicStroke(5); + private static int defaultType = HYBRID; + private static int defaultSize = SMALL; + private static Font font; + private static Color defaultCrossColor = Color.white; + private static int fontSize = 9; + public static final int MAX_COUNTERS = 100; + private static String[] counterChoices; + private static Color[] colors; + private boolean showLabels; + private int type = HYBRID; + private int size = SMALL; + private static int defaultCounter; + private int counter; + private int nCounters = 1; + private short[] counters; //for each point, 0-100 for counter (=category that can be defined by the user) + private int[] positions; //for each point, the stack slice, or 0 for 'show on all' + private int[] counts = new int[MAX_COUNTERS]; + private ResultsTable rt; + private long lastPointTime; + private int[] counterInfo; + private boolean promptBeforeDeleting; + private boolean promptBeforeDeletingCalled; + private int nMarkers; + private boolean addToOverlay; + public static PointRoi savedPoints; + + static { + setDefaultType((int)Prefs.get(TYPE_KEY, HYBRID)); + setDefaultSize((int)Prefs.get(SIZE_KEY, 1)); + } + + public PointRoi() { + this(0.0, 0.0); + deletePoint(0); + } + + /** Creates a new PointRoi using the specified int arrays of offscreen coordinates. */ + public PointRoi(int[] ox, int[] oy, int points) { + super(itof(ox), itof(oy), points, POINT); + width+=1; height+=1; + updateCounts(); + } + + /** Creates a new PointRoi using the specified float arrays of offscreen coordinates. */ + public PointRoi(float[] ox, float[] oy, int points) { + super(ox, oy, points, POINT); + width+=1; height+=1; + updateCounts(); + } + + /** Creates a new PointRoi using the specified float arrays of offscreen coordinates. */ + public PointRoi(float[] ox, float[] oy) { + this(ox, oy, ox.length); + } + + /** Creates a new PointRoi using the specified coordinate arrays and options. */ + public PointRoi(float[] ox, float[] oy, String options) { + this(ox, oy, ox.length); + setOptions(options); + } + + /** Creates a new PointRoi from a FloatPolygon. */ + public PointRoi(FloatPolygon poly) { + this(poly.xpoints, poly.ypoints, poly.npoints); + } + + /** Creates a new PointRoi from a Polygon. */ + public PointRoi(Polygon poly) { + this(itof(poly.xpoints), itof(poly.ypoints), poly.npoints); + } + + /** Creates a new PointRoi using the specified coordinates and options. */ + public PointRoi(double ox, double oy, String options) { + super(makeXorYArray(ox, null, false), makeXorYArray(oy, null, true), 1, POINT); + width=1; height=1; + incrementCounter(null); + setOptions(options); + } + + /** Creates a new PointRoi using the specified offscreen int coordinates. */ + public PointRoi(int ox, int oy) { + super(makeXorYArray(ox, null, false), makeXorYArray(oy, null, true), 1, POINT); + width=1; height=1; + incrementCounter(null); + } + + /** Creates a new PointRoi using the specified offscreen double coordinates. */ + public PointRoi(double ox, double oy) { + super(makeXorYArray(ox, null, false), makeXorYArray(oy, null, true), 1, POINT); + width=1; height=1; + incrementCounter(null); + } + + /** Creates a new PointRoi using the specified screen coordinates. */ + public PointRoi(int sx, int sy, ImagePlus imp) { + super(makeXorYArray(sx, imp, false), makeXorYArray(sy, imp, true), 1, POINT); + //defaultCounter = 0; + setImage(imp); + width=1; height=1; + type = defaultType; + size = defaultSize; + showLabels = !Prefs.noPointLabels; + if (imp!=null) { + int r = 10 + size; + double mag = ic!=null?ic.getMagnification():1; + if (mag<1) + r = (int)(r/mag); + imp.draw(x-r, y-r, 2*r, 2*r); + } + setCounter(Toolbar.getMultiPointMode()?defaultCounter:0); + incrementCounter(imp); + enlargeArrays(50); + if (Recorder.record) { + String add = Prefs.pointAddToOverlay?" add":""; + String options = sizes[convertSizeToIndex(size)]+" "+Colors.colorToString(getColor())+" "+types[type]+add; + options = options.toLowerCase(); + if (Recorder.scriptMode()) + Recorder.recordCall("imp.setRoi(new PointRoi("+x+","+y+",\""+options+"\"));"); + else + Recorder.record("makePoint", x, y, options); + } + } + + public void setOptions(String options) { + if (options==null) + return; + if (options.contains("tiny")) size=TINY; + else if (options.contains("medium")) size=MEDIUM; + else if (options.contains("extra")) size=EXTRA_LARGE; + else if (options.contains("large")) size=LARGE; + else if (options.contains("xxxl")) size=XXXL; + else if (options.contains("xxl")) size=XXL; + if (options.contains("cross")) type=CROSS; + else if (options.contains("dot")) type=DOT; + else if (options.contains("circle")) type=CIRCLE; + if (options.contains("nolabel")) setShowLabels(false); + else if (options.contains("label")) setShowLabels(true); + setStrokeColor(Colors.getColor(options,Roi.getColor())); + addToOverlay = options.contains("add"); + } + + static float[] itof(int[] arr) { + if (arr==null) + return null; + int n = arr.length; + float[] temp = new float[n]; + for (int i=0; i1) { + fontSize = 8; + double scale = size>=XXL?2:1.5; + fontSize += scale*convertSizeToIndex(size); + fontSize = (int)Math.round(fontSize); + //IJ.log("fontSize: "+fontSize+" "+scale); + font = new Font("SansSerif", Font.PLAIN, fontSize); + g.setFont(font); + if (fontSize>9) + Java2.setAntialiasedText(g, true); + } + int slice = imp!=null&&positions!=null&&imp.getStackSize()>1?imp.getCurrentSlice():0; + ImageCanvas ic = imp!=null?imp.getCanvas():null; + if (ic!=null && overlay && ic.getShowAllList()!=null && ic.getShowAllList().contains(this) && !Prefs.showAllSliceOnly) + slice = 0; // draw point irrespective of currently selected slice + if (Prefs.showAllPoints) + slice = 0; + //IJ.log("draw: "+positions+" "+imp.getCurrentSlice()); + for (int i=0; i1.0) { + saveXform = g2d.getTransform(); + g2d.translate(x, y); + g2d.scale(flattenScale, flattenScale); + x = y = 0; + } + Color color = strokeColor!=null?strokeColor:ROIColor; + if (!overlay && isActiveOverlayRoi()) { + if (color==Color.cyan) + color = Color.magenta; + else + color = Color.cyan; + } + if (nCounters>1 && counters!=null && n<=counters.length) + color = getColor(counters[n-1]); + if (type==HYBRID || type==CROSS) { + if (type==HYBRID) + g.setColor(Color.white); + else { + g.setColor(color); + colorSet = true; + } + if (size>XXL) + g2d.setStroke(fivePixelsWide); + else if (size>LARGE) + g2d.setStroke(threePixelsWide); + g.drawLine(x-(size+2), y, x+size+2, y); + g.drawLine(x, y-(size+2), x, y+size+2); + } + if (type!=CROSS && size>SMALL) + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + if (type==HYBRID || type==DOT) { + if (!colorSet) { + g.setColor(color); + colorSet = true; + } + if (size>LARGE) + g2d.setStroke(onePixelWide); + if (size>LARGE && type==DOT) + g.fillOval(x-size2, y-size2, size, size); + else if (size>LARGE && type==HYBRID) + g.fillRect(x-(size2-2), y-(size2-2), size-4, size-4); + else if (size>SMALL && type==HYBRID) + g.fillRect(x-(size2-1), y-(size2-1), size-2, size-2); + else + g.fillRect(x-size2, y-size2, size, size); + } + if (showLabels && nPoints>1) { + int xoffset = 2; + if (size==LARGE) xoffset=3; + if (size==EXTRA_LARGE) xoffset=4; + if (size==XXL) xoffset=5; + if (size==XXXL) xoffset=7; + int yoffset = xoffset; + if (size>=LARGE) yoffset=yoffset-1; + if (nCounters==1) { + if (!colorSet) + g.setColor(color); + g.drawString(""+n, x+xoffset, y+yoffset+fontSize); + } else if (counters!=null) { + g.setColor(getColor(counters[n-1])); + g.drawString(""+counters[n-1], x+xoffset, y+yoffset+fontSize); + } + } + if ((size>TINY||type==DOT) && (type==HYBRID||type==DOT)) { + g.setColor(Color.black); + if (size>LARGE && type==HYBRID) + g.drawOval(x-(size2-1), y-(size2-1), size-3, size-3); + else if (size>SMALL && type==HYBRID) + g.drawOval(x-size2, y-size2, size-1, size-1); + else + g.drawOval(x-(size2+1), y-(size2+1), size+1, size+1); + } + if (type==CIRCLE) { + int scaledSize = (int)Math.round(size+1); + g.setColor(color); + if (size>LARGE) + g2d.setStroke(twoPixelsWide); + g.drawOval(x-scaledSize/2, y-scaledSize/2, scaledSize, scaledSize); + } + if (saveXform!=null) + g2d.setTransform(saveXform); + } + + public void drawPixels(ImageProcessor ip) { + ip.setLineWidth(Analyzer.markWidth); + double x0 = bounds == null ? x : bounds.x; + double y0 = bounds == null ? y : bounds.y; + for (int i=0; i=0 && index<=nPoints && counters!=null) { + counts[counters[index]]--; + for (int i=index; i1; + if (counter!=0 || isStack || counters!=null) { + if (counters==null) { + counters = new short[nPoints*2]; + positions = new int[nPoints*2]; + } + counters[nPoints-1] = (short)counter; + if (imp!=null) + positions[nPoints-1] = imp.getStackSize()>1 ? imp.getCurrentSlice() : 0; + //if (positions[nPoints-1]==0 || positions[nPoints-1]==1 || counters[nPoints-1]==0) + // IJ.log("incrementCounter: "+nPoints+" "+" "+positions[nPoints-1]+" "+counters[nPoints-1]+" "+imp); + if (nPoints+1==counters.length) { + short[] temp = new short[counters.length*2]; + System.arraycopy(counters, 0, temp, 0, counters.length); + counters = temp; + int[] temp1 = new int[counters.length*2]; + System.arraycopy(positions, 0, temp1, 0, positions.length); + positions = temp1; + } + } + if (rt!=null && WindowManager.getFrame(getCountsTitle())!=null) + displayCounts(); + } + + /** Returns the index of the current counter. */ + public int getCounter() { + return counter; + } + + /** Returns the count associated with the specified counter index. + * @see #getLastCounter + * @see PointProperties.js + */ + public int getCount(int counter) { + if (counter==0 && counters==null) + return nPoints; + else + return counts[counter]; + } + + /** Returns the index of the last counter. */ + public int getLastCounter() { + return nCounters - 1; + } + + /** Returns the number of counters. */ + public int getNCounters() { + int n = 0; + for (int counter=0; counter0) n++; + } + return n; + } + + /** Returns the counter assocated with the specified point. */ + public int getCounter(int index) { + if (counters==null || index>=counters.length) + return 0; + else + return counters[index]; + } + + public void resetCounters() { + for (int i=0; iroi if keepContained is true (false). */ + PointRoi checkContained(Roi roi, boolean keepContained) { + if (!roi.isArea()) return null; + FloatPolygon points = getFloatPolygon(); + FloatPolygon points2 = new FloatPolygon(); + for (int i=0; i=0 && type=0 && type=0 && index=0 && sizenCounters-1 && nCounters8||getNCounters()>1) && imp!=null && imp.getWindow()!=null; + } + + public void promptBeforeDeleting(Boolean prompt) { + promptBeforeDeleting = prompt; + promptBeforeDeletingCalled = true; + } + + public static void setDefaultCounter(int counter) { + defaultCounter = counter; + } + + /** Returns an array containing for each point: + * The counter number (0-100) in the lower 8 bits, and the slice number + * (or 0, if the point appears on all slices) in the higher 24 bits. + * Used when writing a Roi to file (RoiEncoder) */ + public int[] getCounters() { + if (nPoints>65535) + return null; + int[] temp = new int[nPoints]; + if (counters!=null) { + for (int i=0; i>8; + //IJ.log(i+" cnt="+counter+" slice="+position); + this.counters[i] = (short)counter; + this.positions[i] = position; + if (counternCounters-1) + nCounters = counter + 1; + } + updateCounts(); + } + } + + /** Updates the counts for each category in 'counters' */ + public void updateCounts() { + Arrays.fill(counts, 0); + for (int i=0; i=counts.length) ? 0 : counters[i]] ++; + } + + /** Returns the stack slice of the point with the given index, or 0 if no slice defined for this point */ + public int getPointPosition(int index) { + if (positions!=null && index1 && positions!=null) { + int nChannels = 1; + int nSlices = 1; + int nFrames = 1; + boolean isHyperstack = true; + if (imp.isComposite() || imp.isHyperStack()) { + nChannels = imp.getNChannels(); + nSlices = imp.getNSlices(); + nFrames = imp.getNFrames(); + int nDimensions = 2; + if (nChannels>1) nDimensions++; + if (nSlices>1) nDimensions++; + if (nFrames>1) nDimensions++; + if (nDimensions==3) { + isHyperstack = false; + if (nChannels>1) + firstColumnHdr = "Channel"; + } else + firstColumnHdr = "Image"; + } + int firstSlice = Integer.MAX_VALUE; + for (int i=0; i0 && positions[i]0) { + for (int i=0; ilastSlice) + lastSlice = positions[i]; + } + } + if (firstSlice>0) { + for (int slice=firstSlice; slice<=lastSlice; slice++) { + rt.setValue(firstColumnHdr, row, slice); + if (isHyperstack) { + int[] position = imp.convertIndexToPosition(slice); + if (nChannels>1) + rt.setValue("Channel", row, position[0]); + if (nSlices>1) + rt.setValue("Slice", row, position[1]); + if (nFrames>1) + rt.setValue("Frame", row, position[2]); + } + for (int counter=0; counter1?imp.getCurrentSlice():0; + for (int i=0; i=sx2 && sx<=sx2+size && sy>=sy2 && sy<=sy2+size) { + handle = i; + break; + } + } + return handle; + } + + /** Returns the points as an array of Points. + * Wilhelm Burger: modified to use FloatPolygon for correct point positions. + */ + public Point[] getContainedPoints() { + FloatPolygon p = getFloatPolygon(); + Point[] points = new Point[p.npoints]; + for (int i=0; i iterator() { + return new Iterator() { + final Point[] pnts = getContainedPoints(); + final int n = pnts.length; + int next = (n == 0) ? 1 : 0; + @Override + public boolean hasNext() { + return next < n; + } + @Override + public Point next() { + if (next >= n) { + throw new NoSuchElementException(); + } + Point pnt = pnts[next]; + next = next + 1; + return pnt; + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + protected int getClosestPoint(double x, double y, FloatPolygon points) { + int index = -1; + double distance = Double.MAX_VALUE; + int slice = imp!=null&&positions!=null&&imp.getStackSize()>1?imp.getCurrentSlice():0; + if (Prefs.showAllPoints) + slice = 0; + for (int i=0; i=0; i--) { + if (!roi.contains(p.xpoints[i],p.ypoints[i])) { + points.deletePoint(i); + } + } + return points; + } + + @Override + public void copyAttributes(Roi roi2) { + super.copyAttributes(roi2); + if (roi2 instanceof PointRoi) { + PointRoi p2 = (PointRoi)roi2; + this.type = p2.type; + this.size = p2.size; + this.showLabels = p2.showLabels; + this.fontSize = p2.fontSize; + } + } + + public void setCounterInfo(int[] info) { + counterInfo = info; + } + + public int[] getCounterInfo() { + return counterInfo; + } + + public boolean addToOverlay() { + return addToOverlay; + } + + public String toString() { + if (nPoints>1) + return ("Roi[Points, count="+nPoints+"]"); + else + return ("Roi[Point, x="+x+", y="+y+"]"); + } + + /** @deprecated */ + public void setHideLabels(boolean hideLabels) { + this.showLabels = !hideLabels; + } + + /** @deprecated */ + public static void setDefaultMarkerSize(String size) { + } + + /** @deprecated */ + public static String getDefaultMarkerSize() { + return sizes[defaultSize]; + } + + /** Deprecated */ + public static void setDefaultCrossColor(Color color) { + } + + /** Deprecated */ + public static Color getDefaultCrossColor() { + return null; + } + +} diff --git a/src/ij/gui/PolygonRoi.java b/src/ij/gui/PolygonRoi.java new file mode 100644 index 0000000..0257ead --- /dev/null +++ b/src/ij/gui/PolygonRoi.java @@ -0,0 +1,1701 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.frame.*; +import ij.util.Tools; +import ij.util.FloatArray; +import java.awt.*; +import java.awt.image.*; +import java.awt.geom.*; +import java.awt.event.*; + +/** This class represents a polygon region of interest or polyline of interest. */ +public class PolygonRoi extends Roi { + + protected int maxPoints = 1000; // will be increased if necessary + protected int[] xp, yp; // image coordinates relative to origin of roi bounding box + protected float[] xpf, ypf; // or alternative sub-pixel coordinates + protected int[] xp2, yp2; // absolute screen coordinates + protected int nPoints; + protected float[] xSpline,ySpline; // relative image coordinates + protected int splinePoints = 200; + Rectangle clip; + + private double angle1, degrees=Double.NaN; + private int xClipMin, yClipMin, xClipMax, yClipMax; + private boolean userCreated; + private int boxSize = 8; + + long mouseUpTime = 0; + + /** Creates a new polygon or polyline ROI from x and y coordinate arrays. + Type must be Roi.POLYGON, Roi.FREEROI, Roi.TRACED_ROI, Roi.POLYLINE, Roi.FREELINE or Roi.ANGLE.*/ + public PolygonRoi(int[] xPoints, int[] yPoints, int nPoints, int type) { + super(0, 0, null); + init1(nPoints, type); + xp = xPoints; + yp = yPoints; + if (type!=TRACED_ROI) { + xp = new int[nPoints]; + yp = new int[nPoints]; + for (int i=0; i1 && isLine()) + updateWideLine(lineWidth); + finishPolygon(); + } + + /** Creates a new polygon or polyline ROI from a Polygon. Type must be Roi.POLYGON, + Roi.FREEROI, Roi.TRACED_ROI, Roi.POLYLINE, Roi.FREELINE or Roi.ANGLE.*/ + public PolygonRoi(Polygon p, int type) { + this(p.xpoints, p.ypoints, p.npoints, type); + } + + /** Creates a new polygon or polyline ROI from a FloatPolygon. Type must be Roi.POLYGON, + Roi.FREEROI, Roi.TRACED_ROI, Roi.POLYLINE, Roi.FREELINE or Roi.ANGLE.*/ + public PolygonRoi(FloatPolygon p, int type) { + this(p.xpoints, p.ypoints, p.npoints, type); + } + + /** @deprecated */ + public PolygonRoi(int[] xPoints, int[] yPoints, int nPoints, ImagePlus imp, int type) { + this(xPoints, yPoints, nPoints, type); + setImage(imp); + } + + /** Starts the process of creating a new user-generated polygon or polyline ROI. */ + public PolygonRoi(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + int tool = Toolbar.getToolId(); + switch (tool) { + case Toolbar.POLYGON: + type = POLYGON; + break; + case Toolbar.FREEROI: + type = FREEROI; + break; + case Toolbar.FREELINE: + type = FREELINE; + break; + case Toolbar.ANGLE: + type = ANGLE; + break; + default: + type = POLYLINE; + break; + } + if (magnificationForSubPixel()) + enableSubPixelResolution(); + previousSX = sx; + previousSY = sy; + x = offScreenX(sx); + y = offScreenY(sy); + startXD = offScreenXD(sx); + startYD = offScreenYD(sy); + if (subPixelResolution()) { + setLocation(startXD, startYD); + xpf = new float[maxPoints]; + ypf = new float[maxPoints]; + double xbase = getXBase(); + double ybase = getYBase(); + xpf[0] = (float)(startXD-xbase); + ypf[0] = (float)(startYD-ybase); + xpf[1] = xpf[0]; + ypf[1] = ypf[0]; + } else { + xp = new int[maxPoints]; + yp = new int[maxPoints]; + } + xp2 = new int[maxPoints]; + yp2 = new int[maxPoints]; + nPoints = 2; + width=1; + height=1; + clipX = x; + clipY = y; + clipWidth = 1; + clipHeight = 1; + state = CONSTRUCTING; + userCreated = true; + if (lineWidth>1 && isLine()) + updateWideLine(lineWidth); + boxSize = (int)(boxSize*Prefs.getGuiScale()); + } + + private void drawStartBox(Graphics g) { + if (type!=ANGLE) + g.drawRect(screenXD(startXD)-4, screenYD(startYD)-4, 8, 8); + } + + public void draw(Graphics g) { + updatePolygon(); + Color color = strokeColor!=null?strokeColor:ROIColor; + boolean hasHandles = xSpline!=null||type==POLYGON||type==POLYLINE||type==ANGLE; + boolean isActiveOverlayRoi = !overlay && isActiveOverlayRoi(); + if (isActiveOverlayRoi) { + if (color==Color.cyan) + color = Color.magenta; + else + color = Color.cyan; + } + boolean fill = false; + mag = getMagnification(); + if (fillColor!=null && !isLine() && state!=CONSTRUCTING) { + color = fillColor; + fill = true; + } + g.setColor(color); + Graphics2D g2d = (Graphics2D)g; + setRenderingHint(g2d); + if (stroke!=null && !isActiveOverlayRoi) + g2d.setStroke(getScaledStroke()); + if (xSpline!=null) { + if (type==POLYLINE || type==FREELINE) { + drawSpline(g, xSpline, ySpline, splinePoints, false, fill, isActiveOverlayRoi); + if (wideLine && !overlay) { + g2d.setStroke(onePixelWide); + g.setColor(getColor()); + drawSpline(g, xSpline, ySpline, splinePoints, false, fill, isActiveOverlayRoi); + } + } else + drawSpline(g, xSpline, ySpline, splinePoints, true, fill, isActiveOverlayRoi); + } else { + if (type==POLYLINE || type==FREELINE || type==ANGLE || state==CONSTRUCTING) { + g.drawPolyline(xp2, yp2, nPoints); + if (wideLine && !overlay) { + g2d.setStroke(onePixelWide); + g.setColor(getColor()); + g.drawPolyline(xp2, yp2, nPoints); + } + } else { + if (fill) { + if (isActiveOverlayRoi) { + g.setColor(Color.cyan); + g.drawPolygon(xp2, yp2, nPoints); + } else + g.fillPolygon(xp2, yp2, nPoints); + } else + g.drawPolygon(xp2, yp2, nPoints); + } + if (state==CONSTRUCTING && type!=FREEROI && type!=FREELINE) + drawStartBox(g); + } + if (hasHandles && clipboard==null && !overlay) { + if (activeHandle>0) + drawHandle(g, xp2[activeHandle-1], yp2[activeHandle-1]); + if (activeHandle1f) + ip.setLineWidth((int)Math.round(getStrokeWidth())); + double xbase = getXBase(); + double ybase = getYBase(); + if (xSpline!=null) { + ip.moveTo((int)Math.round(xbase+xSpline[0]), (int)Math.round(ybase+ySpline[0])); + for (int i=1; i Math.abs(dy)) + dy = 0; + else + dx = 0; + sx = previousSX + dx; + sy = previousSY + dy; + } + + // Do rubber banding + int tool = Toolbar.getToolId(); + if (!(tool==Toolbar.POLYGON || tool==Toolbar.POLYLINE || tool==Toolbar.ANGLE)) { + imp.deleteRoi(); + imp.draw(); + return; + } + if (IJ.altKeyDown()) + wipeBack(); + + drawRubberBand(sx, sy); + + // show status: length & angle + degrees = Double.NaN; + double len = -1; + if (nPoints>1) { + double x1, y1, x2, y2; + if (xpf!=null) { + x1 = xpf[nPoints-2]; + y1 = ypf[nPoints-2]; + x2 = xpf[nPoints-1]; + y2 = ypf[nPoints-1]; + } else { + x1 = xp[nPoints-2]; + y1 = yp[nPoints-2]; + x2 = xp[nPoints-1]; + y2 = yp[nPoints-1]; + } + degrees = getFloatAngle(x1, y1, x2, y2); + if (tool!=Toolbar.ANGLE) { + Calibration cal = imp.getCalibration(); + double pw=cal.pixelWidth, ph=cal.pixelHeight; + if (IJ.altKeyDown()) {pw=1.0; ph=1.0;} + len = Math.sqrt((x2-x1)*pw*(x2-x1)*pw + (y2-y1)*ph*(y2-y1)*ph); + } + } + if (tool==Toolbar.ANGLE) { + if (nPoints==2) + angle1 = degrees; + else if (nPoints==3) { + double angle2 = xpf != null ? getFloatAngle(xpf[1], ypf[1], xpf[2], ypf[2]) : + getAngle(xp[1], yp[1], xp[2], yp[2]); + degrees = Math.abs(180-Math.abs(angle1-angle2)); + if (degrees>180.0) + degrees = 360.0-degrees; + } + } + String length = len!=-1?", length=" + IJ.d2s(len):""; + double degrees2 = tool==Toolbar.ANGLE&&nPoints==3&&Prefs.reflexAngle?360.0-degrees:degrees; + String angle = !Double.isNaN(degrees)?", angle=" + IJ.d2s(degrees2):""; + int ox = ic.offScreenX(sx); + int oy = ic.offScreenY(sy); + IJ.showStatus(imp.getLocationAsString(ox,oy) + length + angle); + } + + //Mouse behaves like an eraser when moved backwards with alt key down. + //Within correction circle, all vertices with sharp angles are removed. + //Norbert Vischer + protected void wipeBack() { + Roi prevRoi = Roi.getPreviousRoi(); + if (prevRoi!=null && prevRoi.modState==SUBTRACT_FROM_ROI) + return; + double correctionRadius = 20; + if (ic!=null) + correctionRadius /= ic.getMagnification(); + boolean found = false; + int p3 = nPoints - 1; + int p1 = p3; + while (p1 > 0 && !found) { + p1--; + double dx = xpf != null ? xpf[p3] - xpf[p1] : xp[p3] - xp[p1]; + double dy = xpf != null ? ypf[p3] - ypf[p1] : yp[p3] - yp[p1]; + double dist = Math.sqrt(dx * dx + dy * dy); + if (dist > correctionRadius) + found = true; + } + //examine all angles p1-p2-p3 + boolean killed = false; + int safety = 10; //don't delete more than this number of points at once + do { + killed = false; + safety--; + for (int p2 = p1 + 1; p2 < p3; p2++) { + double dx1 = xpf != null ? xpf[p2] - xpf[p1] : xp[p2] - xp[p1]; + double dy1 = xpf != null ? ypf[p2] - ypf[p1] : yp[p2] - yp[p1]; + double dx2 = xpf != null ? xpf[p3] - xpf[p1] : xp[p3] - xp[p1]; + double dy2 = xpf != null ? ypf[p3] - ypf[p1] : yp[p3] - yp[p1]; + double kk = 1;//allowed sharpness + if (this instanceof FreehandRoi) + kk = 0.8; + if ((dx1 * dx1 + dy1 * dy1) > kk * (dx2 * dx2 + dy2 * dy2)) { + if (xpf != null) { + xpf[p2] = xpf[p3]; ypf[p2] = ypf[p3]; //replace sharp vertex with end point + } else { + xp[p2] = xp[p3]; yp[p2] = yp[p3]; + } + p3 = p2; + nPoints = p2 + 1; //shorten array + killed = true; + } + } + } while (killed && safety > 0); + } + + void drawRubberBand(int sx, int sy) { + double oxd = offScreenXD(sx); + double oyd = offScreenYD(sy); + int ox = offScreenX(sx); + int oy = offScreenY(sy); + int x1, y1, x2, y2; + if (xpf!=null) { + x1 = (int)xpf[nPoints-2]+x; + y1 = (int)ypf[nPoints-2]+y; + x2 = (int)xpf[nPoints-1]+x; + y2 = (int)ypf[nPoints-1]+y; + } else { + x1 = xp[nPoints-2]+x; + y1 = yp[nPoints-2]+y; + x2 = xp[nPoints-1]+x; + y2 = yp[nPoints-1]+y; + } + int xmin=Integer.MAX_VALUE, ymin=Integer.MAX_VALUE, xmax=0, ymax=0; + if (x1xmax) xmax=x1; + if (x2>xmax) xmax=x2; + if (ox>xmax) xmax=ox; + if (y1ymax) ymax=y1; + if (y2>ymax) ymax=y2; + if (oy>ymax) ymax=oy; + int margin = boxSize; + if (ic!=null) { + double mag = ic.getMagnification(); + if (mag<1.0) margin = (int)(margin/mag); + } + margin = (int)(margin+getStrokeWidth()); + if (IJ.altKeyDown()) + margin+=20; + if (xpf!=null) { + xpf[nPoints-1] = (float)(oxd-getXBase()); + ypf[nPoints-1] = (float)(oyd-getYBase()); + } else { + xp[nPoints-1] = ox-x; + yp[nPoints-1] = oy-y; + } + if (type==POLYLINE && Prefs.splineFitLines) { + fitSpline(); + imp.draw(); + } else + imp.draw(xmin-margin, ymin-margin, (xmax-xmin)+margin*2, (ymax-ymin)+margin*2); + } + + void finishPolygon() { + if (xpf!=null) { + double xbase0 = getXBase(); + double ybase0 = getYBase(); + FloatPolygon poly = new FloatPolygon(xpf, ypf, nPoints); + bounds = poly.getFloatBounds(); + for (int i=0; i Math.abs(dy)) + dy = 0; + else + dx = 0; + sx = previousSX + dx; + sy = previousSY + dy; + } + + if (clipboard!=null) return; + + int ox = offScreenX(sx); + int oy = offScreenY(sy); + if (xpf!=null) { + xpf[activeHandle] = (float)(offScreenXD(sx)-getXBase()); + ypf[activeHandle] = (float)(offScreenYD(sy)-getYBase()); + } else { + xp[activeHandle] = ox-x; + yp[activeHandle] = oy-y; + } + if (xSpline!=null) { + fitSpline(splinePoints); + imp.draw(); + } else { + if (!subPixelResolution() || (type==POINT&&nPoints==1)) + resetBoundingRect(); + if (type==POINT && width==0 && height==0) + {width=1; height=1;} + updateClipRectAndDraw(); + } + String angle = type==ANGLE?getAngleAsString():""; + IJ.showStatus(imp.getLocationAsString(ox,oy) + angle); + } + + /** After handle is moved, find clip rect and repaint. */ + void updateClipRectAndDraw() { + if (xpf!=null) { + xp = toInt(xpf, xp, nPoints); + yp = toInt(ypf, yp, nPoints); + } + int xmin=Integer.MAX_VALUE, ymin=Integer.MAX_VALUE, xmax=0, ymax=0; + int x2, y2; + if (activeHandle>0) + {x2=x+xp[activeHandle-1]; y2=y+yp[activeHandle-1];} + else + {x2=x+xp[nPoints-1]; y2=y+yp[nPoints-1];} + if (x2xmax) xmax = x2; + if (y2>ymax) ymax = y2; + x2=x+xp[activeHandle]; y2=y+yp[activeHandle]; + if (x2xmax) xmax = x2; + if (y2>ymax) ymax = y2; + if (activeHandlexmax) xmax = x2; + if (y2>ymax) ymax = y2; + int xmin2=xmin, ymin2=ymin, xmax2=xmax, ymax2=ymax; + if (xClipMinxmax2) xmax2 = xClipMax; + if (yClipMax>ymax2) ymax2 = yClipMax; + xClipMin=xmin; yClipMin=ymin; xClipMax=xmax; yClipMax=ymax; + double mag = ic.getMagnification(); + int handleSize = type==POINT?getHandleSize()+25:getHandleSize(); + double strokeWidth = getStrokeWidth(); + if (strokeWidth<1.0) strokeWidth=1.0; + if (handleSizexmax) xmax=xx; + yy = yp[i]; + if (yyymax) ymax=yy; + } + if (xmin!=0) { + for (int i=0; i180.0) + degrees = 360.0-degrees; + double degrees2 = Prefs.reflexAngle&&type==ANGLE?360.0-degrees:degrees; + return ", angle=" + IJ.d2s(degrees2); + } + + protected void mouseDownInHandle(int handle, int sx, int sy) { + if (state==CONSTRUCTING) + return; + int ox = offScreenX(sx); + int oy = offScreenY(sy); + double oxd = offScreenXD(sx); + double oyd = offScreenYD(sy); + if ((IJ.altKeyDown()||IJ.controlKeyDown()) && !(nPoints<=3 && type!=POINT) && !(this instanceof RotatedRectRoi)) { + deleteHandle(oxd, oyd); + return; + } else if (IJ.shiftKeyDown() && type!=POINT && !(this instanceof RotatedRectRoi)) { + addHandle(oxd, oyd); + return; + } + super.mouseDownInHandle(handle, sx, sy); //sets state, activeHandle, previousSX&Y + int m = ic!=null?(int)(10.0/ic.getMagnification()):1; + xClipMin=ox-m; yClipMin=oy-m; xClipMax=ox+m; yClipMax=oy+m; + } + + public void deleteHandle(double ox, double oy) { + if (imp==null) + return; + if (nPoints<=1) { + imp.deleteRoi(); + return; + } + boolean splineFit = xSpline!=null; + if (splineFit) + removeSplineFit(); + FloatPolygon points = getFloatPolygon(); + int pointToDelete = getClosestPoint(ox, oy, points); + if (pointToDelete>=0) { + deletePoint(pointToDelete); + if (splineFit) + fitSpline(splinePoints); + imp.draw(); + } + } + + protected void deletePoint(int index) { + if (index<0 || index>=nPoints) + return; + for (int i=index; i=maxPoints) + enlargeArrays(); + float xbase = (float)getXBase(); + float ybase = (float)getYBase(); + if (xp==null) { + xp = new int[maxPoints]; + yp = new int[maxPoints]; + } + for (int i=0; i= 0.0 || i==npoints-1) && pointsWritten < npOut) { // we have to write a new point + double fractionOverNextWrite = distanceOverNextWrite/distance; + if (distance==0) fractionOverNextWrite = 0; + //IJ.log("i="+i+" n="+pointsWritten+"/"+npOut+" leng="+IJ.d2s(lengthRead)+"/"+IJ.d2s(length)+" done="+IJ.d2s(pointsWritten*step)+" over="+IJ.d2s(fractionOverNextWrite)+" x,y="+IJ.d2s(x2 - fractionOverNextWrite*dx)+","+IJ.d2s(y2 - fractionOverNextWrite*dy)); + xpOut[pointsWritten] = (float)(x2 - fractionOverNextWrite*dx); + ypOut[pointsWritten] = (float)(y2 - fractionOverNextWrite*dy); + distanceOverNextWrite -= step; + pointsWritten++; + } + } + return new float[][] {xpOut, ypOut, new float[] {(float)step}}; + } + + /** With segmented selections, ignore first mouse up and finalize + when user double-clicks, control-clicks or clicks in start box. */ + protected void handleMouseUp(int sx, int sy) { + if (state==MOVING) { + state = NORMAL; + return; + } + if (state==MOVING_HANDLE) { + cachedMask = null; //mask is no longer valid + state = NORMAL; + updateClipRect(); + oldX=x; oldY=y; + oldWidth=width; oldHeight=height; + if (subPixelResolution()) + resetBoundingRect(); + return; + } + if (state!=CONSTRUCTING) + return; + if (IJ.spaceBarDown()) // is user scrolling image? + return; + boolean samePoint = false; + if (xpf!=null) + samePoint = (xpf[nPoints-2]==xpf[nPoints-1] && ypf[nPoints-2]==ypf[nPoints-1]); + else + samePoint = (xp[nPoints-2]==xp[nPoints-1] && yp[nPoints-2]==yp[nPoints-1]); + boolean doubleClick = (System.currentTimeMillis()-mouseUpTime)<=300; + int size = boxSize+2; + int size2 = boxSize/2 +1; + Rectangle biggerStartBox = new Rectangle(screenXD(startXD)-5, screenYD(startYD)-5, 10, 10); + if (nPoints>2 && (biggerStartBox.contains(sx, sy) + || (offScreenXD(sx)==startXD && offScreenYD(sy)==startYD) + || (samePoint && doubleClick))) { + boolean okayToFinish = true; + if (type==POLYGON && samePoint && doubleClick && nPoints>25) { + okayToFinish = IJ.showMessageWithCancel("Polygon Tool", "Complete the selection?"); + } + if (okayToFinish) { + nPoints--; + addOffset(); + finishPolygon(); + return; + } + } else if (!samePoint) { + mouseUpTime = System.currentTimeMillis(); + if (type==ANGLE && nPoints==3) { + addOffset(); + finishPolygon(); + return; + } + //add point to polygon + if (xpf!=null) { + xpf[nPoints] = xpf[nPoints-1]; + ypf[nPoints] = ypf[nPoints-1]; + nPoints++; + if (nPoints==xpf.length) + enlargeArrays(); + } else { + xp[nPoints] = xp[nPoints-1]; + yp[nPoints] = yp[nPoints-1]; + nPoints++; + if (nPoints==xp.length) + enlargeArrays(); + } + if (constrain) { // this point was constrained in 90deg steps; correct coordinates + int dx = sx - previousSX; + int dy = sy - previousSY; + if (Math.abs(dx) > Math.abs(dy)) + dy = 0; + else + dx = 0; + sx = previousSX + dx; + sy = previousSY + dy; + } + previousSX = sx; //save for constraining next line if desired + previousSY = sy; + notifyListeners(RoiListener.EXTENDED); + } + } + + protected void addOffset() { + if (xpf!=null) { + float xbase = (float)getXBase(); + float ybase = (float)getYBase(); + for (int i=0; i=sx2 && sx<=sx2+size && sy>=sy2 && sy<=sy2+size) { + handle = i; + break; + } + } + return handle; + } + + public ImageProcessor getMask() { + ImageProcessor mask = cachedMask; + if (mask!=null && mask.getPixels()!=null + && mask.getWidth()==width && mask.getHeight()==height) + return mask; + PolygonFiller pf = new PolygonFiller(); + if (xSpline!=null) + pf.setPolygon(xSpline, ySpline, splinePoints, getXBase()-x, getYBase()-y); + else if (xpf!=null) + pf.setPolygon(xpf, ypf, nPoints, getXBase()-x, getYBase()-y); + else + pf.setPolygon(xp, yp, nPoints); + mask = pf.getMask(width, height); + cachedMask = mask; + return mask; + } + + /** Returns the length of this line selection after + smoothing using a 3-point running average.*/ + double getSmoothedLineLength(ImagePlus imp) { + if (subPixelResolution() && xpf!=null) + return getFloatSmoothedLineLength(imp); + double length = 0.0; + double w2 = 1.0; + double h2 = 1.0; + double dx, dy; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + w2 = cal.pixelWidth*cal.pixelWidth; + h2 = cal.pixelHeight*cal.pixelHeight; + } + dx = (xp[0]+xp[1]+xp[2])/3.0-xp[0]; + dy = (yp[0]+yp[1]+yp[2])/3.0-yp[0]; + length += Math.sqrt(dx*dx*w2+dy*dy*h2); + for (int i=1; i1 || !corner) { + corner = true; + nCorners++; + } else + corner = false; + dx1 = dx2; + dy1 = dy2; + side1 = side2; + } + double w=1.0,h=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + w = cal.pixelWidth; + h = cal.pixelHeight; + } + return sumdx*w+sumdy*h-(nCorners*((w+h)-Math.sqrt(w*w+h*h))); + /* Alternative code leading to slightly different results: + * It does this by calculating the total length of the ROI boundary + * and subtracting 1-sqrt(2)/2 for each corner. + * For example, a 1x1 pixel ROI has a boundary length of 4 and + * and 4 corners so the perimeter is 4-4*(1-sqrt(2)/2) = 2*sqrt(2). + * A 2x2 pixel ROI has a boundary length of 8 and 4 corners so the + * perimeter is 8-4*(1-sqrt(2)/2) = 4 + 2*sqrt(2). /* int x0 = xp[nPoints-2]; + int y0 = yp[nPoints-2]; + int x1 = xp[nPoints-1]; + int y1 = yp[nPoints-1]; + int sumdx = 0; + int sumdy = 0; + int nCorners = 0; + for (int i=0; i2) { + if (type==FREEROI) + return getSmoothedPerimeter(imp); + else if (type==FREELINE && !(width==0 || height==0)) + return getSmoothedLineLength(imp); + } + + boolean closeShape = isArea(); + if (xSpline!=null) + return getLength(xSpline, ySpline, splinePoints, closeShape, imp); + else if (xpf!=null) + return getLength(xpf, ypf, nPoints, closeShape, imp); + else + return getLength(xp, yp, nPoints, closeShape, imp); + } + + /** Returns the length of a polygon with integer coordinates. Uses no calibration if imp is null. */ + static double getLength(int[] xpoints, int[] ypoints, int npoints, boolean closeShape, ImagePlus imp) { + if (npoints < 2) return 0; + double pixelWidth = 1.0, pixelHeight = 1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pixelWidth = cal.pixelWidth; + pixelHeight = cal.pixelHeight; + } + double length = 0; + for (int i=0; i "+newSize); + maxPoints = newSize; + } + + public void setLocation(double x, double y) { + super.setLocation(x, y); + if ((int)x!=x || (int)y!=y) + enableSubPixelResolution(); + } + + public void enableSubPixelResolution() { + super.enableSubPixelResolution(); + if (xpf==null && xp!=null) { + xpf = toFloat(xp); + ypf = toFloat(yp); + } + } + + public String getDebugInfo() { + String s = "ROI Debug Properties\n"; + s += " bounds: "+bounds+"\n"; + s += " x,y,w,h: "+x+","+y+","+width+","+height+"\n"; + if (xpf!=null && xpf.length>0) + s += " xpf[0],ypf[0]: "+xpf[0]+","+ypf[0]+"\n"; + return s; + } + +} diff --git a/src/ij/gui/ProfilePlot.java b/src/ij/gui/ProfilePlot.java new file mode 100644 index 0000000..875a295 --- /dev/null +++ b/src/ij/gui/ProfilePlot.java @@ -0,0 +1,341 @@ +package ij.gui; + +import java.awt.*; +import java.util.ArrayList; +import ij.*; +import ij.process.*; +import ij.util.*; +import ij.measure.*; +import ij.plugin.Straightener; + +/** Creates a density profile plot of a rectangular selection or line selection. */ +public class ProfilePlot { + + static final int MIN_WIDTH = 350; + static final double ASPECT_RATIO = 0.5; + private double min, max; + private boolean minAndMaxCalculated; + private static double fixedMin; + private static double fixedMax; + + protected ImagePlus imp; + protected double[] profile; + protected double magnification; + protected double xInc; + protected String units; + protected String yLabel; + protected float[] xValues; + + + public ProfilePlot() { + } + + public ProfilePlot(ImagePlus imp) { + this(imp, false); + } + + public ProfilePlot(ImagePlus imp, boolean averageHorizontally) { + this.imp = imp; + Roi roi = imp.getRoi(); + if (roi==null) { + IJ.error("Profile Plot", "Selection required."); + return; + } + int roiType = roi.getType(); + if (!(roi.isLine() || roiType==Roi.RECTANGLE)) { + IJ.error("Line or rectangular selection required."); + return; + } + Calibration cal = imp.getCalibration(); + xInc = cal.pixelWidth; + units = cal.getUnits(); + yLabel = cal.getValueUnit(); + ImageProcessor ip = imp.getProcessor(); + if (roiType==Roi.LINE) + profile = getStraightLineProfile(roi, cal, ip); + else if (roiType==Roi.POLYLINE || roiType==Roi.FREELINE) { + int lineWidth = (int)Math.round(roi.getStrokeWidth()); + if (lineWidth<=1) + profile = getIrregularProfile(roi, ip, cal); + else + profile = getWideLineProfile(imp, lineWidth); + } else if (averageHorizontally) + profile = getRowAverageProfile(roi.getBounds(), cal, ip); + else + profile = getColumnAverageProfile(roi.getBounds(), ip); + ip.setCalibrationTable(null); + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) + magnification = ic.getMagnification(); + else + magnification = 1.0; + } + + /** Returns the size of the plot that createWindow() creates. */ + public Dimension getPlotSize() { + if (profile==null) return null; + int width = (int)(profile.length*magnification); + int height = (int)(width*ASPECT_RATIO); + if (widthmaxWidth) { + width = maxWidth; + height = (int)(width*ASPECT_RATIO); + } + return new Dimension(width, height); + } + + /** Displays this profile plot in a window. */ + public void createWindow() { + Plot plot = getPlot(); + if (plot!=null) + plot.show(); + } + + public Plot getPlot() { + if (profile==null) + return null; + String xLabel = "Distance ("+units+")"; + int n = profile.length; + if (xValues==null) { + xValues = new float[n]; + for (int i=0; i0 && (title.length()-index)<=5) + title = title.substring(0, index); + return title; + } + + /** Returns the profile plot data. */ + public double[] getProfile() { + return profile; + } + + /** Returns the calculated minimum value. */ + public double getMin() { + if (!minAndMaxCalculated) + findMinAndMax(); + return min; + } + + /** Returns the calculated maximum value. */ + public double getMax() { + if (!minAndMaxCalculated) + findMinAndMax(); + return max; + } + + /** Sets the y-axis min and max. Specify (0,0) to autoscale. */ + public static void setMinAndMax(double min, double max) { + fixedMin = min; + fixedMax = max; + IJ.register(ProfilePlot.class); + } + + /** Returns the profile plot y-axis min. Auto-scaling is used if min=max=0. */ + public static double getFixedMin() { + return fixedMin; + } + + /** Returns the profile plot y-axis max. Auto-scaling is used if min=max=0. */ + public static double getFixedMax() { + return fixedMax; + } + + double[] getStraightLineProfile(Roi roi, Calibration cal, ImageProcessor ip) { + ip.setInterpolate(PlotWindow.interpolate); + Line line = (Line)roi; + double[] values = line.getPixels(); + if (values==null) return null; + if (cal!=null && cal.pixelWidth!=cal.pixelHeight) { + FloatPolygon p = line.getFloatPoints(); + double dx = p.xpoints[1] - p.xpoints[0]; + double dy = p.ypoints[1] - p.ypoints[0]; + double pixelLength = Math.sqrt(dx*dx + dy*dy); + dx = cal.pixelWidth*dx; + dy = cal.pixelHeight*dy; + double calibratedLength = Math.sqrt(dx*dx + dy*dy); + xInc = calibratedLength * 1.0/pixelLength; + } + return values; + } + + double[] getRowAverageProfile(Rectangle rect, Calibration cal, ImageProcessor ip) { + double[] profile = new double[rect.height]; + int[] counts = new int[rect.height]; + double[] aLine; + ip.setInterpolate(false); + for (int x=rect.x; xsubpixel resolution), + * the line coordinates are interpreted as the roi line shown at high zoom level, + * i.e., integer (x,y) is at the top left corner of pixel (x,y). + * Thus, the coordinates of the pixel center are taken as (x+0.5, y+0.5). + * If subpixel resolution if off, the coordinates of the pixel centers are taken + * as integer (x,y). */ + double[] getIrregularProfile(Roi roi, ImageProcessor ip, Calibration cal) { + boolean interpolate = PlotWindow.interpolate; + boolean calcXValues = cal!=null && cal.pixelWidth!=cal.pixelHeight; + FloatPolygon p = roi.getFloatPolygon(); + int n = p.npoints; + float[] xpoints = p.xpoints; + float[] ypoints = p.ypoints; + ArrayList values = new ArrayList(); + int n2; + double inc = 0.01; + double distance=0.0, distance2=0.0, dx=0.0, dy=0.0, xinc, yinc; + double x, y, lastx=0.0, lasty=0.0, x1, y1, x2=xpoints[0], y2=ypoints[0]; + double value; + for (int i=1; i=1.0-inc/2.0) { + if (interpolate) + value = ip.getInterpolatedValue(x, y); + else + value = ip.getPixelValue((int)Math.round(x), (int)Math.round(y)); + values.add(new Double(value)); + lastx=x; lasty=y; + } + x += xinc; + y += yinc; + } while (--n2>0); + } + double[] values2 = new double[values.size()]; + for (int i=0; imax) + max = value; + } + this.min = min; + this.max = max; + } + + +} diff --git a/src/ij/gui/ProgressBar.java b/src/ij/gui/ProgressBar.java new file mode 100644 index 0000000..7b5ee91 --- /dev/null +++ b/src/ij/gui/ProgressBar.java @@ -0,0 +1,165 @@ +package ij.gui; + +import ij.macro.Interpreter; +import java.awt.*; +import java.awt.image.*; + +/** + * This is the progress bar that is displayed in the lower right hand corner of + * the ImageJ window. Use one of the static IJ.showProgress() methods to display + * and update the progress bar. + */ +public class ProgressBar extends Canvas { + + public static final int WIDTH = 120; + public static final int HEIGHT = 20; + + private int canvasWidth, canvasHeight; + private int x, y, width, height; + private long lastTime = 0; + private boolean showBar; + private boolean batchMode; + + private Color barColor = Color.gray; + private Color fillColor = new Color(204, 204, 255); + private Color backgroundColor = ij.ImageJ.backgroundColor; + private Color frameBrighter = backgroundColor.brighter(); + private Color frameDarker = backgroundColor.darker(); + private boolean dualDisplay = false; + private double slowX = 0.0;//box + private double fastX = 0.0;//dot + + /** + * This constructor is called once by ImageJ at startup. + */ + public ProgressBar(int canvasWidth, int canvasHeight) { + init(canvasWidth, canvasHeight); + } + + public void init(int canvasWidth, int canvasHeight) { + this.canvasWidth = canvasWidth; + this.canvasHeight = canvasHeight; + x = 3; + y = 5; + width = canvasWidth - 8; + height = canvasHeight - 7; + } + + void fill3DRect(Graphics g, int x, int y, int width, int height) { + g.setColor(fillColor); + g.fillRect(x + 1, y + 1, width - 2, height - 2); + g.setColor(frameDarker); + g.drawLine(x, y, x, y + height); + g.drawLine(x + 1, y, x + width - 1, y); + g.setColor(frameBrighter); + g.drawLine(x + 1, y + height, x + width, y + height); + g.drawLine(x + width, y, x + width, y + height - 1); + } + + /** + * Updates the progress bar, where abs(progress) should run from 0 to 1. + * If abs(progress) == 1 the bar is erased. The bar is updated only + * if more than 90 ms have passed since the last call. Does nothing if the + * ImageJ window is not present. + * @param progress Length of the progress bar to display (0...1). + * Using progress with negative sign (0 .. -1) will regard subsequent calls with + * positive argument as sub-ordinate processes that are displayed as moving dot. + */ + public void show(double progress) { + show(progress, false); + } + + /** + * Updates the progress bar, where abs(progress) should run from 0 to 1. + * @param progress Length of the progress bar to display (0...1). + * @param showInBatchMode show progress bar in batch mode macros? + */ + public void show(double progress, boolean showInBatchMode) { + boolean finished = false; + if (progress<=-1) + finished = true; + if (!dualDisplay && progress >= 1) + finished = true; + if (!finished) { + if (progress < 0) { + slowX = -progress; + fastX = 0.0; + dualDisplay = true; + } else if (dualDisplay) + fastX = progress; + if (!dualDisplay) + slowX = progress; + } + if (!showInBatchMode && (batchMode || Interpreter.isBatchMode())) + return; + if (finished) {//clear the progress bar + slowX = 0.0; + fastX = 0.0; + showBar = false; + dualDisplay = false; + repaint(); + return; + } + long time = System.currentTimeMillis(); + if (time-lastTime<90 && progress!=1.0) + return; + lastTime = time; + showBar = true; + repaint(); + } + + /** + * Updates the progress bar, where the length of the bar is set to + * ((abs(currentIndex)+1)/abs(finalIndex) of the maximum bar + * length. Use a negative currentIndex to show subsequent + * plugin calls as moving dot. The bar is erased if + * currentIndex>=finalIndex-1 or finalIndex == 0. + */ + public void show(int currentIndex, int finalIndex) { + boolean wasNegative = currentIndex < 0; + double progress = ((double) Math.abs(currentIndex) + 1.0) / Math.abs(finalIndex); + if (wasNegative) + progress = -progress; + if (finalIndex == 0) + progress = -1; + show(progress); + } + + public void update(Graphics g) { + paint(g); + } + + public void paint(Graphics g) { + if (showBar) { + fill3DRect(g, x - 1, y - 1, width + 1, height + 1); + drawBar(g); + } else { + g.setColor(backgroundColor); + g.fillRect(0, 0, canvasWidth, canvasHeight); + } + } + + void drawBar(Graphics g) { + int barEnd = (int) (width * slowX); + if (Toolbar.getToolId()==Toolbar.ANGLE) + g.setColor(Color.getHSBColor(((float)(System.currentTimeMillis()%1000))/1000, 0.5f, 1.0f)); + else + g.setColor(barColor); + g.fillRect(x, y, barEnd, height); + if (dualDisplay && fastX > 0) { + int dotPos = (int) (width * fastX); + g.setColor(Color.BLACK); + if (dotPos > 1 && dotPos < width - 7) + g.fillOval(dotPos, y + 3, 7, 7); + } + } + + public Dimension getPreferredSize() { + return new Dimension(canvasWidth, canvasHeight); + } + + public void setBatchMode(boolean batchMode) { + this.batchMode = batchMode; + } + +} diff --git a/src/ij/gui/Roi.java b/src/ij/gui/Roi.java new file mode 100644 index 0000000..a986810 --- /dev/null +++ b/src/ij/gui/Roi.java @@ -0,0 +1,2957 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.*; +import ij.plugin.frame.Recorder; +import ij.plugin.filter.Analyzer; +import ij.plugin.filter.ThresholdToSelection; +import ij.macro.Interpreter; +import ij.io.RoiDecoder; +import java.awt.*; +import java.util.*; +import java.io.*; +import java.awt.image.*; +import java.awt.event.*; +import java.awt.geom.*; + +/** + * A rectangular region of interest and superclass for the other ROI classes. + * + * This class implements {@code Iterable} and can thus be + * used to iterate over the contained coordinates. Usage example: + *
+ * Roi roi = ...;
+ * for (Point p : roi) {
+ *   // process p
+ * }
+ * 
+ * + * Convention for subpixel resolution and zooming in: + *
    + *
  • Area ROIs: Integer coordinates refer to the top-left corner of the pixel with these coordinates. + * Thus, pixel (0,0) is enclosed by the rectangle spanned between points (0,0) and (1,1), + * i.e., a rectangle at (0,0) with width = height = 1 pixel. + *
  • Line and Point Rois: Integer coordinates refer to the center of a pixel. + * Thus, a line from (0,0) to (1,0) has its start and end points in the center of + * pixels (0,0) and (1,0), respectively, and drawing the line should affect both + * pixels. For images dispplayed at high zoom levels, this means that (open) lines + * and single points are displayed 0.5 pixels further to the right and bottom than + * the outlines of area ROIs (closed lines) with the same coordinates. + *
+ * Note that rectangular and (nonrotated) oval ROIs do not support subpixel resolution. + * Since ImageJ 1.52t, this convention does not depend on the Prefs.subpixelResolution + * (previously accessible via Edit>Options>Plot) and this flag has no effect any more. + * + */ +public class Roi extends Object implements Cloneable, java.io.Serializable, Iterable { + + public static final int CONSTRUCTING=0, MOVING=1, RESIZING=2, NORMAL=3, MOVING_HANDLE=4; // States + public static final int RECTANGLE=0, OVAL=1, POLYGON=2, FREEROI=3, TRACED_ROI=4, LINE=5, + POLYLINE=6, FREELINE=7, ANGLE=8, COMPOSITE=9, POINT=10; // Types + public static final int HANDLE_SIZE = 5; // replaced by getHandleSize() + public static final int NOT_PASTING = -1; + public static final int FERET_ARRAYSIZE = 16; // Size of array with Feret values + public static final int FERET_ARRAY_POINTOFFSET = 8; // Where point coordinates start in Feret array + private static final String NAMES_KEY = "group.names"; + + static final int NO_MODS=0, ADD_TO_ROI=1, SUBTRACT_FROM_ROI=2; // modification states + + int startX, startY, x, y, width, height; + double startXD, startYD; + Rectangle2D.Double bounds; + int activeHandle; + int state; + int modState = NO_MODS; + int cornerDiameter; //for rounded rectangle + int previousSX, previousSY; //remember for aborting moving with esc and constrain + + public static final BasicStroke onePixelWide = new BasicStroke(1); + protected static Color ROIColor = Prefs.getColor(Prefs.ROICOLOR,Color.yellow); + protected static int pasteMode = Blitter.COPY; + protected static int lineWidth = 1; + protected static Color defaultFillColor; + private static Vector listeners = new Vector(); + private static LUT glasbeyLut; + private static int defaultGroup; // zero is no specific group + private static Color groupColor; + private static double defaultStrokeWidth; + private static String groupNamesString = Prefs.get(NAMES_KEY, null); + private static String[] groupNames; + private static boolean groupNamesChanged; + + /** Get using getPreviousRoi() and set using setPreviousRoi() */ + public static Roi previousRoi; + + protected int type; + protected int xMax, yMax; + protected ImagePlus imp; + private int imageID; + protected ImageCanvas ic; + protected int oldX, oldY, oldWidth, oldHeight; //remembers previous clip rect + protected int clipX, clipY, clipWidth, clipHeight; + protected ImagePlus clipboard; + protected boolean constrain; // to be square or limit to horizontal/vertical motion + protected boolean center; + protected boolean aspect; + protected boolean updateFullWindow; + protected double mag = 1.0; + protected double asp_bk; //saves aspect ratio if resizing takes roi very small + protected ImageProcessor cachedMask; + protected Color handleColor = Color.white; + protected Color strokeColor; + protected Color instanceColor; //obsolete; replaced by strokeColor + protected Color fillColor; + protected BasicStroke stroke; + protected boolean nonScalable; + protected boolean overlay; + protected boolean wideLine; + protected boolean ignoreClipRect; + protected double flattenScale = 1.0; + protected static Color defaultColor; + + private String name; + private int position; + private int channel, slice, frame; + private boolean hyperstackPosition; + private Overlay prototypeOverlay; + private boolean subPixel; + private boolean activeOverlayRoi; + private Properties props; + private boolean isCursor; + private double xcenter = Double.NaN; + private double ycenter; + private boolean listenersNotified; + private boolean antiAlias = true; + private int group; + private boolean usingDefaultStroke; + private static int defaultHandleSize; + private int handleSize = -1; + private boolean scaleStrokeWidth; // Scale stroke width when zooming images? + + /** Creates a rectangular ROI. */ + public Roi(int x, int y, int width, int height) { + this(x, y, width, height, 0); + } + + /** Creates a rectangular ROI using double arguments. */ + public Roi(double x, double y, double width, double height) { + this(x, y, width, height, 0); + } + + /** Creates a new rounded rectangular ROI. */ + public Roi(int x, int y, int width, int height, int cornerDiameter) { + setImage(null); + if (width<1) width = 1; + if (height<1) height = 1; + if (width>xMax) width = xMax; + if (height>yMax) height = yMax; + this.cornerDiameter = cornerDiameter; + this.x = x; + this.y = y; + startX = x; startY = y; + oldX = x; oldY = y; oldWidth=0; oldHeight=0; + this.width = width; + this.height = height; + oldWidth=width; + oldHeight=height; + clipX = x; + clipY = y; + clipWidth = width; + clipHeight = height; + state = NORMAL; + type = RECTANGLE; + if (ic!=null) { + Graphics g = ic.getGraphics(); + draw(g); + g.dispose(); + } + double defaultWidth = defaultStrokeWidth(); + if (defaultWidth>0) { + stroke = new BasicStroke((float)defaultWidth); + usingDefaultStroke = true; + } + fillColor = defaultFillColor; + this.group = defaultGroup; //initialize with current group and associated color + if (defaultGroup>0) + this.strokeColor = groupColor; + } + + /** Creates a rounded rectangular ROI using double arguments. */ + public Roi(double x, double y, double width, double height, int cornerDiameter) { + this((int)x, (int)y, (int)Math.ceil(width), (int)Math.ceil(height), cornerDiameter); + bounds = new Rectangle2D.Double(x, y, width, height); + subPixel = true; + } + + /** Creates a new rectangular Roi. */ + public Roi(Rectangle r) { + this(r.x, r.y, r.width, r.height); + } + + /** Starts the process of creating a user-defined rectangular Roi, + where sx and sy are the starting screen coordinates. */ + public Roi(int sx, int sy, ImagePlus imp) { + this(sx, sy, imp, 0); + } + + /** Starts the process of creating a user-defined rectangular Roi, + where sx and sy are the starting screen coordinates. + For rectangular rois, also a corner diameter may be specified to + make it a rounded rectangle */ + public Roi(int sx, int sy, ImagePlus imp, int cornerDiameter) { + setImage(imp); + int ox=sx, oy=sy; + if (ic!=null) { + ox = ic.offScreenX2(sx); + oy = ic.offScreenY2(sy); + } + setLocation(ox, oy); + this.cornerDiameter = cornerDiameter; + width = 0; + height = 0; + state = CONSTRUCTING; + type = RECTANGLE; + if (cornerDiameter>0) { + double swidth = RectToolOptions.getDefaultStrokeWidth(); + if (swidth>0.0) + setStrokeWidth(swidth); + Color scolor = RectToolOptions.getDefaultStrokeColor(); + if (scolor!=null) + setStrokeColor(scolor); + } + double defaultWidth = defaultStrokeWidth(); + if (defaultWidth>0) { + stroke = new BasicStroke((float)defaultWidth); + usingDefaultStroke = true; + } + fillColor = defaultFillColor; + this.group = defaultGroup; + if (defaultGroup>0) + this.strokeColor = groupColor; + } + + /** Creates a rectangular ROI. */ + public static Roi create(double x, double y, double width, double height) { + return new Roi(x, y, width, height); + } + + /** Creates a rounded rectangular ROI. */ + public static Roi create(double x, double y, double width, double height, int cornerDiameter) { + return new Roi(x, y, width, height, cornerDiameter); + } + + /** @deprecated */ + public Roi(int x, int y, int width, int height, ImagePlus imp) { + this(x, y, width, height); + setImage(imp); + } + + /** Set the location of the ROI in image coordinates. */ + public void setLocation(int x, int y) { + this.x = x; + this.y = y; + startX = x; startY = y; + oldX = x; oldY = y; oldWidth=0; oldHeight=0; + if (bounds!=null) { + if (!isInteger(bounds.x) || !isInteger(bounds.y)) { + cachedMask = null; + width = (int)Math.ceil(bounds.width); + height = (int)Math.ceil(bounds.height); + } + bounds.x = x; + bounds.y = y; + if (this instanceof PolygonRoi) setIntBounds(bounds); + } + } + + /** Set the location of the ROI in image coordinates. */ + public void setLocation(double x, double y) { + setLocation((int)x, (int)y); + if (isInteger(x) && isInteger(y)) + return; + if (bounds!=null) { + if (!isInteger(x-bounds.x) || !isInteger(y-bounds.y)) { + cachedMask = null; + width = (int)Math.ceil(bounds.x + bounds.width) - this.x; //ensure that all pixels are inside + height = (int)Math.ceil(bounds.y + bounds.height) - this.y; + } + bounds.x = x; + bounds.y = y; + } else { + cachedMask = null; + bounds = new Rectangle2D.Double(x, y, width, height); + } + if (this instanceof PolygonRoi) setIntBounds(bounds); + subPixel = true; + } + + /** Sets the ImagePlus associated with this ROI. + * imp may be null to remove the association to an image. */ + public void setImage(ImagePlus imp) { + this.imp = imp; + cachedMask = null; + if (imp==null) { + ic = null; + clipboard = null; + xMax = yMax = Integer.MAX_VALUE; + } else { + ic = imp.getCanvas(); + xMax = imp.getWidth(); + yMax = imp.getHeight(); + } + } + + /** Returns the ImagePlus associated with this ROI, or null. */ + public ImagePlus getImage() { + return imp; + } + + /** Returns the ID of the image associated with this ROI. */ + public int getImageID() { + return imp!=null?imp.getID():imageID; + } + + public int getType() { + return type; + } + + public int getState() { + return state; + } + + /** Returns the perimeter length. */ + public double getLength() { + double pw=1.0, ph=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } + double perimeter = 2.0*width*pw + 2.0*height*ph; + if (cornerDiameter > 0) { //using Ramanujan's approximation for the circumference of an ellipse + double a = 0.5*Math.min(cornerDiameter, width)*pw; + double b = 0.5*Math.min(cornerDiameter, height)*ph; + perimeter += Math.PI*(3*(a + b) - Math.sqrt((3*a + b)*(a + 3*b))) -4*(a+b); + } + return perimeter; + } + + /** Returns Feret's diameter, the greatest distance between + any two points along the ROI boundary. */ + public double getFeretsDiameter() { + double[] a = getFeretValues(); + return a!=null?a[0]:0.0; + } + + /** Returns an array with the following values: + *
[0] "Feret" (maximum caliper width) + *
[1] "FeretAngle" (angle of diameter with maximum caliper width, between 0 and 180 deg) + *
[2] "MinFeret" (minimum caliper width) + *
[3][4] , "FeretX" and "FeretY", the X and Y coordinates of the starting point + * (leftmost point) of the maximum-caliper-width diameter. + *
[5-7] reserved + *
All these values and point coordinates are in calibrated image coordinates. + *

+ * The following array elements are end points of the maximum and minimum caliper diameter, + * in unscaled image pixel coordinates: + *
[8][9] "FeretX1", "FeretY1"; unscaled versions of "FeretX" and "FeretY" + * (subclasses may use any end of the diameter, not necessarily the left one) + *
[10][11] "FeretX2", "FeretY2", end point of the maxium-caliper-width diameter. + * Both of these points are vertices of the convex hull. + *
The final four array elements are the starting and end points of the minimum caliper width, + *
[12],[13] "MinFeretX", "MinFeretY", and + *
[14],[15] "MinFeretX2", "MinFeretY2". These two pooints are not sorted by x, + * but the first point point (MinFeretX, MinFeretY) is guaranteed to be a vertex of the convex hull, + * while second point (MinFeretX2, MinFeretY2) usually is not a vertex point but at a + * boundary line of the convex hull. */ + public double[] getFeretValues() { + double pw=1.0, ph=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } + + FloatPolygon poly = getFloatConvexHull(); + if (poly==null || poly.npoints==0) return null; + + double[] a = new double[FERET_ARRAYSIZE]; + // calculate maximum Feret diameter: largest distance between any two points + int p1=0, p2=0; + double diameterSqr = 0.0; //square of maximum Feret diameter + for (int i=0; idiameterSqr) {diameterSqr=dsqr; p1=i; p2=j;} + } + } + if (poly.xpoints[p1] > poly.xpoints[p2]) { + int p2swap = p1; p1 = p2; p2 = p2swap; + } + double xf1=poly.xpoints[p1], yf1=poly.ypoints[p1]; + double xf2=poly.xpoints[p2], yf2=poly.ypoints[p2]; + double angle = (180.0/Math.PI)*Math.atan2((yf1-yf2)*ph, (xf2-xf1)*pw); + if (angle < 0.0) + angle += 180.0; + a[0] = Math.sqrt(diameterSqr); + a[1] = angle; + a[3] = xf1; a[4] = yf1; + { int i = FERET_ARRAY_POINTOFFSET; //array elements 8-11 are start and end points of max Feret diameter + a[i++] = poly.xpoints[p1]; a[i++] = poly.ypoints[p1]; + a[i++] = poly.xpoints[p2]; a[i++] = poly.ypoints[p2]; + } + + // Calculate minimum Feret diameter: + // For all pairs of points on the convex hull: + // Get the point with the largest distance from the line between these two points + // Of all these pairs, take the one where the distance is the lowest + // The following code requires a counterclockwise convex hull with no duplicate points + double x0 = poly.xpoints[poly.npoints-1]; + double y0 = poly.ypoints[poly.npoints-1]; + double minFeret = Double.MAX_VALUE; + double[] xyEnd = new double[4]; //start and end points of the minFeret diameter, uncalibrated + double[] xyEi = new double[4]; //intermediate values of xyEnd + for (int i=0; i maxDist) { + maxDist = dist; + xyEi[0] = x1; + xyEi[1] = y1; + xyEi[2] = xyEi[0] - (xnorm/pw * dist)/pw; + xyEi[3] = xyEi[1] - (ynorm/ph * dist)/ph; + } + } + if (maxDist < minFeret) { + minFeret = maxDist; + System.arraycopy(xyEi, 0, xyEnd, 0, 4); + } + } + a[2] = minFeret; + System.arraycopy(xyEnd, 0, a, FERET_ARRAY_POINTOFFSET+4, 4); //a[12]-a[15] are minFeretX, Y, X2, Y2 + return a; + } + + /** Returns the convex hull of this Roi as a Polygon with integer coordinates + * by rounding the floating-point values. + * Coordinates of the convex hull are image pixel coordinates. */ + public Polygon getConvexHull() { + FloatPolygon fp = getFloatConvexHull(); + return new Polygon(toIntR(fp.xpoints), toIntR(fp.ypoints), fp.npoints); + } + + /** Returns the convex hull of this Roi as a FloatPolygon. + * Coordinates of the convex hull are image pixel coordinates. */ + public FloatPolygon getFloatConvexHull() { + FloatPolygon fp = getFloatPolygon(""); //no duplicate closing points, no path-separating NaNs needed + return fp == null ? null : fp.getConvexHull(); + } + + double getFeretBreadth(Shape shape, double angle, double x1, double y1, double x2, double y2) { + double cx = x1 + (x2-x1)/2; + double cy = y1 + (y2-y1)/2; + AffineTransform at = new AffineTransform(); + at.rotate(angle*Math.PI/180.0, cx, cy); + Shape s = at.createTransformedShape(shape); + Rectangle2D r = s.getBounds2D(); + return Math.min(r.getWidth(), r.getHeight()); + } + + /** Returns this selection's bounding rectangle. */ + public Rectangle getBounds() { + return new Rectangle(x, y, width, height); + } + + /** Returns this selection's bounding rectangle (with subpixel accuracy). */ + public Rectangle2D.Double getFloatBounds() { + if (bounds!=null) + return new Rectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height); + else + return new Rectangle2D.Double(x, y, width, height); + } + + /** Sets the bounds of rectangular, oval or text selections. + * Note that for these types, subpixel resolution is ignored, + * and the x,y values are rounded down, the width and height values rounded up. + * Do not use for other ROI types since their width and height are results of + * a calculation. + * For translating ROIs, use setLocation. */ + public void setBounds(Rectangle2D.Double b) { + if (!(type==RECTANGLE||type==OVAL||(this instanceof TextRoi))) + return; + this.x = (int)b.x; + this.y = (int)b.y; + this.width = (int)Math.ceil(b.width); + this.height = (int)Math.ceil(b.height); + bounds = new Rectangle2D.Double(b.x, b.y, b.width, b.height); + cachedMask = null; + } + + /** Sets the integer boundaries x, y, width, height from given sub-pixel + * boundaries, such that all points are within the integer bounding rectangle. + * For open line selections and (multi)Point Rois, note that integer Roi + * coordinates correspond to the center of the 1x1 rectangle enclosing a pixel. + * Points at the boundary of such a rectangle are counted for the higher x or y + * value, in agreement to how (poly-)line or PointRois are displayed at the + * screen at high zoom levels. (For lines and points, it should include all + * pixels affected by 'draw' */ + void setIntBounds(Rectangle2D.Double bounds) { + if (useLineSubpixelConvention()) { //for PointRois & open lines, ensure the 'draw' area is enclosed + x = (int)Math.floor(bounds.x + 0.5); + y = (int)Math.floor(bounds.y + 0.5); + width = (int)Math.floor(bounds.x + bounds.width + 1.5) - x; + height = (int)Math.floor(bounds.y + bounds.height + 1.5) - y; + } else { //for area Rois, the subpixel bounds must be enclosed in the int bounds + x = (int)Math.floor(bounds.x); + y = (int)Math.floor(bounds.y); + width = (int)Math.ceil(bounds.x + bounds.width) - x; + height = (int)Math.ceil(bounds.y + bounds.height) - y; + } + } + + /** + * @deprecated + * replaced by getBounds() + */ + public Rectangle getBoundingRect() { + return getBounds(); + } + + /** Returns the outline of this selection as a Polygon, or + null if this is a straight line selection. + @see ij.process.ImageProcessor#setRoi + @see ij.process.ImageProcessor#drawPolygon + @see ij.process.ImageProcessor#fillPolygon + */ + public Polygon getPolygon() { + int[] xpoints = new int[4]; + int[] ypoints = new int[4]; + xpoints[0] = x; + ypoints[0] = y; + xpoints[1] = x+width; + ypoints[1] = y; + xpoints[2] = x+width; + ypoints[2] = y+height; + xpoints[3] = x; + ypoints[3] = y+height; + return new Polygon(xpoints, ypoints, 4); + } + + /** Returns the outline (in image pixel coordinates) as a FloatPolygon */ + public FloatPolygon getFloatPolygon() { + if (cornerDiameter>0) { // Rounded Rectangle + ShapeRoi s = new ShapeRoi(this); + return s.getFloatPolygon(); + } else if (subPixelResolution() && bounds!=null) { + float[] xpoints = new float[4]; + float[] ypoints = new float[4]; + xpoints[0] = (float)bounds.x; + ypoints[0] = (float)bounds.y; + xpoints[1] = (float)(bounds.x+bounds.width); + ypoints[1] = (float)bounds.y; + xpoints[2] = (float)(bounds.x+bounds.width); + ypoints[2] = (float)(bounds.y+bounds.height); + xpoints[3] = (float)bounds.x; + ypoints[3] = (float)(bounds.y+bounds.height); + return new FloatPolygon(xpoints, ypoints); + } else { + Polygon p = getPolygon(); + return new FloatPolygon(toFloat(p.xpoints), toFloat(p.ypoints), p.npoints); + } + } + + /** Returns the outline in image pixel coordinates, + * where options may include "close" to add a point to close the outline + * if this is an area roi and the outline is not closed yet. + * (For ShapeRois, "separate" inserts NaN values between subpaths). */ + public FloatPolygon getFloatPolygon(String options) { + options = options.toLowerCase(); + boolean addPointForClose = options.indexOf("close") >= 0; + FloatPolygon fp = getFloatPolygon(); + int n = fp.npoints; + if (isArea() && n > 1) { + boolean isClosed = fp.xpoints[0] == fp.xpoints[n-1] && fp.ypoints[0] == fp.ypoints[n-1]; + if (addPointForClose && !isClosed) + fp.addPoint(fp.xpoints[0], fp.ypoints[0]); + else if (!addPointForClose && isClosed) + fp.npoints--; + } + return fp; + } + + /** Returns, as a FloatPolygon, an interpolated version + * of this selection that has points spaced 1.0 pixel apart. + */ + public FloatPolygon getInterpolatedPolygon() { + return getInterpolatedPolygon(1.0, false); + } + + /** Returns, as a FloatPolygon, an interpolated version of + * this selection with points spaced 'interval' pixels apart. + * If 'smooth' is true, traced and freehand selections are + * first smoothed using a 3 point running average. + */ + public FloatPolygon getInterpolatedPolygon(double interval, boolean smooth) { + FloatPolygon p = (this instanceof Line)?((Line)this).getFloatPoints():getFloatPolygon(); + return getInterpolatedPolygon(p, interval, smooth); + } + + /** + * Returns, as a FloatPolygon, an interpolated version of this selection + * with points spaced abs('interval') pixels apart. If 'smooth' is true, traced + * and freehand selections are first smoothed using a 3 point running + * average. + * If 'interval' is negative, the program is allowed to decrease abs('interval') + * so that the last segment will hit the end point + */ + protected FloatPolygon getInterpolatedPolygon(FloatPolygon p, double interval, boolean smooth) { + boolean allowToAdjust = interval < 0; + interval = Math.abs(interval); + boolean isLine = this.isLine(); + double length = p.getLength(isLine); + + int npoints = p.npoints; + if (npoints<2) + return p; + if (Math.abs(interval)<0.01) { + IJ.error("Interval must be >= 0.01"); + return p; + } + + if (!isLine) {//**append (and later remove) closing point to end of array + npoints++; + p.xpoints = java.util.Arrays.copyOf(p.xpoints, npoints); + p.xpoints[npoints - 1] = p.xpoints[0]; + p.ypoints = java.util.Arrays.copyOf(p.ypoints, npoints); + p.ypoints[npoints - 1] = p.ypoints[0]; + } + int npoints2 = (int) (10 + (length * 1.5) / interval);//allow some headroom + + double tryInterval = interval; + double minDiff = 1e9; + double bestInterval = 0; + int srcPtr = 0;//index of source polygon + int destPtr = 0;//index of destination polygon + double[] destXArr = new double[npoints2]; + double[] destYArr = new double[npoints2]; + int nTrials = 50; + int trial = 0; + while (trial <= nTrials) { + destXArr[0] = p.xpoints[0]; + destYArr[0] = p.ypoints[0]; + srcPtr = 0; + destPtr = 0; + double xA = p.xpoints[0];//start of current segment + double yA = p.ypoints[0]; + + while (srcPtr < npoints - 1) {//collect vertices + double xC = destXArr[destPtr];//center circle + double yC = destYArr[destPtr]; + double xB = p.xpoints[srcPtr + 1];//end of current segment + double yB = p.ypoints[srcPtr + 1]; + double[] intersections = lineCircleIntersection(xA, yA, xB, yB, xC, yC, tryInterval, true); + if (intersections.length >= 2) { + xA = intersections[0];//only use first of two intersections + yA = intersections[1]; + destPtr++; + destXArr[destPtr] = xA; + destYArr[destPtr] = yA; + } else { + srcPtr++;//no intersection found, pick next segment + xA = p.xpoints[srcPtr]; + yA = p.ypoints[srcPtr]; + } + } + destPtr++; + destXArr[destPtr] = p.xpoints[npoints - 1]; + destYArr[destPtr] = p.ypoints[npoints - 1]; + destPtr++; + if (!allowToAdjust) { + if (isLine) + destPtr--; + break; + } + + int nSegments = destPtr - 1; + double dx = destXArr[destPtr - 2] - destXArr[destPtr - 1]; + double dy = destYArr[destPtr - 2] - destYArr[destPtr - 1]; + double lastSeg = Math.sqrt(dx * dx + dy * dy); + + double diff = lastSeg - tryInterval;//always <= 0 + if (Math.abs(diff) < minDiff) { + minDiff = Math.abs(diff); + bestInterval = tryInterval; + } + double feedBackFactor = 0.66;//factor <1: applying soft successive approximation + tryInterval = tryInterval + feedBackFactor * diff / nSegments; + //stop if tryInterval < 80% of interval, OR if last segment differs < 0.05 pixels + if ((tryInterval < 0.8 * interval || Math.abs(diff) < 0.05 || trial == nTrials - 1) && trial < nTrials) { + trial = nTrials;//run one more loop with bestInterval to get best polygon + tryInterval = bestInterval; + } else + trial++; + } + if (!isLine) //**remove closing point from end of array + destPtr--; + float[] xPoints = new float[destPtr]; + float[] yPoints = new float[destPtr]; + for (int jj = 0; jj < destPtr; jj++) { + xPoints[jj] = (float) destXArr[jj]; + yPoints[jj] = (float) destYArr[jj]; + } + FloatPolygon fPoly = new FloatPolygon(xPoints, yPoints); + return fPoly; + } + + /** Returns the coordinates of the pixels inside this ROI as an array of Points. + * @see #getContainedFloatPoints + * @see #iterator + */ + public Point[] getContainedPoints() { + Roi roi = this; + if (isLine()) + roi = convertLineToArea(this); + ImageProcessor mask = roi.getMask(); + Rectangle bounds = roi.getBounds(); + ArrayList points = new ArrayList(); + for (int y=0; y + * Calculates intersections of a line segment with a circle + * Author N.Vischer + * ax, ay, bx, by: points A and B of line segment + * cx, cy, rad: Circle center and radius. + * ignoreOutside: if true, ignores intersections outside the line segment A-B + * Returns an array of 0, 2 or 4 coordinates (for 0, 1, or 2 intersection + * points). If two intersection points are returned, they are listed in travel + * direction A->B + * + */ + public static double[] lineCircleIntersection(double ax, double ay, double bx, double by, double cx, double cy, double rad, boolean ignoreOutside) { + //rotates & translates points A, B and C, creating new points A2, B2 and C2. + //A2 is then on origin, and B2 is on positive x-axis + + double dxAC = cx - ax; + double dyAC = cy - ay; + double lenAC = Math.sqrt(dxAC * dxAC + dyAC * dyAC); + + double dxAB = bx - ax; + double dyAB = by - ay; + + //calculate B2 and C2: + double xB2 = Math.sqrt(dxAB * dxAB + dyAB * dyAB); + + double phi1 = Math.atan2(dyAB, dxAB);//amount of rotation + double phi2 = Math.atan2(dyAC, dxAC); + double phi3 = phi1 - phi2; + double xC2 = lenAC * Math.cos(phi3); + double yC2 = lenAC * Math.sin(phi3);//rotation & translation is done + if (Math.abs(yC2) > rad) + return new double[0];//no intersection found + double halfChord = Math.sqrt(rad * rad - yC2 * yC2); + double sectOne = xC2 - halfChord;//first intersection point, still on x axis + double sectTwo = xC2 + halfChord;//second intersection point, still on x axis + double[] xyCoords = new double[4]; + int ptr = 0; + if ((sectOne >= 0 && sectOne <= xB2) || !ignoreOutside) { + double sectOneX = Math.cos(phi1) * sectOne + ax;//undo rotation and translation + double sectOneY = Math.sin(phi1) * sectOne + ay; + xyCoords[ptr++] = sectOneX; + xyCoords[ptr++] = sectOneY; + } + if ((sectTwo >= 0 && sectTwo <= xB2) || !ignoreOutside) { + double sectTwoX = Math.cos(phi1) * sectTwo + ax;//undo rotation and translation + double sectTwoY = Math.sin(phi1) * sectTwo + ay; + xyCoords[ptr++] = sectTwoX; + xyCoords[ptr++] = sectTwoY; + } + if (halfChord == 0 && ptr > 2) //tangent line returns only one intersection + ptr = 2; + xyCoords = java.util.Arrays.copyOf(xyCoords,ptr); + return xyCoords; + } + + /** Returns a copy of this roi. See Thinking is Java by Bruce Eckel + (www.eckelobjects.com) for a good description of object cloning. */ + public synchronized Object clone() { + try { + Roi r = (Roi)super.clone(); + r.setImage(null); + if (!usingDefaultStroke) + r.setStroke(getStroke()); + r.setFillColor(getFillColor()); + r.imageID = getImageID(); + r.listenersNotified = false; + if (bounds!=null) + r.bounds = (Rectangle2D.Double)bounds.clone(); + return r; + } + catch (CloneNotSupportedException e) {return null;} + } + + /** Aborts constructing or modifying the roi (called by the ImageJ class on escape) */ + public void abortModification(ImagePlus imp) { + if (state == CONSTRUCTING) { + setImage(null); + if (imp!=null) { + Roi savedPreviousRoi = getPreviousRoi(); + imp.setRoi(previousRoi!=null && previousRoi.getImage() == imp ? previousRoi : null); + setPreviousRoi(savedPreviousRoi); //(overrule saving this aborted roi as previousRoi) + } + } else if (state==MOVING) + move(previousSX, previousSY); //move back to starting point + else if (state == MOVING_HANDLE) + moveHandle(previousSX, previousSY); //move handle back to starting point + state = NORMAL; + } + + protected void grow(int sx, int sy) { + if (clipboard!=null) return; + int xNew = ic.offScreenX2(sx); + int yNew = ic.offScreenY2(sy); + if (type==RECTANGLE) { + if (xNew < 0) xNew = 0; + if (yNew < 0) yNew = 0; + } + if (constrain) { + // constrain selection to be square + if (!center) + {growConstrained(xNew, yNew); return;} + int dx, dy, d; + dx = xNew - x; + dy = yNew - y; + if (dx=startX)?startX:startX - width; + y = (yNew>=startY)?startY:startY - height; + if (type==RECTANGLE) { + if ((x+width) > xMax) width = xMax-x; + if ((y+height) > yMax) height = yMax-y; + } + } + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; + oldY = y; + oldWidth = width; + oldHeight = height; + bounds = null; + } + + private void growConstrained(int xNew, int yNew) { + int dx = xNew - startX; + int dy = yNew - startY; + width = height = (int)Math.round(Math.sqrt(dx*dx + dy*dy)); + if (type==RECTANGLE) { + x = (xNew>=startX)?startX:startX - width; + y = (yNew>=startY)?startY:startY - height; + if (x<0) x = 0; + if (y<0) y = 0; + if ((x+width) > xMax) width = xMax-x; + if ((y+height) > yMax) height = yMax-y; + } else { + x = startX + dx/2 - width/2; + y = startY + dy/2 - height/2; + } + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; + oldY = y; + oldWidth = width; + oldHeight = height; + } + + protected void moveHandle(int sx, int sy) { + double asp; + if (clipboard!=null) return; + int ox = ic.offScreenX2(sx); + int oy = ic.offScreenY2(sy); + if (ox<0) ox=0; if (oy<0) oy=0; + if (ox>xMax) ox=xMax; if (oy>yMax) oy=yMax; + int x1=x, y1=y, x2=x1+width, y2=y+height, xc=x+width/2, yc=y+height/2; + if (width > 7 && height > 7) { + asp = (double)width/(double)height; + asp_bk = asp; + } else + asp = asp_bk; + + switch (activeHandle) { + case 0: + x=ox; y=oy; + break; + case 1: + y=oy; + break; + case 2: + x2=ox; y=oy; + break; + case 3: + x2=ox; + break; + case 4: + x2=ox; y2=oy; + break; + case 5: + y2=oy; + break; + case 6: + x=ox; y2=oy; + break; + case 7: + x=ox; + break; + } + if (x=x2) { + width=1; + x=x2=xc; + } + if (y>=y2) { + height=1; + y=y2=yc; + } + bounds = null; + } + + if (constrain) { + if (activeHandle==1 || activeHandle==5) + width=height; + else + height=width; + + if(x>=x2) { + width=1; + x=x2=xc; + } + if (y>=y2) { + height=1; + y=y2=yc; + } + switch (activeHandle) { + case 0: + x=x2-width; + y=y2-height; + break; + case 1: + x=xc-width/2; + y=y2-height; + break; + case 2: + y=y2-height; + break; + case 3: + y=yc-height/2; + break; + case 5: + x=xc-width/2; + break; + case 6: + x=x2-width; + break; + case 7: + y=yc-height/2; + x=x2-width; + break; + } + if (center) { + x=xc-width/2; + y=yc-height/2; + } + } + + if (aspect && !constrain) { + if (activeHandle==1 || activeHandle==5) width=(int)Math.rint((double)height*asp); + else height=(int)Math.rint((double)width/asp); + + switch (activeHandle){ + case 0: + x=x2-width; + y=y2-height; + break; + case 1: + x=xc-width/2; + y=y2-height; + break; + case 2: + y=y2-height; + break; + case 3: + y=yc-height/2; + break; + case 5: + x=xc-width/2; + break; + case 6: + x=x2-width; + break; + case 7: + y=yc-height/2; + x=x2-width; + break; + } + if (center) { + x=xc-width/2; + y=yc-height/2; + } + + // Attempt to preserve aspect ratio when roi very small: + if (width<8) { + if(width<1) width = 1; + height=(int)Math.rint((double)width/asp_bk); + } + if (height<8) { + if(height<1) height =1; + width=(int)Math.rint((double)height*asp_bk); + } + } + + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX=x; oldY=y; + oldWidth=width; oldHeight=height; + bounds = null; + subPixel = false; + } + + void move(int sx, int sy) { + if (constrain) { // constrain translation in 90deg steps + int dx = sx - previousSX; + int dy = sy - previousSY; + if (Math.abs(dx) > Math.abs(dy)) + dy = 0; + else + dx = 0; + sx = previousSX + dx; + sy = previousSY + dy; + } + int xNew = ic.offScreenX(sx); + int yNew = ic.offScreenY(sy); + int dx = xNew - startX; + int dy = yNew - startY; + if (dx==0 && dy==0) + return; + x += dx; + y += dy; + if (bounds!=null) + setLocation(bounds.x + dx, bounds.y + dy); + boolean isImageRoi = this instanceof ImageRoi; + if (clipboard==null && type==RECTANGLE && !isImageRoi) { + if (x<0) x=0; if (y<0) y=0; + if ((x+width)>xMax) x = xMax-width; + if ((y+height)>yMax) y = yMax-height; + } + startX = xNew; + startY = yNew; + if (type==POINT || ((this instanceof TextRoi) && ((TextRoi)this).getAngle()!=0.0)) + ignoreClipRect = true; + updateClipRect(); + if ((lineWidth>1 && isLine()) || ignoreClipRect || ((this instanceof PolygonRoi)&&((PolygonRoi)this).isSplineFit())) + imp.draw(); + else + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; + oldY = y; + oldWidth = width; + oldHeight=height; + if (isImageRoi) showStatus(); + } + + /** Nudge ROI one pixel on arrow key press. */ + public void nudge(int key) { + if (WindowManager.getActiveWindow() instanceof ij.plugin.frame.RoiManager) + return; + if (bounds != null && (!isInteger(bounds.x) || !isInteger(bounds.y))) + cachedMask = null; + switch(key) { + case KeyEvent.VK_UP: + this.y--; + if (this.y<0 && (type!=RECTANGLE||clipboard==null)) + this.y = 0; + break; + case KeyEvent.VK_DOWN: + this.y++; + if ((this.y+height)>=yMax && (type!=RECTANGLE||clipboard==null)) + this.y = yMax-height; + break; + case KeyEvent.VK_LEFT: + this.x--; + if (this.x<0 && (type!=RECTANGLE||clipboard==null)) + this.x = 0; + break; + case KeyEvent.VK_RIGHT: + this.x++; + if ((this.x+width)>=xMax && (type!=RECTANGLE||clipboard==null)) + this.x = xMax-width; + break; + } + updateClipRect(); + if (type==POINT) + imp.draw(); + else + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = this.x; oldY = this.y; + bounds = null; + setLocation(this.x, this.y); + showStatus(); + notifyListeners(RoiListener.MOVED); + } + + /** Nudge lower right corner of rectangular and oval ROIs by + one pixel based on arrow key press. */ + public void nudgeCorner(int key) { + if (type>OVAL || clipboard!=null) + return; + switch(key) { + case KeyEvent.VK_UP: + height--; + if (height<1) height = 1; + notifyListeners(RoiListener.MODIFIED); + break; + case KeyEvent.VK_DOWN: + height++; + if ((y+height) > yMax) height = yMax-y; + notifyListeners(RoiListener.MODIFIED); + break; + case KeyEvent.VK_LEFT: + width--; + if (width<1) width = 1; + notifyListeners(RoiListener.MODIFIED); + break; + case KeyEvent.VK_RIGHT: + width++; + if ((x+width) > xMax) width = xMax-x; + notifyListeners(RoiListener.MODIFIED); + break; + } + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + oldX = x; oldY = y; + cachedMask = null; + showStatus(); + notifyListeners(RoiListener.MOVED); + } + + // Finds the union of current and previous roi + protected void updateClipRect() { + clipX = (x<=oldX)?x:oldX; + clipY = (y<=oldY)?y:oldY; + clipWidth = ((x+width>=oldX+oldWidth)?x+width:oldX+oldWidth) - clipX + 1; + clipHeight = ((y+height>=oldY+oldHeight)?y+height:oldY+oldHeight) - clipY + 1; + int handleSize = getHandleSize(); + double mag = ic!=null?ic.getMagnification():1; + int m = mag<1.0?(int)(handleSize/mag):handleSize; + m += clipRectMargin(); + double strokeWidth = getStrokeWidth(); + if (strokeWidth==0.0) + strokeWidth = defaultStrokeWidth(); + m = (int)(m+strokeWidth*2); + clipX-=m; clipY-=m; + clipWidth+=m*2; clipHeight+=m*2; + } + + protected int clipRectMargin() { + return 0; + } + + protected void handleMouseDrag(int sx, int sy, int flags) { + if (ic==null) return; + constrain = (flags&Event.SHIFT_MASK)!=0; + center = (flags&Event.CTRL_MASK)!=0 || (IJ.isMacintosh()&&(flags&Event.META_MASK)!=0); + aspect = (flags&Event.ALT_MASK)!=0; + switch(state) { + case CONSTRUCTING: + grow(sx, sy); + break; + case MOVING: + move(sx, sy); + break; + case MOVING_HANDLE: + moveHandle(sx, sy); + break; + default: + break; + } + notifyListeners(state==MOVING?RoiListener.MOVED:RoiListener.MODIFIED); + } + + public void draw(Graphics g) { + Color color = strokeColor!=null?strokeColor:ROIColor; + if (fillColor!=null) color = fillColor; + if (Interpreter.isBatchMode() && imp!=null && imp.getOverlay()!=null && strokeColor==null && fillColor==null) + return; + g.setColor(color); + mag = getMagnification(); + int sw = (int)(width*mag); + int sh = (int)(height*mag); + int sx1 = screenX(x); + int sy1 = screenY(y); + if (subPixelResolution() && bounds!=null) { + sw = (int)(bounds.width*mag); + sh = (int)(bounds.height*mag); + sx1 = screenXD(bounds.x); + sy1 = screenYD(bounds.y); + } + int sx2 = sx1+sw/2; + int sy2 = sy1+sh/2; + int sx3 = sx1+sw; + int sy3 = sy1+sh; + Graphics2D g2d = (Graphics2D)g; + if (stroke!=null) + g2d.setStroke(getScaledStroke()); + setRenderingHint(g2d); + if (cornerDiameter>0) { + int sArcSize = (int)Math.round(cornerDiameter*mag); + if (fillColor!=null) + g.fillRoundRect(sx1, sy1, sw, sh, sArcSize, sArcSize); + else + g.drawRoundRect(sx1, sy1, sw, sh, sArcSize, sArcSize); + } else { + if (fillColor!=null) { + if (!overlay && isActiveOverlayRoi()) { + g.setColor(Color.cyan); + g.drawRect(sx1, sy1, sw, sh); + } else { + if (!(this instanceof TextRoi)) + g.fillRect(sx1, sy1, sw, sh); + else + g.drawRect(sx1, sy1, sw, sh); + } + } else + g.drawRect(sx1, sy1, sw, sh); + } + if (clipboard==null && !overlay) { + drawHandle(g, sx1, sy1); + drawHandle(g, sx2, sy1); + drawHandle(g, sx3, sy1); + drawHandle(g, sx3, sy2); + drawHandle(g, sx3, sy3); + drawHandle(g, sx2, sy3); + drawHandle(g, sx1, sy3); + drawHandle(g, sx1, sy2); + } + drawPreviousRoi(g); + if (state!=NORMAL) + showStatus(); + if (updateFullWindow) + {updateFullWindow = false; imp.draw();} + } + + public void drawOverlay(Graphics g) { + overlay = true; + draw(g); + overlay = false; + } + + void drawPreviousRoi(Graphics g) { + if (previousRoi!=null && previousRoi!=this && previousRoi.modState!=NO_MODS) { + if (type!=POINT && previousRoi.getType()==POINT && previousRoi.modState!=SUBTRACT_FROM_ROI) + return; + previousRoi.setImage(imp); + previousRoi.draw(g); + } + } + + private static double defaultStrokeWidth() { + double defaultWidth = defaultStrokeWidth; + double guiScale = Prefs.getGuiScale(); + if (defaultWidth<=1 && guiScale>1.0) { + defaultWidth = guiScale; + if (defaultWidth<1.5) defaultWidth = 1.5; + } + return defaultWidth; + } + + /** Returns the current handle size. */ + public int getHandleSize() { + if (handleSize>=0) + return handleSize; + else + return getDefaultHandleSize(); + } + + /** Sets the current handle size. */ + public void setHandleSize(int size) { + if (size>=0 && ((size&1)==0)) + size++; // add 1 if odd + handleSize = size; + if (imp!=null) + imp.draw(); + } + + /** Returns the default handle size. */ + public static int getDefaultHandleSize() { + if (defaultHandleSize>0) + return defaultHandleSize; + double defaultWidth = defaultStrokeWidth(); + int size = 7; + if (defaultWidth>1.5) size=9; + if (defaultWidth>=3) size=11; + if (defaultWidth>=4) size=13; + if (defaultWidth>=5) size=15; + if (defaultWidth>=11) size=(int)defaultWidth; + defaultHandleSize = size; + return defaultHandleSize; + } + + public static void resetDefaultHandleSize() { + defaultHandleSize = 0; + } + + void drawHandle(Graphics g, int x, int y) { + int threshold1 = 7500; + int threshold2 = 1500; + double size = (this.width*this.height)*this.mag*this.mag; + if (this instanceof Line) { + size = ((Line)this).getLength()*this.mag; + threshold1 = 150; + threshold2 = 50; + } else { + if (state==CONSTRUCTING && !(type==RECTANGLE||type==OVAL)) + size = threshold1 + 1; + } + int width = 7; + int x0=x, y0=y; + if (size>threshold1) { + x -= 3; + y -= 3; + } else if (size>threshold2) { + x -= 2; + y -= 2; + width = 5; + } else { + x--; y--; + width = 3; + } + int inc = getHandleSize() - 7; + width += inc; + x -= inc/2; + y -= inc/2; + g.setColor(Color.black); + if (width<3) { + g.fillRect(x0,y0,1,1); + return; + } + g.fillRect(x++,y++,width,width); + g.setColor(handleColor); + width -= 2; + g.fillRect(x,y,width,width); + } + + /** + * @deprecated + * replaced by drawPixels(ImageProcessor) + */ + public void drawPixels() { + if (imp!=null) + drawPixels(imp.getProcessor()); + } + + /** Draws the selection outline on the specified ImageProcessor. + @see ij.process.ImageProcessor#setColor + @see ij.process.ImageProcessor#setLineWidth + */ + public void drawPixels(ImageProcessor ip) { + endPaste(); + int saveWidth = ip.getLineWidth(); + if (getStrokeWidth()>1f) + ip.setLineWidth((int)Math.round(getStrokeWidth())); + if (cornerDiameter>0) + drawRoundedRect(ip); + else { + if (ip.getLineWidth()==1) + ip.drawRect(x, y, width+1, height+1); + else + ip.drawRect(x, y, width, height); + } + ip.setLineWidth(saveWidth); + if (Line.getWidth()>1 || getStrokeWidth()>1) + updateFullWindow = true; + } + + private void drawRoundedRect(ImageProcessor ip) { + int margin = (int)getStrokeWidth()/2; + BufferedImage bi = new BufferedImage(width+margin*2+1, height+margin*2+1, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g = bi.createGraphics(); + if (stroke!=null) + g.setStroke(stroke); + g.drawRoundRect(margin, margin, width, height, cornerDiameter, cornerDiameter); + ByteProcessor mask = new ByteProcessor(bi); + ip.setRoi(x-margin, y-margin, width+margin*2+1, height+margin*2+1); + ip.fill(mask); + } + + /** Returns whether the center of pixel (x,y) is contained in the Roi. + * The position of a pixel center determines whether a pixel is selected. + * Points exactly at the left (right) border are considered outside (inside); + * points exactly on horizontal borders are considered outside (inside) at the border + * with the lower (higher) y. This convention is opposite to that of the java.awt.Shape class. */ + public boolean contains(int x, int y) { + Rectangle r = new Rectangle(this.x, this.y, width, height); + boolean contains = r.contains(x, y); + if (cornerDiameter==0 || contains==false) + return contains; + RoundRectangle2D rr = new RoundRectangle2D.Double(this.x, this.y, width, height, cornerDiameter, cornerDiameter); + return rr.contains(x+0.4999, y+0.4999); + } + + /** Returns whether coordinate (x,y) is contained in the Roi. + * Note that the coordinate (0,0) is the top-left corner of pixel (0,0). + * Use contains(int, int) to determine whether a given pixel is contained in the Roi. */ + public boolean containsPoint(double x, double y) { + boolean contains = false; + if (bounds == null) + contains = x>=this.x && y>=this.y && x1280?5:3; + int size = getHandleSize()+margin; + int halfSize = size/2; + double x = getXBase(); + double y = getYBase(); + double width = getFloatWidth(); + double height = getFloatHeight(); + int sx1 = screenXD(x) - halfSize; + int sy1 = screenYD(y) - halfSize; + int sx3 = screenXD(x+width) - halfSize; + int sy3 = screenYD(y+height) - halfSize; + int sx2 = sx1 + (sx3 - sx1)/2; + int sy2 = sy1 + (sy3 - sy1)/2; + if (sx>=sx1&&sx<=sx1+size&&sy>=sy1&&sy<=sy1+size) return 0; + if (sx>=sx2&&sx<=sx2+size&&sy>=sy1&&sy<=sy1+size) return 1; + if (sx>=sx3&&sx<=sx3+size&&sy>=sy1&&sy<=sy1+size) return 2; + if (sx>=sx3&&sx<=sx3+size&&sy>=sy2&&sy<=sy2+size) return 3; + if (sx>=sx3&&sx<=sx3+size&&sy>=sy3&&sy<=sy3+size) return 4; + if (sx>=sx2&&sx<=sx2+size&&sy>=sy3&&sy<=sy3+size) return 5; + if (sx>=sx1&&sx<=sx1+size&&sy>=sy3&&sy<=sy3+size) return 6; + if (sx>=sx1&&sx<=sx1+size&&sy>=sy2&&sy<=sy2+size) return 7; + return -1; + } + + protected void mouseDownInHandle(int handle, int sx, int sy) { + state = MOVING_HANDLE; + previousSX = sx; + previousSY = sy; + activeHandle = handle; + } + + protected void handleMouseDown(int sx, int sy) { + if (state==NORMAL && ic!=null) { + state = MOVING; + previousSX = sx; + previousSY = sy; + startX = offScreenX(sx); + startY = offScreenY(sy); + startXD = offScreenXD(sx); + startYD = offScreenYD(sy); + } + } + + protected void handleMouseUp(int screenX, int screenY) { + state = NORMAL; + if (imp==null) return; + imp.draw(clipX-5, clipY-5, clipWidth+10, clipHeight+10); + if (Recorder.record) { + String method; + if (type==OVAL) + Recorder.record("makeOval", x, y, width, height); + else if (!(this instanceof TextRoi)) { + if (cornerDiameter==0) + Recorder.record("makeRectangle", x, y, width, height); + else { + if (Recorder.scriptMode()) + Recorder.recordCall("imp.setRoi(new Roi("+x+","+y+","+width+","+height+","+cornerDiameter+"));"); + else + Recorder.record("makeRectangle", x, y, width, height, cornerDiameter); + } + } + } + if (Toolbar.getToolId()==Toolbar.OVAL&&Toolbar.getBrushSize()>0) { + int flags = ic!=null?ic.getModifiers():16; + if ((flags&16)==0) // erase ROI Brush + {imp.draw(); return;} + } + modifyRoi(); + } + + void modifyRoi() { + if (previousRoi==null || previousRoi.modState==NO_MODS || imp==null) + return; + if (type==POINT || previousRoi.getType()==POINT) { + if (type==POINT && previousRoi.getType()==POINT) { + addPoint(); + } else if (isArea() && previousRoi.getType()==POINT && previousRoi.modState==SUBTRACT_FROM_ROI) + subtractPoints(); + return; + } + Roi previous = (Roi)previousRoi.clone(); + previous.modState = NO_MODS; + ShapeRoi s1 = null; + ShapeRoi s2 = null; + if (previousRoi instanceof ShapeRoi) + s1 = (ShapeRoi)previousRoi; + else + s1 = new ShapeRoi(previousRoi); + if (this instanceof ShapeRoi) + s2 = (ShapeRoi)this; + else + s2 = new ShapeRoi(this); + if (previousRoi.modState==ADD_TO_ROI) + s1.or(s2); + else + s1.not(s2); + previousRoi.modState = NO_MODS; + Roi roi2 = s1.trySimplify(); + if (roi2 == null) return; + if (roi2!=null) + roi2.copyAttributes(previousRoi); + imp.setRoi(roi2); + setPreviousRoi(previous); + } + + void addPoint() { + if (!(type==POINT && previousRoi.getType()==POINT)) { + modState = NO_MODS; + imp.draw(); + return; + } + previousRoi.modState = NO_MODS; + PointRoi p1 = (PointRoi)previousRoi; + FloatPolygon poly = getFloatPolygon(); + p1.addPoint(imp, poly.xpoints[0], poly.ypoints[0]); + imp.setRoi(p1); + } + + void subtractPoints() { + previousRoi.modState = NO_MODS; + PointRoi p1 = (PointRoi)previousRoi; + PointRoi p2 = p1.subtractPoints(this); + if (p2!=null) + imp.setRoi(p1.subtractPoints(this)); + else + imp.deleteRoi(); + } + + /** If 'add' is true, adds this selection to the previous one. If 'subtract' is true, subtracts + it from the previous selection. Called by the IJ.doWand() method, and the makeRectangle(), + makeOval(), makePolygon() and makeSelection() macro functions. */ + public void update(boolean add, boolean subtract) { + if (previousRoi==null) return; + if (add) { + previousRoi.modState = ADD_TO_ROI; + modifyRoi(); + } else if (subtract) { + previousRoi.modState = SUBTRACT_FROM_ROI; + modifyRoi(); + } else + previousRoi.modState = NO_MODS; + } + + public void showStatus() { + if (imp==null) + return; + String value; + if (state!=CONSTRUCTING && (type==RECTANGLE||type==POINT) && width<=25 && height<=25) { + ImageProcessor ip = imp.getProcessor(); + double v = ip.getPixelValue(this.x,this.y); + int digits = (imp.getType()==ImagePlus.GRAY8||imp.getType()==ImagePlus.GRAY16)?0:2; + value = ", value="+IJ.d2s(v,digits); + } else + value = ""; + Calibration cal = imp.getCalibration(); + String size; + if (cal.scaled()) + size = ", w="+IJ.d2s(width*cal.pixelWidth)+" ("+width+"), h="+IJ.d2s(height*cal.pixelHeight)+" ("+height+")"; + else + size = ", w="+width+", h="+height; + IJ.showStatus(imp.getLocationAsString(this.x,this.y)+size+value); + } + + /** Always returns null for rectangular Roi's */ + public ImageProcessor getMask() { + if (cornerDiameter>0) + return (new ShapeRoi(new RoundRectangle2D.Float(x, y, width, height, cornerDiameter, cornerDiameter))).getMask(); + else + return null; + } + + public void startPaste(ImagePlus clipboard) { + IJ.showStatus("Pasting..."); + IJ.wait(10); + this.clipboard = clipboard; + imp.getProcessor().snapshot(); + updateClipRect(); + imp.draw(clipX, clipY, clipWidth, clipHeight); + } + + void updatePaste() { + if (clipboard!=null) { + imp.getMask(); + ImageProcessor ip = imp.getProcessor(); + ip.reset(); + int xoffset=0, yoffset=0; + Roi croi = clipboard.getRoi(); + if (croi!=null) { + Rectangle r = croi.getBounds(); + if (r.x<0) xoffset=-r.x; + if (r.y<0) yoffset=-r.y; + } + ip.copyBits(clipboard.getProcessor(), x+xoffset, y+yoffset, pasteMode); + if (type!=RECTANGLE) + ip.reset(ip.getMask()); + if (ic!=null) + ic.setImageUpdated(); + } + } + + public void endPaste() { + if (clipboard!=null) { + updatePaste(); + clipboard = null; + Undo.setup(Undo.FILTER, imp); + } + activeOverlayRoi = false; + } + + public void abortPaste() { + clipboard = null; + imp.getProcessor().reset(); + imp.updateAndDraw(); + } + + /** Returns the default stroke width. */ + public static double getDefaultStrokeWidth() { + return defaultStrokeWidth; + } + + /** Sets the default stroke width. */ + public static void setDefaultStrokeWidth(double width) { + defaultStrokeWidth = width<0.0?0.0:width; + resetDefaultHandleSize(); + } + + /** Returns the group value assigned to newly created ROIs. */ + public static int getDefaultGroup() { + return defaultGroup; + } + + /** Sets the group value assigned to newly created ROIs, and also + * sets the default ROI color to the group color. Set to zero to not + * have a default group and to use the default ROI color. + * @see #setGroup + * @see #getGroup + * @see #getGroupColor + */ + public static void setDefaultGroup(int group) { + if (group<0 || group>255) + throw new IllegalArgumentException("Invalid group: "+group); + defaultGroup = group; + groupColor = getGroupColor(group); + } + + /** Returns the group attribute of this ROI. */ + public int getGroup() { + return this.group; + } + + /** Returns the group name associtated with the specified group. */ + public static String getGroupName(int groupNumber) { + if (groupNumber<1 || groupNumber>255) + return null; + if (groupNames==null && groupNamesString==null) + return null; + if (groupNames==null) + groupNames = groupNamesString.split(","); + if (groupNumber>groupNames.length) + return null; + String name = groupNames[groupNumber-1]; + if (name==null) + return null; + return name.length()>0?name:null; + } + + public static synchronized void setGroupName(int groupNumber, String name) { + if (groupNumber<1 || groupNumber>255) + return; + if (groupNamesString==null && groupNames==null) + groupNames = new String[groupNumber]; + if (groupNames==null) + groupNames = groupNamesString.split(","); + if (groupNumber>groupNames.length) { + String[] temp = new String[groupNumber]; + for (int i=0; i255) + throw new IllegalArgumentException("Invalid group: "+group); + if (group>0) + setStrokeColor(getGroupColor(group)); + if (group==0 && this.group>0) + setStrokeColor(null); + this.group = group; + if (imp!=null) // Update Roi Color in the GUI + imp.draw(); + } + + /** Retrieves color associated to a given roi group. */ + private static Color getGroupColor(int group) { + Color color = ROIColor; // default ROI color + if (group>0) { // read Glasbey Lut + if (glasbeyLut==null) { + String path = IJ.getDir("luts")+"Glasbey.lut"; + glasbeyLut = LutLoader.openLut("noerror:"+path); + if (glasbeyLut==null) + IJ.log("LUT not found: "+path); + } + if (glasbeyLut!=null) + color = new Color(glasbeyLut.getRGB(group)); + } + return color; + } + + /** Returns the angle in degrees between the specified line and a horizontal line. */ + public double getAngle(int x1, int y1, int x2, int y2) { + return getFloatAngle(x1, y1, x2, y2); + } + + /** Returns the angle in degrees between the specified line and a horizontal line. */ + public double getFloatAngle(double x1, double y1, double x2, double y2) { + double dx = x2-x1; + double dy = y1-y2; + if (imp!=null && !IJ.altKeyDown()) { + Calibration cal = imp.getCalibration(); + dx *= cal.pixelWidth; + dy *= cal.pixelHeight; + } + return (180.0/Math.PI)*Math.atan2(dy, dx); + } + + /** Sets the default (global) color used for ROI outlines. + * @see #getColor() + * @see #setStrokeColor(Color) + */ + public static void setColor(Color c) { + ROIColor = c; + } + + /** Returns the default (global) color used for drawing ROI outlines. + * @see #setColor(Color) + * @see #getStrokeColor() + */ + public static Color getColor() { + return ROIColor; + } + + /** Sets the color used by this ROI to draw its outline. This color, if not null, + * overrides the global color set by the static setColor() method. + * @see #getStrokeColor + * @see #setStrokeWidth + * @see ij.ImagePlus#setOverlay(ij.gui.Overlay) + */ + public void setStrokeColor(Color c) { + strokeColor = c; + } + + /** Returns the the color used to draw the ROI outline or null if the default color is being used. + * @see #setStrokeColor(Color) + */ + public Color getStrokeColor() { + return strokeColor; + } + + /** Sets the default stroke color. */ + public static void setDefaultColor(Color color) { + defaultColor = color; + } + + /** Sets the fill color used to display this ROI, or set to null to display it transparently. + * @see #getFillColor + * @see #setStrokeColor + */ + public void setFillColor(Color color) { + fillColor = color; + } + + /** Returns the fill color used to display this ROI, or null if it is displayed transparently. + * @see #setFillColor + * @see #getStrokeColor + */ + public Color getFillColor() { + return fillColor; + } + + public static void setDefaultFillColor(Color color) { + defaultFillColor = color; + } + + public static Color getDefaultFillColor() { + return defaultFillColor; + } + + public void setAntiAlias(boolean antiAlias) { + this.antiAlias = antiAlias; + } + + public boolean getAntiAlias() { + return antiAlias; + } + + protected void setRenderingHint(Graphics2D g2d) { + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + antiAlias?RenderingHints.VALUE_ANTIALIAS_ON:RenderingHints.VALUE_ANTIALIAS_OFF); + } + + /** Copy the attributes (outline color, fill color, outline width) + of 'roi2' to the this selection. */ + public void copyAttributes(Roi roi2) { + this. strokeColor = roi2. strokeColor; + this.fillColor = roi2.fillColor; + this.setStrokeWidth(roi2.getStrokeWidth()); + this.setName(roi2.getName()); + this.group = roi2.group; + } + + /** + * @deprecated + * replaced by setStrokeColor() + */ + public void setInstanceColor(Color c) { + strokeColor = c; + } + + /** + * @deprecated + * replaced by setStrokeWidth(int) + */ + public void setLineWidth(int width) { + setStrokeWidth(width) ; + } + + public void updateWideLine(float width) { + if (isLine()) { + wideLine = true; + setStrokeWidth(width); + if (getStrokeColor()==null) { + Color c = getColor(); + setStrokeColor(new Color(c.getRed(),c.getGreen(),c.getBlue(), 77)); + } + } + } + + /** Set 'nonScalable' true to have TextRois in a display + list drawn at a fixed location and size. */ + public void setNonScalable(boolean nonScalable) { + this.nonScalable = nonScalable; + } + + /** Sets the width of the line used to draw this ROI. Set + * the width to 0.0 and the ROI will be drawn using a + * a 1 pixel stroke width regardless of the magnification. + * @see #setDefaultStrokeWidth(double) + * @see #setStrokeColor(Color) + * @see ij.ImagePlus#setOverlay(ij.gui.Overlay) + */ + public void setStrokeWidth(float strokeWidth) { + if (strokeWidth<0f) + strokeWidth = 0f; + if (strokeWidth==0f && usingDefaultStroke) + return; + if (strokeWidth>0f) { + scaleStrokeWidth = true; + usingDefaultStroke = false; + } + boolean notify = listeners.size()>0 && isLine() && getStrokeWidth()!=strokeWidth; + if (strokeWidth==0f) + this.stroke = null; + else if (wideLine) + this.stroke = new BasicStroke(strokeWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); + else + this.stroke = new BasicStroke(strokeWidth); + if (strokeWidth>1f) + fillColor = null; + if (notify) + notifyListeners(RoiListener.MODIFIED); + } + + /** This is a version of setStrokeWidth() that accepts a double argument. */ + public void setStrokeWidth(double strokeWidth) { + setStrokeWidth((float)strokeWidth); + } + + public void setUnscalableStrokeWidth(double strokeWidth) { + setStrokeWidth((float)strokeWidth); + scaleStrokeWidth = false; + + } + + /** Returns the lineWidth. */ + public float getStrokeWidth() { + return (stroke!=null&&!usingDefaultStroke)?stroke.getLineWidth():0f; + } + + /** Sets the Stroke used to draw this ROI. */ + public void setStroke(BasicStroke stroke) { + this.stroke = stroke; + if (stroke!=null) + usingDefaultStroke = false; + } + + /** Returns the Stroke used to draw this ROI, or null if no Stroke is used. */ + public BasicStroke getStroke() { + if (usingDefaultStroke) + return null; + else + return stroke; + } + + /** Returns 'true' if the stroke width is scaled as images are zoomed. */ + public boolean getScaleStrokeWidth() { + return scaleStrokeWidth; + } + + protected BasicStroke getScaledStroke() { + if (ic==null || usingDefaultStroke || !scaleStrokeWidth) + return stroke; + double mag = ic.getMagnification(); + if (mag!=1.0) { + float width = (float)(stroke.getLineWidth()*mag); + //return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); + return new BasicStroke(width, stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(), stroke.getDashArray(), stroke.getDashPhase()); + } else + return stroke; + } + + /** Returns the name of this ROI, or null. */ + public String getName() { + return name; + } + + /** Sets the name of this ROI. */ + public void setName(String name) { + this.name = name; + } + + /** Sets the Paste transfer mode. + @see ij.process.Blitter + */ + public static void setPasteMode(int transferMode) { + if (transferMode==pasteMode) return; + pasteMode = transferMode; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) + imp.updateAndDraw(); + } + + /** Sets the rounded rectangle corner diameter (pixels). */ + public void setCornerDiameter(int cornerDiameter) { + if (cornerDiameter<0) cornerDiameter = 0; + this.cornerDiameter = cornerDiameter; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null && this==imp.getRoi()) + imp.updateAndDraw(); + } + + /** Returns the rounded rectangle corner diameter (pixels). */ + public int getCornerDiameter() { + return cornerDiameter; + } + + /** Obsolete; replaced by setCornerDiameter(). */ + public void setRoundRectArcSize(int cornerDiameter) { + setCornerDiameter(cornerDiameter); + } + + /** Obsolete; replaced by getCornerDiameter(). */ + public int getRoundRectArcSize() { + return cornerDiameter; + } + + /** Sets the stack position (image number) of this ROI. In an overlay, this + * ROI is only displayed when the stack is at the specified position. + * Set to zero to have the ROI displayed on all images in the stack. + * @see ij.gui.Overlay + */ + public void setPosition(int n) { + if (n<0) n=0; + position = n; + channel = slice = frame = 0; + hyperstackPosition = false; + } + + /** Returns the stack position (image number) of this ROI, or + * zero if the ROI is not associated with a particular stack image. + * @see ij.gui.Overlay + */ + public int getPosition() { + return position; + } + + /** Sets the hyperstack position of this ROI. In an overlay, this + * ROI is only displayed when the hyperstack is at the specified position. + * @see ij.gui.Overlay + */ + public void setPosition(int channel, int slice, int frame) { + if (channel<0) channel=0; + this.channel = channel; + if (slice<0) slice=0; + this.slice = slice; + if (frame<0) frame=0; + this.frame = frame; + position = 0; + hyperstackPosition = true; + } + + /** Returns 'true' if setPosition(C,Z,T) has been called. */ + public boolean hasHyperStackPosition() { + return hyperstackPosition; + } + + /** Sets the position of this ROI based on the stack position of the specified image. */ + public void setPosition(ImagePlus imp ) { + if (imp==null) + return; + if (imp.isHyperStack()) { + int channel = imp.getDisplayMode()==IJ.COMPOSITE?0:imp.getChannel(); + setPosition(channel, imp.getSlice(), imp.getFrame()); + } else if (imp.getStackSize()>1) + setPosition(imp.getCurrentSlice()); + else + setPosition(0); + } + + /** Returns the channel position of this ROI, or zero + * if this ROI is not associated with a particular channel. + */ + public final int getCPosition() { + return channel; + } + + /** Returns the slice position of this ROI, or zero + * if this ROI is not associated with a particular slice. + */ + public final int getZPosition() { + return slice==0&&!hyperstackPosition?position:slice; + } + + /** Returns the frame position of this ROI, or zero + * if this ROI is not associated with a particular frame. + */ + public final int getTPosition() { + return frame; + } + + // Used by the FileSaver and RoiEncoder to save overlay settings. */ + public void setPrototypeOverlay(Overlay overlay) { + prototypeOverlay = new Overlay(); + prototypeOverlay.drawLabels(overlay.getDrawLabels()); + prototypeOverlay.drawNames(overlay.getDrawNames()); + prototypeOverlay.drawBackgrounds(overlay.getDrawBackgrounds()); + prototypeOverlay.setLabelColor(overlay.getLabelColor()); + prototypeOverlay.setLabelFont(overlay.getLabelFont(), overlay.scalableLabels()); + } + + // Used by the FileOpener and RoiDecoder to restore overlay settings. */ + public Overlay getPrototypeOverlay() { + if (prototypeOverlay!=null) + return prototypeOverlay; + else + return new Overlay(); + } + + /** Returns the current paste transfer mode, or NOT_PASTING (-1) + if no paste operation is in progress. + @see ij.process.Blitter + */ + public int getPasteMode() { + if (clipboard==null) + return NOT_PASTING; + else + return pasteMode; + } + + /** Returns the current paste transfer mode. */ + public static int getCurrentPasteMode() { + return pasteMode; + } + + /** Returns 'true' if this is an area selection. */ + public boolean isArea() { + return (type>=RECTANGLE && type<=TRACED_ROI) || type==COMPOSITE; + } + + /** Returns 'true' if this is a line selection. */ + public boolean isLine() { + return type>=LINE && type<=FREELINE; + } + + + /** Return 'true' if this is a line or point selection. */ + protected boolean isLineOrPoint() { + return isLine() || type==POINT; + } + + /** Returns 'true' if this is an ROI primarily used from drawing + (e.g., TextRoi or Arrow). */ + public boolean isDrawingTool() { + //return cornerDiameter>0; + return false; + } + + protected double getMagnification() { + return ic!=null?ic.getMagnification():1.0; + } + + /** Convenience method that converts Roi type to a human-readable form. */ + public String getTypeAsString() { + String s=""; + switch(type) { + case POLYGON: s="Polygon"; + if (this instanceof EllipseRoi) s="Ellipse"; + if (this instanceof RotatedRectRoi) s="Rotated Rectangle"; + break; + case FREEROI: s="Freehand"; break; + case TRACED_ROI: s="Traced"; break; + case POLYLINE: s="Polyline"; break; + case FREELINE: s="Freeline"; break; + case ANGLE: s="Angle"; break; + case LINE: s=this instanceof Arrow ? "Arrow" : "Straight Line"; break; + case OVAL: s="Oval"; break; + case COMPOSITE: s = "Composite"; break; + case POINT: s="Point"; break; + default: + if (this instanceof TextRoi) + s = "Text"; + else if (this instanceof ImageRoi) + s = "Image"; + else + s = "Rectangle"; + break; + } + return s; + } + + /** Returns true if this ROI is currently displayed on an image. */ + public boolean isVisible() { + return ic!=null; + } + + /** Returns true if this is a slection that supports sub-pixel resolution. */ + public boolean subPixelResolution() { + return subPixel; + } + + /** @deprecated Drawoffset is not used any more. */ + @Deprecated + public boolean getDrawOffset() { + return false; + } + + /** @deprecated This method was previously used to draw lines and polylines shifted + * by 0.5 pixels top the bottom and right, for better agreement with the + * position used by ProfilePlot, with the default taken from + * Prefs.subPixelResolution. Now the shift is independent of this + * setting and only depends on the ROI type (area or line/point ROI). */ + @Deprecated + public void setDrawOffset(boolean drawOffset) { + } + + public void setIgnoreClipRect(boolean ignoreClipRect) { + this.ignoreClipRect = ignoreClipRect; + } + + /** Returns 'true' if this ROI is displayed and is also in an overlay. */ + public final boolean isActiveOverlayRoi() { + if (imp==null || this!=imp.getRoi()) + return false; + Overlay overlay = imp.getOverlay(); + if (overlay!=null && overlay.contains(this)) + return true; + ImageCanvas ic = imp.getCanvas(); + overlay = ic!=null?ic.getShowAllList():null; // ROI Manager overlay + return overlay!=null && overlay.contains(this); + } + + /** Checks whether two rectangles are equal. */ + public boolean equals(Object obj) { + if (obj instanceof Roi) { + Roi roi2 = (Roi)obj; + if (type!=roi2.getType()) return false; + if (!getBounds().equals(roi2.getBounds())) return false; + if (getLength()!=roi2.getLength()) return false; + return true; + } else + return false; + } + + /** Converts image canvas screen x coordinates to integer offscreen image pixel + * coordinates, depending on whether this roi uses the line or area convention + * for coordinates. */ + protected int offScreenX(int sx) { + if (ic == null) return sx; + return useLineSubpixelConvention() ? ic.offScreenX(sx) : ic.offScreenX2(sx); + } + + /** Converts image canvas screen y coordinates to integer offscreen image pixel + * coordinates, depending on whether this roi uses the line or area convention + * for coordinates. */ + protected int offScreenY(int sy) { + if (ic == null) return sy; + return useLineSubpixelConvention() ? ic.offScreenY(sy) : ic.offScreenY2(sy); + } + + /** Converts image canvas screen x coordinates to floating-point offscreen image pixel + * coordinates, depending on whether this roi uses the line or area convention + * for coordinates. */ + protected double offScreenXD(int sx) { + if (ic == null) return sx; + double offScreenValue = ic.offScreenXD(sx); + if (useLineSubpixelConvention()) + offScreenValue -= 0.5; + return offScreenValue; + } + + /** Converts image canvas screen y coordinates to floating-point offscreen image pixel + * coordinates, depending on whether this roi uses the line or area convention + * for coordinates. */ + protected double offScreenYD(int sy) { + if (ic == null) return sy; + double offScreenValue = ic.offScreenYD(sy); + if (useLineSubpixelConvention()) + offScreenValue -= 0.5; + return offScreenValue; + } + + /** Returns 'true' if this ROI uses for drawing the convention for + * line and point ROIs, where the coordinates are with respect + * to the pixel center. + * Returns false for area rois, which have coordinates with respect to + * the upper left corners of the pixels */ + protected boolean useLineSubpixelConvention() { + return isLineOrPoint(); + } + + /** Returns whether a roi created interactively should have subpixel resolution, + * (if the roi type supports it), i.e., whether the magnification is high enough */ + protected boolean magnificationForSubPixel() { + return magnificationForSubPixel(getMagnification()); + } + + protected static boolean magnificationForSubPixel(double magnification) { + return magnification > 1.5; + } + + /**Converts an image pixel x (offscreen)coordinate to a screen x coordinate, + * taking the the line or area convention for coordinates into account */ + protected int screenXD(double ox) { + if (ic == null) return (int)ox; + if (useLineSubpixelConvention()) ox += 0.5; + return ic.screenXD(ox); + } + + /**Converts an image pixel y (offscreen)coordinate to a screen y coordinate, + * taking the the line or area convention for coordinates into account */ + protected int screenYD(double oy) { + if (ic == null) return (int)oy; + if (useLineSubpixelConvention()) oy += 0.5; + return ic.screenYD(oy); + } + + protected int screenX(int ox) {return screenXD(ox);} + protected int screenY(int oy) {return screenYD(oy);} + + /** Converts a float array to an int array using truncation. */ + public static int[] toInt(float[] arr) { + return toInt(arr, null, arr.length); + } + + public static int[] toInt(float[] arr, int[] arr2, int size) { + int n = arr.length; + if (size>n) size=n; + int[] temp = arr2; + if (temp==null || temp.length + * Author: Peter Haub (phaub at dipsystems.de) + */ + public double[] getContourCentroid() { + double xC=0, yC=0, lSum=0, x, y, dx, dy, l; + FloatPolygon poly = getFloatPolygon(); + int nPoints = poly.npoints; + int n2 = nPoints-1; + for (int n1=0; n1 + * Author: Michael Schmid + */ + public static Roi convertLineToArea(Roi line) { + if (line==null || !line.isLine()) + throw new IllegalArgumentException("Line selection required"); + double lineWidth = line.getStrokeWidth(); + Roi roi2 = null; + if (line.getType()==Roi.LINE) { + if (lineWidth<=1.0) + lineWidth = 1.0000001; + FloatPolygon p = ((Line)line).getFloatPolygon(lineWidth); + roi2 = new PolygonRoi(p, Roi.POLYGON); + line.setStrokeWidth(lineWidth); + } else { + if (lineWidth<1) + lineWidth = 1; + Rectangle bounds = line.getBounds(); + double width = bounds.x+bounds.width + lineWidth; + double height = bounds.y+bounds.height + lineWidth; + ByteProcessor ip = new ByteProcessor((int)Math.round(width), (int)Math.round(height)); + PolygonFiller polygonFiller = new PolygonFiller(); + //ip.setColor(255); + double radius = lineWidth/2.0; + FloatPolygon p = line.getFloatPolygon(); + int n = p.npoints; + float[] xv = new float[4]; //vertex points of rectangle will be filled for each line segment + float[] yv = new float[4]; + float[] xt = new float[3]; //vertex points of triangle will be filled between line segments + float[] yt = new float[3]; + double dx1 = p.xpoints[1]-p.xpoints[0]; + double dy1 = p.ypoints[1]-p.ypoints[0]; + double l = length(dx1, dy1); + dx1 = dx1/l; //unit vector along current line segment + dy1 = dy1/l; + double dx0 = dx1; + double dy0 = dy1; + double xfrom = p.xpoints[0] - 0.5*dx1; + double yfrom = p.ypoints[0] - 0.5*dy1; + //Overlay ovly = new Overlay(); + for (int i=1; i1) { //fill triangle to previous line segment + boolean rightTurn=(dx1*dy0>dx0*dy1); + xt[0] = (float)xfrom; + yt[0] = (float)yfrom; + if (rightTurn) { + xt[1] = (float)(xfrom-radius*dy0); + yt[1] = (float)(yfrom+radius*dx0); + xt[2] = (float)(xfrom-radius*dy1); + yt[2] = (float)(yfrom+radius*dx1); + xt[0] += (float)(0.5*(radius*dy0+radius*dy1)); //extend triangle to avoid missing pixels (due to rounding errors) + yt[0] -= (float)(0.5*(radius*dx0+radius*dx1)); //where it touches a rectangle + } else { + xt[1] = (float)(xfrom+radius*dy0); + yt[1] = (float)(yfrom-radius*dx0); + xt[2] = (float)(xfrom+radius*dy1); + yt[2] = (float)(yfrom-radius*dx1); + xt[0] -= (float)(0.5*(radius*dy0+radius*dy1)); + yt[0] += (float)(0.5*(radius*dx0+radius*dx1)); + } + polygonFiller.setPolygon(xt, yt, 3, 0.5f, 0.5f); + polygonFiller.fillByteProcessorMask(ip); + //ovly.add(new PolygonRoi(xt,yt,Roi.POLYGON)); + } + dx0 = dx1; + dy0 = dy1; + xfrom = xto; + yfrom = yto; + if (i + * for (Point p : roi) { + * // process p + * } + * + * Author: Wilhelm Burger + * @see #getContainedPoints() + * @see #getContainedFloatPoints() + */ + public Iterator iterator() { + // Returns the default (mask-based) point iterator. Note that 'Line' overrides the + // iterator() method and returns a specific point iterator. + return new RoiPointsIteratorMask(); + } + + + /** + * Default iterator over points contained in a mask-backed {@link Roi}. + * Author: W. Burger + */ + private class RoiPointsIteratorMask implements Iterator { + private ImageProcessor mask; + private final Rectangle bounds; + private final int xbase, ybase; + private final int n; + private int next; + + RoiPointsIteratorMask() { + if (isLine()) { + Roi roi2 = Roi.convertLineToArea(Roi.this); + mask = roi2.getMask(); + xbase = roi2.x; + ybase = roi2.y; + } else { + mask = getMask(); + if (mask==null && type==RECTANGLE) { + mask = new ByteProcessor(width, height); + mask.invert(); + } + xbase = Roi.this.x; + ybase = Roi.this.y; + } + bounds = new Rectangle(mask.getWidth(), mask.getHeight()); + n = bounds.width * bounds.height; + findNext(0); // sets next + } + + @Override + public boolean hasNext() { + return next < n; + } + + @Override + public Point next() { + if (next >= n) + throw new NoSuchElementException(); + int x = next % bounds.width; + int y = next / bounds.width; + findNext(next+1); + return new Point(xbase+x, ybase+y); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + // finds the next element (from start), sets next + private void findNext(int start) { + if (mask == null) + next = start; + else { + next = n; + for (int i=start; i0", null, Color.gray); + gd.addDialogListener(this); + gd.showDialog(); + if (gd.wasCanceled()) { + Roi.setGroupNames(groupNames); + Roi.setColor(color); + Roi.setDefaultStrokeWidth(strokeWidth); + Roi.setDefaultGroup(group); + return; + } + if (nameChanges) + Roi.saveGroupNames(); + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + int currentGroup = Roi.getDefaultGroup(); + String cname = gd.getNextChoice(); + Color color = Colors.getColor(cname, Color.yellow); + Roi.setColor(color); + if (color.equals(Roi.getColor())) { + Toolbar.repaintTool(Toolbar.POINT); + Toolbar.repaintTool(Toolbar.WAND); + } + Roi.setDefaultStrokeWidth(gd.getNextNumber()); + int group = (int)gd.getNextNumber(); + Vector stringFields = gd.getStringFields(); + TextField nameField = (TextField)(stringFields.get(0)); + if (group>=0 && group<=255 && group!=currentGroup) { + Roi.setDefaultGroup(group); + String name = getGroupName(group); + nameField.setText(name); + } else { + String name = getGroupName(group); + String name2 = nameField.getText(); + if (name2!=null && !name2.equals(name)) { + Roi.setGroupName(group, name2); + nameChanges = true; + } + } + return true; + } + + private String getGroupName(int group) { + String gname = Roi.getGroupName(group); + if (group==0) + gname = "0 = no group"; + else if (gname==null) + gname = "unnamed"; + return gname; + } + +} diff --git a/src/ij/gui/RoiListener.java b/src/ij/gui/RoiListener.java new file mode 100644 index 0000000..5e847eb --- /dev/null +++ b/src/ij/gui/RoiListener.java @@ -0,0 +1,18 @@ +package ij.gui; +import ij.ImagePlus; + + /** Plugins that implement this interface are notified when + an ROI is created, modified or deleted. The + Plugins/Utilities/Monitor Events command uses this interface. + */ + public interface RoiListener { + public static final int CREATED = 1; + public static final int MOVED = 2; + public static final int MODIFIED = 3; + public static final int EXTENDED = 4; + public static final int COMPLETED = 5; + public static final int DELETED = 6; + + public void roiModified(ImagePlus imp, int id); + +} diff --git a/src/ij/gui/RoiProperties.java b/src/ij/gui/RoiProperties.java new file mode 100644 index 0000000..71cabe8 --- /dev/null +++ b/src/ij/gui/RoiProperties.java @@ -0,0 +1,428 @@ +package ij.gui; +import ij.*; +import ij.plugin.Colors; +import ij.io.RoiDecoder; +import ij.process.*; +import ij.measure.*; +import ij.util.Tools; +import ij.plugin.filter.Analyzer; +import ij.text.TextWindow; +import java.awt.*; +import java.util.*; +import java.awt.event.*; + + + /** Displays a dialog that allows the user to specify ROI properties such as color and line width. */ +public class RoiProperties implements TextListener, WindowListener { + private ImagePlus imp; + private Roi roi; + private Overlay overlay; + private String title; + private boolean showName = true; + private boolean showListCoordinates; + private boolean addToOverlay; + private boolean overlayOptions; + private boolean setPositions; + private boolean listCoordinates; + private boolean listProperties; + private boolean showPointCounts; + private static final String[] justNames = {"Left", "Center", "Right"}; + private int nProperties; + private TextField groupField, colorField; + private Label groupName; + + /** Constructs a ColorChooser using the specified title and initial color. */ + public RoiProperties(String title, Roi roi) { + if (roi==null) + throw new IllegalArgumentException("ROI is null"); + this.title = title; + showName = title.startsWith("Prop"); + showListCoordinates = showName && title.endsWith(" "); + nProperties = showListCoordinates?roi.getPropertyCount():0; + addToOverlay = title.equals("Add to Overlay"); + overlayOptions = title.equals("Overlay Options"); + if (overlayOptions) { + imp = WindowManager.getCurrentImage(); + overlay = imp!=null?imp.getOverlay():null; + setPositions = roi.getPosition()!=0; + } + this.roi = roi; + } + + /** Displays the dialog box and returns 'false' if the user cancels it. */ + public boolean showDialog() { + String name= roi.getName(); + boolean isRange = name!=null && name.startsWith("range:"); + String nameLabel = isRange?"Range:":"Name:"; + if (isRange) name = name.substring(7); + if (name==null) name = ""; + if (!isRange && (roi instanceof ImageRoi) && !overlayOptions) + return showImageDialog(name); + Color strokeColor = roi.getStrokeColor(); + Color fillColor = roi.getFillColor(); + double strokeWidth = roi.getStrokeWidth(); + double strokeWidth2 = strokeWidth; + boolean isText = roi instanceof TextRoi; + boolean isLine = roi.isLine(); + boolean isPoint = roi instanceof PointRoi; + int justification = TextRoi.LEFT; + double angle = 0.0; + boolean antialias = true; + if (isText) { + TextRoi troi = (TextRoi)roi; + Font font = troi.getCurrentFont(); + strokeWidth = font.getSize(); + angle = troi.getAngle(); + justification = troi.getJustification(); + antialias = troi.getAntiAlias(); + } + String position = ""+roi.getPosition(); + if (roi.hasHyperStackPosition()) + position = roi.getCPosition() +","+roi.getZPosition()+","+ roi.getTPosition(); + if (position.equals("0")) + position = "none"; + String group = ""+roi.getGroup(); + if (group.equals("0")) + group = "none"; + String linec = Colors.colorToString(strokeColor); + String fillc = Colors.colorToString(fillColor); + if (IJ.isMacro()) { + fillc = "none"; + setPositions = false; + } + int digits = (int)strokeWidth==strokeWidth?0:1; + GenericDialog gd = new GenericDialog(title); + if (showName) { + gd.addStringField(nameLabel, name, 20); + String label = "Position:"; + ImagePlus imp = WindowManager.getCurrentImage(); + if (position.contains(",") || (imp!=null&&imp.isHyperStack())) + label = "Position (c,s,f):"; + gd.addStringField(label, position); + gd.addStringField("Group:", group); + gd.addToSameRow(); gd.addMessage("wwwwwwwwwwww"); + } + if (isText) { + gd.addStringField("Stroke color:", linec); + gd.addNumericField("Font size:", strokeWidth, digits, 4, "points"); + digits = (int)angle==angle?0:1; + gd.addNumericField("Angle:", angle, digits, 4, "degrees"); + gd.setInsets(0, 0, 0); + gd.addChoice("Justification:", justNames, justNames[justification]); + } else { + if (isPoint) + gd.addStringField("Stroke (point) color:", linec); + else { + gd.addStringField("Stroke color:", linec); + gd.addNumericField("Width:", strokeWidth, digits); + } + } + groupName = (Label)gd.getMessage(); + if (showName && !IJ.isMacro()) { + Vector v = gd.getStringFields(); + groupField = (TextField)v.elementAt(v.size()-2); + groupField.addTextListener(this); + colorField = (TextField)v.elementAt(v.size()-1); + } + + + if (!isLine) { + if (isPoint) { + int index = ((PointRoi)roi).getPointType(); + gd.addChoice("Point type:", PointRoi.types, PointRoi.types[index]); + index = ((PointRoi)roi).getSize(); + gd.addChoice("Size:", PointRoi.sizes, PointRoi.sizes[index]); + } else { + gd.addMessage(""); + gd.addStringField("Fill color:", fillc); + } + } + if (addToOverlay) + gd.addCheckbox("New overlay", false); + if (overlayOptions) { + gd.addCheckbox("Set stack positions", setPositions); + if (overlay!=null) { + int size = overlay.size(); + gd.setInsets(15,20,0); + if (imp!=null && imp.getHideOverlay()) + gd.addMessage("Current overlay is hidden", null, Color.darkGray); + else + gd.addMessage("Current overlay has "+size+" element"+(size>1?"s":""), null, Color.darkGray); + gd.setInsets(0,30,0); + gd.addCheckbox("Apply", false); + gd.setInsets(0,30,0); + gd.addCheckbox("Show labels", overlay.getDrawLabels()); + gd.setInsets(0,30,0); + gd.addCheckbox("Hide", imp!=null?imp.getHideOverlay():false); + } else + gd.addMessage("No overlay", null, Color.darkGray); + } + if (isText) + gd.addCheckbox("Antialiased text", antialias); + if (showListCoordinates) { + if ((roi instanceof PointRoi) && Toolbar.getMultiPointMode()) + showPointCounts = true; + if (showPointCounts) + gd.addCheckbox("Show point counts (shortcut: alt+y)", listCoordinates); + else + gd.addCheckbox("List coordinates ("+roi.size()+")", listCoordinates); + if (nProperties>0) + gd.addCheckbox("List properties ("+nProperties+")", listProperties); + else { + gd.setInsets(5,20,0); + gd.addMessage("No properties"); + } + } + if (isText && !isRange) { + String text = ((TextRoi)roi).getText(); + int nLines = Tools.split(text, "\n").length + 1; + gd.addTextAreas(text, null, Math.min(nLines+1, 5), 30); + } + if (showName && "".equals(name) && "none".equals(position) && "none".equals(group) && "none".equals(fillc)) + gd.setSmartRecording(true); + gd.addWindowListener(this); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + String position2 = ""; + String group2 = ""; + if (showName) { + name = gd.getNextString(); + if (!isRange) roi.setName(name.length()>0?name:null); + position2 = gd.getNextString(); + group2 = gd.getNextString(); + } + linec = gd.getNextString(); + if (!isPoint) + strokeWidth2 = gd.getNextNumber(); + if (isText) { + angle = gd.getNextNumber(); + justification = gd.getNextChoiceIndex(); + } + if (!isLine) { + if (isPoint) { + int index = gd.getNextChoiceIndex(); + ((PointRoi)roi).setPointType(index); + index = gd.getNextChoiceIndex(); + ((PointRoi)roi).setSize(index); + } else + fillc = gd.getNextString(); + } + boolean applyToOverlay = false; + boolean newOverlay = addToOverlay?gd.getNextBoolean():false; + if (overlayOptions) { + setPositions = gd.getNextBoolean(); + if (overlay!=null) { + applyToOverlay = gd.getNextBoolean(); + boolean labels = gd.getNextBoolean(); + boolean hideOverlay = gd.getNextBoolean(); + if (hideOverlay && imp!=null) { + if (!imp.getHideOverlay()) + imp.setHideOverlay(true); + } else { + overlay.drawLabels(labels); + Analyzer.drawLabels(labels); + overlay.drawBackgrounds(true); + if (imp.getHideOverlay()) + imp.setHideOverlay(false); + if (!applyToOverlay && imp!=null) + imp.draw(); + } + } + roi.setPosition(setPositions?1:0); + } + if (isText) + antialias = gd.getNextBoolean(); + if (showListCoordinates) { + listCoordinates = gd.getNextBoolean(); + if (nProperties>0) + listProperties = gd.getNextBoolean(); + } + strokeColor = Colors.decode(linec, null); + fillColor = Colors.decode(fillc, null); + if (isText) { + TextRoi troi = (TextRoi)roi; + Font font = troi.getCurrentFont(); + if (strokeWidth2!=strokeWidth) { + font = new Font(font.getName(), font.getStyle(), (int)strokeWidth2); + troi.setCurrentFont(font); + } + troi.setAngle(angle); + if (justification!=troi.getJustification()) + troi.setJustification(justification); + troi.setAntiAlias(antialias); + if (!isRange) troi.setText(gd.getNextText()); + } else if (strokeWidth2!=strokeWidth) + roi.setStrokeWidth((float)strokeWidth2); + roi.setStrokeColor(strokeColor); + roi.setFillColor(fillColor); + if (showName) { + setPosition(roi, position, position2); + setGroup(roi, group, group2); + } + if (newOverlay) roi.setName("new-overlay"); + if (applyToOverlay) { + if (imp==null || overlay==null) + return true; + Roi[] rois = overlay.toArray(); + for (int i=0; i0) + listProperties(roi); + return true; + } + + private void setPosition(Roi roi, String pos1, String pos2) { + if (pos1.equals(pos2)) + return; + if (pos2.equals("none") || pos2.equals("0")) { + roi.setPosition(0); + return; + } + String[] positions = Tools.split(pos2, " ,"); + if (positions.length==1) { + double stackPos = Tools.parseDouble(positions[0]); + if (!Double.isNaN(stackPos)) + roi.setPosition((int)stackPos); + return; + } + if (positions.length==3) { + int[] pos = new int[3]; + for (int i=0; i<3; i++) { + double dpos = Tools.parseDouble(positions[i]); + if (Double.isNaN(dpos)) + return; + else + pos[i] = (int)dpos; + } + roi.setPosition(pos[0], pos[1], pos[2]); + return; + } + } + + private void setGroup(Roi roi, String group1, String group2) { + if (group1.equals(group2)) + return; + if (group2.equals("none") || group2.equals("0")) { + roi.setGroup(0); + return; + } + double group = Tools.parseDouble(group2); + if (!Double.isNaN(group)) + roi.setGroup((int)group); + } + + public boolean showImageDialog(String name) { + ImageRoi iRoi = (ImageRoi)roi; + boolean zeroTransparent = iRoi.getZeroTransparent(); + GenericDialog gd = new GenericDialog("Image ROI Properties"); + gd.addStringField("Name:", name, 15); + gd.addNumericField("Opacity (0-100%):", iRoi.getOpacity()*100.0, 0); + gd.addCheckbox("Transparent background", zeroTransparent); + if (addToOverlay) + gd.addCheckbox("New Overlay", false); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + name = gd.getNextString(); + roi.setName(name.length()>0?name:null); + double opacity = gd.getNextNumber()/100.0; + iRoi.setOpacity(opacity); + boolean zeroTransparent2 = gd.getNextBoolean(); + if (zeroTransparent!=zeroTransparent2) + iRoi.setZeroTransparent(zeroTransparent2); + boolean newOverlay = addToOverlay?gd.getNextBoolean():false; + if (newOverlay) roi.setName("new-overlay"); + return true; + } + + void listCoordinates(Roi roi) { + if (roi==null) return; + boolean allIntegers = true; + FloatPolygon fp = roi.getFloatPolygon(); + ImagePlus imp = roi.getImage(); + String title = "Coordinates"; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + int height = imp.getHeight(); + for (int i=0; i=0 && group<=255) { + roi.setGroup((int)group); + String name = Roi.getGroupName((int)group); + if (name==null) + name="unnamed"; + if (group==0) + name = ""; + groupName.setText(" "+name); + Color strokeColor = roi.getStrokeColor(); + colorField.setText(Colors.colorToString(strokeColor)); + } else + groupName.setText(""); + } + + public void windowActivated(WindowEvent e) { + if (groupName!=null) { + String gname = Roi.getGroupName(roi.getGroup()); + groupName.setText(gname!=null?" "+gname:""); // add space to separate label from field + } + } + + public void windowClosing(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + +} diff --git a/src/ij/gui/RotatedRectRoi.java b/src/ij/gui/RotatedRectRoi.java new file mode 100644 index 0000000..f2d7ff9 --- /dev/null +++ b/src/ij/gui/RotatedRectRoi.java @@ -0,0 +1,229 @@ +package ij.gui; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; +import ij.*; +import ij.plugin.frame.Recorder; +import ij.process.FloatPolygon; +import ij.measure.Calibration; + +/** This class implements the rotated rectangle selection tool. */ +public class RotatedRectRoi extends PolygonRoi { + private double xstart, ystart; + private static double DefaultRectWidth = 50; + private double rectWidth = DefaultRectWidth; + + public RotatedRectRoi(double x1, double y1, double x2, double y2, double rectWidth) { + super(new float[5], new float[5], 5, FREEROI); + this.rectWidth = rectWidth; + makeRectangle(x1, y1, x2, y2); + state = NORMAL; + bounds = null; + } + + public RotatedRectRoi(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + type = FREEROI; + xstart = offScreenXD(sx); + ystart = offScreenYD(sy); + ImageWindow win = imp.getWindow(); + int pixels = win!=null?(int)(win.getSize().height/win.getCanvas().getMagnification()):imp.getHeight(); + if (IJ.debugMode) IJ.log("RotatedRectRoi: "+(int)rectWidth+" "+pixels); + if (rectWidth>pixels) + rectWidth = pixels/3; + setDrawOffset(false); + bounds = null; + } + + public void draw(Graphics g) { + super.draw(g); + if (!overlay && ic!=null) { + double mag = ic.getMagnification(); + for (int i=0; i<4; i++) { + if (i==3) //mark starting point + handleColor = strokeColor!=null?strokeColor:ROIColor; + else + handleColor=Color.white; + drawHandle(g, hxs(i), hys(i)); + } + } + } + + private int hxs(int index) { + int indexPlus1 = index<3?index+1:0; + return xp2[index]+(xp2[indexPlus1]-xp2[index])/2; + } + + private int hys(int index) { + int indexPlus1 = index<3?index+1:0; + return yp2[index]+(yp2[indexPlus1]-yp2[index])/2; + } + + private double hx(int index) { + int indexPlus1 = index<3?index+1:0; + return xpf[index]+(xpf[indexPlus1]-xpf[index])/2+x; + } + + private double hy(int index) { + int indexPlus1 = index<3?index+1:0; + return ypf[index]+(ypf[indexPlus1]-ypf[index])/2+y; + } + + protected void grow(int sx, int sy) { + double x1 = xstart; + double y1 = ystart; + double x2 = offScreenXD(sx); + double y2 = offScreenYD(sy); + makeRectangle(x1, y1, x2, y2); + imp.draw(); + } + + void makeRectangle(double x1, double y1, double x2, double y2) { + double length = Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)); + double angle = Math.atan ((x2-x1)/(y2-y1)); + double wsa = (rectWidth/2.0)*Math.sin((Math.PI/2.0)+angle); + double wca = (rectWidth/2.0)*Math.cos((Math.PI/2)+angle); + nPoints = 5; + xpf[3] = (float)(x1-wsa); + ypf[3] = (float)(y1-wca); + xpf[0] = (float)(x1+wsa); + ypf[0] = (float)(y1+wca); + xpf[1] = (float)(x2+wsa); + ypf[1] = (float)(y2+wca); + xpf[2] = (float)(x2-wsa); + ypf[2] = (float)(y2-wca); + xpf[4] = xpf[0]; + ypf[4] = ypf[0]; + makePolygonRelative(); + cachedMask = null; + DefaultRectWidth = rectWidth; + showStatus(); + } + + public void showStatus() { + double[] p = getParams(); + double dx = p[2] - p[0]; + double dy = p[3] - p[1]; + double length = Math.sqrt(dx*dx+dy*dy); + double width = p[4]; + if (imp!=null && !IJ.altKeyDown()) { + Calibration cal = imp.getCalibration(); + if (cal.scaled() && cal.pixelWidth==cal.pixelHeight) { + dx *= cal.pixelWidth; + dy *= cal.pixelHeight; + length = Math.sqrt(dx*dx+dy*dy); + width = p[4]*cal.pixelWidth; + } + } + double angle = getFloatAngle(p[0], p[1], p[2], p[3]); + IJ.showStatus("length=" + IJ.d2s(length)+", width=" + IJ.d2s(width)+", angle=" + IJ.d2s(angle)); + } + + public void nudgeCorner(int key) { + if (ic==null) return; + double[] p = getParams(); + double x1 = p[0]; + double y1 = p[1]; + double x2 = p[2]; + double y2 = p[3]; + double inc = 1.0/ic.getMagnification(); + switch(key) { + case KeyEvent.VK_UP: y2-=inc; break; + case KeyEvent.VK_DOWN: y2+=inc; break; + case KeyEvent.VK_LEFT: x2-=inc; break; + case KeyEvent.VK_RIGHT: x2+=inc; break; + } + makeRectangle(x1, y1, x2, y2); + imp.draw(); + notifyListeners(RoiListener.MOVED); + showStatus(); + } + + void makePolygonRelative() { + FloatPolygon poly = new FloatPolygon(xpf, ypf, nPoints); + Rectangle r = poly.getBounds(); + x = r.x; + y = r.y; + width = r.width; + height = r.height; + bounds = null; + for (int i=0; i=sx2 && sx<=sx2+size && sy>=sy2 && sy<=sy2+size) { + index = i; + break; + } + } + return index; + } + + /** Returns x1, y1, x2, y2 and width as a 5 element array. */ + public double[] getParams() { + double[] params = new double[5]; + params[0] = hx(3); + params[1] = hy(3); + params[2] = hx(1); + params[3] = hy(1); + params[4] = rectWidth; + return params; + } + + /** Always returns true. */ + public boolean subPixelResolution() { + return true; + } + +} diff --git a/src/ij/gui/SaveChangesDialog.java b/src/ij/gui/SaveChangesDialog.java new file mode 100644 index 0000000..203fd11 --- /dev/null +++ b/src/ij/gui/SaveChangesDialog.java @@ -0,0 +1,98 @@ +package ij.gui; +import ij.IJ; +import java.awt.*; +import java.awt.event.*; + +/** A modal dialog box with a one line message and + "Don't Save", "Cancel" and "Save" buttons. */ +public class SaveChangesDialog extends Dialog implements ActionListener, KeyListener { + private Button dontSave, cancel, save; + private boolean cancelPressed, savePressed; + + public SaveChangesDialog(Frame parent, String fileName) { + super(parent, "Save?", true); + setLayout(new BorderLayout()); + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 10, 10)); + Component message; + if (fileName.startsWith("Save ")) + message = new Label(fileName); + else { + if (fileName.length()>22) + message = new MultiLineLabel("Save changes to\n" + "\"" + fileName + "\"?"); + else + message = new Label("Save changes to \"" + fileName + "\"?"); + } + message.setFont(new Font("Dialog", Font.BOLD, 12)); + panel.add(message); + add("Center", panel); + + panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.CENTER, 8, 8)); + save = new Button(" Save "); + save.addActionListener(this); + save.addKeyListener(this); + cancel = new Button(" Cancel "); + cancel.addActionListener(this); + cancel.addKeyListener(this); + dontSave = new Button("Don't Save"); + dontSave.addActionListener(this); + dontSave.addKeyListener(this); + if (ij.IJ.isMacintosh()) { + panel.add(dontSave); + panel.add(cancel); + panel.add(save); + } else { + panel.add(save); + panel.add(dontSave); + panel.add(cancel); + } + add("South", panel); + if (ij.IJ.isMacintosh()) + setResizable(false); + pack(); + GUI.centerOnImageJScreen(this); + show(); + } + + public void actionPerformed(ActionEvent e) { + if (e.getSource()==cancel) + cancelPressed = true; + else if (e.getSource()==save) + savePressed = true; + closeDialog(); + } + + /** Returns true if the user dismissed dialog by pressing "Cancel". */ + public boolean cancelPressed() { + if (cancelPressed) + ij.Macro.abort(); + return cancelPressed; + } + + /** Returns true if the user dismissed dialog by pressing "Save". */ + public boolean savePressed() { + return savePressed; + } + + void closeDialog() { + //setVisible(false); + dispose(); + } + + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyDown(keyCode); + if (keyCode==KeyEvent.VK_ENTER) + closeDialog(); + else if (keyCode==KeyEvent.VK_ESCAPE) { + cancelPressed = true; + closeDialog(); + IJ.resetEscape(); + } + } + + public void keyReleased(KeyEvent e) {} + public void keyTyped(KeyEvent e) {} + +} diff --git a/src/ij/gui/ScrollbarWithLabel.java b/src/ij/gui/ScrollbarWithLabel.java new file mode 100644 index 0000000..3d1d13a --- /dev/null +++ b/src/ij/gui/ScrollbarWithLabel.java @@ -0,0 +1,233 @@ +package ij.gui; +import ij.ImageJ; +import ij.IJ; +import ij.Prefs; +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.*; + + +/** This class, based on Joachim Walter's Image5D package, adds "c", "z" labels + and play-pause icons (T) to the stack and hyperstacks dimension sliders. + * @author Joachim Walter + */ +public class ScrollbarWithLabel extends Panel implements Adjustable, AdjustmentListener { + Scrollbar bar; + private Icon icon; + private StackWindow stackWindow; + transient AdjustmentListener adjustmentListener; + + public ScrollbarWithLabel() { + } + + public ScrollbarWithLabel(StackWindow stackWindow, int value, int visible, int minimum, int maximum, char label) { + super(new BorderLayout(2, 0)); + this.stackWindow = stackWindow; + bar = new Scrollbar(Scrollbar.HORIZONTAL, value, visible, minimum, maximum); + GUI.fixScrollbar(bar); + icon = new Icon(label); + add(icon, BorderLayout.WEST); + add(bar, BorderLayout.CENTER); + bar.addAdjustmentListener(this); + addKeyListener(IJ.getInstance()); + } + + /* (non-Javadoc) + * @see java.awt.Component#getPreferredSize() + */ + public Dimension getPreferredSize() { + Dimension dim = new Dimension(0,0); + int width = bar.getPreferredSize().width; + Dimension minSize = getMinimumSize(); + if (widthheight) + height = iconHeight; + dim = new Dimension(width, (int)(height)); + return dim; + } + + public Dimension getMinimumSize() { + return new Dimension(80, 15); + } + + /* Adds KeyListener also to all sub-components. + */ + public synchronized void addKeyListener(KeyListener l) { + super.addKeyListener(l); + bar.addKeyListener(l); + } + + /* Removes KeyListener also from all sub-components. + */ + public synchronized void removeKeyListener(KeyListener l) { + super.removeKeyListener(l); + bar.removeKeyListener(l); + } + + /* + * Methods of the Adjustable interface + */ + public synchronized void addAdjustmentListener(AdjustmentListener l) { + if (l == null) { + return; + } + adjustmentListener = AWTEventMulticaster.add(adjustmentListener, l); + } + public int getBlockIncrement() { + return bar.getBlockIncrement(); + } + public int getMaximum() { + return bar.getMaximum(); + } + public int getMinimum() { + return bar.getMinimum(); + } + public int getOrientation() { + return bar.getOrientation(); + } + public int getUnitIncrement() { + return bar.getUnitIncrement(); + } + public int getValue() { + return bar.getValue(); + } + public int getVisibleAmount() { + return bar.getVisibleAmount(); + } + public synchronized void removeAdjustmentListener(AdjustmentListener l) { + if (l == null) { + return; + } + adjustmentListener = AWTEventMulticaster.remove(adjustmentListener, l); + } + public void setBlockIncrement(int b) { + bar.setBlockIncrement(b); + } + public void setMaximum(int max) { + bar.setMaximum(max); + } + public void setMinimum(int min) { + bar.setMinimum(min); + } + public void setUnitIncrement(int u) { + bar.setUnitIncrement(u); + } + public void setValue(int v) { + bar.setValue(v); + } + public void setVisibleAmount(int v) { + bar.setVisibleAmount(v); + } + + public void setFocusable(boolean focusable) { + super.setFocusable(focusable); + bar.setFocusable(focusable); + } + + /* + * Method of the AdjustmenListener interface. + */ + public void adjustmentValueChanged(AdjustmentEvent e) { + if (bar != null && e.getSource() == bar) { + AdjustmentEvent myE = new AdjustmentEvent(this, e.getID(), e.getAdjustmentType(), + e.getValue(), e.getValueIsAdjusting()); + AdjustmentListener listener = adjustmentListener; + if (listener != null) { + listener.adjustmentValueChanged(myE); + } + } + } + + public void updatePlayPauseIcon() { + icon.repaint(); + } + + + class Icon extends Canvas implements MouseListener { + private final double SCALE = Prefs.getGuiScale(); + private final int WIDTH = (int)(12*SCALE); + private final int HEIGHT= (int)(14*SCALE); + private BasicStroke stroke = new BasicStroke((float)(2*SCALE)); + private char type; + private Image image; + + public Icon(char type) { + addMouseListener(this); + addKeyListener(IJ.getInstance()); + setSize(WIDTH, HEIGHT); + this.type = type; + } + + /** Overrides Component getPreferredSize(). */ + public Dimension getPreferredSize() { + return new Dimension(WIDTH, HEIGHT); + } + + public void update(Graphics g) { + paint(g); + } + + public void paint(Graphics g) { + g.setColor(Color.white); + g.fillRect(0, 0, WIDTH, HEIGHT); + Graphics2D g2d = (Graphics2D)g; + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + if (type=='t') + drawPlayPauseButton(g2d); + else + drawLetter(g); + } + + private void drawLetter(Graphics g) { + Font font = new Font("SansSerif", Font.PLAIN, 14); + if (SCALE>1.0) + font = font.deriveFont((float)(font.getSize()*SCALE)); + g.setFont(font); + g.setColor(Color.black); + g.drawString(String.valueOf(type), (int)(2*SCALE), (int)(12*SCALE)); + } + + private void drawPlayPauseButton(Graphics2D g) { + if (stackWindow.getAnimate()) { + g.setColor(Color.black); + g.setStroke(stroke); + int s3 = (int)(3*SCALE); + int s8 = (int)(8*SCALE); + int s11 = (int)(11*SCALE); + g.drawLine(s3, s3, s3, s11); + g.drawLine(s8, s3, s8, s11); + } else { + g.setColor(Color.darkGray); + GeneralPath path = new GeneralPath(); + path.moveTo(3f, 2f); + path.lineTo(10f, 7f); + path.lineTo(3f, 12f); + path.lineTo(3f, 2f); + if (SCALE>1.0) { + AffineTransform at = new AffineTransform(); + at.scale(SCALE, SCALE); + path = new GeneralPath(at.createTransformedShape(path)); + } + g.fill(path); + } + } + + public void mousePressed(MouseEvent e) { + if (type!='t') return; + int flags = e.getModifiers(); + if ((flags&(Event.ALT_MASK|Event.META_MASK|Event.CTRL_MASK))!=0) + IJ.doCommand("Animation Options..."); + else + IJ.doCommand("Start Animation [\\]"); + } + + public void mouseReleased(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + public void mouseClicked(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + + } // Icon class + +} diff --git a/src/ij/gui/ShapeRoi.java b/src/ij/gui/ShapeRoi.java new file mode 100644 index 0000000..1f943a5 --- /dev/null +++ b/src/ij/gui/ShapeRoi.java @@ -0,0 +1,1207 @@ +package ij.gui; +import java.awt.*; +import java.awt.image.*; +import java.awt.geom.*; +import java.awt.event.KeyEvent; +import java.util.*; +import ij.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.filter.Analyzer; +import ij.util.Tools; +import ij.util.FloatArray; + +/**A subclass of ij.gui.Roi (2D Regions Of Interest) implemented in terms of java.awt.Shape. + * A ShapeRoi is constructed from a ij.gui.Roi object, or as a result of logical operators + * (i.e., union, intersection, exclusive or, and subtraction) provided by this class. These operators use the package + * java.awt.geom as a backend.
+ * This code is in the public domain. + * @author Cezar M.Tigaret + */ +public class ShapeRoi extends Roi { + + /***/ + static final int NO_TYPE = 128; + + /**The maximum tolerance allowed in calculating the length of the curve segments of this ROI's shape.*/ + static final double MAXERROR = 1.0e-3; + + /** Coefficient used to obtain a flattened version of this ROI's shape. A flattened shape is the + * closest approximation of the original shape's curve segments with line segments. + * The FLATNESS is an indication of the maximum deviation between the flattened and the original shape. */ + static final double FLATNESS = 0.1; + + /** Flatness used for filling the shape when creating a mask. Lower values result in higher accuracy + * (determining which pixels near the border are filled), but lower speed when filling shapes with + * curved borders. */ + static final double FILL_FLATNESS = 0.01; + + /**Parsing a shape composed of linear segments less than this value will result in Roi objects of type + * {@link ij.gui.Roi#POLYLINE} and {@link ij.gui.Roi#POLYGON} for open and closed shapes, respectively. + * Conversion of shapes open and closed with more than MAXPOLY line segments will result, + * respectively, in {@link ij.gui.Roi#FREELINE} and {@link ij.gui.Roi#FREEROI} (or + * {@link ij.gui.Roi#TRACED_ROI} if {@link #forceTrace} flag is true. + */ + private static final int MAXPOLY = 10; // I hate arbitrary values !!!! + + private static final int OR=0, AND=1, XOR=2, NOT=3; + + /**The java.awt.Shape encapsulated by this object.*/ + private Shape shape; + + /**The instance value of the maximum tolerance (MAXERROR) allowed in calculating the + * length of the curve segments of this ROI's shape. + */ + private double maxerror = ShapeRoi.MAXERROR; + + /**The instance value of the coefficient (FLATNESS) used to + * obtain a flattened version of this ROI's shape. + */ + private double flatness = ShapeRoi.FLATNESS; + + /**The instance value of MAXPOLY.*/ + private int maxPoly = ShapeRoi.MAXPOLY; + + /**If true then methods that manipulate this ROI's shape will work on + * a flattened version of the shape. */ + private boolean flatten; + + /**Flag which specifies how Roi objects will be constructed from closed (sub)paths having more than + * MAXPOLY and composed exclusively of line segments. + * If true then (sub)path will be parsed into a + * {@link ij.gui.Roi#TRACED_ROI}; else, into a {@link ij.gui.Roi#FREEROI}. */ + private boolean forceTrace = false; + + /**Flag which specifies if Roi objects constructed from open (sub)paths composed of only two line segments + * will be of type {@link ij.gui.Roi#ANGLE}. + * If true then (sub)path will be parsed into a {@link ij.gui.Roi#ANGLE}; + * else, into a {@link ij.gui.Roi#POLYLINE}. */ + private boolean forceAngle = false; + + private Vector savedRois; //not really used any more + private static Stroke defaultStroke = new BasicStroke(); + + + /** Constructs a ShapeRoi from an Roi. */ + public ShapeRoi(Roi r) { + this(r, ShapeRoi.FLATNESS, ShapeRoi.MAXERROR, false, false, false, ShapeRoi.MAXPOLY); + } + + /** Constructs a ShapeRoi from a Shape. */ + public ShapeRoi(Shape s) { + super(s.getBounds()); + AffineTransform at = new AffineTransform(); + at.translate(-x, -y); + shape = new GeneralPath(at.createTransformedShape(s)); + type = COMPOSITE; + } + + /** Constructs a ShapeRoi from a Shape. */ + public ShapeRoi(int x, int y, Shape s) { + super(x, y, s.getBounds().width, s.getBounds().height); + shape = new GeneralPath(s); + type = COMPOSITE; + } + + /**Creates a ShapeRoi object from a "classical" ImageJ ROI. + * @param r An ij.gui.Roi object + * @param flatness The flatness factor used in convertion of curve segments into line segments. + * @param maxerror Error correction for calculating length of Bezeir curves. + * @param forceAngle flag used in the conversion of Shape objects to Roi objects (see {@link #shapeToRois()}. + * @param forceTrace flag for conversion of Shape objects to Roi objects (see {@link #shapeToRois()}. + * @param flatten if true then the shape of this ROI will be flattened + * (i.e., curve segments will be aproximated by line segments). + * @param maxPoly Roi objects constructed from shapes composed of linear segments fewer than this + * value will be of type {@link ij.gui.Roi#POLYLINE} or {@link ij.gui.Roi#POLYGON}; conversion of + * shapes with linear segments more than this value will result in Roi objects of type + * {@link ij.gui.Roi#FREELINE} or {@link ij.gui.Roi#FREEROI} unless the average side length + * is large (see {@link #shapeToRois()}). + */ + ShapeRoi(Roi r, double flatness, double maxerror, boolean forceAngle, boolean forceTrace, boolean flatten, int maxPoly) { + super(r.startX, r.startY, r.width, r.height); + this.type = COMPOSITE; + this.flatness = flatness; + this.maxerror = maxerror; + this.forceAngle = forceAngle; + this.forceTrace = forceTrace; + this.maxPoly= maxPoly; + this.flatten = flatten; + shape = roiToShape((Roi)r.clone()); + } + + /** Constructs a ShapeRoi from an array of variable length path segments. Each + segment consists of the segment type followed by 0-6 coordintes (0-3 end points and control + points). Depending on the type, a segment uses from 1 to 7 elements of the array. */ + public ShapeRoi(float[] shapeArray) { + super(0,0,null); + shape = makeShapeFromArray(shapeArray); + Rectangle r = shape.getBounds(); + x = r.x; + y = r.y; + width = r.width; + height = r.height; + state = NORMAL; + oldX=x; oldY=y; oldWidth=width; oldHeight=height; + AffineTransform at = new AffineTransform(); + at.translate(-x, -y); + shape = new GeneralPath(at.createTransformedShape(shape)); + flatness = ShapeRoi.FLATNESS; + maxerror = ShapeRoi.MAXERROR; + maxPoly = ShapeRoi.MAXPOLY; + flatten = false; + type = COMPOSITE; + } + + /**Returns a deep copy of this. */ + public synchronized Object clone() { // the equivalent of "operator=" ? + ShapeRoi sr = (ShapeRoi)super.clone(); + sr.type = COMPOSITE; + sr.flatness = flatness; + sr.maxerror = maxerror; + sr.forceAngle = forceAngle; + sr.forceTrace = forceTrace; + //sr.setImage(imp); //wsr + sr.setShape(ShapeRoi.cloneShape(shape)); + return sr; + } + + /** Returns a deep copy of the argument. */ + static Shape cloneShape(Shape rhs) { + if (rhs==null) return null; + else if (rhs instanceof Rectangle2D.Double) + return (Rectangle2D.Double)((Rectangle2D.Double)rhs).clone(); + else if (rhs instanceof Ellipse2D.Double) + return (Ellipse2D.Double)((Ellipse2D.Double)rhs).clone(); + else if (rhs instanceof Line2D.Double) + return (Line2D.Double)((Line2D.Double)rhs).clone(); + else if (rhs instanceof Polygon) + return new Polygon(((Polygon)rhs).xpoints, ((Polygon)rhs).ypoints, ((Polygon)rhs).npoints); + else if (rhs instanceof GeneralPath) + return (GeneralPath)((GeneralPath)rhs).clone(); + else + return makeShapeFromArray(getShapeAsArray(rhs, 0, 0)); + } + + /**********************************************************************************/ + /*** Logical operations on shaped rois ****/ + /**********************************************************************************/ + + /**Unary union operator. + * The caller is set to its union with the argument. + * @return the union of this and sr + */ + public ShapeRoi or(ShapeRoi sr) {return unaryOp(sr, OR);} + + /**Unary intersection operator. + * The caller is set to its intersection with the argument (i.e., the overlapping regions between the + * operands). + * @return the overlapping regions between this and sr + */ + public ShapeRoi and(ShapeRoi sr) {return unaryOp(sr, AND);} + + /**Unary exclusive or operator. + * The caller is set to the non-overlapping regions between the operands. + * @return the union of the non-overlapping regions of this and sr + * @see ij.gui.Roi#xor(Roi[]) + * @see ij.gui.Overlay#xor(int[]) + */ + public ShapeRoi xor(ShapeRoi sr) {return unaryOp(sr, XOR);} + + /**Unary subtraction operator. + * The caller is set to the result of the operation between the operands. + * @return this subtracted from sr + */ + public ShapeRoi not(ShapeRoi sr) {return unaryOp(sr, NOT);} + + ShapeRoi unaryOp(ShapeRoi sr, int op) { + AffineTransform at = new AffineTransform(); + at.translate(x, y); + Area a1 = new Area(at.createTransformedShape(getShape())); + at = new AffineTransform(); + at.translate(sr.x, sr.y); + Area a2 = new Area(at.createTransformedShape(sr.getShape())); + try { + switch (op) { + case OR: a1.add(a2); break; + case AND: a1.intersect(a2); break; + case XOR: a1.exclusiveOr(a2); break; + case NOT: a1.subtract(a2); break; + } + } catch(Exception e) {} + Rectangle r = a1.getBounds(); + at = new AffineTransform(); + at.translate(-r.x, -r.y); + setShape(new GeneralPath(at.createTransformedShape(a1))); + x = r.x; + y = r.y; + cachedMask = null; + return this; + } + + /**********************************************************************************/ + /*** Interconversions between "regular" rois and shaped rois ****/ + /**********************************************************************************/ + + /**Converts the Roi argument to an instance of java.awt.Shape. + * Currently, the following conversions are supported:
+ + + < + + + + + + + + + + + + + + +
Roi class Roi type Shape
ij.gui.Roi Roi.RECTANGLE java.awt.geom.Rectangle2D.Double
ij.gui.OvalRoi Roi.OVAL java.awt.Polygon of the corresponding traced roi
ij.gui.Line Roi.LINE java.awt.geom.Line2D.Double
ij.gui.PolygonRoi Roi.POLYGON java.awt.Polygon or (if subpixel resolution) closed java.awt.geom.GeneralPath
ij.gui.PolygonRoi Roi.FREEROI java.awt.Polygon or (if subpixel resolution) closed java.awt.geom.GeneralPath
ij.gui.PolygonRoi Roi.TRACED_ROI java.awt.Polygon or (if subpixel resolution) closed java.awt.geom.GeneralPath
ij.gui.PolygonRoi Roi.POLYLINE open java.awt.geom.GeneralPath
ij.gui.PolygonRoi Roi.FREELINE open java.awt.geom.GeneralPath
ij.gui.PolygonRoi Roi.ANGLE open java.awt.geom.GeneralPath
ij.gui.ShapeRoi Roi.COMPOSITE shape of argument
ij.gui.ShapeRoi ShapeRoi.NO_TYPE null
+ * + * @return A java.awt.geom.* object that inherits from java.awt.Shape interface. + * + */ + private Shape roiToShape(Roi roi) { + if (roi.isLine()) + roi = Roi.convertLineToArea(roi); + Shape shape = null; + Rectangle r = roi.getBounds(); + boolean closeShape = true; + int roiType = roi.getType(); + switch(roiType) { + case Roi.LINE: + Line line = (Line)roi; + shape = new Line2D.Double ((double)(line.x1-r.x), (double)(line.y1-r.y), (double)(line.x2-r.x), (double)(line.y2-r.y) ); + break; + case Roi.RECTANGLE: + int arcSize = roi.getCornerDiameter(); + if (arcSize>0) + shape = new RoundRectangle2D.Double(0, 0, r.width, r.height, arcSize, arcSize); + else + shape = new Rectangle2D.Double(0.0, 0.0, (double)r.width, (double)r.height); + break; + case Roi.POLYLINE: case Roi.FREELINE: case Roi.ANGLE: + closeShape = false; + case Roi.POLYGON: case Roi.FREEROI: case Roi.TRACED_ROI: case Roi.OVAL: + if (roiType == Roi.OVAL) { + //shape = new Ellipse2D.Double(-0.001, -0.001, r.width+0.002, r.height+0.002); //inaccurate (though better with increased diameter) + shape = ((OvalRoi)roi).getPolygon(false); + } else if (closeShape && !roi.subPixelResolution()) { + int nPoints =((PolygonRoi)roi).getNCoordinates(); + int[] xCoords = ((PolygonRoi)roi).getXCoordinates(); + int[] yCoords = ((PolygonRoi)roi).getYCoordinates(); + shape = new Polygon(xCoords, yCoords, nPoints); + } else { + FloatPolygon floatPoly = roi.getFloatPolygon(); + if (floatPoly.npoints <=1) break; + shape = new GeneralPath(closeShape ? GeneralPath.WIND_EVEN_ODD : GeneralPath.WIND_NON_ZERO, floatPoly.npoints); + ((GeneralPath)shape).moveTo(floatPoly.xpoints[0] - r.x, floatPoly.ypoints[0] - r.y); + for (int i=1; ior(ShapeRoi). */ + void saveRoi(Roi roi) { + if (savedRois==null) + savedRois = new Vector(); + savedRois.addElement(roi); + } + + /**Converts a Shape into Roi object(s). + *
This method parses the shape into (possibly more than one) Roi objects + * and returns them in an array. + *
A simple, "regular" path results in a single Roi following these simple rules: + + + + + + + + +
Shape type Roi class Roi type
java.awt.geom.Rectangle2D.Double ij.gui.Roi Roi.RECTANGLE
java.awt.geom.Ellipse2D.Double ij.gui.OvalRoi Roi.OVAL
java.awt.geom.Line2D.Double ij.gui.Line Roi.LINE
java.awt.Polygon ij.gui.PolygonRoi Roi.POLYGON
+ *

Each subpath of a java.awt.geom.GeneralPath is converted following these rules: + + + + + + + + + + + + + + + + + + + + + + + + +
Segment
types
Number of
segments
Closed
path
Value of
forceAngle
Value of
forceTrace
Roi type
lines only: 0 ShapeRoi.NO_TYPE
1 ShapeRoi.NO_TYPE
2 Y ShapeRoi.NO_TYPE
N Roi.LINE
3 Y N Roi.POLYGON
N Y Roi.ANGLE
N N Roi.POLYLINE
4 Y Roi.RECTANGLE
N Roi.POLYLINE
<= MAXPOLY Y Roi.POLYGON
N Roi.POLYLINE
> MAXPOLY Y Y Roi.TRACED_ROI
N Roi.FREEROI
N Roi.FREELINE
anything
else:
<= 2 ShapeRoi.NO_TYPE
> 2 ShapeRoi.SHAPE_ROI
+ * @return an array of ij.gui.Roi objects. + */ + public Roi[] getRois () { + if (shape==null) + return new Roi[0]; + if (savedRois!=null) + return (Roi[])savedRois.toArray(new Roi[savedRois.size()]); + ArrayList rois = new ArrayList(); + if (shape instanceof Rectangle2D.Double) { + Roi r = new Roi((int)((Rectangle2D.Double)shape).getX(), (int)((Rectangle2D.Double)shape).getY(), (int)((Rectangle2D.Double)shape).getWidth(), (int)((Rectangle2D.Double)shape).getHeight()); + rois.add(r); + } else if (shape instanceof Ellipse2D.Double) { + Roi r = new OvalRoi((int)((Ellipse2D.Double)shape).getX(), (int)((Ellipse2D.Double)shape).getY(), (int)((Ellipse2D.Double)shape).getWidth(), (int)((Ellipse2D.Double)shape).getHeight()); + rois.add(r); + } else if (shape instanceof Line2D.Double) { + Roi r = new ij.gui.Line((int)((Line2D.Double)shape).getX1(), (int)((Line2D.Double)shape).getY1(), (int)((Line2D.Double)shape).getX2(), (int)((Line2D.Double)shape).getY2()); + rois.add(r); + } else if (shape instanceof Polygon) { + Roi r = new PolygonRoi(((Polygon)shape).xpoints, ((Polygon)shape).ypoints, ((Polygon)shape).npoints, Roi.POLYGON); + rois.add(r); + } else { + PathIterator pIter; + if (flatten) + pIter = getFlatteningPathIterator(shape,flatness); + else + pIter = shape.getPathIterator(new AffineTransform()); + parsePath(pIter, ALL_ROIS, rois); + } + return (Roi[])rois.toArray(new Roi[rois.size()]); + } + + + /**Attempts to convert this ShapeRoi into a single non-composite Roi. + * @return an ij.gui.Roi object or null if it cannot be simplified to become a non-composite roi. + */ + public Roi shapeToRoi() { + if (shape==null || !(shape instanceof GeneralPath)) + return null; + PathIterator pIter = shape.getPathIterator(new AffineTransform()); + ArrayList rois = new ArrayList(); + parsePath(pIter, ONE_ROI, rois); + if (rois.size() == 1) + return (Roi)rois.get(0); + else + return null; + } + + /**Attempts to convert this ShapeRoi into a single non-composite Roi. + * For showing as a Roi, one should apply copyAttributes + * @return an ij.gui.Roi object, which is either the non-composite roi, + * or this ShapeRoi (if such a conversion is not possible) or null if + * this is an empty roi. + */ + public Roi trySimplify() { + Roi roi = shapeToRoi(); + return (roi==null) ? this : roi; + } + + /**Implements the rules of conversion from java.awt.geom.GeneralPath to ij.gui.Roi. + * @param nSegments The number of segments that compose the path (= number of vertices for a polygon) + * @param polygonLength length of polygon in pixels, or NaN if curved segments + * @param horizontalVerticalIntOnly Indicates whether the GeneralPath is composed of only vertical and horizontal lines with integer coordinates + * @param forceTrace Indicates that closed shapes with horizontalVerticalIntOnly=true should become TRACED_ROIs + * @param closed Indicates a closed GeneralPath + * @see #shapeToRois() + * @return a type flag like Roi.RECTANGLE or NO_TYPE if the type cannot be determined + */ + private int guessType(int nSegments, double polygonLength, boolean horizontalVerticalIntOnly, boolean forceTrace, boolean closed) { + int roiType = Roi.RECTANGLE; + if (Double.isNaN(polygonLength)) { + roiType = Roi.COMPOSITE; + } else { + // For more segments, they should be longer to qualify for a polygon with handles: + // The threshold for the average segment length is 4.0 for 4 segments, 16.0 for 64 segments, 32.0 for 256 segments + boolean longEdges = polygonLength/(nSegments*Math.sqrt(nSegments)) >= 2; + if (nSegments < 2) + roiType = NO_TYPE; + else if (nSegments == 2) + roiType = closed ? NO_TYPE : Roi.LINE; + else if (nSegments == 3 && !closed && forceAngle) + roiType = Roi.ANGLE; + else if (nSegments == 4 && closed && horizontalVerticalIntOnly && longEdges && !forceTrace && !this.forceTrace) + roiType = Roi.RECTANGLE; + else if (closed && horizontalVerticalIntOnly && (!longEdges || forceTrace || this.forceTrace)) + roiType = Roi.TRACED_ROI; + else if (nSegments <= MAXPOLY || longEdges) + roiType = closed ? Roi.POLYGON : Roi.POLYLINE; + else + roiType = closed ? Roi.FREEROI : Roi.FREELINE; + } + //IJ.log("guessType n= "+nSegments+" len="+polygonLength+" longE="+(polygonLength/(nSegments*Math.sqrt(nSegments)) >= 2)+" hvert="+horizontalVerticalIntOnly+" clos="+closed+" -> "+roiType); + return roiType; + } + + /**Creates a 'classical' (non-Shape) Roi object based on the arguments. + * @see #shapeToRois() + * @param xPoints the x coordinates + * @param yPoints the y coordinates + * @param type the type flag + * @return a ij.gui.Roi object or null + */ + private Roi createRoi(float[] xPoints, float[] yPoints, int roiType) { + if (roiType == NO_TYPE || roiType == Roi.COMPOSITE) return null; + Roi roi = null; + if (xPoints == null || yPoints == null || xPoints.length != yPoints.length || xPoints.length==0) return null; + + Tools.addToArray(xPoints, (float)getXBase()); + Tools.addToArray(yPoints, (float)getYBase()); + + switch(roiType) { + case Roi.LINE: roi = new ij.gui.Line(xPoints[0],yPoints[0],xPoints[1],yPoints[1]); break; + case Roi.RECTANGLE: + double[] xMinMax = Tools.getMinMax(xPoints); + double[] yMinMax = Tools.getMinMax(yPoints); + roi = new Roi((int)xMinMax[0], (int)yMinMax[0], + (int)xMinMax[1] - (int)xMinMax[0], (int)yMinMax[1] - (int)yMinMax[0]); + break; + case TRACED_ROI: + roi = new PolygonRoi(toIntR(xPoints), toIntR(yPoints), xPoints.length, roiType); + break; + default: + roi = new PolygonRoi(xPoints, yPoints, xPoints.length, roiType); + break; + } + return roi; + } + + /**********************************************************************************/ + /*** Geometry ****/ + /**********************************************************************************/ + + /** Checks whether the center of the specified pixel inside of this ROI's shape boundaries. + * Note the ImageJ convention of 0.5 pixel shift between outline and pixel center, + * i.e., pixel (0,0) is enclosed by the rectangle spanned between (0,0) and (1,1). + * The value slightly below 0.5 is for rounding according to the ImageJ convention + * (which is opposite to that of the java.awt.Shape class): + * In ImageJ, points exactly at the left (right) border are considered outside (inside); + * points exactly on horizontal borders, are considered outside (inside) at the border + * with the lower (higher) y. + */ + public boolean contains(int x, int y) { + if (shape==null) return false; + return shape.contains(x-this.x+0.494, y-this.y+0.49994); + } + + /** Returns whether coordinate (x,y) is contained in the Roi. + * Note that the coordinate (0,0) is the top-left corner of pixel (0,0). + * Use contains(int, int) to determine whether a given pixel is contained in the Roi. */ + public boolean containsPoint(double x, double y) { + if (!super.containsPoint(x, y)) + return false; + return shape.contains(x-this.x+1e-3, y-this.y+1e-6); //adding a bit to reduce the likelyhood of numerical errors at integers + } + + /** Returns the perimeter of this ShapeRoi. */ + public double getLength() { + if (width==0 && height==0) + return 0.0; + return parsePath(shape.getPathIterator(new AffineTransform()), GET_LENGTH, null); + } + + /** Returns a path iterator for this ROI's shape containing no curved (only straight) segments */ + PathIterator getFlatteningPathIterator(Shape s, double fl) { + return s.getPathIterator(new AffineTransform(),fl); + } + + /**Length of the control polygon of the cubic Bézier curve argument, in double precision.*/ + double cplength(CubicCurve2D.Double c) { + return Math.sqrt(sqr(c.ctrlx1-c.x1) + sqr(c.ctrly1-c.y1)) + + Math.sqrt(sqr(c.ctrlx2-c.ctrlx1) + sqr(c.ctrly2-c.ctrly1)) + + Math.sqrt(sqr(c.x2-c.ctrlx2) + sqr(c.y2-c.ctrly2)); + } + + /**Length of the control polygon of the quadratic Bézier curve argument, in double precision.*/ + double qplength(QuadCurve2D.Double c) { + return Math.sqrt(sqr(c.ctrlx-c.x1) + sqr(c.ctrly-c.y1)) + + Math.sqrt(sqr(c.x2-c.ctrlx) + sqr(c.y2-c.ctrly)); + } + + /**Length of the chord between the end points of the cubic Bézier curve argument, in double precision.*/ + double cclength(CubicCurve2D.Double c) { + return Math.sqrt(sqr(c.x2-c.x1) + sqr(c.y2-c.y1)); + } + + /**Length of the chord between the end points of the quadratic Bézier curve argument, in double precision.*/ + double qclength(QuadCurve2D.Double c) { + return Math.sqrt(sqr(c.x2-c.x1) + sqr(c.y2-c.y1)); + } + + /**Calculates the length of a cubic Bézier curve specified in double precision. + * The algorithm is based on the theory presented in paper
+ * "Jens Gravesen. Adaptive subdivision and the length and energy of Bézier curves. Computational Geometry 8:13-31 (1997)" + * implemented using java.awt.geom.CubicCurve2D.Double. + * Please visit {@link Graphics Gems IV} for + * examples of other possible implementations in C and C++. + */ + double cBezLength(CubicCurve2D.Double c) { + double l = 0.0; + double cl = cclength(c); + double pl = cplength(c); + if((pl-cl)/2.0 > maxerror) { + CubicCurve2D.Double[] cc = cBezSplit(c); + for(int i=0; i<2; i++) l+=cBezLength(cc[i]); + return l; + } + l = 0.5*pl+0.5*cl; + return l; + } + + /**Calculates the length of a quadratic Bézier curve specified in double precision. + * The algorithm is based on the theory presented in paper
+ * "Jens Gravesen. Adaptive subdivision and the length and energy of Bézier curves. Computational Geometry 8:13-31 (1997)" + * implemented using java.awt.geom.CubicCurve2D.Double. + * Please visit {@link Graphics Gems IV} for + * examples of other possible implementations in C and C++. + */ + double qBezLength(QuadCurve2D.Double c) { + double l = 0.0; + double cl = qclength(c); + double pl = qplength(c); + if((pl-cl)/2.0 > maxerror) + { + QuadCurve2D.Double[] cc = qBezSplit(c); + for(int i=0; i<2; i++) l+=qBezLength(cc[i]); + return l; + } + l = (2.0*pl+cl)/3.0; + return l; + } + + /**Splits a cubic Bézier curve in half. + * @param c A cubic Bézier curve to be divided + * @return an array with the left and right cubic Bézier subcurves + * + */ + CubicCurve2D.Double[] cBezSplit(CubicCurve2D.Double c) { + CubicCurve2D.Double[] cc = new CubicCurve2D.Double[2]; + for (int i=0; i<2 ; i++) cc[i] = new CubicCurve2D.Double(); + c.subdivide(cc[0],cc[1]); + return cc; + } + + /**Splits a quadratic Bézier curve in half. + * @param c A quadratic Bézier curve to be divided + * @return an array with the left and right quadratic Bézier subcurves + * + */ + QuadCurve2D.Double[] qBezSplit(QuadCurve2D.Double c) { + QuadCurve2D.Double[] cc = new QuadCurve2D.Double[2]; + for(int i=0; i<2; i++) cc[i] = new QuadCurve2D.Double(); + c.subdivide(cc[0],cc[1]); + return cc; + } + + // c is an array of even length with x0, y0, x1, y1, ... ,xn, yn coordinate pairs + /**Scales a coordinate array with the size calibration of a 2D image. + * The array is modified in place. + * @param c Array of coordinates in double precision with a fixed structure:
+ * x0,y0,x1,y1,....,xn,yn and with even length of 2*(n+1). + * @param pw The x-scale of the image. + * @param ph The y-scale of the image. + * @param n number of values in c that should be modified (must be less or equal to the size of c + * + */ + void scaleCoords(double[] c, int n, double pw, double ph) { + for(int i=0; i 0) { + addOffset(coords, nCoords, xBase, yBase); + shapeArray.add(coords, nCoords); + } + pIt.next(); + } + return shapeArray.toArray(); + } + + final static int ALL_ROIS=0, ONE_ROI=1, GET_LENGTH=2; //task types + final static int NO_SEGMENT_ANY_MORE = -1; //pseudo segment type when closed + /**Parses the geometry of this ROI's shape by means of the shape's PathIterator; + * Depending on the task and rois argument it will: + *
- create a single non-Shape Roi and add it to rois in case + * there is only one subpath, otherwise add this Roi unchanged to rois + * (task = ONE_ROI and rois non-null) + *
- add each subpath as a Roi to rois; curved subpaths will be flattened, i.e. converted to a + * polygon approximation (task != ONE_ROI and rois non-null) + *
- measure the combined length of all subpaths/Rois and return it (task = GET_LENGTH, rois may be null) + * @param pIter the PathIterator to be parsed. + * @param params an array with one element that will hold the calculated total length of the rois if its initial value is 0. + * If params holds the value SHAPE_TO_ROI, it will be tried to convert this ShapeRoi to a non-composite Roi. If this + * is not possible and this ShapeRoi is not empty, a reference to this ShapeRoi will be returned. + * @param rois an ArrayList that will hold ij.gui.Roi objects constructed from subpaths of this path; + * may be null only when task = GET_LENGTH + * (see @link #shapeToRois()} for details; + * @return Total length if task = GET_LENGTH.*/ + double parsePath(PathIterator pIter, int task, ArrayList rois) { + if (pIter==null || pIter.isDone()) + return 0.0; + double pw = 1.0, ph = 1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } + float xbase = (float)getXBase(); + float ybase = (float)getYBase(); + + FloatArray xPoints = new FloatArray(); //vertex coordinates of current subpath + FloatArray yPoints = new FloatArray(); + FloatArray shapeArray = new FloatArray(); //values for creating a GeneralPath for the current subpath + boolean getLength = task == GET_LENGTH; + int nSubPaths = 0; // the number of subpaths + boolean horVertOnly = true; // subpath has only horizontal or vertical lines + boolean closed = false; + //boolean success = false; + float[] fcoords = new float[6]; // unscaled float coordinates of the path segment + double[] coords = new double[6]; // scaled (calibrated) coordinates of the path segment + double startCalX = 0.0; // start x of subpath (scaled) + double startCalY = 0.0; // start y of subpath (scaled) + double lastCalX = 0.0; // x of previous point in the subpath (scaled) + double lastCalY = 0.0; // y of previous point in the subpath (scaled) + double pathLength = 0.0; // calibrated pathLength/perimeter of current curve + double totalLength = 0.0; // sum of all calibrated path lengths/perimeters + double uncalLength = 0.0; // uncalibrated length of polygon, NaN in case of curves + boolean done = false; + while (true) { + int segType = done ? NO_SEGMENT_ANY_MORE : pIter.currentSegment(fcoords); //read segment (if there is one more) + int nCoords = 0; //will be number of coordinates supplied with the segment + if (!done) { + nCoords = nCoords(segType); + if (getLength) { //make scaled coodinates to calculate the length + pIter.currentSegment(coords); + scaleCoords(coords, nCoords, pw, ph); + } + pIter.next(); + done = pIter.isDone(); + } + + //IJ.log("segType="+segType+" nCoord="+nCoords+" done="+done+" nPoi="+nPoints+" len="+pathLength); + if (segType == NO_SEGMENT_ANY_MORE || (segType == PathIterator.SEG_MOVETO && xPoints.size()>0)) { + // subpath finished: analyze it & create roi if appropriate + closed = closed || (xPoints.size()>0 && xPoints.get(0) == xPoints.getLast() && yPoints.get(0) == yPoints.getLast()); + float[] xpf = xPoints.toArray(); + float[] ypf = yPoints.toArray(); + if (Double.isNaN(uncalLength) || !allInteger(xpf) || !allInteger(ypf)) + horVertOnly = false; //allow conversion to rectangle or traced roi only for integer coordinates + boolean forceTrace = getLength && (!done || nSubPaths>0); //when calculating the length for >1 subpath, assume traced rois if it can be such + int roiType = guessType(xPoints.size(), uncalLength, horVertOnly, forceTrace, closed); + Roi roi = null; + if (roiType == COMPOSITE && rois != null) { //for ShapeRois with curves, we have the length from the path already, make roi only if needed + Shape shape = makeShapeFromArray(shapeArray.toArray()); //the curved subpath (image pixel coordinates) + FloatPolygon fp = getFloatPolygon(shape, FLATNESS, /*separateSubpaths=*/ false, /*addPointForClose=*/ false, /*absoluteCoord=*/ false); + roi = new PolygonRoi(fp, FREEROI); + } else if (roiType != NO_TYPE) { //NO_TYPE looks like an empty roi; only return non-empty rois + roi = createRoi(xpf, ypf, roiType); + } + if (rois != null && roi != null) + rois.add(roi); + if (task == ONE_ROI) { + if (rois.size() > 1) { //we can't make a single roi from this; so we can only keep the roi as it is + rois.clear(); + rois.add(this); + return 0.0; + } + } + if (getLength && roi != null && !Double.isNaN(uncalLength)) { + roi.setImage(imp); //calibration + pathLength = roi.getLength();//we don't use the path length of the Shape; e.g. for traced rois ImageJ has a better algorithm + roi.setImage(null); + } + totalLength += pathLength; + } + if (segType == NO_SEGMENT_ANY_MORE) // b r e a k t h e l o o p + return getLength ? totalLength : 0; + + closed = false; + switch(segType) { + case PathIterator.SEG_MOVETO: //we start a new subpath + xPoints.clear(); + yPoints.clear(); + shapeArray.clear(); + nSubPaths++; + pathLength = 0; + startCalX = coords[0]; + startCalY = coords[1]; + closed = false; + horVertOnly = true; + break; + case PathIterator.SEG_LINETO: + pathLength += Math.sqrt(sqr(lastCalY-coords[1])+sqr(lastCalX-coords[0])); + break; + case PathIterator.SEG_QUADTO: + if (getLength) { + QuadCurve2D.Double curve = new QuadCurve2D.Double(lastCalX,lastCalY,coords[0],coords[2],coords[2],coords[3]); + pathLength += qBezLength(curve); + } + uncalLength = Double.NaN; // not a polygon + break; + case PathIterator.SEG_CUBICTO: + if (getLength) { + CubicCurve2D.Double curve = new CubicCurve2D.Double(lastCalX,lastCalY,coords[0],coords[1],coords[2],coords[3],coords[4],coords[5]); + pathLength += cBezLength(curve); + } + uncalLength = Double.NaN; // not a polygon + break; + case PathIterator.SEG_CLOSE: + pathLength += Math.sqrt(sqr(lastCalX-startCalX) + sqr(lastCalY-startCalY)); + fcoords[0] = xPoints.get(0); //destination coordinates; with these we can handle it as SEG_LINETO + fcoords[1] = yPoints.get(0); + closed = true; + break; + default: + break; + } + if (xPoints.size()>0 && (segType == PathIterator.SEG_LINETO || segType == PathIterator.SEG_CLOSE)) { + float dx = fcoords[0] - xPoints.getLast(); + float dy = fcoords[1] - yPoints.getLast(); + uncalLength += Math.sqrt(sqr(dx) + sqr(dy)); + if (dx != 0f && dy != 0f) horVertOnly = false; + } + + if (nCoords > 0) { + xPoints.add(fcoords[nCoords - 2]); // the last coordinates are the end point of the segment + yPoints.add(fcoords[nCoords - 1]); + lastCalX = coords[nCoords - 2]; + lastCalY = coords[nCoords - 1]; + } + shapeArray.add(segType); + addOffset(fcoords, nCoords, xbase, ybase); // shift the shape to image origin + shapeArray.add(fcoords, nCoords(segType)); + } + } + + /** Non-destructively draws the shape of this object on the associated ImagePlus. */ + public void draw(Graphics g) { + Color color = strokeColor!=null? strokeColor:ROIColor; + boolean isActiveOverlayRoi = !overlay && isActiveOverlayRoi(); + //IJ.log("draw: "+overlay+" "+isActiveOverlayRoi); + if (isActiveOverlayRoi) { + if (color==Color.cyan) + color = Color.magenta; + else + color = Color.cyan; + } + if (fillColor!=null) color = fillColor; + g.setColor(color); + AffineTransform aTx = (((Graphics2D)g).getDeviceConfiguration()).getDefaultTransform(); + Graphics2D g2d = (Graphics2D)g; + if (stroke!=null && !isActiveOverlayRoi) + g2d.setStroke((ic!=null&&ic.getCustomRoi())||isCursor()?stroke:getScaledStroke()); + mag = getMagnification(); + int basex=0, basey=0; + if (ic!=null) { + Rectangle r = ic.getSrcRect(); + basex=r.x; basey=r.y; + } + aTx.setTransform(mag, 0.0, 0.0, mag, -basex*mag, -basey*mag); + aTx.translate(getXBase(), getYBase()); + if (fillColor!=null) { + if (isActiveOverlayRoi) { + g2d.setColor(Color.cyan); + g2d.draw(aTx.createTransformedShape(shape)); + } else + g2d.fill(aTx.createTransformedShape(shape)); + } else + g2d.draw(aTx.createTransformedShape(shape)); + if (stroke!=null) g2d.setStroke(defaultStroke); + if (Toolbar.getToolId()==Toolbar.OVAL) + drawRoiBrush(g); + if (state!=NORMAL && imp!=null && imp.getRoi()!=null) + showStatus(); + if (updateFullWindow) + {updateFullWindow = false; imp.draw();} + } + + public void drawRoiBrush(Graphics g) { + g.setColor(ROIColor); + int size = Toolbar.getBrushSize(); + if (size==0 || ic==null) + return; + int flags = ic.getModifiers(); + if ((flags&16)==0) return; // exit if mouse button up + int osize = size; + size = (int)(size*mag); + Point p = ic.getCursorLoc(); + int sx = ic.screenX(p.x); + int sy = ic.screenY(p.y); + int offset = (int)Math.round(ic.getMagnification()/2.0); + if ((osize&1)==0) + offset=0; // not needed when brush width even + g.drawOval(sx-size/2+offset, sy-size/2+offset, size, size); + } + + /**Draws the shape of this object onto the specified ImageProcessor. + *
This method will always draw a flattened version of the actual shape + * (i.e., all curve segments will be approximated by line segments). + */ + public void drawPixels(ImageProcessor ip) { + PathIterator pIter = shape.getPathIterator(new AffineTransform(), flatness); + float[] coords = new float[6]; + float sx=0f, sy=0f; + while (!pIter.isDone()) { + int segType = pIter.currentSegment(coords); + switch(segType) { + case PathIterator.SEG_MOVETO: + sx = coords[0]; + sy = coords[1]; + ip.moveTo(x+(int)sx, y+(int)sy); + break; + case PathIterator.SEG_LINETO: + ip.lineTo(x+(int)coords[0], y+(int)coords[1]); + break; + case PathIterator.SEG_CLOSE: + ip.lineTo(x+(int)sx, y+(int)sy); + break; + default: break; + } + pIter.next(); + } + } + + /** Returns this ROI's mask pixels as a ByteProcessor with pixels "in" the mask + * set to white (255) and pixels "outside" the mask set to black (0). + * Takes into account the usual ImageJ convention of 0.5 pxl shift between the outline and pixel + * coordinates; e.g., pixel (0,0) is surrounded by the rectangle spanned between (0,0) and (1,1). + * Note that apart from the 0.5 pixel shift, ImageJ has different convention for the border points + * than the java.awt.Shape class: + * In ImageJ, points exactly at the left (right) border are considered outside (inside); + * points exactly on horizontal borders, are considered outside (inside) at the border + * with the lower (higher) y. + * */ + public ImageProcessor getMask() { + if (shape==null) + return null; + ImageProcessor mask = cachedMask; + if (mask!=null && mask.getPixels()!=null && mask.getWidth()==width && mask.getHeight()==height) + return mask; + /* The following code using Graphics2D.fill would in principle work, but is very inaccurate + * at least with Oracle Java 8 or OpenJDK Java 10. + * For near-vertical polgon edges of 1000 pixels length, the deviation can be >0.8 pixels in x. + * Thus, approximating the shape by a polygon and using the PolygonFiller is more accurate + * (and roughly equally fast). --ms Jan 2018 */ + /*BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g2d = bi.createGraphics(); + g2d.setColor(Color.white); + g2d.transform(AffineTransform.getTranslateInstance(-0.48, -0.49994)); //very inaccurate, only reasonable with "-0.48" + g2d.fill(shape); + Raster raster = bi.getRaster(); + DataBufferByte buffer = (DataBufferByte)raster.getDataBuffer(); + byte[] mask = buffer.getData(); + cachedMask = new ByteProcessor(width, height, mask, null); + cachedMask.setThreshold(255,255,ImageProcessor.NO_LUT_UPDATE);*/ + FloatPolygon fpoly = getFloatPolygon(FILL_FLATNESS, true, false, false); + PolygonFiller pf = new PolygonFiller(fpoly.xpoints, fpoly.ypoints, fpoly.npoints, (float)(getXBase()-x), (float)(getYBase()-y)); + mask = pf.getMask(width, height); + cachedMask = mask; + return mask; + } + + /**Returns a reference to the Shape object encapsulated by this ShapeRoi. */ + public Shape getShape() { + return shape; + } + + /**Sets the java.awt.Shape object encapsulated by this + * to the argument. + *
This object will hold a (shallow) copy of the shape argument. If a deep copy + * of the shape argumnt is required, then a clone of the argument should be passed + * in; a possible example is setShape(ShapeRoi.cloneShape(shape)). + * @return false if the argument is null. + */ + boolean setShape(Shape rhs) { + boolean result = true; + if (rhs==null) return false; + if (shape.equals(rhs)) return false; + shape = rhs; + type = Roi.COMPOSITE; + Rectangle rect = shape.getBounds(); + width = rect.width; + height = rect.height; + return true; + } + + /**Returns the element with the smallest value in the array argument.*/ + private int min(int[] array) { + int val = array[0]; + for (int i=1; iaddPointForClose = false, there is no distinction between open and closed subpaths. + * @param absoluteCoord specifies whether the coordinates should be with respect to image bounds, not Roi bounds. */ + public FloatPolygon getFloatPolygon(double flatness, boolean separateSubpaths, boolean addPointForClose, boolean absoluteCoord) { + return getFloatPolygon(shape, flatness, separateSubpaths, addPointForClose, absoluteCoord); + } + + public FloatPolygon getFloatPolygon(Shape shape, double flatness, boolean separateSubpaths, boolean addPointForClose, boolean absoluteCoord) { + if (shape == null) return null; + PathIterator pIter = getFlatteningPathIterator(shape, flatness); + FloatArray xp = new FloatArray(); + FloatArray yp = new FloatArray(); + float[] coords = new float[6]; + int subPathStart = 0; + while (!pIter.isDone()) { + int segType = pIter.currentSegment(coords); + switch(segType) { + case PathIterator.SEG_MOVETO: + if (separateSubpaths && xp.size()>0 && !Float.isNaN(xp.get(xp.size()-1))) { + xp.add(Float.NaN); + yp.add(Float.NaN); + } + subPathStart = xp.size(); + case PathIterator.SEG_LINETO: + xp.add(coords[0]); + yp.add(coords[1]); + break; + case PathIterator.SEG_CLOSE: + boolean isClosed = xp.getLast() == xp.get(subPathStart) && yp.getLast() == yp.get(subPathStart); + if (addPointForClose && !isClosed) { + xp.add(xp.get(subPathStart)); + yp.add(yp.get(subPathStart)); + } else if (isClosed) { + xp.removeLast(1); //remove duplicate point if we should not add point to close the shape + yp.removeLast(1); + } + if (separateSubpaths && xp.size()>0 && !Float.isNaN(xp.get(xp.size()-1))) { + xp.add(Float.NaN); + yp.add(Float.NaN); + } + break; + default: + throw new RuntimeException("Invalid Segment Type: "+segType); + } + pIter.next(); + } + float[] xpf = xp.toArray(); + float[] ypf = yp.toArray(); + if (absoluteCoord) { + Tools.addToArray(xpf, (float)getXBase()); + Tools.addToArray(ypf, (float)getYBase()); + } + int n = xpf.length; + if (n>0 && Float.isNaN(xpf[n-1])) n--; //omit NaN at the end + return new FloatPolygon(xpf, ypf, n); + } + + public FloatPolygon getFloatConvexHull() { + FloatPolygon fp = getFloatPolygon(FLATNESS, /*separateSubpaths=*/ false, /*addPointForClose=*/ false, /*absoluteCoord=*/ true); + return fp == null ? null : fp.getConvexHull(); + } + + public Polygon getPolygon() { + FloatPolygon fp = getFloatPolygon(); + return new Polygon(toIntR(fp.xpoints), toIntR(fp.ypoints), fp.npoints); + } + + /** Returns all vertex points of the shape as approximated by polygons, + * in image pixel coordinates */ + public FloatPolygon getFloatPolygon() { + return getFloatPolygon(FLATNESS, /*separateSubpaths=*/ false, /*addPointForClose=*/ false, /*absoluteCoord=*/ true); + } + + /** Returns all vertex points of the shape as approximated by polygons, + * where options may include "close" to add points to close each subpath, and + * "separate" to insert NaN values between subpaths (= individual polygons) */ + public FloatPolygon getFloatPolygon(String options) { + options = options.toLowerCase(); + boolean separateSubpaths = options.indexOf("separate") >= 0; + boolean addPointForClose = options.indexOf("close") >= 0; + return getFloatPolygon(FLATNESS, separateSubpaths, addPointForClose, /*absoluteCoord=*/ true); + } + + /** Retuns the number of vertices, of this shape as approximated by straight lines. + * Note that points might be counted twice where the shape gets closed. */ + public int size() { + return getPolygon().npoints; + } + + boolean allInteger(float[] a) { + for (int i=0; i1 && previousSlice<=imp.getStackSize()) + imp.setSlice(previousSlice); + else + imp.setSlice(1); + thread = new Thread(this, "zSelector"); + thread.start(); + } + + void addScrollbars(ImagePlus imp) { + ImageStack s = imp.getStack(); + int stackSize = s.getSize(); + int sliderHeight = 0; + nSlices = stackSize; + hyperStack = imp.getOpenAsHyperStack(); + //imp.setOpenAsHyperStack(false); + int[] dim = imp.getDimensions(); + int nDimensions = 2+(dim[2]>1?1:0)+(dim[3]>1?1:0)+(dim[4]>1?1:0); + if (nDimensions<=3 && dim[2]!=nSlices) + hyperStack = false; + if (hyperStack) { + nChannels = dim[2]; + nSlices = dim[3]; + nFrames = dim[4]; + } + if (nSlices==stackSize) hyperStack = false; + if (nChannels*nSlices*nFrames!=stackSize) hyperStack = false; + if (cSelector!=null||zSelector!=null||tSelector!=null) + removeScrollbars(); + ImageJ ij = IJ.getInstance(); + //IJ.log("StackWindow: "+hyperStack+" "+nChannels+" "+nSlices+" "+nFrames+" "+imp); + if (nChannels>1) { + cSelector = new ScrollbarWithLabel(this, 1, 1, 1, nChannels+1, 'c'); + add(cSelector); + sliderHeight += cSelector.getPreferredSize().height + ImageWindow.VGAP; + if (ij!=null) cSelector.addKeyListener(ij); + cSelector.addAdjustmentListener(this); + cSelector.setFocusable(false); // prevents scroll bar from blinking on Windows + cSelector.setUnitIncrement(1); + cSelector.setBlockIncrement(1); + } + if (nSlices>1) { + char label = nChannels>1||nFrames>1?'z':'t'; + if (stackSize==dim[2] && imp.isComposite()) label = 'c'; + zSelector = new ScrollbarWithLabel(this, 1, 1, 1, nSlices+1, label); + if (label=='t') animationSelector = zSelector; + add(zSelector); + sliderHeight += zSelector.getPreferredSize().height + ImageWindow.VGAP; + if (ij!=null) zSelector.addKeyListener(ij); + zSelector.addAdjustmentListener(this); + zSelector.setFocusable(false); + int blockIncrement = nSlices/10; + if (blockIncrement<1) blockIncrement = 1; + zSelector.setUnitIncrement(1); + zSelector.setBlockIncrement(blockIncrement); + sliceSelector = zSelector.bar; + } + if (nFrames>1) { + animationSelector = tSelector = new ScrollbarWithLabel(this, 1, 1, 1, nFrames+1, 't'); + add(tSelector); + sliderHeight += tSelector.getPreferredSize().height + ImageWindow.VGAP; + if (ij!=null) tSelector.addKeyListener(ij); + tSelector.addAdjustmentListener(this); + tSelector.setFocusable(false); + int blockIncrement = nFrames/10; + if (blockIncrement<1) blockIncrement = 1; + tSelector.setUnitIncrement(1); + tSelector.setBlockIncrement(blockIncrement); + } + ImageWindow win = imp.getWindow(); + if (win!=null) + win.setSliderHeight(sliderHeight); + } + + /** Enables or disables the sliders. Used when locking/unlocking an image. */ + public synchronized void setSlidersEnabled(final boolean b) { + EventQueue.invokeLater(new Runnable() { + public void run() { + if (sliceSelector != null) sliceSelector.setEnabled(b); + if (cSelector != null) cSelector.setEnabled(b); + if (zSelector != null) zSelector.setEnabled(b); + if (tSelector != null) tSelector.setEnabled(b); + if (animationSelector != null) animationSelector.setEnabled(b); + } + }); + } + + public synchronized void adjustmentValueChanged(AdjustmentEvent e) { + if (!running2 || imp.isHyperStack()) { + if (e.getSource()==cSelector) { + c = cSelector.getValue(); + if (c==imp.getChannel()&&e.getAdjustmentType()==AdjustmentEvent.TRACK) return; + } else if (e.getSource()==zSelector) { + z = zSelector.getValue(); + int slice = hyperStack?imp.getSlice():imp.getCurrentSlice(); + if (z==slice&&e.getAdjustmentType()==AdjustmentEvent.TRACK) return; + } else if (e.getSource()==tSelector) { + t = tSelector.getValue(); + if (t==imp.getFrame()&&e.getAdjustmentType()==AdjustmentEvent.TRACK) return; + } + slice = (t-1)*nChannels*nSlices + (z-1)*nChannels + c; + notify(); + } + if (!running) + syncWindows(e.getSource()); + } + + private void syncWindows(Object source) { + if (SyncWindows.getInstance()==null) + return; + if (source==cSelector) + SyncWindows.setC(this, cSelector.getValue()); + else if (source==zSelector) { + int stackSize = imp.getStackSize(); + if (imp.getNChannels()==stackSize) + SyncWindows.setC(this, zSelector.getValue()); + else if (imp.getNFrames()==stackSize) + SyncWindows.setT(this, zSelector.getValue()); + else + SyncWindows.setZ(this, zSelector.getValue()); + } else if (source==tSelector) + SyncWindows.setT(this, tSelector.getValue()); + else + throw new RuntimeException("Unknownsource:"+source); + } + + public void actionPerformed(ActionEvent e) { + } + + public void mouseWheelMoved(MouseWheelEvent e) { + synchronized(this) { + int rotation = e.getWheelRotation(); + boolean ctrl = (e.getModifiers()&Event.CTRL_MASK)!=0; + if ((ctrl||IJ.shiftKeyDown()) && ic!=null) { + Point loc = ic.getCursorLoc(); + int x = ic.screenX(loc.x); + int y = ic.screenY(loc.y); + if (rotation<0) + ic.zoomIn(x,y); + else + ic.zoomOut(x,y); + return; + } + if (hyperStack) { + if (rotation>0) + IJ.run(imp, "Next Slice [>]", ""); + else if (rotation<0) + IJ.run(imp, "Previous Slice [<]", ""); + } else { + int slice = imp.getCurrentSlice() + rotation; + if (slice<1) + slice = 1; + else if (slice>imp.getStack().getSize()) + slice = imp.getStack().getSize(); + setSlice(imp,slice); + imp.updateStatusbarValue(); + SyncWindows.setZ(this, slice); + } + } + } + + public boolean close() { + if (!super.close()) + return false; + synchronized(this) { + done = true; + notify(); + } + return true; + } + + /** Displays the specified slice and updates the stack scrollbar. */ + public void showSlice(int index) { + if (imp!=null && index>=1 && index<=imp.getStackSize()) { + setSlice(imp,index); + SyncWindows.setZ(this, index); + } + } + + /** Updates the stack scrollbar. */ + public void updateSliceSelector() { + if (hyperStack || zSelector==null || imp==null) + return; + int stackSize = imp.getStackSize(); + int max = zSelector.getMaximum(); + if (max!=(stackSize+1)) + zSelector.setMaximum(stackSize+1); + EventQueue.invokeLater(new Runnable() { + public void run() { + if (imp!=null && zSelector!=null) + zSelector.setValue(imp.getCurrentSlice()); + } + }); + } + + public void run() { + while (!done) { + synchronized(this) { + try {wait();} + catch(InterruptedException e) {} + } + if (done) return; + if (slice>0) { + int s = slice; + slice = 0; + if (s!=imp.getCurrentSlice()) { + imp.updatePosition(c, z, t); + setSlice(imp,s); + } + } + } + } + + public String createSubtitle() { + String subtitle = super.createSubtitle(); + if (!hyperStack || imp.getStackSize()==1) + return subtitle; + String s=""; + int[] dim = imp.getDimensions(false); + int channels=dim[2], slices=dim[3], frames=dim[4]; + if (channels>1) { + s += "c:"+imp.getChannel()+"/"+channels; + if (slices>1||frames>1) s += " "; + } + if (slices>1) { + s += "z:"+imp.getSlice()+"/"+slices; + if (frames>1) s += " "; + } + if (frames>1) + s += "t:"+imp.getFrame()+"/"+frames; + if (running2) return s; + int index = subtitle.indexOf(";"); + if (index!=-1) { + int index2 = subtitle.indexOf("("); + if (index2>=0 && index2index2+4 && !subtitle.substring(index2+1, index2+4).equals("ch:")) { + index = index2; + s = s + " "; + } + subtitle = subtitle.substring(index, subtitle.length()); + } else + subtitle = ""; + return s + subtitle; + } + + public boolean isHyperStack() { + return hyperStack && getNScrollbars()>0; + } + + public void setPosition(int channel, int slice, int frame) { + if (cSelector!=null && channel!=c) { + c = channel; + cSelector.setValue(channel); + SyncWindows.setC(this, channel); + } + if (zSelector!=null && slice!=z) { + z = slice; + zSelector.setValue(slice); + SyncWindows.setZ(this, slice); + } + if (tSelector!=null && frame!=t) { + t = frame; + tSelector.setValue(frame); + SyncWindows.setT(this, frame); + } + this.slice = (t-1)*nChannels*nSlices + (z-1)*nChannels + c; + imp.updatePosition(c, z, t); + if (this.slice>0) { + int s = this.slice; + this.slice = 0; + if (s!=imp.getCurrentSlice()) + imp.setSlice(s); + } + } + + private void setSlice(ImagePlus imp, int n) { + if (imp.isLocked()) { + IJ.beep(); + IJ.showStatus("Image is locked"); + } else + imp.setSlice(n); + } + + public boolean validDimensions() { + int c = imp.getNChannels(); + int z = imp.getNSlices(); + int t = imp.getNFrames(); + //IJ.log(c+" "+z+" "+t+" "+nChannels+" "+nSlices+" "+nFrames+" "+imp.getStackSize()); + int size = imp.getStackSize(); + if (c==size && c*z*t==size && nSlices==size && nChannels*nSlices*nFrames==size) + return true; + if (c!=nChannels||z!=nSlices||t!=nFrames||c*z*t!=size) + return false; + else + return true; + } + + public void setAnimate(boolean b) { + if (running2!=b && animationSelector!=null) + animationSelector.updatePlayPauseIcon(); + running2 = b; + } + + public boolean getAnimate() { + return running2; + } + + public int getNScrollbars() { + int n = 0; + if (cSelector!=null) n++; + if (zSelector!=null) n++; + if (tSelector!=null) n++; + return n; + } + + void removeScrollbars() { + if (cSelector!=null) { + remove(cSelector); + cSelector.removeAdjustmentListener(this); + cSelector = null; + } + if (zSelector!=null) { + remove(zSelector); + zSelector.removeAdjustmentListener(this); + zSelector = null; + } + if (tSelector!=null) { + remove(tSelector); + tSelector.removeAdjustmentListener(this); + tSelector = null; + } + } + +} diff --git a/src/ij/gui/TextRoi.java b/src/ij/gui/TextRoi.java new file mode 100644 index 0000000..44b920a --- /dev/null +++ b/src/ij/gui/TextRoi.java @@ -0,0 +1,743 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.util.*; +import ij.macro.Interpreter; +import ij.plugin.frame.Recorder; +import ij.plugin.Colors; +import java.awt.geom.*; +import java.awt.*; +import java.awt.image.BufferedImage; + + +/** This class is a rectangular ROI containing text. */ +public class TextRoi extends Roi { + + public static final int LEFT=0, CENTER=1, RIGHT=2; + static final int MAX_LINES = 50; + + private static final String line1 = "Enter text, then press"; + private static final String line2 = "ctrl+b to add to overlay"; + private static final String line3 = "or ctrl+d to draw."; + private static final String line1a = "Enter text..."; + private String[] theText = new String[MAX_LINES]; + private static String name = "SansSerif"; + private static int style = Font.PLAIN; + private static int size = 18; + private Font font; + private static boolean antialiasedText = true; // global flag used by text tool + private static int globalJustification = LEFT; + private static Color defaultFillColor; + private int justification = LEFT; + private double previousMag; + private boolean firstChar = true; + private boolean firstMouseUp = true; + private double angle; // degrees + private static double defaultAngle; + private static boolean firstTime = true; + private Roi previousRoi; + private Graphics fontGraphics; + private static Font defaultFont = IJ.font12; + + /** Creates a TextRoi using the defaultFont.*/ + public TextRoi(int x, int y, String text) { + this(x, y, text, defaultFont); + } + + /** Use this constructor as a drop-in replacement for ImageProcessor.drawString(). */ + public TextRoi(String text, double x, double y, Font font) { + super(x, y, 1, 1); + init(text,font); + if (font!=null) { + Graphics g = getFontGraphics(font); + FontMetrics metrics = g.getFontMetrics(font); + Rectangle2D.Double fbounds = getFloatBounds(); + fbounds.y = fbounds.y-metrics.getAscent(); + setBounds(fbounds); + } + } + + /** Creates a TextRoi using sub-pixel coordinates.*/ + public TextRoi(double x, double y, String text) { + super(x, y, 1.0, 1.0); + init(text, null); + } + + /** Creates a TextRoi using the specified location and Font. + * @see ij.gui.Roi#setStrokeColor + * @see ij.gui.Roi#setNonScalable + * @see ij.ImagePlus#setOverlay(ij.gui.Overlay) + */ + public TextRoi(int x, int y, String text, Font font) { + super(x, y, 1, 1); + init(text, font); + } + + /** Creates a TextRoi using the specified sub-pixel location and Font. */ + public TextRoi(double x, double y, String text, Font font) { + super(x, y, 1.0, 1.0); + init(text, font); + } + + /** Creates a TextRoi using the specified sub-pixel location, size and Font. */ + public TextRoi(double x, double y, double width, double height, String text, Font font) { + super(x, y, width, height); + init(text, font); + } + + /** Creates a TextRoi using the specified text and location. */ + public static TextRoi create(String text, double x, double y, Font font) { + return new TextRoi(text, x, y, font); + } + + /** Obsolete. */ + public static TextRoi create(double x, double y, String text, Font font) { + return new TextRoi(x, y, text, font); + } + + private void init(String text, Font font) { + String[] lines = Tools.split(text, "\n"); + int count = Math.min(lines.length, MAX_LINES); + for (int i=0; i1.0) + mag = 1.0; + if (size<(12/mag)) + size = (int)(12/mag); + if (firstTime) { + theText[0] = line1; + theText[1] = line2; + theText[2] = line3; + firstTime = false; + } else + theText[0] = line1a; + if (previousRoi!=null && (previousRoi instanceof TextRoi)) { + firstMouseUp = false; + previousRoi = null; + } + font = new Font(name, style, size); + justification = globalJustification; + setStrokeColor(Toolbar.getForegroundColor()); + setAntiAlias(antialiasedText); + if (WindowManager.getWindow("Fonts")!=null) { + setFillColor(defaultFillColor); + setAngle(defaultAngle); + } + } + + /** This method is used by the text tool to add typed + characters to displayed text selections. */ + public void addChar(char c) { + if (imp==null) return; + if (!(c>=' ' || c=='\b' || c=='\n')) return; + int cline = 0; + if (firstChar) { + theText[cline] = new String(""); + for (int i=1; i0) + theText[cline] = theText[cline].substring(0, theText[cline].length()-1); + else if (cline>0) { + theText[cline] = null; + cline--; + } + if (angle!=0.0) + imp.draw(); + else + imp.draw(clipX, clipY, clipWidth, clipHeight); + firstChar = false; + return; + } else if ((int)c=='\n') { + // newline + if (cline<(MAX_LINES-1)) cline++; + theText[cline] = ""; + updateBounds(); + updateText(); + } else { + char[] chr = {c}; + theText[cline] += new String(chr); + updateBounds(); + updateText(); + firstChar = false; + return; + } + } + + Font getScaledFont() { + if (font==null) + font = new Font("SansSerif", Font.PLAIN, 14); + double mag = getMagnification(); + if (nonScalable || imp==null || mag==1.0) + return font; + else + return font.deriveFont((float)(font.getSize()*mag)); + } + + /** Renders the text on the image. Draws the text in + * the foreground color if ip.setColor(Color) has + * not been called. + * @see ij.process.ImageProcessor#setFont(Font) + * @see ij.process.ImageProcessor#setAntialiasedText(boolean) + * @see ij.process.ImageProcessor#setColor(Color) + */ + public void drawPixels(ImageProcessor ip) { + if (!ip.fillValueSet()) + ip.setColor(Toolbar.getForegroundColor()); + ip.setFont(font); + ip.setAntialiasedText(getAntiAlias()); + FontMetrics metrics = ip.getFontMetrics(); + int fontHeight = metrics.getHeight(); + int descent = metrics.getDescent(); + int i = 0; + int yy = 0; + int xi = (int)Math.round(getXBase()); + int yi = (int)Math.round(getYBase()); + while (i0.0) + setAntiAlias(true); + } + + /** Returns the state of the 'antiAlias' instance variable. */ + public boolean getAntialiased() { + return getAntiAlias(); + } + + /** Sets the default text tool justification (LEFT, CENTER or RIGHT). */ + public static void setGlobalJustification(int justification) { + if (justification<0 || justification>RIGHT) + justification = LEFT; + globalJustification = justification; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + Roi roi = imp.getRoi(); + if (roi instanceof TextRoi) { + ((TextRoi)roi).setJustification(justification); + imp.draw(); + } + } + } + + /** Returns the default text tool justification (LEFT, CENTER or RIGHT). */ + public static int getGlobalJustification() { + return globalJustification; + } + + /** Sets the 'justification' instance variable (LEFT, CENTER or RIGHT) */ + public void setJustification(int justification) { + if (justification<0 || justification>RIGHT) + justification = LEFT; + this.justification = justification; + updateBounds(); + if (imp!=null) + imp.draw(); + } + + /** Returns the value of the 'justification' instance variable (LEFT, CENTER or RIGHT). */ + public int getJustification() { + return justification; + } + + /** Sets the global font face, size and style that will be used by + TextROIs interactively created using the text tool. */ + public static void setFont(String fontName, int fontSize, int fontStyle) { + setFont(fontName, fontSize, fontStyle, true); + } + + /** Sets the font face, size, style and antialiasing mode that will + be used by TextROIs interactively created using the text tool. */ + public static void setFont(String fontName, int fontSize, int fontStyle, boolean antialiased) { + name = fontName; + size = fontSize; + style = fontStyle; + globalJustification = LEFT; + antialiasedText = antialiased; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + Roi roi = imp.getRoi(); + if (roi instanceof TextRoi) { + roi.setAntiAlias(antialiased); + ((TextRoi)roi).setCurrentFont(new Font(name, style, size)); + imp.draw(); + } + } + + } + + /** Sets the default font. */ + public static void setDefaultFont(Font font) { + defaultFont = font; + } + + /** Sets the default font size. */ + public static void setDefaultFontSize(int size) { + defaultFont = defaultFont.deriveFont((float)size); + } + + /** Sets the default fill (background) color. */ + public static void setDefaultFillColor(Color fillColor) { + defaultFillColor = fillColor; + } + + /** Sets the default angle. */ + public static void setDefaultAngle(double angle) { + defaultAngle = angle; + } + + protected void handleMouseUp(int screenX, int screenY) { + super.handleMouseUp(screenX, screenY); + if (this.width<5 && this.height<5 && imp!=null && previousRoi==null) { + int ox = ic!=null?ic.offScreenX(screenX):screenX; + int oy = ic!=null?ic.offScreenY(screenY):screenY; + TextRoi roi = new TextRoi(ox, oy, line1a); + roi.setStrokeColor(Toolbar.getForegroundColor()); + roi.firstChar = true; + imp.setRoi(roi); + return; + } else if (firstMouseUp) { + updateBounds(); + updateText(); + firstMouseUp = false; + } + if (this.width<5 || this.height<5) + imp.deleteRoi(); + } + + /** Increases the size of bounding rectangle so it's large enough to hold the text. */ + private void updateBounds() { + if (firstChar ) + return; + double lineHeight = 0; + double mag = getMagnification(); + Font font = getScaledFont(); + Graphics g = getFontGraphics(font); + Java2.setAntialiasedText(g, getAntiAlias()); + FontMetrics metrics = g.getFontMetrics(font); + double fontHeight = metrics.getHeight()/mag; + int i=0, nLines=0; + Rectangle2D.Double b = getFloatBounds(); + double newWidth = 10; + while (inewWidth) + newWidth = w; + i++; + } + newWidth += 2.0; + b.width = newWidth; + switch (justification) { + case LEFT: + break; + case CENTER: + b.x = this.oldX+this.oldWidth - newWidth/2.0; + break; + case RIGHT: + b.x = this.oldX+this.oldWidth - newWidth; + break; + } + b.height = nLines*fontHeight+2; + setBounds(b); + } + + private Graphics getFontGraphics(Font font) { + if (fontGraphics==null) { + BufferedImage bi =new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + fontGraphics = (Graphics2D)bi.getGraphics(); + } + fontGraphics.setFont(font); + return fontGraphics; + } + + void updateText() { + if (imp!=null) { + updateClipRect(); + if (angle!=0.0) + imp.draw(); + else + imp.draw(clipX, clipY, clipWidth, clipHeight); + } + } + + double stringWidth(String s, FontMetrics metrics, Graphics g) { + java.awt.geom.Rectangle2D r = metrics.getStringBounds(s, g); + return r.getWidth(); + } + + /** Used by the Recorder for recording the text tool. */ + public String getMacroCode(String cmd, ImagePlus imp) { + String code = ""; + boolean script = Recorder.scriptMode(); + boolean addSelection = cmd.startsWith("Add"); + if (script && !addSelection) + code += "ip = imp.getProcessor();\n"; + if (script) { + String str = "Font.PLAIN"; + if (style==Font.BOLD) + str = "Font.BOLD"; + else if (style==Font.ITALIC) + str = "Font.ITALIC"; + code += "font = new Font(\""+name+"\", "+str+", "+size+");\n"; + if (addSelection) + return getAddSelectionScript(code); + code += "ip.setFont(font);\n"; + } else { + String options = ""; + if (style==Font.BOLD) + options += "bold"; + if (style==Font.ITALIC) + options += " italic"; + if (antialiasedText) + options += " antialiased"; + if (options.equals("")) + options = "plain"; + code += "setFont(\""+name+"\", "+size+", \""+options+"\");\n"; + } + ImageProcessor ip = imp.getProcessor(); + ip.setFont(new Font(name, style, size)); + FontMetrics metrics = ip.getFontMetrics(); + int fontHeight = metrics.getHeight(); + if (script) + code += "ip.setColor(new Color("+getColorArgs(getStrokeColor())+"));\n"; + else + code += "setColor(\""+Colors.colorToString(getStrokeColor())+"\");\n"; + if (addSelection) { + code += "Overlay.drawString(\""+text()+"\", "+this.x+", "+(this.y+fontHeight)+", "+getAngle()+");\n"; + code += "Overlay.show();\n"; + } else { + code += (script?"ip.":"")+"drawString(\""+text()+"\", "+this.x+", "+(this.y+fontHeight)+");\n"; + if (script) + code += "imp.updateAndDraw();\n"; + else + code += "//makeText(\""+text()+"\", "+this.x+", "+(this.y+fontHeight)+");\n"; + } + return (code); + } + + private String text() { + String text = ""; + for (int i=0; iLEFT) { + if (just==CENTER) + code += "roi.setJustification(TextRoi.CENTER);\n"; + else if (just==RIGHT) + code += "roi.setJustification(TextRoi.RIGHT);\n"; + } + if (getAngle()!=0.0) + code += "roi.setAngle("+getAngle()+");\n"; + code += "overlay.add(roi);\n"; + return code; + } + + private String getColorArgs(Color c) { + return IJ.d2s(c.getRed()/255.0,2)+", "+IJ.d2s(c.getGreen()/255.0,2)+", "+IJ.d2s(c.getBlue()/255.0,2); + } + + public String getText() { + String text = ""; + for (int i=0; iw); + w = w2; + i++; + } + Rectangle r = ip.getRoi(); + if (w>r.width) { + r.width = w; + ip.setRoi(r); + } + ip.fill(); + } + } + + @Override + public void setLocation(int x, int y) { + super.setLocation(x, y); + oldWidth = this.width; + } + + /** Returns a copy of this TextRoi. */ + public synchronized Object clone() { + TextRoi tr = (TextRoi)super.clone(); + tr.theText = new String[MAX_LINES]; + for (int i=0; i>" + addPopupMenus(); + } + + public void init() { + dscale = Prefs.getGuiScale(); + scale = (int)Math.round(dscale); + if ((dscale>=1.5&&dscale<2.0) || (dscale>=2.5&&dscale<3.0)) + dscale = scale; + if (dscale>1.0) { + buttonWidth = (int)((BUTTON_WIDTH-2)*dscale); + buttonHeight = (int)((BUTTON_HEIGHT-2)*dscale); + offset = (int)Math.round((OFFSET-1)*dscale); + } else { + buttonWidth = BUTTON_WIDTH; + buttonHeight = BUTTON_HEIGHT; + offset = OFFSET; + } + gapSize = GAP_SIZE; + ps = new Dimension(buttonWidth*NUM_BUTTONS-(buttonWidth-gapSize), buttonHeight); + } + + void addPopupMenus() { + rectPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + rectPopup.setFont(Menus.getFont()); + rectItem = new CheckboxMenuItem("Rectangle", rectType==RECT_ROI); + rectItem.addItemListener(this); + rectPopup.add(rectItem); + roundRectItem = new CheckboxMenuItem("Rounded Rectangle", rectType==ROUNDED_RECT_ROI); + roundRectItem.addItemListener(this); + rectPopup.add(roundRectItem); + rotatedRectItem = new CheckboxMenuItem("Rotated Rectangle", rectType==ROTATED_RECT_ROI); + rotatedRectItem.addItemListener(this); + rectPopup.add(rotatedRectItem); + add(rectPopup); + + ovalPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + ovalPopup.setFont(Menus.getFont()); + ovalItem = new CheckboxMenuItem("Oval selections", ovalType==OVAL_ROI); + ovalItem.addItemListener(this); + ovalPopup.add(ovalItem); + ellipseItem = new CheckboxMenuItem("Elliptical selections", ovalType==ELLIPSE_ROI); + ellipseItem.addItemListener(this); + ovalPopup.add(ellipseItem); + brushItem = new CheckboxMenuItem("Selection Brush Tool", ovalType==BRUSH_ROI); + brushItem.addItemListener(this); + ovalPopup.add(brushItem); + add(ovalPopup); + + pointPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + pointPopup.setFont(Menus.getFont()); + pointItem = new CheckboxMenuItem("Point Tool", !multiPointMode); + pointItem.addItemListener(this); + pointPopup.add(pointItem); + multiPointItem = new CheckboxMenuItem("Multi-point Tool", multiPointMode); + multiPointItem.addItemListener(this); + pointPopup.add(multiPointItem); + add(pointPopup); + + linePopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + linePopup.setFont(Menus.getFont()); + straightLineItem = new CheckboxMenuItem("Straight Line", lineType==LINE&&!arrowMode); + straightLineItem.addItemListener(this); + linePopup.add(straightLineItem); + polyLineItem = new CheckboxMenuItem("Segmented Line", lineType==POLYLINE); + polyLineItem.addItemListener(this); + linePopup.add(polyLineItem); + freeLineItem = new CheckboxMenuItem("Freehand Line", lineType==FREELINE); + freeLineItem.addItemListener(this); + linePopup.add(freeLineItem); + arrowItem = new CheckboxMenuItem("Arrow tool", lineType==LINE&&!arrowMode); + arrowItem.addItemListener(this); + linePopup.add(arrowItem); + add(linePopup); + + zoomPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + zoomPopup.setFont(Menus.getFont()); + addMenuItem(zoomPopup, "Reset Zoom"); + addMenuItem(zoomPopup, "Zoom In"); + addMenuItem(zoomPopup, "Zoom Out"); + addMenuItem(zoomPopup, "View 100%"); + addMenuItem(zoomPopup, "Zoom To Selection"); + addMenuItem(zoomPopup, "Scale to Fit"); + addMenuItem(zoomPopup, "Set..."); + addMenuItem(zoomPopup, "Maximize"); + add(zoomPopup); + + pickerPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + pickerPopup.setFont(Menus.getFont()); + addMenuItem(pickerPopup, "White/Black"); + addMenuItem(pickerPopup, "Black/White"); + addMenuItem(pickerPopup, "Red"); + addMenuItem(pickerPopup, "Green"); + addMenuItem(pickerPopup, "Blue"); + addMenuItem(pickerPopup, "Yellow"); + addMenuItem(pickerPopup, "Cyan"); + addMenuItem(pickerPopup, "Magenta"); + pickerPopup.addSeparator(); + addMenuItem(pickerPopup, "Foreground..."); + addMenuItem(pickerPopup, "Background..."); + addMenuItem(pickerPopup, "Colors..."); + addMenuItem(pickerPopup, "Color Picker..."); + add(pickerPopup); + + switchPopup = new PopupMenu(); + if (Menus.getFontSize()!=0) + switchPopup.setFont(Menus.getFont()); + add(switchPopup); + } + + private void addMenuItem(PopupMenu menu, String command) { + MenuItem item = new MenuItem(command); + item.addActionListener(this); + menu.add(item); + } + + /** Returns the ID of the current tool (Toolbar.RECTANGLE, Toolbar.OVAL, etc.). */ + public static int getToolId() { + int id = current; + if (legacyMode) { + if (id==CUSTOM1) + id=UNUSED; + else if (id>=CUSTOM2) + id--; + } + return id; + } + + /** Returns the ID of the tool whose name (the description displayed in the status bar) + starts with the specified string, or -1 if the tool is not found. */ + public int getToolId(String name) { + int tool = -1; + for (int i=0; i1.0) + g2d.setStroke(new BasicStroke(IJ.isMacOSX()?1.4f:1.25f)); + } else + g2d.setStroke(new BasicStroke(scale)); + } + + private void fill3DRect(Graphics g, int x, int y, int width, int height, boolean raised) { + if (null==g) return; + if (raised) + g.setColor(gray); + else + g.setColor(darker); + g.fillRect(x+1, y+1, width-2, height-2); + g.setColor(raised ? brighter : evenDarker); + g.drawLine(x, y, x, y + height - 1); + g.drawLine(x + 1, y, x + width - 2, y); + g.setColor(raised ? evenDarker : brighter); + g.drawLine(x + 1, y + height - 1, x + width - 1, y + height - 1); + g.drawLine(x + width - 1, y, x + width - 1, y + height - 2); + } + + private void drawButton(Graphics g, int tool) { + if (g==null) return; + if (legacyMode) { + if (tool==UNUSED) + tool = CUSTOM1; + else if (tool>=CUSTOM1) + tool++; + if ((tool==POLYLINE && lineType!=POLYLINE) || (tool==FREELINE && lineType!=FREELINE)) + return; + } + int index = toolIndex(tool); + int x = index*buttonWidth + 1*scale; + if (tool>=CUSTOM1) + x -= buttonWidth-gapSize; + if (tool!=UNUSED) + fill3DRect(g, x, 1, buttonWidth, buttonHeight-1, !down[tool]); + g.setColor(toolColor); + x = index*buttonWidth + offset; + if (tool>=CUSTOM1) + x -= buttonWidth-gapSize; + int y = offset; + if (dscale==1.3) {x++; y++;} + if (dscale==1.4) {x+=2; y+=2;} + if (down[tool]) { x++; y++;} + this.g = g; + if (tool>=CUSTOM1 && tool<=getNumTools() && icons[tool]!=null) { + drawIcon(g, tool, x+1*scale, y+1*scale); + return; + } + switch (tool) { + case RECTANGLE: + xOffset = x; yOffset = y; + if (rectType==ROUNDED_RECT_ROI) + g.drawRoundRect(x-1*scale, y+1*scale, 17*scale, 13*scale, 8*scale, 8*scale); + else if (rectType==ROTATED_RECT_ROI) + polyline(0,10,7,0,15,6,8,16,0,10); + else + g.drawRect(x-1*scale, y+1*scale, 17*scale, 13*scale); + drawTriangle(16,16); + return; + case OVAL: + xOffset = x; yOffset = y; + if (ovalType==BRUSH_ROI) { + yOffset = y - 1; + polyline(6,4,8,2,12,1,15,2,16,4,15,7,12,8,9,11,9,14,6,16,2,16,0,13,1,10,4,9,6,7,6,4); + } else if (ovalType==ELLIPSE_ROI) { + xOffset = x - 1; + yOffset = y + 1; + polyline(11,0,13,0,14,1,15,1,16,2,17,3,17,7,12,12,11,12,10,13,8,13,7,14,4,14,3,13,2,13,1,12,1,11,0,10,0,9,1,8,1,7,6,2,7,2,8,1,10,1,11,0); + } else + g.drawOval(x, y+1*scale, 17*scale, 13*scale); + drawTriangle(16,16); + return; + case POLYGON: + xOffset = x+1; yOffset = y+2; + polyline(4,0,15,0,15,1,11,5,11,6,14,11,14,12,0,12,0,4,4,0); + return; + case FREEROI: + xOffset = x; yOffset = y+3; + polyline(2,0,5,0,7,3,10,3,12,0,15,0,17,2,17,5,16,8,13,10,11,11,6,11,4,10,1,8,0,6,0,2,2,0); + return; + case LINE: + if (arrowMode) { + xOffset = x; yOffset = y; + m(1,14); d(14,1); m(6,5); d(14,1); m(10,9); d(14,1); m(6,5); d(10,9); + } else { + xOffset = x-1; yOffset = y-1; + m(1,17); d(18,0); + drawDot(0,16); drawDot(17,0); + } + drawTriangle(16,16); + return; + case POLYLINE: + xOffset = x; yOffset = y; + polyline(15,6,11,2,1,2,1,3,7,9,2,14); + drawTriangle(14,16); + return; + case FREELINE: + xOffset = x; yOffset = y; + polyline(16,4,14,6,12,6,9,3,8,3,6,7,2,11,1,11); + drawTriangle(14,16); + return; + case POINT: + xOffset = x; yOffset = y; + if (multiPointMode) { + drawPoint(1,3); drawPoint(9,0); drawPoint(15,5); + drawPoint(10,11); drawPoint(2,13); + } else { + m(1,8); d(6,8); d(6,6); d(10,6); d(10,10); d(6,10); d(6,9); + m(8,1); d(8,5); m(11,8); d(15,8); m(8,11); d(8,15); + m(8,8); d(8,8); + g.setColor(Roi.getColor()); + m(7,7); d(9,7); + m(7,8); d(9,8); + m(7,9); d(9,9); + } + drawTriangle(16,16); + return; + case WAND: + xOffset = x+2; yOffset = y+1; + dot(4,0); m(2,0); d(3,1); d(4,2); m(0,0); d(1,1); + m(0,2); d(1,3); d(2,4); dot(0,4); m(3,3); d(15,15); + g.setColor(Roi.getColor()); + m(1,2); d(3,2); m(2,1); d(2,3); + return; + case TEXT: + xOffset = x; yOffset = y; + m(1,16); d(9,0); d(16,16); + m(0,16); d(2,16); + m(15,16); d(17,16); + m(4,10); d(13,10); + return; + case MAGNIFIER: + xOffset = x; yOffset = y; + g.drawOval(x+3, y, 13*scale, 13*scale); + m(5,12); d(-1,18); + drawTriangle(15,17); + return; + case HAND: + xOffset = x; yOffset = y; + polyline(5,17,5,16,0,11,0,8,1,8,5,11,5,2,8,2,8,8,8,0,11,0,11,8,11,1,14,1,14,9,14,3,17,3,17,12,16,13,16,17); + return; + case DROPPER: + // draw foreground/background rectangles + g.setColor(backgroundColor); + g.fillRect(x+2*scale, y+3*scale, 15*scale, 16*scale); + g.drawRect(x, y+2*scale, 13*scale, 13*scale); + g.setColor(foregroundColor); + g.fillRect(x, y+2*scale, 13*scale, 13*scale); + // draw dropper icon + xOffset = x+3; yOffset = y-4; + g.setColor(toolColor); + m(12,2); d(14,2); + m(11,3); d(15,3); + m(11,4); d(15,4); + m(8,5); d(15,5); + m(9,6); d(14,6); + polyline(10,7,12,7,12,9); + polyline(9,6,2,13,2,15,4,15,11,8); + g.setColor(gray); + polygon(9,6,2,13,2,15,4,15,11,8); + drawTriangle(12,21); + return; + case ANGLE: + xOffset = x; yOffset = y+3; + m(0,11); d(13,-1); m(0,11); d(16,11); + m(10,11); d(10,8); m(9,7); d(9,6); dot(8,5); + drawDot(13,-2); drawDot(16,10); + return; + } + } + + void drawTriangle(int x, int y) { + g.setColor(triangleColor); + xOffset+=x*scale; yOffset+=y*scale; + m(0,0); d(4,0); m(1,1); d(3,1); m(2,2); d(2,2); + } + + void drawDot(int x, int y) { + g.fillRect(xOffset+x*scale, yOffset+y*scale, 2*scale, 2*scale); + } + + void drawPoint(int x, int y) { + g.setColor(toolColor); + m(x-3,y); d(x+3,y); + m(x,y-3); d(x,y+3); + g.setColor(Roi.getColor()); + dot(x,y); dot(x-1,y-1); + } + + void drawIcon(Graphics g, int tool, int x, int y) { + if (null==g) return; + icon = icons[tool]; + if (icon==null) return; + this.icon = icon; + int x1, y1, x2, y2; + pc = 0; + if (icon.trim().startsWith("icon:")) { + String path = IJ.getDir("macros")+"toolsets/icons/"+icon.substring(icon.indexOf(":")+1); + try { + BufferedImage bi = ImageIO.read(new File(path)); + if (scale==1) + g.drawImage(bi,x-5,y-5,null); + else { + int size = Math.max(bi.getWidth(),bi.getHeight()); + g.drawImage(bi,x-5*scale,y-5*scale, size*scale,size*scale,null); + } + } catch (Exception e) { + IJ.error("Toolbar", "Error reading tool icon:\n"+path); + } + } else { + while (true) { + char command = icon.charAt(pc++); + if (pc>=icon.length()) break; + switch (command) { + case 'B': x+=v(); y+=v(); break; // adjust base + case 'N': x-=v(); y-=v(); break; // adjust base negatively + case 'R': g.drawRect(x+v(), y+v(), v(), v()); break; // rectangle + case 'F': g.fillRect(x+v(), y+v(), v(), v()); break; // filled rectangle + case 'O': g.drawOval(x+v(), y+v(), v(), v()); break; // oval + case 'V': case 'o': g.fillOval(x+v(), y+v(), v(), v()); break; // filled oval + case 'C': // set color + int saveScale = scale; + scale = 1; + int v1=v(), v2=v(), v3=v(); + int red=v1*16, green=v2*16, blue=v3*16; + if (red>255) red=255; if (green>255) green=255; if (blue>255) blue=255; + Color color = v1==1&&v2==2&&v3==3?foregroundColor:new Color(red,green,blue); + g.setColor(color); + scale = saveScale; + break; + case 'L': g.drawLine(x+v(), y+v(), x+v(), y+v()); break; // line + case 'D': g.fillRect(x+v(), y+v(), scale, scale); break; // dot + case 'P': // polyline + Polygon p = new Polygon(); + p.addPoint(x+v(), y+v()); + while (true) { + x2=v(); if (x2==0) break; + y2=v(); if (y2==0) break; + p.addPoint(x+x2, y+y2); + } + g.drawPolyline(p.xpoints, p.ypoints, p.npoints); + break; + case 'G': case 'H':// polygon or filled polygon + p = new Polygon(); + p.addPoint(x+v(), y+v()); + while (true) { + x2=v(); y2=v(); + if (x2==0 && y2==0 && p.npoints>2) + break; + p.addPoint(x+x2, y+y2); + } + if (command=='G') + g.drawPolygon(p.xpoints, p.ypoints, p.npoints); + else + g.fillPolygon(p.xpoints, p.ypoints, p.npoints); + break; + case 'T': // text (one character) + x2 = x+v()-2; + y2 = y+v(); + int size = v()*10+v()+1; + char[] c = new char[1]; + c[0] = pc=icon.length()) break; + } + } + if (menus[tool]!=null && menus[tool].getItemCount()>0) { + xOffset = x; yOffset = y; + drawTriangle(15, 16); + } + } + + int v() { + if (pc>=icon.length()) return 0; + char c = icon.charAt(pc++); + switch (c) { + case '0': return 0*scale; + case '1': return 1*scale; + case '2': return 2*scale; + case '3': return 3*scale; + case '4': return 4*scale; + case '5': return 5*scale; + case '6': return 6*scale; + case '7': return 7*scale; + case '8': return 8*scale; + case '9': return 9*scale; + case 'a': return 10*scale; + case 'b': return 11*scale; + case 'c': return 12*scale; + case 'd': return 13*scale; + case 'e': return 14*scale; + case 'f': return 15*scale; + case 'g': return 16*scale; + case 'h': return 17*scale; + case 'i': return 18*scale; + case 'j': return 19*scale; + case 'k': return 20*scale; + case 'l': return 21*scale; + case 'm': return 22*scale; + case 'n': return 23*scale; + default: return 0; + } + } + + private void showMessage(int tool) { + if (IJ.statusBarProtected()) + return; + if (tool>=UNUSED && tool=UNUSED && current=CUSTOM1) + tool++; + if (IJ.debugMode) IJ.log("Toolbar.setTool: "+tool); + if ((tool==current&&!(tool==RECTANGLE||tool==OVAL||tool==POINT)) || tool<0 || tool>=getNumTools()-1) + return; + if (tool>=CUSTOM1&&tool<=getNumTools()-2) { + if (names[tool]==null) + names[tool] = "Spare tool"; // enable tool + if (names[tool].indexOf("Action Tool")!=-1) + return; + } + if (isLine(tool)) lineType = tool; + setTool2(tool); + } + + private void setTool2(int tool) { + if (!isValidTool(tool)) return; + String previousName = getToolName(); + previousTool = current; + current = tool; + Graphics g = this.getGraphics(); + if (g==null) + return; + down[current] = true; + if (current!=previousTool) + down[previousTool] = false; + Graphics2D g2d = (Graphics2D)g; + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + setStrokeWidth(g2d); + drawButton(g, previousTool); + drawButton(g, current); + if (null==g) return; + g.dispose(); + showMessage(current); + if (Recorder.record) { + String name = getName(current); + if (name!=null && name.equals("dropper")) disableRecording=true; + if (name!=null && !disableRecording) { + IJ.wait(100); // workaround for OSX/Java 8 bug + Recorder.record("setTool", name); + } + if (name!=null && !name.equals("dropper")) disableRecording=false; + } + if (legacyMode) + repaint(); + if (!previousName.equals(getToolName())) { + IJ.notifyEventListeners(IJEventListener.TOOL_CHANGED);; + repaint(); + } + } + + boolean isValidTool(int tool) { + if (tool<0 || tool>=getNumTools()) + return false; + if (tool>=CUSTOM1 && tool=0) { + int v = (int)value; + if (v>255) v=255; + setForegroundColor(new Color(v,v,v)); + } + foregroundValue = value; + } + + public static double getBackgroundValue() { + return backgroundValue; + } + + public static void setBackgroundValue(double value) { + if (value>=0) { + int v = (int)value; + if (v>255) v=255; + setBackgroundColor(new Color(v,v,v)); + } + backgroundValue = value; + } + + private static void setRoiColor(Color c) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) return; + Roi roi = imp.getRoi(); + if (roi!=null && (roi.isDrawingTool())) { + roi.setStrokeColor(c); + imp.draw(); + } + } + + /** Returns the size of the selection brush tool, or 0 if the brush tool is not enabled. */ + public static int getBrushSize() { + if (ovalType==BRUSH_ROI) + return brushSize; + else + return 0; + } + + /** Set the size of the selection brush tool, in pixels. */ + public static void setBrushSize(int size) { + brushSize = size; + if (brushSize<1) brushSize = 1; + Prefs.set(BRUSH_SIZE, brushSize); + } + + /** Returns the rounded rectangle arc size, or 0 if the rounded rectangle tool is not enabled. */ + public static int getRoundRectArcSize() { + if (rectType==ROUNDED_RECT_ROI) + return arcSize; + else + return 0; + } + + /** Sets the rounded rectangle corner diameter (pixels). */ + public static void setRoundRectArcSize(int size) { + if (size<=0) + rectType = RECT_ROI; + else { + arcSize = size; + Prefs.set(CORNER_DIAMETER, arcSize); + } + repaintTool(RECTANGLE); + ImagePlus imp = WindowManager.getCurrentImage(); + Roi roi = imp!=null?imp.getRoi():null; + if (roi!=null && roi.getType()==Roi.RECTANGLE) + roi.setCornerDiameter(rectType==ROUNDED_RECT_ROI?arcSize:0); + } + + /** Returns 'true' if the multi-point tool is enabled. */ + public static boolean getMultiPointMode() { + return multiPointMode; + } + + /** Returns the rectangle tool type (RECT_ROI, ROUNDED_RECT_ROI or ROTATED_RECT_ROI). */ + public static int getRectToolType() { + return rectType; + } + + /** Returns the oval tool type (OVAL_ROI, ELLIPSE_ROI or BRUSH_ROI). */ + public static int getOvalToolType() { + return ovalType; + } + + /** Returns the button width (button spacing). */ + public static int getButtonSize() { + return buttonWidth; + } + + public static void repaintTool(int tool) { + Toolbar tb = getInstance(); + if (tb!=null) { + Graphics g = tb.getGraphics(); + if (IJ.debugMode) IJ.log("Toolbar.repaintTool: "+tool+" "+g); + if (g==null) return; + if (dscale>1.0) + tb.setStrokeWidth((Graphics2D)g); + ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + tb.drawButton(g, tool); + if (g!=null) g.dispose(); + } + } + + // Returns the toolbar position index of the specified tool + int toolIndex(int tool) { + switch (tool) { + case RECTANGLE: return 0; + case OVAL: return 1; + case POLYGON: return 2; + case FREEROI: return 3; + case LINE: return 4; + case POLYLINE: return 4; + case FREELINE: return 4; + case POINT: return 6; + case WAND: return 7; + case TEXT: return 8; + case MAGNIFIER: return 9; + case HAND: return 10; + case DROPPER: return 11; + case ANGLE: return 5; + case UNUSED: return 12; + default: return tool - 2; + } + } + + // Returns the tool corresponding to the specified x coordinate + private int toolID(int x) { + if (x>buttonWidth*12+gapSize) + x -= gapSize; + int index = x/buttonWidth; + switch (index) { + case 0: return RECTANGLE; + case 1: return OVAL; + case 2: return POLYGON; + case 3: return FREEROI; + case 4: return lineType; + case 5: return ANGLE; + case 6: return POINT; + case 7: return WAND; + case 8: return TEXT; + case 9: return MAGNIFIER; + case 10: return HAND; + case 11: return DROPPER; + default: return index + 3; + } + } + + private boolean inGap(int x) { + return x>=(buttonWidth*12) && x<(buttonWidth*12+gapSize); + } + + public void triggerPopupMenu(int newTool, MouseEvent e, boolean isRightClick, boolean isLongPress) { + mpPrevious = current; + if (isMacroTool(newTool)) { + String name = names[newTool]; + if (newTool==UNUSED || name.contains("Unused Tool")) + return; + if (name.indexOf("Action Tool")!=-1) { + if (e.isPopupTrigger()||e.isMetaDown()) { + name = name.endsWith(" ")?name:name+" "; + tools[newTool].runMacroTool(name+"Options"); + } else { + //drawTool(newTool, true); + //IJ.wait(50); + drawTool(newTool, false); + runMacroTool(newTool); + } + return; + } else { + name = name.endsWith(" ")?name:name+" "; + tools[newTool].runMacroTool(name+"Selected"); + } + } + if (!isLongPress) + setTool2(newTool); + int x = e.getX(); + int y = e.getY(); + if (current==RECTANGLE && isRightClick) { + rectItem.setState(rectType==RECT_ROI); + roundRectItem.setState(rectType==ROUNDED_RECT_ROI); + rotatedRectItem.setState(rectType==ROTATED_RECT_ROI); + if (IJ.isMacOSX()) IJ.wait(10); + rectPopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (current==OVAL && isRightClick) { + ovalItem.setState(ovalType==OVAL_ROI); + ellipseItem.setState(ovalType==ELLIPSE_ROI); + brushItem.setState(ovalType==BRUSH_ROI); + if (IJ.isMacOSX()) IJ.wait(10); + ovalPopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (current==POINT && isRightClick) { + pointItem.setState(!multiPointMode); + multiPointItem.setState(multiPointMode); + if (IJ.isMacOSX()) IJ.wait(10); + pointPopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (isLine(current) && isRightClick) { + straightLineItem.setState(lineType==LINE&&!arrowMode); + polyLineItem.setState(lineType==POLYLINE); + freeLineItem.setState(lineType==FREELINE); + arrowItem.setState(lineType==LINE&&arrowMode); + if (IJ.isMacOSX()) IJ.wait(10); + linePopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (current==MAGNIFIER && isRightClick) { + zoomPopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (current==DROPPER && isRightClick) { + pickerPopup.show(e.getComponent(),x,y); + mouseDownTime = 0L; + } + if (isMacroTool(current) && isRightClick) { + String name = names[current].endsWith(" ")?names[current]:names[current]+" "; + tools[current].runMacroTool(name+"Options"); + } + if (isPlugInTool(current) && isRightClick) { + tools[current].showPopupMenu(e, this); + } + } + + public void mousePressed(final MouseEvent e) { + int x = e.getX(); + if (inGap(x)) + return; + final int newTool = toolID(x); + if (newTool==getNumTools()-1) { + showSwitchPopupMenu(e); + return; + } + if (!isValidTool(newTool)) + return; + if (menus[newTool]!=null && menus[newTool].getItemCount()>0) { + menus[newTool].show(e.getComponent(), e.getX(), e.getY()); + return; + } + int flags = e.getModifiers(); + boolean isRightClick = e.isPopupTrigger()||(!IJ.isMacintosh()&&(flags&Event.META_MASK)!=0); + boolean doubleClick = newTool==current && (System.currentTimeMillis()-mouseDownTime)<=DOUBLE_CLICK_THRESHOLD; + mouseDownTime = System.currentTimeMillis(); + if (!doubleClick || isRightClick) { + triggerPopupMenu(newTool, e, isRightClick, false); + if (isRightClick) mouseDownTime = 0L; + } else if(!isRightClick) { //double click + if (isMacroTool(current)) { + String name = names[current].endsWith(" ")?names[current]:names[current]+" "; + tools[current].runMacroTool(name+"Options"); + return; + } + if (isPlugInTool(current)) { + tools[current].showOptionsDialog(); + return; + } + ImagePlus imp = WindowManager.getCurrentImage(); + switch (current) { + case RECTANGLE: + if (rectType==ROUNDED_RECT_ROI) + IJ.doCommand("Rounded Rect Tool..."); + else + IJ.doCommand("Roi Defaults..."); + break; + case OVAL: + showBrushDialog(); + break; + case MAGNIFIER: + if (imp!=null) { + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) ic.unzoom(); + } + break; + case LINE: case POLYLINE: case FREELINE: + if (current==LINE && arrowMode) { + IJ.doCommand("Arrow Tool..."); + } else + IJ.runPlugIn("ij.plugin.frame.LineWidthAdjuster", ""); + break; + case ANGLE: + showAngleDialog(); + break; + case POINT: + IJ.doCommand("Point Tool..."); + break; + case WAND: + IJ.doCommand("Wand Tool..."); + break; + case TEXT: + IJ.run("Fonts..."); + break; + case DROPPER: + IJ.doCommand("Color Picker..."); + setTool2(mpPrevious); + break; + default: + } + } + + if (!isRightClick && longClickDelay>0) { + if (pressTimer==null) + pressTimer = new Timer(); + pressTimer.schedule(new TimerTask() { + public void run() { + if (pressTimer != null) { + pressTimer.cancel(); + pressTimer = null; + } + triggerPopupMenu(newTool, e, true, true); + } + }, longClickDelay); + } + + } + + public void mouseReleased(MouseEvent e) { + if (pressTimer!=null) { + pressTimer.cancel(); + pressTimer = null; + } + } + + void showSwitchPopupMenu(MouseEvent e) { + String path = IJ.getDir("macros")+"toolsets/"; + if (path==null) + return; + boolean applet = IJ.getApplet()!=null; + File f = new File(path); + String[] list; + if (!applet && f.exists() && f.isDirectory()) { + list = f.list(); + if (list==null) return; + Arrays.sort(list); + } else + list = new String[0]; + switchPopup.removeAll(); + path = IJ.getDir("macros") + "StartupMacros.txt"; + f = new File(path); + if (!f.exists()) { + path = IJ.getDir("macros") + "StartupMacros.ijm"; + f = new File(path); + } + if (!applet && f.exists()) + addItem("Startup Macros"); + else + addItem("StartupMacros*"); + for (int i=0; i=5) + pluginsMenu = menuBar.getMenu(5); + if (pluginsMenu==null || !"Plugins".equals(pluginsMenu.getLabel())) + return; + n = pluginsMenu.getItemCount(); + Menu toolsMenu = null; + for (int i=0; i=CUSTOM1 && tool=CUSTOM1 && toolTools submenu.\n"+ + " \n"+ + "Hold the shift key down while selecting a\n"+ + "toolset to view its source code.\n"+ + " \n"+ + "More macro toolsets are available at\n"+ + " <"+IJ.URL+"/macros/toolsets/>\n"+ + " \n"+ + "Plugin tools can be downloaded from\n"+ + "the Tools section of the Plugins page at\n"+ + " <"+IJ.URL+"/plugins/>\n" + ); + return; + } else if (label.endsWith("*")) { + // load from ij.jar + MacroInstaller mi = new MacroInstaller(); + label = label.substring(0, label.length()-1) + ".txt"; + path = "/macros/"+label; + if (IJ.shiftKeyDown()) + showCode(label, mi.openFromIJJar(path)); + else { + resetTools(); + mi.installFromIJJar(path); + } + } else { + // load from ImageJ/macros/toolsets + if (label.equals("Startup Macros")) { + installStartupMacros(); + return; + } else if (label.endsWith(" ")) + path = IJ.getDir("macros")+"toolsets"+File.separator+label.substring(0, label.length()-1)+".ijm"; + else + path = IJ.getDir("macros")+"toolsets"+File.separator+label+".txt"; + try { + if (IJ.shiftKeyDown()) { + IJ.open(path); + IJ.setKeyUp(KeyEvent.VK_SHIFT); + } else + new MacroInstaller().run(path); + } catch(Exception ex) {} + } + } + } + + private void removeTools() { + removeMacroTools(); + setTool(RECTANGLE); + currentSet = "Startup Macros"; + resetPrefs(); + if (nExtraTools>0) { + String name = names[getNumTools()-1]; + String icon = icons[getNumTools()-1]; + nExtraTools = 0; + names[getNumTools()-1] = name; + icons[getNumTools()-1] = icon; + ps = new Dimension(buttonWidth*NUM_BUTTONS-(BUTTON_WIDTH-gapSize)+nExtraTools*BUTTON_WIDTH, buttonHeight); + IJ.getInstance().pack(); + } + } + + private void resetPrefs() { + for (int i=0; i<7; i++) { + String key = TOOL_KEY+(i/10)%10+i%10; + if (!Prefs.get(key, "").equals("")) + Prefs.set(key, ""); + } + } + + public static void restoreTools() { + Toolbar tb = Toolbar.getInstance(); + if (tb!=null) { + if (tb.getToolId()>=UNUSED) + tb.setTool(RECTANGLE); + tb.installStartupMacros(); + } + } + + private void installStartupMacros() { + resetTools(); + String path = IJ.getDir("macros")+"StartupMacros.txt"; + File f = new File(path); + if (!f.exists()) { + path = IJ.getDir("macros")+"StartupMacros.ijm"; + f = new File(path); + } + if (!f.exists()) { + path = IJ.getDir("macros")+"StartupMacros.fiji.ijm"; + f = new File(path); + } + if (!f.exists()) { + IJ.error("StartupMacros not found in\n \n"+IJ.getDir("macros")); + return; + } + if (IJ.shiftKeyDown()) { + IJ.open(path); + IJ.setKeyUp(KeyEvent.VK_SHIFT); + } else { + try { + MacroInstaller mi = new MacroInstaller(); + mi.installFile(path); + } catch + (Exception ex) {} + } + } + + public void actionPerformed(ActionEvent e) { + MenuItem item = (MenuItem)e.getSource(); + String cmd = e.getActionCommand(); + PopupMenu popup = (PopupMenu)item.getParent(); + + if (zoomPopup==popup) { + if ("Zoom In".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "in"); + else if ("Zoom Out".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "out"); + else if ("Reset Zoom".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "orig"); + else if ("View 100%".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "100%"); + else if ("Zoom To Selection".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "to"); + else if ("Scale to Fit".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "scale"); + else if ("Set...".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "set"); + else if ("Maximize".equals(cmd)) + IJ.runPlugIn("ij.plugin.Zoom", "max"); + disableRecording = true; + setTool(previousTool); + disableRecording = false; + return; + } + + if (pickerPopup==popup) { + if ("White/Black".equals(cmd)) { + setAndRecordForgroundColor(Color.white); + setAndRecordBackgroundColor(Color.black); + } else if ("Black/White".equals(cmd)) { + setAndRecordForgroundColor(Color.black); + setAndRecordBackgroundColor(Color.white); + } else if ("Red".equals(cmd)) + setAndRecordForgroundColor(Color.red); + else if ("Green".equals(cmd)) + setAndRecordForgroundColor(Color.green); + else if ("Blue".equals(cmd)) + setAndRecordForgroundColor(Color.blue); + else if ("Yellow".equals(cmd)) + setAndRecordForgroundColor(Color.yellow); + else if ("Cyan".equals(cmd)) + setAndRecordForgroundColor(Color.cyan); + else if ("Magenta".equals(cmd)) + setAndRecordForgroundColor(Color.magenta); + else if ("Foreground...".equals(cmd)) + setAndRecordForgroundColor(new ColorChooser("Select Foreground Color", foregroundColor, false).getColor()); + else if ("Background...".equals(cmd)) + setAndRecordBackgroundColor(new ColorChooser("Select Background Color", backgroundColor, false).getColor()); + else if ("Colors...".equals(cmd)) { + IJ.run("Colors...", ""); + Recorder.setForegroundColor(getForegroundColor()); + Recorder.setBackgroundColor(getBackgroundColor()); + } else + IJ.run("Color Picker...", ""); + if (!"Color Picker".equals(cmd)) + ColorPicker.update(); + setTool(previousTool); + return; + } + + int tool = -1; + for (int i=CUSTOM1; i=0 && (toolTip.length()-index)>4; + int tool =-1; + for (int i=CUSTOM1; i<=getNumTools()-2; i++) { + if (names[i]==null || toolTip.startsWith(names[i])) { + tool = i; + break; + } + } + if (tool==CUSTOM1) + legacyMode = toolTip.startsWith("Select and Transform Tool"); //TrakEM2 + if (tool==-1 && (nExtraTools0 && toolTip.charAt(index-1)==' ') + names[tool] = toolTip.substring(0, index-1); + else + names[tool] = toolTip.substring(0, index); + } else { + if (toolTip.endsWith("-")) + toolTip = toolTip.substring(0, toolTip.length()-1); + else if (toolTip.endsWith("- ")) + toolTip = toolTip.substring(0, toolTip.length()-2); + names[tool] = toolTip; + } + if (tool==current && (names[tool].indexOf("Action Tool")!=-1||names[tool].indexOf("Unused Tool")!=-1)) + setTool(RECTANGLE); + if (names[tool].endsWith(" Menu Tool")) + installMenu(tool); + if (IJ.debugMode) IJ.log("Toolbar.addTool: "+tool+" "+toolTip); + return tool; + } + + void installMenu(int tool) { + Program pgm = macroInstaller.getProgram(); + Hashtable h = pgm.getMenus(); + if (h==null) return; + String[] commands = (String[])h.get(names[tool]); + if (commands==null) + return; + if (menus[tool]==null) { + menus[tool] = new PopupMenu(""); + if (Menus.getFontSize()!=0) + menus[tool].setFont(Menus.getFont()); + add(menus[tool] ); + } else + menus[tool].removeAll(); + for (int i=0; i0 || custom1Name==null) + setPrefs(tool); + } + } + + private void setPrefs(int id) { + if (doNotSavePrefs) + return; + boolean ok = isBuiltInTool(names[id]); + String prefsName = instance.names[id]; + if (!ok) { + String name = names[id]; + int i = name.indexOf(" ("); // remove any hint in parens + if (i>0) { + name = name.substring(0, i); + prefsName=name; + } + } + int index = id - CUSTOM1; + String key = TOOL_KEY + (index/10)%10 + index%10; + Prefs.set(key, prefsName); + } + + private boolean isBuiltInTool(String name) { + for (int i=0; i=CUSTOM1) + instance.setTool(RECTANGLE); + instance.resetTools(); + instance.repaint(); + } + } + + /** Adds a plugin tool to the first available toolbar slot, + or to the last slot if the toolbar is full. */ + public static void addPlugInTool(PlugInTool tool) { + if (instance==null) return; + String nameAndIcon = tool.getToolName()+" - "+tool.getToolIcon(); + instance.addingSingleTool = true; + int id = instance.addTool(nameAndIcon); + instance.addingSingleTool = false; + if (id!=-1) { + instance.tools[id] = tool; + if (instance.menus[id]!=null) + instance.menus[id].removeAll(); + instance.repaintTool(id); + if (!instance.installingStartupTool) + instance.setTool(id); + else + instance.installingStartupTool = false; + instance.setPrefs(id); + } + } + + public static PlugInTool getPlugInTool() { + PlugInTool tool = null; + if (instance==null) + return null; + if (current2; + return rtn; + } + + public static boolean installStartupMacrosTools() { + String customTool0 = Prefs.get(Toolbar.TOOL_KEY+"00", ""); + return customTool0.equals("") || Character.isDigit(customTool0.charAt(0)); + } + + public int getNumTools() { + return NUM_TOOLS + nExtraTools; + } + + /** Sets the tool menu long click delay in milliseconds + * (default is 600). Set to 0 to disable long click triggering. + */ + public static void setLongClickDelay(int delay) { + longClickDelay = delay; + } + + /** Sets the icon of the specified macro or plugin tool.
+ * See: Help>Examples>Tool>Animated Icon Tool; + */ + public static void setIcon(String toolName, String icon) { + if (instance==null) + return; + int tool = 0; + for (int i=CUSTOM1; i0) { + instance.icons[tool] = icon; + Graphics2D g = (Graphics2D)instance.getGraphics(); + instance.setStrokeWidth(g); + instance.drawButton(g, tool); + } + } + +} diff --git a/src/ij/gui/TrimmedButton.java b/src/ij/gui/TrimmedButton.java new file mode 100644 index 0000000..7949b80 --- /dev/null +++ b/src/ij/gui/TrimmedButton.java @@ -0,0 +1,28 @@ +package ij.gui; +import java.awt.*; +import javax.swing.*; + +/** This is an extended Button class used to reduce the width of the HUGE buttons on Mac OS X. */ +public class TrimmedButton extends Button { + private int trim = 0; + + public TrimmedButton(String title, int trim) { + super(title); + if (trim>0) { + LookAndFeel laf = UIManager.getLookAndFeel(); + String name = laf!=null?laf.getName():""; + if (ij.IJ.isMacOSX() && name!=null && !name.equals("Mac OS X")) + trim = 0; + } + this.trim = trim; + } + + public Dimension getMinimumSize() { + return new Dimension(super.getMinimumSize().width-trim, super.getMinimumSize().height); + } + + public Dimension getPreferredSize() { + return getMinimumSize(); + } + +} diff --git a/src/ij/gui/WaitForUserDialog.java b/src/ij/gui/WaitForUserDialog.java new file mode 100644 index 0000000..31a758c --- /dev/null +++ b/src/ij/gui/WaitForUserDialog.java @@ -0,0 +1,126 @@ +package ij.gui; +import ij.*; +import ij.plugin.frame.RoiManager; +import java.awt.*; +import java.awt.event.*; +import java.lang.reflect.*; + + +/** +* This is a non-modal dialog box used to ask the user to perform some task +* while a macro or plugin is running. It implements the waitForUser() macro +* function. It is based on Michael Schmid's Wait_For_User plugin.
+* Example: +* new WaitForUserDialog("Use brush to draw on overlay").show(); +*/ +public class WaitForUserDialog extends Dialog implements ActionListener, KeyListener { + protected Button button; + protected Button cancelButton; + protected MultiLineLabel label; + static protected int xloc=-1, yloc=-1; + private boolean escPressed; + + public WaitForUserDialog(String title, String text) { + super(IJ.getInstance(), title, false); + IJ.protectStatusBar(false); + if (text!=null && text.startsWith("IJ: ")) + text = text.substring(4); + label = new MultiLineLabel(text, 175); + if (!IJ.isLinux()) label.setFont(new Font("SansSerif", Font.PLAIN, 14)); + if (IJ.isMacOSX()) { + RoiManager rm = RoiManager.getInstance(); + if (rm!=null) rm.runCommand("enable interrupts"); + } + GridBagLayout gridbag = new GridBagLayout(); //set up the layout + GridBagConstraints c = new GridBagConstraints(); + setLayout(gridbag); + c.insets = new Insets(6, 6, 0, 6); + c.gridx = 0; c.gridy = 0; c.anchor = GridBagConstraints.WEST; + add(label,c); + + button = new Button(" OK "); + button.addActionListener(this); + button.addKeyListener(this); + c.insets = new Insets(2, 6, 6, 6); + c.gridx = 0; c.gridy = 1; c.anchor = GridBagConstraints.EAST; + add(button, c); + + if (IJ.isMacro()) { + cancelButton = new Button(" Cancel "); + cancelButton.addActionListener(this); + cancelButton.addKeyListener(this); + c.anchor = GridBagConstraints.WEST; //same as OK button but WEST + add(cancelButton, c); + } + + setResizable(false); + addKeyListener(this); + GUI.scale(this); + pack(); + if (xloc==-1) + GUI.centerOnImageJScreen(this); + else + setLocation(xloc, yloc); + setAlwaysOnTop(true); + } + + public WaitForUserDialog(String text) { + this("Action Required", text); + } + + public void show() { + super.show(); + synchronized(this) { //wait for OK + try {wait();} + catch(InterruptedException e) {return;} + } + } + + public void close() { + synchronized(this) { notify(); } + xloc = getLocation().x; + yloc = getLocation().y; + dispose(); + } + + public void actionPerformed(ActionEvent e) { + String s = e.getActionCommand(); + if(s.indexOf("Cancel") >= 0){ + escPressed = true; + } + close(); + } + + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyDown(keyCode); + if (keyCode==KeyEvent.VK_ENTER || keyCode==KeyEvent.VK_ESCAPE) { + escPressed = keyCode==KeyEvent.VK_ESCAPE; + close(); + } + } + + public boolean escPressed() { + return escPressed; + } + + public void keyReleased(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyUp(keyCode); + } + + public void keyTyped(KeyEvent e) {} + + /** Returns a reference to the 'OK' button */ + public Button getButton() { + return button; + } + + /** Display the next WaitForUser dialog at the specified location. */ + public static void setNextLocation(int x, int y) { + xloc = x; + yloc = y; + } + + +} diff --git a/src/ij/gui/Wand.java b/src/ij/gui/Wand.java new file mode 100644 index 0000000..c6c79b6 --- /dev/null +++ b/src/ij/gui/Wand.java @@ -0,0 +1,353 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.plugin.WandToolOptions; +import java.awt.*; + +/** This class implements ImageJ's wand (tracing) tool. + * The wand selects pixels of equal or similar value or thresholded pixels + * forming a contiguous area. + * The wand creates selections that have only one boundary line (inner holes + * are not excluded from the selection). There may be holes at the boundary, + * however, if the boundary line touches the same vertex twice (both in + * 4-connected and 8-connected mode). + * + * Version 2009-06-01 (code refurbished; tolerance, 4- & 8-connected options added) + */ +public class Wand { + /** Wand operation type: trace outline of 4-connected pixels */ + public final static int FOUR_CONNECTED = 4; + /** Wand operation type: trace outline of 8-connected pixels */ + public final static int EIGHT_CONNECTED = 8; + /** Wand operation type similar to that of ImageJ 1.42p and before; for backwards + * compatibility. + * In this mode, no checking is done whether the foreground or the background + * gets selected; four- or 8-connected behaviour depends on foreground/background + * and (if no selection) on whether the initial pixel is on a 1-pixel wide line. */ + public final static int LEGACY_MODE = 1; + /** The number of points in the generated outline. */ + public int npoints; + private int maxPoints = 1000; // will be increased if necessary + /** The x-coordinates of the points in the outline. + A vertical boundary at x separates the pixels at x-1 and x. */ + public int[] xpoints = new int[maxPoints]; + /** The y-coordinates of the points in the outline. + A horizontal boundary at y separates the pixels at y-1 and y. */ + public int[] ypoints = new int[maxPoints]; + + private final static int THRESHOLDED_MODE = 256; //work on threshold + private ImageProcessor ip; + private byte[] bpixels; + private int[] cpixels; + private short[] spixels; + private float[] fpixels; + private int width, height; + private float lowerThreshold, upperThreshold; + private int xmin; // of selection created + private boolean exactPixelValue; // For color, match RGB, not gray value + private static boolean allPoints; // output contains intermediate points + + + /** Constructs a Wand object from an ImageProcessor. */ + public Wand(ImageProcessor ip) { + this.ip = ip; + Object pixels = ip.getPixels(); + if (pixels instanceof byte[]) + bpixels = (byte[])pixels; + else if (pixels instanceof int[]) + cpixels = (int[])pixels; + else if (pixels instanceof short[]) + spixels = (short[])pixels; + else if (pixels instanceof float[]) + fpixels = (float[])pixels; + width = ip.getWidth(); + height = ip.getHeight(); + } + + + + /** Traces an object defined by lower and upper threshold values. + * 'mode' can be FOUR_CONNECTED or EIGHT_CONNECTED. + * ('LEGACY_MODE' is also supported and may result in selection of + * interior holes instead of the thresholded area if one clicks left + * of an interior hole). + * The start coordinates must be inside the area or left of it. + * When successful, npoints>0 and the boundary points can be accessed + * in the public xpoints and ypoints fields. */ + public void autoOutline(int startX, int startY, double lower, double upper, int mode) { + lowerThreshold = (float)lower; + upperThreshold = (float)upper; + autoOutline(startX, startY, 0.0, mode|THRESHOLDED_MODE); + } + + /** Traces an object defined by lower and upper threshold values or an + * interior hole; whatever is found first ('legacy mode'). + * For compatibility with previous versions of ImageJ. + * The start coordinates must be inside the area or left of it. + * When successful, npoints>0 and the boundary points can be accessed + * in the public xpoints and ypoints fields. */ + public void autoOutline(int startX, int startY, double lower, double upper) { + autoOutline(startX, startY, lower, upper, THRESHOLDED_MODE|LEGACY_MODE); + } + + /** This is a variation of legacy autoOutline that uses int threshold arguments. */ + public void autoOutline(int startX, int startY, int lower, int upper) { + autoOutline(startX, startY, (double)lower, (double)upper, THRESHOLDED_MODE|LEGACY_MODE); + } + + /** Traces the boundary of an area of uniform color, where + * 'startX' and 'startY' are somewhere inside the area. + * When successful, npoints>0 and the boundary points can be accessed + * in the public xpoints and ypoints fields. + * For compatibility with previous versions of ImageJ only; otherwise + * use the reliable method specifying 4-connected or 8-connected mode + * and the tolerance. */ + public void autoOutline(int startX, int startY) { + autoOutline(startX, startY, 0.0, LEGACY_MODE); + } + + /** Traces the boundary of the area with pixel values within + * 'tolerance' of the value of the pixel at the starting location. + * 'tolerance' is in uncalibrated units. + * 'mode' can be FOUR_CONNECTED or EIGHT_CONNECTED. + * Mode LEGACY_MODE is for compatibility with previous versions of ImageJ; + * ignored if tolerance > 0. + * Mode bit THRESHOLDED_MODE for internal use only; it is set by autoOutline + * with 'upper' and 'lower' arguments. + * When successful, npoints>0 and the boundary points can be accessed + * in the public xpoints and ypoints fields. */ + public void autoOutline(int startX, int startY, double tolerance, int mode) { + if (startX<0 || startX>=width || startY<0 || startY>=height) return; + if (fpixels!=null && Float.isNaN(getPixel(startX, startY))) return; + WandToolOptions.setStart(startX, startY); + exactPixelValue = tolerance==0; + boolean thresholdMode = (mode & THRESHOLDED_MODE) != 0; + boolean legacyMode = (mode & LEGACY_MODE) != 0 && tolerance == 0; + if (!thresholdMode) { + double startValue = getPixel(startX, startY); + lowerThreshold = (float)(startValue - tolerance); + upperThreshold = (float)(startValue + tolerance); + } + int x = startX; + int y = startY; + int seedX; // the first inside pixel + if (inside(x,y)) { // find a border when coming from inside + seedX = x; // (seedX, startY) is an inside pixel + do {x++;} while (inside(x,y)); + } else { // find a border when coming from outside (thresholded only) + do { + x++; + if (x>=width) return; // no border found + } while (!inside(x,y)); + seedX = x; + } + boolean fourConnected; + if (legacyMode) + fourConnected = !thresholdMode && !(isLine(x, y)); + else + fourConnected = (mode & FOUR_CONNECTED) != 0; + //now, we have a border between (x-1, y) and (x,y) + boolean first = true; + while (true) { // loop until we have not traced an inner hole + boolean insideSelected = traceEdge(x, y, fourConnected); + if (legacyMode) return; // in legacy mode, don't care what we have got + if (insideSelected) { // not an inner hole + if (first) return; // started at seed, so we got it (sucessful) + if (xmin<=seedX) { // possibly the correct particle + Polygon poly = new Polygon(xpoints, ypoints, npoints); + if (poly.contains(seedX, startY)) + return; // successful, particle contains seed + } + } + first = false; + // we have traced an inner hole or the wrong particle + if (!inside(x,y)) do { + x++; // traverse the hole + if (x>width) throw new RuntimeException("Wand Malfunction"); //should never happen + } while (!inside(x,y)); + do {x++;} while (inside(x,y)); //retry here; maybe no inner hole any more + } + } + + + /* Trace the outline, starting at a point (startX, startY). + * Pixel (startX-1, startY) must be outside, (startX, startY) must be inside, + * or reverse. Otherwise an endless loop will occur (and eat up all memory). + * Traces 8-connected inside pixels unless fourConnected is true. + * Returns whether the selection created encloses an 'inside' area + * and not an inner hole. + */ + private boolean traceEdge(int startX, int startY, boolean fourConnected) { + // Let us name the crossings between 4 pixels vertices, then the + // vertex (x,y) marked with '+', is between pixels (x-1, y-1) and (x,y): + // + // pixel x-1 x + // y-1 | + // ----+---- + // y | + // + // The four principal directions are numbered such that the direction + // number * 90 degrees gives the angle in the mathematical sense; and + // the directions to the adjacent pixels (for inside(x,y,direction) are + // at (number * 90 - 45) degrees: + // walking pixel + // directions: 1 directions: 2 | 1 + // 2 + 0 ----+---- + // 3 3 | 0 + // + // Directions, like angles, are cyclic; direction -1 = direction 3, etc. + // + // The algorithm: We walk along the border, from one vertex to the next, + // with the outside pixels always being at the left-hand side. + // For 8-connected tracing, we always trying to turn left as much as + // possible, to encompass an area as large as possible. + // Thus, when walking in direction 1 (up, -y), we start looking + // at the pixel in direction 2; if it is inside, we proceed in this + // direction (left); otherwise we try with direction 1 (up); if pixel 1 + // is not inside, we must proceed in direction 0 (right). + // + // 2 | 1 (i=inside, o=outside) + // direction 2 < ---+---- > direction 0 + // o | i + // ^ direction 1 = up = starting direction + // + // For 4-connected pixels, we try to go right as much as possible: + // First try with pixel 1; if it is outside we go in direction 0 (right). + // Otherwise, we examine pixel 2; if it is outside, we go in + // direction 1 (up); otherwise in direction 2 (left). + // + // When moving a closed loop, 'direction' gets incremented or decremented + // by a total of 360 degrees (i.e., 4) for counterclockwise and clockwise + // loops respectively. As the inside pixels are at the right side, we have + // got an outline of inner pixels after a cw loop (direction decremented + // by 4). + // + npoints = 0; + xmin = width; + final int startDirection; + if (inside(startX,startY)) // inside at left, outside right + startDirection = 1; // starting in direction 1 = up + else { + startDirection = 3; // starting in direction 3 = down + startY++; // continue after the boundary that has direction 3 + } + int x = startX; + int y = startY; + int direction = startDirection; + do { + int newDirection; + if (fourConnected) { + newDirection = direction; + do { + if (!inside(x, y, newDirection)) break; + newDirection++; + } while (newDirection < direction+2); + newDirection--; + } else { // 8-connected + newDirection = direction + 1; + do { + if (inside(x, y, newDirection)) break; + newDirection--; + } while (newDirection >= direction); + } + if (allPoints || newDirection!=direction) + addPoint(x,y); // a corner point of the outline polygon: add to list + switch (newDirection & 3) { // '& 3' is remainder modulo 4 + case 0: x++; break; + case 1: y--; break; + case 2: x--; break; + case 3: y++; break; + } + direction = newDirection; + } while (x!=startX || y!=startY || (direction&3)!=startDirection); + if (xpoints[0]!=x && !allPoints) // if the start point = end point is a corner: add to list + addPoint(x, y); + return (direction <= 0); // if we have done a clockwise loop, inside pixels are enclosed + } + + // add a point x,y to the outline polygon + private void addPoint (int x, int y) { + if (npoints==maxPoints) { + int[] xtemp = new int[maxPoints*2]; + int[] ytemp = new int[maxPoints*2]; + System.arraycopy(xpoints, 0, xtemp, 0, maxPoints); + System.arraycopy(ypoints, 0, ytemp, 0, maxPoints); + xpoints = xtemp; + ypoints = ytemp; + maxPoints *= 2; + } + xpoints[npoints] = x; + ypoints[npoints] = y; + npoints++; + if (xmin > x) xmin = x; + } + + // check pixel at (x,y), whether it is inside traced area + private boolean inside(int x, int y) { + if (x<0 || x>=width || y<0 || y>=height) + return false; + float value = getPixel(x, y); + return value>=lowerThreshold && value<=upperThreshold; + } + + // check pixel in a given direction from vertex (x,y) + private boolean inside(int x, int y, int direction) { + switch(direction & 3) { // '& 3' is remainder modulo 4 + case 0: return inside(x, y); + case 1: return inside(x, y-1); + case 2: return inside(x-1, y-1); + case 3: return inside(x-1, y); + } + return false; //will never occur, needed for the compiler + } + + // get a pixel value; returns Float.NaN if outside the field. + private float getPixel(int x, int y) { + if (x<0 || x>=width || y<0 || y>=height) + return Float.NaN; + if (bpixels!=null) + return bpixels[y*width + x] & 0xff; + else if (spixels!=null) + return spixels[y*width + x] & 0xffff; + else if (fpixels!=null) + return fpixels[y*width + x]; + else if (exactPixelValue) //RGB for exact match + return cpixels[y*width + x] & 0xffffff; //don't care for upper byte + else //gray value of RGB + return ip.getPixelValue(x,y); + } + + /* Are we tracing a one pixel wide line? Makes Legacy mode 8-connected instead of 4-connected */ + private boolean isLine(int xs, int ys) { + int r = 5; + int xmin=xs; + int xmax=xs+2*r; + if (xmax>=width) xmax=width-1; + int ymin=ys-r; + if (ymin<0) ymin=0; + int ymax=ys+r; + if (ymax>=height) ymax=height-1; + int area = 0; + int insideCount = 0; + for (int x=xmin; (x<=xmax); x++) + for (int y=ymin; y<=ymax; y++) { + area++; + if (inside(x,y)) + insideCount++; + } + if (IJ.debugMode) + IJ.log((((double)insideCount)/area<0.25?"line ":"blob ")+insideCount+" "+area+" "+IJ.d2s(((double)insideCount)/area)); + return ((double)insideCount)/area<0.25; + } + + /** Set 'true' and output will contain intermediate points for straight lines longer than one pixel. */ + public static void setAllPoints(boolean b) { + allPoints = b; + } + + /** Returns 'true' if output contains intermediate points for straight lines longer than one pixel. */ + public static boolean allPoints() { + return allPoints; + } + +} diff --git a/src/ij/gui/YesNoCancelDialog.java b/src/ij/gui/YesNoCancelDialog.java new file mode 100644 index 0000000..7f7e79b --- /dev/null +++ b/src/ij/gui/YesNoCancelDialog.java @@ -0,0 +1,138 @@ +package ij.gui; +import ij.*; +import java.awt.*; +import java.awt.event.*; + +/** A modal dialog box with a one line message and + "Yes", "No" and "Cancel" buttons. */ +public class YesNoCancelDialog extends Dialog implements ActionListener, KeyListener, WindowListener { + private Button yesB, noB, cancelB; + private boolean cancelPressed, yesPressed; + private boolean firstPaint = true; + + public YesNoCancelDialog(Frame parent, String title, String msg) { + this(parent, title, msg, " Yes ", " No "); + } + + public YesNoCancelDialog(Frame parent, String title, String msg, String yesLabel, String noLabel) { + super(parent, title, true); + setLayout(new BorderLayout()); + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 10, 10)); + MultiLineLabel message = new MultiLineLabel(msg); + message.setFont(new Font("Dialog", Font.PLAIN, 14)); + panel.add(message); + add("North", panel); + + panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.RIGHT, 15, 8)); + if (msg.startsWith("Save")) { + yesB = new Button(" Save "); + noB = new Button("Don't Save"); + cancelB = new Button(" Cancel "); + } else { + yesB = new Button(yesLabel); + noB = new Button(noLabel); + cancelB = new Button(" Cancel "); + } + yesB.addActionListener(this); + noB.addActionListener(this); + cancelB.addActionListener(this); + yesB.addKeyListener(this); + noB.addKeyListener(this); + cancelB.addKeyListener(this); + if (IJ.isWindows() || Prefs.dialogCancelButtonOnRight) { + panel.add(yesB); + panel.add(noB); + panel.add(cancelB); + } else { + panel.add(noB); + panel.add(cancelB); + panel.add(yesB); + } + if (IJ.isMacintosh()) + setResizable(false); + add("South", panel); + addWindowListener(this); + GUI.scale(this); + pack(); + yesB.requestFocusInWindow(); + GUI.centerOnImageJScreen(this); + show(); + } + + public void actionPerformed(ActionEvent e) { + if (e.getSource()==cancelB) + cancelPressed = true; + else if (e.getSource()==yesB) + yesPressed = true; + closeDialog(); + } + + /** Returns true if the user dismissed dialog by pressing "Cancel". */ + public boolean cancelPressed() { + return cancelPressed; + } + + /** Returns true if the user dismissed dialog by pressing "Yes". */ + public boolean yesPressed() { + return yesPressed; + } + + void closeDialog() { + dispose(); + } + + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyDown(keyCode); + if (keyCode==KeyEvent.VK_ENTER) { + if (cancelB.isFocusOwner()) { + cancelPressed = true; + closeDialog(); + } else if (noB.isFocusOwner()) { + closeDialog(); + } else { + yesPressed = true; + closeDialog(); + } + } else if (keyCode==KeyEvent.VK_Y||keyCode==KeyEvent.VK_S) { + yesPressed = true; + closeDialog(); + } else if (keyCode==KeyEvent.VK_N || keyCode==KeyEvent.VK_D) { + closeDialog(); + } else if (keyCode==KeyEvent.VK_ESCAPE||keyCode==KeyEvent.VK_C) { + cancelPressed = true; + closeDialog(); + IJ.resetEscape(); + } + } + + public void keyReleased(KeyEvent e) { + int keyCode = e.getKeyCode(); + IJ.setKeyUp(keyCode); + } + + public void keyTyped(KeyEvent e) {} + + public void paint(Graphics g) { + super.paint(g); + if (firstPaint) { + yesB.requestFocus(); + firstPaint = false; + } + } + + public void windowClosing(WindowEvent e) { + cancelPressed = true; + closeDialog(); + } + + public void windowActivated(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + +} diff --git a/src/ij/io/BitBuffer.java b/src/ij/io/BitBuffer.java new file mode 100644 index 0000000..18832e3 --- /dev/null +++ b/src/ij/io/BitBuffer.java @@ -0,0 +1,65 @@ +package ij.io; + +/** + * A class for reading arbitrary numbers of bits from a byte array. + * @author Eric Kjellman egkjellman at wisc.edu + */ +public class BitBuffer { + + private int currentByte; + private int currentBit; + private byte[] byteBuffer; + private int eofByte; + private int[] backMask; + private int[] frontMask; + private boolean eofFlag; + + public BitBuffer(byte[] byteBuffer) { + this.byteBuffer = byteBuffer; + currentByte = 0; + currentBit = 0; + eofByte = byteBuffer.length; + backMask = new int[] {0x0000, 0x0001, 0x0003, 0x0007, + 0x000F, 0x001F, 0x003F, 0x007F}; + frontMask = new int[] {0x0000, 0x0080, 0x00C0, 0x00E0, + 0x00F0, 0x00F8, 0x00FC, 0x00FE}; + } + + public int getBits(int bitsToRead) { + if (bitsToRead == 0) + return 0; + if (eofFlag) + return -1; // Already at end of file + int toStore = 0; + while(bitsToRead != 0 && !eofFlag) { + if (bitsToRead >= 8 - currentBit) { + if (currentBit == 0) { // special + toStore = toStore << 8; + int cb = ((int) byteBuffer[currentByte]); + toStore += (cb<0 ? (int) 256 + cb : (int) cb); + bitsToRead -= 8; + currentByte++; + } else { + toStore = toStore << (8 - currentBit); + toStore += ((int) byteBuffer[currentByte]) & backMask[8 - currentBit]; + bitsToRead -= (8 - currentBit); + currentBit = 0; + currentByte++; + } + } else { + toStore = toStore << bitsToRead; + int cb = ((int) byteBuffer[currentByte]); + cb = (cb<0 ? (int) 256 + cb : (int) cb); + toStore += ((cb) & (0x00FF - frontMask[currentBit])) >> (8 - (currentBit + bitsToRead)); + currentBit += bitsToRead; + bitsToRead = 0; + } + if (currentByte == eofByte) { + eofFlag = true; + return toStore; + } + } + return toStore; + } + +} diff --git a/src/ij/io/DirectoryChooser.java b/src/ij/io/DirectoryChooser.java new file mode 100644 index 0000000..f2944ad --- /dev/null +++ b/src/ij/io/DirectoryChooser.java @@ -0,0 +1,137 @@ +package ij.io; +import ij.*; +import ij.gui.*; +import ij.plugin.frame.Recorder; +import ij.util.Java2; +import java.awt.*; +import java.io.*; +import javax.swing.*; +import javax.swing.filechooser.*; + +/** This class displays a dialog box that allows the user can select a directory. */ + public class DirectoryChooser { + private String directory; + private String title; + + /** Display a dialog using the specified title. */ + public DirectoryChooser(String title) { + this.title = title; + if (IJ.isMacOSX() && !Prefs.useJFileChooser) + getDirectoryUsingFileDialog(title); + else { + String macroOptions = Macro.getOptions(); + if (macroOptions!=null) + directory = Macro.getValue(macroOptions, title, null); + if (directory==null) { + if (EventQueue.isDispatchThread()) + getDirectoryUsingJFileChooserOnThisThread(title); + else + getDirectoryUsingJFileChooser(title); + } + } + } + + // runs JFileChooser on event dispatch thread to avoid possible thread deadlocks + void getDirectoryUsingJFileChooser(final String title) { + Java2.setSystemLookAndFeel(); + try { + EventQueue.invokeAndWait(new Runnable() { + public void run() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle(title); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setDragEnabled(true); + chooser.setTransferHandler(new DragAndDropHandler(chooser)); + String defaultDir = OpenDialog.getDefaultDirectory(); + if (defaultDir!=null) { + File f = new File(defaultDir); + if (IJ.debugMode) + IJ.log("DirectoryChooser,setSelectedFileW: "+f); + chooser.setSelectedFile(f); + } + chooser.setApproveButtonText("Select"); + if (chooser.showOpenDialog(null)==JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + directory = file.getAbsolutePath(); + directory = IJ.addSeparator(directory); + OpenDialog.setDefaultDirectory(directory); + } + } + }); + } catch (Exception e) {} + } + + // Choose a directory using JFileChooser on the current thread + void getDirectoryUsingJFileChooserOnThisThread(final String title) { + Java2.setSystemLookAndFeel(); + try { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle(title); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setDragEnabled(true); + chooser.setTransferHandler(new DragAndDropHandler(chooser)); + String defaultDir = OpenDialog.getDefaultDirectory(); + if (defaultDir!=null) { + File f = new File(defaultDir); + if (IJ.debugMode) + IJ.log("DirectoryChooser,setSelectedFile: "+f); + chooser.setSelectedFile(f); + } + chooser.setApproveButtonText("Select"); + if (chooser.showOpenDialog(null)==JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + directory = file.getAbsolutePath(); + directory = IJ.addSeparator(directory); + OpenDialog.setDefaultDirectory(directory); + } + } catch (Exception e) {} + } + + // On Mac OS X, we can select directories using the native file open dialog + void getDirectoryUsingFileDialog(String title) { + boolean saveUseJFC = Prefs.useJFileChooser; + Prefs.useJFileChooser = false; + System.setProperty("apple.awt.fileDialogForDirectories", "true"); + String dir=null, name=null; + String defaultDir = OpenDialog.getDefaultDirectory(); + if (defaultDir!=null) { + File f = new File(defaultDir); + dir = f.getParent(); + name = f.getName(); + } + if (IJ.debugMode) + IJ.log("DirectoryChooser: dir=\""+dir+"\", file=\""+name+"\""); + OpenDialog od = new OpenDialog(title, dir, null); + String odDir = od.getDirectory(); + if (odDir==null) + directory = null; + else { + directory = odDir + od.getFileName() + "/"; + OpenDialog.setDefaultDirectory(directory); + } + System.setProperty("apple.awt.fileDialogForDirectories", "false"); + Prefs.useJFileChooser = saveUseJFC; + } + + /** Returns the directory selected by the user. */ + public String getDirectory() { + if (IJ.debugMode) + IJ.log("DirectoryChooser.getDirectory: "+directory); + if (Recorder.record && !IJ.isMacOSX()) + Recorder.recordPath(title, directory); + return directory; + } + + /** Sets the default directory presented in the dialog. */ + public static void setDefaultDirectory(String dir) { + if (dir==null || (new File(dir)).isDirectory()) + OpenDialog.setDefaultDirectory(dir); + } + + //private void setSystemLookAndFeel() { + // try { + // UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + // } catch(Throwable t) {} + //} + +} diff --git a/src/ij/io/DragAndDropHandler.java b/src/ij/io/DragAndDropHandler.java new file mode 100644 index 0000000..d14227f --- /dev/null +++ b/src/ij/io/DragAndDropHandler.java @@ -0,0 +1,95 @@ +package ij.io; +import ij.*; +import ij.gui.*; +import ij.plugin.frame.Recorder; +import ij.util.Java2; +import java.awt.*; +import java.io.*; +import java.util.ArrayList; //no need to import java.util.List; it would be ambiguous because of java.awt.List +import java.net.URI; +import java.net.URISyntaxException; +import javax.swing.*; +import javax.swing.filechooser.*; +import java.awt.datatransfer.*; + +/** This class handles drag&drop onto JFileChoosers. */ + public class DragAndDropHandler extends TransferHandler { + private JFileChooser jFileChooser; + + /** Given a JFileChooser 'fc', this is how to use this class: + *

+	 *     fc.setDragEnabled(true);
+	 *     fc.setTransferHandler(new DragAndDropHandler(fc));
+	 * 
+ */ + public DragAndDropHandler(JFileChooser jFileChooser) { + super(); + this.jFileChooser = jFileChooser; + } + + /** Returns whether any of the transfer flavors is supported */ + public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { + for (DataFlavor dataFlavor : transferFlavors) { + if (isSupportedTransferFlavor(dataFlavor)) + return true; + } + return false; + } + + /** Imports the drag&drop file or list of files and sets the JFileChooser to this. + * Returns true if successful */ + public boolean importData(JComponent comp, Transferable t) { + DataFlavor[] transferFlavors = t.getTransferDataFlavors(); + for (DataFlavor dataFlavor : transferFlavors) { + try { + java.util.List fileList = null; + if (dataFlavor.isFlavorJavaFileListType()) { + fileList = (java.util.List)t.getTransferData(DataFlavor.javaFileListFlavor); + if (IJ.debugMode) IJ.log("dragAndDrop FileList size="+fileList.size()+" first: "+fileList.get(0)); + } else if (isSupportedTransferFlavor(dataFlavor)) { + String str = (String)t.getTransferData(dataFlavor); + if (IJ.debugMode) IJ.log("dragAndDrop str="+str); + String[] strs = str.split("[\n\r]+"); //multiple files are separate lines + fileList = new ArrayList(strs.length); + for (String s : strs) { + if (s.length() > 0) { + File file = new File(s); //Try whether it is a plain path (pointing to an existing file) + if (!file.exists()) try { + file = new File(new URI(s)); //When not successful, try a whether it is a URL, e.g. file:///absolute/path/to/file + } catch (URISyntaxException e) {continue;} + if (file.exists()) //We only accept drag&drop of existing files + fileList.add(file); + } + } + } + if (fileList == null || fileList.size() ==0) continue; //this data flavor did not work + + File firstFile = fileList.get(0); + if (jFileChooser.isMultiSelectionEnabled()) { //multiple files accepted + File dir = firstFile.getParentFile(); + jFileChooser.setCurrentDirectory(dir); + File[] files = fileList.toArray(new File[0]); + jFileChooser.setSelectedFiles(files); + } else { + File file = firstFile; //single file required; if we get more take the first one + if (jFileChooser.getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY && !file.isDirectory()) + file = file.getParentFile(); //if a directory is required and we get a file, use its directory + if (jFileChooser.getDialogType() == JFileChooser.SAVE_DIALOG && file.isDirectory()) + jFileChooser.setCurrentDirectory(file); //in a save operation, if we get a directory, move there + else + jFileChooser.setSelectedFile(file); + } + jFileChooser.rescanCurrentDirectory(); + return true; + } catch (Exception e) {if (IJ.debugMode) IJ.handleException(e);} + } + return false; + } + + /** Returns whether this transfer flavor is supported. We support File Lists and Strings (plain or as list of URLs). */ + public boolean isSupportedTransferFlavor(DataFlavor flavor) { + return flavor.isFlavorJavaFileListType() || + (flavor.getRepresentationClass() == String.class && + (flavor.getMimeType().startsWith("text/uri-list") || flavor.getMimeType().startsWith("text/plain"))); + } +} diff --git a/src/ij/io/FileInfo.java b/src/ij/io/FileInfo.java new file mode 100644 index 0000000..f2f303d --- /dev/null +++ b/src/ij/io/FileInfo.java @@ -0,0 +1,270 @@ +package ij.io; +import ij.VirtualStack; +import ij.IJ; +import java.io.*; +import java.util.Properties; + +/** This class consists of public fields that describe an image file. */ +public class FileInfo implements Cloneable { + + /** 8-bit unsigned integer (0-255). */ + public static final int GRAY8 = 0; + + /** 16-bit signed integer (-32768-32767). Imported signed images + are converted to unsigned by adding 32768. */ + public static final int GRAY16_SIGNED = 1; + + /** 16-bit unsigned integer (0-65535). */ + public static final int GRAY16_UNSIGNED = 2; + + /** 32-bit signed integer. Imported 32-bit integer images are + converted to floating-point. */ + public static final int GRAY32_INT = 3; + + /** 32-bit floating-point. */ + public static final int GRAY32_FLOAT = 4; + + /** 8-bit unsigned integer with color lookup table. */ + public static final int COLOR8 = 5; + + /** 24-bit interleaved RGB. Import/export only. */ + public static final int RGB = 6; + + /** 24-bit planer RGB. Import only. */ + public static final int RGB_PLANAR = 7; + + /** 1-bit black and white. Import only. */ + public static final int BITMAP = 8; + + /** 32-bit interleaved ARGB. Import only. */ + public static final int ARGB = 9; + + /** 24-bit interleaved BGR. Import only. */ + public static final int BGR = 10; + + /** 32-bit unsigned integer. Imported 32-bit integer images are + converted to floating-point. */ + public static final int GRAY32_UNSIGNED = 11; + + /** 48-bit interleaved RGB. */ + public static final int RGB48 = 12; + + /** 12-bit unsigned integer (0-4095). Import only. */ + public static final int GRAY12_UNSIGNED = 13; + + /** 24-bit unsigned integer. Import only. */ + public static final int GRAY24_UNSIGNED = 14; + + /** 32-bit interleaved BARG (MCID). Import only. */ + public static final int BARG = 15; + + /** 64-bit floating-point. Import only.*/ + public static final int GRAY64_FLOAT = 16; + + /** 48-bit planar RGB. Import only. */ + public static final int RGB48_PLANAR = 17; + + /** 32-bit interleaved ABGR. Import only. */ + public static final int ABGR = 18; + + /** 32-bit interleaved CMYK. Import only. */ + public static final int CMYK = 19; + + // File formats + public static final int UNKNOWN = 0; + public static final int RAW = 1; + public static final int TIFF = 2; + public static final int GIF_OR_JPG = 3; + public static final int FITS = 4; + public static final int BMP = 5; + public static final int DICOM = 6; + public static final int ZIP_ARCHIVE = 7; + public static final int PGM = 8; + public static final int IMAGEIO = 9; + + // Compression modes + public static final int COMPRESSION_UNKNOWN = 0; + public static final int COMPRESSION_NONE= 1; + public static final int LZW = 2; + public static final int LZW_WITH_DIFFERENCING = 3; + public static final int JPEG = 4; + public static final int PACK_BITS = 5; + public static final int ZIP = 6; + + /* File format (TIFF, GIF_OR_JPG, BMP, etc.). Used by the File/Revert command */ + public int fileFormat; + + /* File type (GRAY8, GRAY_16_UNSIGNED, RGB, etc.) */ + public int fileType; + public String fileName; + public String directory; + public String url; + public int width; + public int height; + public int offset=0; // Use getOffset() to read + public int nImages; + public int gapBetweenImages; // Use getGap() to read + public boolean whiteIsZero; + public boolean intelByteOrder; + public int compression; + public int[] stripOffsets; + public int[] stripLengths; + public int rowsPerStrip; + public int lutSize; + public byte[] reds; + public byte[] greens; + public byte[] blues; + public Object pixels; + public String debugInfo; + public String[] sliceLabels; + public String info; + public InputStream inputStream; + public VirtualStack virtualStack; + public int sliceNumber; // used by FileInfoVirtualStack + + public double pixelWidth=1.0; + public double pixelHeight=1.0; + public double pixelDepth=1.0; + public String unit; + public int calibrationFunction; + public double[] coefficients; + public String valueUnit; + public double frameInterval; + public String description; + // Use longOffset instead of offset when offset>2147483647. + public long longOffset; // Use getOffset() to read + // Use longGap instead of gapBetweenImages when gap>2147483647. + public long longGap; // Use getGap() to read + // Extra metadata to be stored in the TIFF header + public int[] metaDataTypes; // must be < 0xffffff + public byte[][] metaData; + public double[] displayRanges; + public byte[][] channelLuts; + public byte[] plot; // serialized plot + public byte[] roi; // serialized roi + public byte[][] overlay; // serialized overlay objects + public int samplesPerPixel; + public String openNextDir, openNextName; + public String[] properties; // {key,value,key,value,...} + public boolean imageSaved; + + /** Creates a FileInfo object with all of its fields set to their default value. */ + public FileInfo() { + // assign default values + fileFormat = UNKNOWN; + fileType = GRAY8; + fileName = "Untitled"; + directory = ""; + url = ""; + nImages = 1; + compression = COMPRESSION_NONE; + samplesPerPixel = 1; + } + + /** Returns the file path. */ + public String getFilePath() { + String dir = directory; + if (dir==null) + dir = ""; + dir = IJ.addSeparator(dir); + return dir + fileName; + } + + /** Returns the offset as a long. */ + public final long getOffset() { + return longOffset>0L?longOffset:((long)offset)&0xffffffffL; + } + + /** Returns the gap between images as a long. */ + public final long getGap() { + return longGap>0L?longGap:((long)gapBetweenImages)&0xffffffffL; + } + + /** Returns the number of bytes used per pixel. */ + public int getBytesPerPixel() { + switch (fileType) { + case GRAY8: case COLOR8: case BITMAP: return 1; + case GRAY16_SIGNED: case GRAY16_UNSIGNED: return 2; + case GRAY32_INT: case GRAY32_UNSIGNED: case GRAY32_FLOAT: case ARGB: case GRAY24_UNSIGNED: case BARG: case ABGR: case CMYK: return 4; + case RGB: case RGB_PLANAR: case BGR: return 3; + case RGB48: case RGB48_PLANAR: return 6; + case GRAY64_FLOAT : return 8; + default: return 0; + } + } + + public String toString() { + return + "name=" + fileName + + ", dir=" + directory + + ", width=" + width + + ", height=" + height + + ", nImages=" + nImages + + ", offset=" + getOffset() + + ", gap=" + getGap() + + ", type=" + getType() + + ", byteOrder=" + (intelByteOrder?"little":"big") + + ", format=" + fileFormat + + ", url=" + url + + ", whiteIsZero=" + (whiteIsZero?"t":"f") + + ", lutSize=" + lutSize + + ", comp=" + compression + + ", ranges=" + (displayRanges!=null?""+displayRanges.length/2:"null") + + ", samples=" + samplesPerPixel; + } + + /** Returns JavaScript code that can be used to recreate this FileInfo. */ + public String getCode() { + String code = "fi = new FileInfo();\n"; + String type = null; + if (fileType==GRAY8) + type = "GRAY8"; + else if (fileType==GRAY16_UNSIGNED) + type = "GRAY16_UNSIGNED"; + else if (fileType==GRAY32_FLOAT) + type = "GRAY32_FLOAT"; + else if (fileType==RGB) + type = "RGB"; + if (type!=null) + code += "fi.fileType = FileInfo."+type+";\n"; + code += "fi.width = "+width+";\n"; + code += "fi.height = "+height+";\n"; + if (nImages>1) + code += "fi.nImages = "+nImages+";\n"; + if (getOffset()>0) + code += "fi.longOffset = "+getOffset()+";\n"; + if (intelByteOrder) + code += "fi.intelByteOrder = true;\n"; + return code; + } + + private String getType() { + switch (fileType) { + case GRAY8: return "byte"; + case GRAY16_SIGNED: return "short"; + case GRAY16_UNSIGNED: return "ushort"; + case GRAY32_INT: return "int"; + case GRAY32_UNSIGNED: return "uint"; + case GRAY32_FLOAT: return "float"; + case COLOR8: return "byte(lut)"; + case RGB: return "RGB"; + case RGB_PLANAR: return "RGB(p)"; + case RGB48: return "RGB48"; + case BITMAP: return "bitmap"; + case ARGB: return "ARGB"; + case ABGR: return "ABGR"; + case BGR: return "BGR"; + case BARG: return "BARG"; + case CMYK: return "CMYK"; + case GRAY64_FLOAT: return "double"; + case RGB48_PLANAR: return "RGB48(p)"; + default: return ""; + } + } + + public synchronized Object clone() { + try {return super.clone();} + catch (CloneNotSupportedException e) {return null;} + } + +} diff --git a/src/ij/io/FileOpener.java b/src/ij/io/FileOpener.java new file mode 100644 index 0000000..202121a --- /dev/null +++ b/src/ij/io/FileOpener.java @@ -0,0 +1,676 @@ +package ij.io; +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.net.*; +import java.util.*; +import java.util.zip.GZIPInputStream; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import ij.*; +import ij.plugin.frame.*; + +/** + * Opens or reverts an image specified by a FileInfo object. Images can + * be loaded from either a file (directory+fileName) or a URL (url+fileName). + * Here is an example: + *
+ *   public class FileInfo_Test implements PlugIn {
+ *     public void run(String arg) {
+ *       FileInfo fi = new FileInfo();
+ *       fi.width = 256;
+ *       fi.height = 254;
+ *       fi.offset = 768;
+ *       fi.fileName = "blobs.tif";
+ *       fi.directory = "/Users/wayne/Desktop/";
+ *       new FileOpener(fi).open();
+ *     }  
+ *   }	
+ * 
+ */ +public class FileOpener { + + private FileInfo fi; + private int width, height; + private static boolean showConflictMessage = true; + private double minValue, maxValue; + private static boolean silentMode; + + public FileOpener(FileInfo fi) { + this.fi = fi; + if (fi!=null) { + width = fi.width; + height = fi.height; + } + if (IJ.debugMode) IJ.log("FileInfo: "+fi); + } + + /** Opens the image and returns it has an ImagePlus object. */ + public ImagePlus openImage() { + boolean wasRecording = Recorder.record; + Recorder.record = false; + ImagePlus imp = open(false); + Recorder.record = wasRecording; + return imp; + } + + /** Opens the image and displays it. */ + public void open() { + open(true); + } + + /** Obsolete, replaced by openImage() and open(). */ + public ImagePlus open(boolean show) { + + ImagePlus imp=null; + Object pixels; + ProgressBar pb=null; + ImageProcessor ip; + + ColorModel cm = createColorModel(fi); + if (fi.nImages>1) + return openStack(cm, show); + switch (fi.fileType) { + case FileInfo.GRAY8: + case FileInfo.COLOR8: + case FileInfo.BITMAP: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ByteProcessor(width, height, (byte[])pixels, cm); + imp = new ImagePlus(fi.fileName, ip); + break; + case FileInfo.GRAY16_SIGNED: + case FileInfo.GRAY16_UNSIGNED: + case FileInfo.GRAY12_UNSIGNED: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ShortProcessor(width, height, (short[])pixels, cm); + imp = new ImagePlus(fi.fileName, ip); + break; + case FileInfo.GRAY32_INT: + case FileInfo.GRAY32_UNSIGNED: + case FileInfo.GRAY32_FLOAT: + case FileInfo.GRAY24_UNSIGNED: + case FileInfo.GRAY64_FLOAT: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new FloatProcessor(width, height, (float[])pixels, cm); + imp = new ImagePlus(fi.fileName, ip); + break; + case FileInfo.RGB: + case FileInfo.BGR: + case FileInfo.ARGB: + case FileInfo.ABGR: + case FileInfo.BARG: + case FileInfo.RGB_PLANAR: + case FileInfo.CMYK: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ColorProcessor(width, height, (int[])pixels); + if (fi.fileType==FileInfo.CMYK) + ip.invert(); + imp = new ImagePlus(fi.fileName, ip); + break; + case FileInfo.RGB48: + case FileInfo.RGB48_PLANAR: + boolean planar = fi.fileType==FileInfo.RGB48_PLANAR; + Object[] pixelArray = (Object[])readPixels(fi); + if (pixelArray==null) return null; + int nChannels = 3; + ImageStack stack = new ImageStack(width, height); + stack.addSlice("Red", pixelArray[0]); + stack.addSlice("Green", pixelArray[1]); + stack.addSlice("Blue", pixelArray[2]); + if (fi.samplesPerPixel==4 && pixelArray.length==4) { + stack.addSlice("Gray", pixelArray[3]); + nChannels = 4; + } + imp = new ImagePlus(fi.fileName, stack); + imp.setDimensions(nChannels, 1, 1); + if (planar) + imp.getProcessor().resetMinAndMax(); + imp.setFileInfo(fi); + int mode = IJ.COMPOSITE; + if (fi.description!=null) { + if (fi.description.indexOf("mode=color")!=-1) + mode = IJ.COLOR; + else if (fi.description.indexOf("mode=gray")!=-1) + mode = IJ.GRAYSCALE; + } + imp = new CompositeImage(imp, mode); + if (!planar && fi.displayRanges==null) { + if (nChannels==4) + ((CompositeImage)imp).resetDisplayRanges(); + else { + for (int c=1; c<=3; c++) { + imp.setPosition(c, 1, 1); + imp.setDisplayRange(minValue, maxValue); + } + imp.setPosition(1, 1, 1); + } + } + if (fi.whiteIsZero) // cmyk? + IJ.run(imp, "Invert", ""); + break; + } + imp.setFileInfo(fi); + setCalibration(imp); + if (fi.info!=null) + imp.setProperty("Info", fi.info); + if (fi.sliceLabels!=null&&fi.sliceLabels.length==1&&fi.sliceLabels[0]!=null) + imp.setProp("Slice_Label", fi.sliceLabels[0]); + if (fi.plot!=null) try { + Plot plot = new Plot(imp, new ByteArrayInputStream(fi.plot)); + imp.setProperty(Plot.PROPERTY_KEY, plot); + } catch (Exception e) { IJ.handleException(e); } + if (fi.roi!=null) + decodeAndSetRoi(imp, fi); + if (fi.overlay!=null) + setOverlay(imp, fi.overlay); + if (fi.properties!=null) + imp.setProperties(fi.properties); + if (show) imp.show(); + return imp; + } + + public ImageProcessor openProcessor() { + Object pixels; + ProgressBar pb=null; + ImageProcessor ip = null; + ColorModel cm = createColorModel(fi); + switch (fi.fileType) { + case FileInfo.GRAY8: + case FileInfo.COLOR8: + case FileInfo.BITMAP: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ByteProcessor(width, height, (byte[])pixels, cm); + break; + case FileInfo.GRAY16_SIGNED: + case FileInfo.GRAY16_UNSIGNED: + case FileInfo.GRAY12_UNSIGNED: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ShortProcessor(width, height, (short[])pixels, cm); + break; + case FileInfo.GRAY32_INT: + case FileInfo.GRAY32_UNSIGNED: + case FileInfo.GRAY32_FLOAT: + case FileInfo.GRAY24_UNSIGNED: + case FileInfo.GRAY64_FLOAT: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new FloatProcessor(width, height, (float[])pixels, cm); + break; + case FileInfo.RGB: + case FileInfo.BGR: + case FileInfo.ARGB: + case FileInfo.ABGR: + case FileInfo.BARG: + case FileInfo.RGB_PLANAR: + case FileInfo.CMYK: + pixels = readPixels(fi); + if (pixels==null) return null; + ip = new ColorProcessor(width, height, (int[])pixels); + if (fi.fileType==FileInfo.CMYK) + ip.invert(); + break; + } + return ip; + } + + void setOverlay(ImagePlus imp, byte[][] rois) { + Overlay overlay = new Overlay(); + Overlay proto = null; + for (int i=0; i1) + IJ.setTool("multi-point"); + } + + void setStackDisplayRange(ImagePlus imp) { + ImageStack stack = imp.getStack(); + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + int n = stack.size(); + for (int i=1; i<=n; i++) { + if (!silentMode) + IJ.showStatus("Calculating stack min and max: "+i+"/"+n); + ImageProcessor ip = stack.getProcessor(i); + ip.resetMinAndMax(); + if (ip.getMin()max) + max = ip.getMax(); + } + imp.getProcessor().setMinAndMax(min, max); + imp.updateAndDraw(); + } + + /** Restores the original version of the specified image. */ + public void revertToSaved(ImagePlus imp) { + if (fi==null) + return; + String path = fi.getFilePath(); + if (fi.url!=null && !fi.url.equals("") && (fi.directory==null||fi.directory.equals(""))) + path = fi.url; + IJ.showStatus("Loading: " + path); + ImagePlus imp2 = null; + if (!path.endsWith(".raw")) + imp2 = IJ.openImage(path); + if (imp2!=null) + imp.setImage(imp2); + else { + if (fi.nImages>1) + return; + Object pixels = readPixels(fi); + if (pixels==null) return; + ColorModel cm = createColorModel(fi); + ImageProcessor ip = null; + switch (fi.fileType) { + case FileInfo.GRAY8: + case FileInfo.COLOR8: + case FileInfo.BITMAP: + ip = new ByteProcessor(width, height, (byte[])pixels, cm); + imp.setProcessor(null, ip); + break; + case FileInfo.GRAY16_SIGNED: + case FileInfo.GRAY16_UNSIGNED: + case FileInfo.GRAY12_UNSIGNED: + ip = new ShortProcessor(width, height, (short[])pixels, cm); + imp.setProcessor(null, ip); + break; + case FileInfo.GRAY32_INT: + case FileInfo.GRAY32_FLOAT: + ip = new FloatProcessor(width, height, (float[])pixels, cm); + imp.setProcessor(null, ip); + break; + case FileInfo.RGB: + case FileInfo.BGR: + case FileInfo.ARGB: + case FileInfo.ABGR: + case FileInfo.RGB_PLANAR: + Image img = Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(width, height, (int[])pixels, 0, width)); + imp.setImage(img); + break; + case FileInfo.CMYK: + ip = new ColorProcessor(width, height, (int[])pixels); + ip.invert(); + imp.setProcessor(null, ip); + break; + } + } + } + + void setCalibration(ImagePlus imp) { + if (fi.fileType==FileInfo.GRAY16_SIGNED) { + if (IJ.debugMode) IJ.log("16-bit signed"); + imp.getLocalCalibration().setSigned16BitCalibration(); + } + Properties props = decodeDescriptionString(fi); + Calibration cal = imp.getCalibration(); + boolean calibrated = false; + if (fi.pixelWidth>0.0 && fi.unit!=null) { + if (Prefs.convertToMicrons && fi.pixelWidth<=0.0001 && fi.unit.equals("cm")) { + fi.pixelWidth *= 10000.0; + fi.pixelHeight *= 10000.0; + if (fi.pixelDepth!=1.0) + fi.pixelDepth *= 10000.0; + fi.unit = "um"; + } + cal.pixelWidth = fi.pixelWidth; + cal.pixelHeight = fi.pixelHeight; + cal.pixelDepth = fi.pixelDepth; + cal.setUnit(fi.unit); + calibrated = true; + } + + if (fi.valueUnit!=null) { + if (imp.getBitDepth()==32) + cal.setValueUnit(fi.valueUnit); + else { + int f = fi.calibrationFunction; + if ((f>=Calibration.STRAIGHT_LINE && f<=Calibration.EXP_RECOVERY && fi.coefficients!=null) + || f==Calibration.UNCALIBRATED_OD) { + boolean zeroClip = props!=null && props.getProperty("zeroclip", "false").equals("true"); + cal.setFunction(f, fi.coefficients, fi.valueUnit, zeroClip); + calibrated = true; + } + } + } + + if (calibrated) + checkForCalibrationConflict(imp, cal); + + if (fi.frameInterval!=0.0) + cal.frameInterval = fi.frameInterval; + + if (props==null) + return; + + cal.xOrigin = getDouble(props,"xorigin"); + cal.yOrigin = getDouble(props,"yorigin"); + cal.zOrigin = getDouble(props,"zorigin"); + cal.setInvertY(getBoolean(props, "inverty")); + cal.info = props.getProperty("info"); + + cal.fps = getDouble(props,"fps"); + cal.loop = getBoolean(props, "loop"); + cal.frameInterval = getDouble(props,"finterval"); + cal.setTimeUnit(props.getProperty("tunit", "sec")); + cal.setYUnit(props.getProperty("yunit")); + cal.setZUnit(props.getProperty("zunit")); + + double displayMin = getDouble(props,"min"); + double displayMax = getDouble(props,"max"); + if (!(displayMin==0.0&&displayMax==0.0)) { + int type = imp.getType(); + ImageProcessor ip = imp.getProcessor(); + if (type==ImagePlus.GRAY8 || type==ImagePlus.COLOR_256) + ip.setMinAndMax(displayMin, displayMax); + else if (type==ImagePlus.GRAY16 || type==ImagePlus.GRAY32) { + if (ip.getMin()!=displayMin || ip.getMax()!=displayMax) + ip.setMinAndMax(displayMin, displayMax); + } + } + + if (getBoolean(props, "8bitcolor")) + imp.setTypeToColor256(); // set type to COLOR_256 + + int stackSize = imp.getStackSize(); + if (stackSize>1) { + int channels = (int)getDouble(props,"channels"); + int slices = (int)getDouble(props,"slices"); + int frames = (int)getDouble(props,"frames"); + if (channels==0) channels = 1; + if (slices==0) slices = 1; + if (frames==0) frames = 1; + //IJ.log("setCalibration: "+channels+" "+slices+" "+frames); + if (channels*slices*frames==stackSize) { + imp.setDimensions(channels, slices, frames); + if (getBoolean(props, "hyperstack")) + imp.setOpenAsHyperStack(true); + } + } + } + + + void checkForCalibrationConflict(ImagePlus imp, Calibration cal) { + Calibration gcal = imp.getGlobalCalibration(); + if (gcal==null || !showConflictMessage || IJ.isMacro()) + return; + if (cal.pixelWidth==gcal.pixelWidth && cal.getUnit().equals(gcal.getUnit())) + return; + GenericDialog gd = new GenericDialog(imp.getTitle()); + gd.addMessage("The calibration of this image conflicts\nwith the current global calibration."); + gd.addCheckbox("Disable_Global Calibration", true); + gd.addCheckbox("Disable_these Messages", false); + gd.showDialog(); + if (gd.wasCanceled()) return; + boolean disable = gd.getNextBoolean(); + if (disable) { + imp.setGlobalCalibration(null); + imp.setCalibration(cal); + WindowManager.repaintImageWindows(); + } + boolean dontShow = gd.getNextBoolean(); + if (dontShow) showConflictMessage = false; + } + + /** Returns an IndexColorModel for the image specified by this FileInfo. */ + public ColorModel createColorModel(FileInfo fi) { + if (fi.lutSize>0) + return new IndexColorModel(8, fi.lutSize, fi.reds, fi.greens, fi.blues); + else + return LookUpTable.createGrayscaleColorModel(fi.whiteIsZero); + } + + /** Returns an InputStream for the image described by this FileInfo. */ + public InputStream createInputStream(FileInfo fi) throws IOException, MalformedURLException { + InputStream is = null; + boolean gzip = fi.fileName!=null && (fi.fileName.endsWith(".gz")||fi.fileName.endsWith(".GZ")); + if (fi.inputStream!=null) + is = fi.inputStream; + else if (fi.url!=null && !fi.url.equals("")) + is = new URL(fi.url+fi.fileName).openStream(); + else { + if (fi.directory!=null && fi.directory.length()>0 && !(fi.directory.endsWith(Prefs.separator)||fi.directory.endsWith("/"))) + fi.directory += Prefs.separator; + File f = new File(fi.getFilePath()); + if (gzip) fi.compression = FileInfo.COMPRESSION_UNKNOWN; + if (f==null || !f.exists() || f.isDirectory() || !validateFileInfo(f, fi)) + is = null; + else + is = new FileInputStream(f); + } + if (is!=null) { + if (fi.compression>=FileInfo.LZW) + is = new RandomAccessStream(is); + else if (gzip) + is = new GZIPInputStream(is, 50000); + } + return is; + } + + static boolean validateFileInfo(File f, FileInfo fi) { + long offset = fi.getOffset(); + long length = 0; + if (fi.width<=0 || fi.height<=0) { + error("Width or height <= 0.", fi, offset, length); + return false; + } + if (offset>=0 && offset<1000L) + return true; + if (offset<0L) { + error("Offset is negative.", fi, offset, length); + return false; + } + if (fi.fileType==FileInfo.BITMAP || fi.compression!=FileInfo.COMPRESSION_NONE) + return true; + length = f.length(); + long size = fi.width*fi.height*fi.getBytesPerPixel(); + size = fi.nImages>1?size:size/4; + if (fi.height==1) size = 0; // allows plugins to read info of unknown length at end of file + if (offset+size>length) { + error("Offset + image size > file length.", fi, offset, length); + return false; + } + return true; + } + + static void error(String msg, FileInfo fi, long offset, long length) { + String msg2 = "FileInfo parameter error. \n" + +msg + "\n \n" + +" Width: " + fi.width + "\n" + +" Height: " + fi.height + "\n" + +" Offset: " + offset + "\n" + +" Bytes/pixel: " + fi.getBytesPerPixel() + "\n" + +(length>0?" File length: " + length + "\n":""); + if (silentMode) { + IJ.log("Error opening "+fi.getFilePath()); + IJ.log(msg2); + } else + IJ.error("FileOpener", msg2); + } + + + /** Reads the pixel data from an image described by a FileInfo object. */ + Object readPixels(FileInfo fi) { + Object pixels = null; + try { + InputStream is = createInputStream(fi); + if (is==null) + return null; + ImageReader reader = new ImageReader(fi); + pixels = reader.readPixels(is); + minValue = reader.min; + maxValue = reader.max; + is.close(); + } + catch (Exception e) { + if (!Macro.MACRO_CANCELED.equals(e.getMessage())) + IJ.handleException(e); + } + return pixels; + } + + public Properties decodeDescriptionString(FileInfo fi) { + if (fi.description==null || fi.description.length()<7) + return null; + if (IJ.debugMode) + IJ.log("Image Description: " + new String(fi.description).replace('\n',' ')); + if (!fi.description.startsWith("ImageJ")) + return null; + Properties props = new Properties(); + InputStream is = new ByteArrayInputStream(fi.description.getBytes()); + try {props.load(is); is.close();} + catch (IOException e) {return null;} + String dsUnit = props.getProperty("unit",""); + if ("cm".equals(fi.unit) && "um".equals(dsUnit)) { + fi.pixelWidth *= 10000; + fi.pixelHeight *= 10000; + } + fi.unit = dsUnit; + Double n = getNumber(props,"cf"); + if (n!=null) fi.calibrationFunction = n.intValue(); + double c[] = new double[5]; + int count = 0; + for (int i=0; i<5; i++) { + n = getNumber(props,"c"+i); + if (n==null) break; + c[i] = n.doubleValue(); + count++; + } + if (count>=2) { + fi.coefficients = new double[count]; + for (int i=0; i1.0) + fi.nImages = (int)n.doubleValue(); + n = getNumber(props, "spacing"); + if (n!=null) { + double spacing = n.doubleValue(); + if (spacing<0) spacing = -spacing; + fi.pixelDepth = spacing; + } + String name = props.getProperty("name"); + if (name!=null) + fi.fileName = name; + return props; + } + + private Double getNumber(Properties props, String key) { + String s = props.getProperty(key); + if (s!=null) { + try { + return Double.valueOf(s); + } catch (NumberFormatException e) {} + } + return null; + } + + private double getDouble(Properties props, String key) { + Double n = getNumber(props, key); + return n!=null?n.doubleValue():0.0; + } + + private boolean getBoolean(Properties props, String key) { + String s = props.getProperty(key); + return s!=null&&s.equals("true")?true:false; + } + + public static void setShowConflictMessage(boolean b) { + showConflictMessage = b; + } + + static void setSilentMode(boolean mode) { + silentMode = mode; + } + + +} diff --git a/src/ij/io/FileSaver.java b/src/ij/io/FileSaver.java new file mode 100644 index 0000000..a7ef326 --- /dev/null +++ b/src/ij/io/FileSaver.java @@ -0,0 +1,837 @@ +package ij.io; +import java.awt.*; +import java.io.*; +import java.util.zip.*; +import ij.*; +import ij.process.*; +import ij.measure.Calibration; +import ij.plugin.filter.Analyzer; +import ij.plugin.frame.Recorder; +import ij.plugin.JpegWriter; +import ij.plugin.Orthogonal_Views; +import ij.gui.*; +import ij.measure.Measurements; +import ij.util.Tools; +import javax.imageio.*; + +/** Saves images in tiff, gif, jpeg, raw, zip and text format. */ +public class FileSaver { + + public static final int DEFAULT_JPEG_QUALITY = 85; + private static int jpegQuality; + private static int bsize = 32768; // 32K default buffer size + + static {setJpegQuality(ij.Prefs.getInt(ij.Prefs.JPEG, DEFAULT_JPEG_QUALITY));} + + private static String defaultDirectory = null; + private ImagePlus imp; + private FileInfo fi; + private String name; + private String directory; + private boolean saveName; + + /** Constructs a FileSaver from an ImagePlus. */ + public FileSaver(ImagePlus imp) { + this.imp = imp; + fi = imp.getFileInfo(); + } + + /** Resaves the image. Calls saveAsTiff() if this is a new image, not a TIFF, + or if the image was loaded using a URL. Returns false if saveAsTiff() is + called and the user selects cancel in the file save dialog box. */ + public boolean save() { + FileInfo ofi = null; + if (imp!=null) ofi = imp.getOriginalFileInfo(); + boolean validName = ofi!=null && imp.getTitle().equals(ofi.fileName); + if (validName && ofi.fileFormat==FileInfo.TIFF && ofi.directory!=null && !ofi.directory.equals("") && (ofi.url==null||ofi.url.equals(""))) { + name = imp.getTitle(); + directory = ofi.directory; + String path = directory+name; + File f = new File(path); + if (f==null || !f.exists()) + return saveAsTiff(); + if (!IJ.isMacro()) { + GenericDialog gd = new GenericDialog("Save as TIFF"); + gd.addMessage("\""+ofi.fileName+"\" already exists.\nDo you want to replace it?"); + gd.setOKLabel("Replace"); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + } + IJ.showStatus("Saving "+path); + if (imp.getStackSize()>1) { + IJ.saveAs(imp, "tif", path); + return true; + } else + return saveAsTiff(path); + } else + return saveAsTiff(); + } + + String getPath(String type, String extension) { + name = imp.getTitle(); + SaveDialog sd = new SaveDialog("Save as "+type, name, extension); + name = sd.getFileName(); + if (name==null) + return null; + directory = sd.getDirectory(); + imp.startTiming(); + String path = directory+name; + return path; + } + + /** Saves the image or stack in TIFF format using a save file + dialog. Returns false if the user selects cancel. Equivalent to + IJ.saveAsTiff(imp,""), which is more convenient. */ + public boolean saveAsTiff() { + String path = getPath("TIFF", ".tif"); + if (path==null) + return false; + if (fi.nImages>1) + return saveAsTiffStack(path); + else + return saveAsTiff(path); + } + + /** Saves the image in TIFF format using the specified path. Equivalent to + IJ.saveAsTiff(imp,path), which is more convenient. */ + public boolean saveAsTiff(String path) { + if (fi.nImages>1) + return saveAsTiffStack(path); + if (imp.getProperty("FHT")!=null && path.contains("FFT of ")) + setupFFTSave(); + fi.info = imp.getInfoProperty(); + String label = imp.isStack()?imp.getStack().getSliceLabel(1):null; + if (label!=null) { + fi.sliceLabels = new String[1]; + fi.sliceLabels[0] = label; + } + fi.description = getDescriptionString(); + if (imp.getProperty(Plot.PROPERTY_KEY) != null) { + Plot plot = (Plot)(imp.getProperty(Plot.PROPERTY_KEY)); + fi.plot = plot.toByteArray(); + } + fi.roi = RoiEncoder.saveAsByteArray(imp.getRoi()); + fi.overlay = getOverlay(imp); + fi.properties = imp.getPropertiesAsArray(); + DataOutputStream out = null; + try { + TiffEncoder file = new TiffEncoder(fi); + out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(path),bsize)); + file.write(out); + out.close(); + } catch (IOException e) { + showErrorMessage("saveAsTiff", path, e); + return false; + } finally { + if (out!=null) + try {out.close();} catch (IOException e) {} + } + updateImp(fi, FileInfo.TIFF); + return true; + } + + private void setupFFTSave() { + Object obj = imp.getProperty("FHT"); + if (obj==null) return; + FHT fht = (obj instanceof FHT)?(FHT)obj:null; + if (fht==null) return; + if (fht.originalColorModel!=null && fht.originalBitDepth!=24) + fht.setColorModel(fht.originalColorModel); + ImagePlus imp2 = new ImagePlus(imp.getTitle(), fht); + imp2.setProperty("Info", imp.getProperty("Info")); + imp2.setProperties(imp.getPropertiesAsArray()); + imp2.setCalibration(imp.getCalibration()); + imp = imp2; + fi = imp.getFileInfo(); + } + + public static byte[][] getOverlay(ImagePlus imp) { + if (imp.getHideOverlay()) + return null; + Overlay overlay = imp.getOverlay(); + if (overlay==null) { + ImageCanvas ic = imp.getCanvas(); + if (ic==null) return null; + overlay = ic.getShowAllList(); // ROI Manager "Show All" list + if (overlay==null) return null; + } + int n = overlay.size(); + if (n==0) + return null; + if (Orthogonal_Views.isOrthoViewsImage(imp)) + return null; + byte[][] array = new byte[n][]; + for (int i=0; i1 && imp.getStack().isVirtual()) + fi.virtualStack = (VirtualStack)imp.getStack(); + DataOutputStream out = null; + try { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(path)); + out = new DataOutputStream(new BufferedOutputStream(zos,bsize)); + zos.putNextEntry(new ZipEntry(name)); + TiffEncoder te = new TiffEncoder(fi); + te.write(out); + out.close(); + } + catch (IOException e) { + showErrorMessage("saveAsZip", path, e); + return false; + } finally { + if (out!=null) + try {out.close();} catch (IOException e) {} + } + updateImp(fi, FileInfo.TIFF); + return true; + } + + public static boolean okForGif(ImagePlus imp) { + if (imp.getType()==ImagePlus.COLOR_RGB) + return false; + else + return true; + } + + /** Save the image in GIF format using a save file + dialog. Returns false if the user selects cancel + or the image is not 8-bits. */ + public boolean saveAsGif() { + String path = getPath("GIF", ".gif"); + if (path==null) + return false; + else + return saveAsGif(path); + } + + /** Save the image in Gif format using the specified path. Returns + false if the image is not 8-bits or there is an I/O error. */ + public boolean saveAsGif(String path) { + IJ.runPlugIn(imp, "ij.plugin.GifWriter", path); + updateImp(fi, FileInfo.GIF_OR_JPG); + return true; + } + + /** Always returns true. */ + public static boolean okForJpeg(ImagePlus imp) { + return true; + } + + /** Save the image in JPEG format using a save file + dialog. Returns false if the user selects cancel. + @see #setJpegQuality + @see #getJpegQuality + */ + public boolean saveAsJpeg() { + String type = "JPEG ("+getJpegQuality()+")"; + String path = getPath(type, ".jpg"); + if (path==null) + return false; + else + return saveAsJpeg(path); + } + + /** Save the image in JPEG format using the specified path. + @see #setJpegQuality + @see #getJpegQuality + */ + public boolean saveAsJpeg(String path) { + String err = JpegWriter.save(imp, path, jpegQuality); + if (err==null && !(imp.getType()==ImagePlus.GRAY16 || imp.getType()==ImagePlus.GRAY32)) + updateImp(fi, FileInfo.GIF_OR_JPG); + return true; + } + + /** Save the image in BMP format using a save file dialog. + Returns false if the user selects cancel. */ + public boolean saveAsBmp() { + String path = getPath("BMP", ".bmp"); + if (path==null) + return false; + else + return saveAsBmp(path); + } + + /** Save the image in BMP format using the specified path. */ + public boolean saveAsBmp(String path) { + IJ.runPlugIn(imp, "ij.plugin.BMP_Writer", path); + updateImp(fi, FileInfo.BMP); + return true; + } + + /** Saves grayscale images in PGM (portable graymap) format + and RGB images in PPM (portable pixmap) format, + using a save file dialog. + Returns false if the user selects cancel. + */ + public boolean saveAsPgm() { + String extension = imp.getBitDepth()==24?".pnm":".pgm"; + String path = getPath("PGM", extension); + if (path==null) + return false; + else + return saveAsPgm(path); + } + + /** Saves grayscale images in PGM (portable graymap) format + and RGB images in PPM (portable pixmap) format, + using the specified path. */ + public boolean saveAsPgm(String path) { + IJ.runPlugIn(imp, "ij.plugin.PNM_Writer", path); + updateImp(fi, FileInfo.PGM); + return true; + } + + /** Save the image in PNG format using a save file dialog. + Returns false if the user selects cancel. */ + public boolean saveAsPng() { + String path = getPath("PNG", ".png"); + if (path==null) + return false; + else + return saveAsPng(path); + } + + /** Save the image in PNG format using the specified path. */ + public boolean saveAsPng(String path) { + IJ.runPlugIn(imp, "ij.plugin.PNG_Writer", path); + updateImp(fi, FileInfo.IMAGEIO); + return true; + } + + /** Save the image in FITS format using a save file dialog. + Returns false if the user selects cancel. */ + public boolean saveAsFits() { + if (!okForFits(imp)) return false; + String path = getPath("FITS", ".fits"); + if (path==null) + return false; + else + return saveAsFits(path); + } + + /** Save the image in FITS format using the specified path. */ + public boolean saveAsFits(String path) { + if (!okForFits(imp)) return false; + IJ.runPlugIn(imp, "ij.plugin.FITS_Writer", path); + updateImp(fi, FileInfo.FITS); + return true; + } + + public static boolean okForFits(ImagePlus imp) { + if (imp.getBitDepth()==24) { + IJ.error("FITS Writer", "Grayscale image required"); + return false; + } else + return true; + } + + /** Save the image or stack as raw data using a save file + dialog. Returns false if the user selects cancel. */ + public boolean saveAsRaw() { + String path = getPath("Raw", ".raw"); + if (path==null) + return false; + if (imp.getStackSize()==1) + return saveAsRaw(path); + else + return saveAsRawStack(path); + } + + /** Save the image as raw data using the specified path. */ + public boolean saveAsRaw(String path) { + fi.nImages = 1; + fi.intelByteOrder = Prefs.intelByteOrder; + boolean signed16Bit = false; + short[] pixels = null; + int n = 0; + OutputStream out = null; + try { + signed16Bit = imp.getCalibration().isSigned16Bit(); + if (signed16Bit) { + pixels = (short[])imp.getProcessor().getPixels(); + n = imp.getWidth()*imp.getHeight(); + for (int i=0; i1 && imp.getStack().isVirtual(); + if (virtualStack) { + fi.virtualStack = (VirtualStack)imp.getStack(); + if (imp.getProperty("AnalyzeFormat")!=null) fi.fileName="FlipTheseImages"; + } + OutputStream out = null; + try { + signed16Bit = imp.getCalibration().isSigned16Bit(); + if (signed16Bit && !virtualStack) { + stack = (Object[])fi.pixels; + n = imp.getWidth()*imp.getHeight(); + for (int slice=0; slice100) + msg = msg.substring(0, 100); + msg = "File saving error (IOException):\n \"" + msg + "\""; + IJ.error("FileSaver."+title, msg+" \n "+path); + IJ.showProgress(1.0); + } + + private void error(String msg) { + IJ.error("FileSaver", msg); + } + + /** Returns a string containing information about the specified image. */ + public String getDescriptionString() { + Calibration cal = imp.getCalibration(); + StringBuffer sb = new StringBuffer(100); + sb.append("ImageJ="+ImageJ.VERSION+"\n"); + if (fi.nImages>1 && fi.fileType!=FileInfo.RGB48) + sb.append("images="+fi.nImages+"\n"); + int channels = imp.getNChannels(); + if (channels>1) + sb.append("channels="+channels+"\n"); + int slices = imp.getNSlices(); + if (slices>1) + sb.append("slices="+slices+"\n"); + int frames = imp.getNFrames(); + if (frames>1) + sb.append("frames="+frames+"\n"); + if (imp.isHyperStack()) sb.append("hyperstack=true\n"); + if (imp.isComposite()) { + String mode = ((CompositeImage)imp).getModeAsString(); + sb.append("mode="+mode+"\n"); + } + if (fi.unit!=null) + appendEscapedLine(sb, "unit="+fi.unit); + int bitDepth = imp.getBitDepth(); + if (fi.valueUnit!=null && (fi.calibrationFunction!=Calibration.CUSTOM||bitDepth==32)) { + if (bitDepth!=32) { + sb.append("cf="+fi.calibrationFunction+"\n"); + if (fi.coefficients!=null) { + for (int i=0; i=0x20 && c<0x7f && c!='\\') + sb.append(c); + else if (c<=0xffff) { //(supplementary unicode characters >0xffff unsupported) + sb.append("\\u"); + sb.append(Tools.int2hex(c, 4)); + } + } + sb.append('\n'); + } + + /** Specifies the image quality (0-100). 0 is poorest image quality, + highest compression, and 100 is best image quality, lowest compression. */ + public static void setJpegQuality(int quality) { + jpegQuality = quality; + if (jpegQuality<0) jpegQuality = 0; + if (jpegQuality>100) jpegQuality = 100; + } + + /** Returns the current JPEG quality setting (0-100). */ + public static int getJpegQuality() { + return jpegQuality; + } + + /** Sets the BufferedOutputStream buffer size in bytes (default is 32K). */ + public static void setBufferSize(int bufferSize) { + bsize = bufferSize; + if (bsize<2048) bsize = 2048; + } + +} diff --git a/src/ij/io/ImageReader.java b/src/ij/io/ImageReader.java new file mode 100644 index 0000000..ed929b1 --- /dev/null +++ b/src/ij/io/ImageReader.java @@ -0,0 +1,1069 @@ +package ij.io; +import ij.*; +import ij.process.*; +import java.io.*; +import java.net.*; +import java.awt.image.BufferedImage; +import javax.imageio.ImageIO; +import java.util.zip.Inflater; +import java.util.zip.DataFormatException; + + +/** Reads raw 8-bit, 16-bit or 32-bit (float or RGB) + images from a stream or URL. */ +public class ImageReader { + + private static final int CLEAR_CODE = 256; + private static final int EOI_CODE = 257; + + private FileInfo fi; + private int width, height; + private long skipCount; + private int bytesPerPixel, bufferSize, nPixels; + private long byteCount; + private boolean showProgressBar=true; + private int eofErrorCount; + private int imageCount; + private long startTime; + public double min, max; // readRGB48() calculates min/max pixel values + + /** + Constructs a new ImageReader using a FileInfo object to describe the file to be read. + @see ij.io.FileInfo + */ + public ImageReader (FileInfo fi) { + this.fi = fi; + width = fi.width; + height = fi.height; + skipCount = fi.getOffset(); + } + + void eofError() { + eofErrorCount++; + } + + byte[] read8bitImage(InputStream in) throws IOException { + if (fi.compression>FileInfo.COMPRESSION_NONE) + return readCompressed8bitImage(in); + byte[] pixels = new byte[nPixels]; + // assume contiguous strips + int count, actuallyRead; + int totalRead = 0; + while (totalReadbyteCount) + count = (int)(byteCount-totalRead); + else + count = bufferSize; + actuallyRead = in.read(pixels, totalRead, count); + if (actuallyRead==-1) {eofError(); break;} + totalRead += actuallyRead; + showProgress(totalRead, byteCount); + } + return pixels; + } + + byte[] readCompressed8bitImage(InputStream in) throws IOException { + byte[] pixels = new byte[nPixels]; + int current = 0; + byte last = 0; + for (int i=0; i 0) { + long skip = (fi.stripOffsets[i]&0xffffffffL) - (fi.stripOffsets[i-1]&0xffffffffL) - fi.stripLengths[i-1]; + if (skip > 0L) in.skip(skip); + } + byte[] byteArray = new byte[fi.stripLengths[i]]; + int read = 0, left = byteArray.length; + while (left > 0) { + int r = in.read(byteArray, read, left); + if (r == -1) {eofError(); break;} + read += r; + left -= r; + } + byteArray = uncompress(byteArray); + int length = byteArray.length; + length = length - (length%fi.width); + if (fi.compression==FileInfo.LZW_WITH_DIFFERENCING) { + for (int b=0; bpixels.length) + length = pixels.length-current; + System.arraycopy(byteArray, 0, pixels, current, length); + current += length; + showProgress(i+1, fi.stripOffsets.length); + } + return pixels; + } + + /** Reads a 16-bit image. Signed pixels are converted to unsigned by adding 32768. */ + short[] read16bitImage(InputStream in) throws IOException { + if (fi.compression>FileInfo.COMPRESSION_NONE || (fi.stripOffsets!=null&&fi.stripOffsets.length>1) && fi.fileType!=FileInfo.RGB48_PLANAR) + return readCompressed16bitImage(in); + int pixelsRead; + byte[] buffer = new byte[bufferSize]; + short[] pixels = new short[nPixels]; + long totalRead = 0L; + int base = 0; + int count, value; + int bufferCount; + + while (totalReadbyteCount) + bufferSize = (int)(byteCount-totalRead); + bufferCount = 0; + while (bufferCount 0) { + long skip = (fi.stripOffsets[k]&0xffffffffL) - (fi.stripOffsets[k-1]&0xffffffffL) - fi.stripLengths[k-1]; + if (skip > 0L) in.skip(skip); + } + byte[] byteArray = new byte[fi.stripLengths[k]]; + int read = 0, left = byteArray.length; + while (left > 0) { + int r = in.read(byteArray, read, left); + if (r == -1) {eofError(); break;} + read += r; + left -= r; + } + byteArray = uncompress(byteArray); + int pixelsRead = byteArray.length/bytesPerPixel; + pixelsRead = pixelsRead - (pixelsRead%fi.width); + int pmax = base+pixelsRead; + if (pmax > nPixels) pmax = nPixels; + if (fi.intelByteOrder) { + for (int i=base,j=0; iFileInfo.COMPRESSION_NONE || (fi.stripOffsets!=null&&fi.stripOffsets.length>1)) + return readCompressed32bitImage(in); + int pixelsRead; + byte[] buffer = new byte[bufferSize]; + float[] pixels = new float[nPixels]; + long totalRead = 0L; + int base = 0; + int count, value; + int bufferCount; + int tmp; + + while (totalReadbyteCount) + bufferSize = (int)(byteCount-totalRead); + bufferCount = 0; + while (bufferCountnPixels) pmax = nPixels; + int j = 0; + if (fi.intelByteOrder) + for (int i=base; i 0) { + long skip = (fi.stripOffsets[k]&0xffffffffL) - (fi.stripOffsets[k-1]&0xffffffffL) - fi.stripLengths[k-1]; + if (skip > 0L) in.skip(skip); + } + byte[] byteArray = new byte[fi.stripLengths[k]]; + int read = 0, left = byteArray.length; + while (left > 0) { + int r = in.read(byteArray, read, left); + if (r == -1) {eofError(); break;} + read += r; + left -= r; + } + byteArray = uncompress(byteArray); + int pixelsRead = byteArray.length/bytesPerPixel; + pixelsRead = pixelsRead - (pixelsRead%fi.width); + int pmax = base+pixelsRead; + if (pmax > nPixels) pmax = nPixels; + int tmp; + if (fi.intelByteOrder) { + for (int i=base,j=0; ibyteCount) + bufferSize = (int)(byteCount-totalRead); + bufferCount = 0; + while (bufferCountFileInfo.COMPRESSION_NONE || (fi.stripOffsets!=null&&fi.stripOffsets.length>1)) + return readCompressedChunkyRGB(in); + int pixelsRead; + bufferSize = 24*width; + byte[] buffer = new byte[bufferSize]; + int[] pixels = new int[nPixels]; + long totalRead = 0L; + int base = 0; + int count, value; + int bufferCount; + int r, g, b, a; + + while (totalReadbyteCount) + bufferSize = (int)(byteCount-totalRead); + bufferCount = 0; + while (bufferCount0) { // if k>0 then c=c*(1-k)+k + r = ((r*(256 - a))>>8) + a; + g = ((g*(256 - a))>>8) + a; + b = ((b*(256 - a))>>8) + a; + } // else r=1-c, g=1-m and b=1-y, which IJ does by inverting image + } else { // ARGB + r = buffer[j++]&0xff; + g = buffer[j++]&0xff; + b = buffer[j++]&0xff; + j++; // ignore alfa byte + } + } else { + r = buffer[j++]&0xff; + g = buffer[j++]&0xff; + b = buffer[j++]&0xff; + } + if (bgr) + pixels[i] = 0xff000000 | (b<<16) | (g<<8) | r; + else + pixels[i] = 0xff000000 | (r<<16) | (g<<8) | b; + } + base += pixelsRead; + } + return pixels; + } + + int[] readCompressedChunkyRGB(InputStream in) throws IOException { + int[] pixels = new int[nPixels]; + int base = 0; + int lastRed=0, lastGreen=0, lastBlue=0; + int nextByte; + int red=0, green=0, blue=0, alpha = 0; + boolean bgr = fi.fileType==FileInfo.BGR; + boolean cmyk = fi.fileType==FileInfo.CMYK; + boolean differencing = fi.compression == FileInfo.LZW_WITH_DIFFERENCING; + for (int i=0; i 0) { + long skip = (fi.stripOffsets[i]&0xffffffffL) - (fi.stripOffsets[i-1]&0xffffffffL) - fi.stripLengths[i-1]; + if (skip > 0L) in.skip(skip); + } + byte[] byteArray = new byte[fi.stripLengths[i]]; + int read = 0, left = byteArray.length; + while (left > 0) { + int r = in.read(byteArray, read, left); + if (r == -1) {eofError(); break;} + read += r; + left -= r; + } + byteArray = uncompress(byteArray); + if (differencing) { + for (int b=0; b nPixels) pmax = nPixels; + for (int j=base; j0) { + red = ((red*(256-alpha))>>8) + alpha; + green = ((green*(256-alpha))>>8) + alpha; + blue = ((blue*(256-alpha))>>8) + alpha; + } + } else { + red = byteArray[k++]&0xff; + green = byteArray[k++]&0xff; + blue = byteArray[k++]&0xff; + } + if (bgr) + pixels[j] = 0xff000000 | (blue<<16) | (green<<8) | red; + else + pixels[j] = 0xff000000 | (red<<16) | (green<<8) | blue; + } + base += pixelsRead; + showProgress(i+1, fi.stripOffsets.length); + } + return pixels; + } + + int[] readJPEG(InputStream in) throws IOException { + BufferedImage bi = ImageIO.read(in); + ImageProcessor ip = new ColorProcessor(bi); + return (int[])ip.getPixels(); + } + + int[] readPlanarRGB(InputStream in) throws IOException { + if (fi.compression>FileInfo.COMPRESSION_NONE || (fi.stripOffsets!=null&&fi.stripOffsets.length>1)) + return readCompressedPlanarRGBImage(in); + DataInputStream dis = new DataInputStream(in); + int planeSize = nPixels; // 1/3 image size + byte[] buffer = new byte[planeSize]; + int[] pixels = new int[nPixels]; + int r, g, b; + + startTime = 0L; + showProgress(10, 100); + dis.readFully(buffer); + for (int i=0; i < planeSize; i++) { + r = buffer[i]&0xff; + pixels[i] = 0xff000000 | (r<<16); + } + + showProgress(40, 100); + dis.readFully(buffer); + for (int i=0; i < planeSize; i++) { + g = buffer[i]&0xff; + pixels[i] |= g<<8; + } + + showProgress(70, 100); + dis.readFully(buffer); + for (int i=0; i < planeSize; i++) { + b = buffer[i]&0xff; + pixels[i] |= b; + } + + showProgress(90, 100); + return pixels; + } + + int[] readCompressedPlanarRGBImage(InputStream in) throws IOException { + int[] pixels = new int[nPixels]; + int r, g, b; + nPixels *= 3; // read all 3 planes + byte[] buffer = readCompressed8bitImage(in); + nPixels /= 3; + for (int i=0; i500L) + IJ.showProgress(current, last); + } + + private void showProgress(long current, long last) { + showProgress((int)(current/10L), (int)(last/10L)); + } + + Object readRGB48(InputStream in) throws IOException { + if (fi.compression>FileInfo.COMPRESSION_NONE) + return readCompressedRGB48(in); + int channels = fi.samplesPerPixel; + if (channels==1) channels=3; + short[][] stack = new short[channels][nPixels]; + DataInputStream dis = new DataInputStream(in); + int pixel = 0; + int min=65535, max=0; + if (fi.stripLengths==null) { + fi.stripLengths = new int[fi.stripOffsets.length]; + fi.stripLengths[0] = width*height*bytesPerPixel; + } + for (int i=0; i0) { + long skip = (fi.stripOffsets[i]&0xffffffffL) - (fi.stripOffsets[i-1]&0xffffffffL) - fi.stripLengths[i-1]; + if (skip>0L) dis.skip(skip); + } + int len = fi.stripLengths[i]; + int bytesToGo = (nPixels-pixel)*channels*2; + if (len>bytesToGo) len = bytesToGo; + byte[] buffer = new byte[len]; + dis.readFully(buffer); + int value; + int channel=0; + boolean intel = fi.intelByteOrder; + for (int base=0; basemax) + max = value; + stack[channel][pixel] = (short)(value); + channel++; + if (channel==channels) { + channel = 0; + pixel++; + } + } + showProgress(i+1, fi.stripOffsets.length); + } + this.min=min; this.max=max; + return stack; + } + + Object readCompressedRGB48(InputStream in) throws IOException { + if (fi.compression==FileInfo.LZW_WITH_DIFFERENCING) + throw new IOException("ImageJ cannot open 48-bit LZW compressed TIFFs with predictor"); + int channels = 3; + short[][] stack = new short[channels][nPixels]; + DataInputStream dis = new DataInputStream(in); + int pixel = 0; + int min=65535, max=0; + for (int i=0; i0) { + long skip = (fi.stripOffsets[i]&0xffffffffL) - (fi.stripOffsets[i-1]&0xffffffffL) - fi.stripLengths[i-1]; + if (skip>0L) dis.skip(skip); + } + int len = fi.stripLengths[i]; + byte[] buffer = new byte[len]; + dis.readFully(buffer); + buffer = uncompress(buffer); + len = buffer.length; + if (len % 2 != 0) len--; + int value; + int channel=0; + boolean intel = fi.intelByteOrder; + for (int base=0; basemax) + max = value; + stack[channel][pixel] = (short)(value); + channel++; + if (channel==channels) { + channel = 0; + pixel++; + } + } + showProgress(i+1, fi.stripOffsets.length); + } + this.min=min; this.max=max; + return stack; + } + + Object readRGB48Planar(InputStream in) throws IOException { + int channels = fi.samplesPerPixel; + if (channels==1) channels=3; + Object[] stack = new Object[channels]; + for (int i=0; i>4)&0xf)); + count++; + if (count==width) break; + pixels[index2+count] = (short)(((buffer[index1+1]&0xf)*256) + (buffer[index1+2]&0xff)); + count++; index1+=3; + } + } + return pixels; + } + + float[] read24bitImage(InputStream in) throws IOException { + byte[] buffer = new byte[width*3]; + float[] pixels = new float[nPixels]; + int b1, b2, b3; + DataInputStream dis = new DataInputStream(in); + for (int y=0; y=0; i--) { + value2 = (value1&(1<0) { + long bytesRead = 0; + int skipAttempts = 0; + long count; + while (bytesRead5) break; + bytesRead += count; + } + } + byteCount = ((long)width)*height*bytesPerPixel; + if (fi.fileType==FileInfo.BITMAP) { + int scan=width/8, pad = width%8; + if (pad>0) scan++; + byteCount = scan*height; + } + nPixels = width*height; + bufferSize = (int)(byteCount/25L); + if (bufferSize<8192) + bufferSize = 8192; + else + bufferSize = (bufferSize/8192)*8192; + } + + /** + Reads the image from the InputStream and returns the pixel + array (byte, short, int or float). Returns null if there + was an IO exception. Does not close the InputStream. + */ + public Object readPixels(InputStream in) { + Object pixels; + startTime = System.currentTimeMillis(); + try { + switch (fi.fileType) { + case FileInfo.GRAY8: + case FileInfo.COLOR8: + bytesPerPixel = 1; + skip(in); + pixels = (Object)read8bitImage(in); + break; + case FileInfo.GRAY16_SIGNED: + case FileInfo.GRAY16_UNSIGNED: + bytesPerPixel = 2; + skip(in); + pixels = (Object)read16bitImage(in); + break; + case FileInfo.GRAY32_INT: + case FileInfo.GRAY32_UNSIGNED: + case FileInfo.GRAY32_FLOAT: + bytesPerPixel = 4; + skip(in); + pixels = (Object)read32bitImage(in); + break; + case FileInfo.GRAY64_FLOAT: + bytesPerPixel = 8; + skip(in); + pixels = (Object)read64bitImage(in); + break; + case FileInfo.RGB: + case FileInfo.BGR: + case FileInfo.ARGB: + case FileInfo.ABGR: + case FileInfo.BARG: + case FileInfo.CMYK: + bytesPerPixel = fi.getBytesPerPixel(); + skip(in); + pixels = (Object)readChunkyRGB(in); + break; + case FileInfo.RGB_PLANAR: + if (!(in instanceof RandomAccessStream) && fi.stripOffsets!=null && fi.stripOffsets.length>1) + in = new RandomAccessStream(in); + bytesPerPixel = 3; + skip(in); + pixels = (Object)readPlanarRGB(in); + break; + case FileInfo.BITMAP: + bytesPerPixel = 1; + skip(in); + pixels = (Object)read1bitImage(in); + break; + case FileInfo.RGB48: + bytesPerPixel = 6; + skip(in); + pixels = (Object)readRGB48(in); + break; + case FileInfo.RGB48_PLANAR: + bytesPerPixel = 2; + skip(in); + pixels = (Object)readRGB48Planar(in); + break; + case FileInfo.GRAY12_UNSIGNED: + skip(in); + short[] data = read12bitImage(in); + pixels = (Object)data; + break; + case FileInfo.GRAY24_UNSIGNED: + skip(in); + pixels = (Object)read24bitImage(in); + break; + default: + pixels = null; + } + showProgress(1, 1); + imageCount++; + return pixels; + } + catch (IOException e) { + IJ.log("" + e); + return null; + } + } + + /** + Skips the specified number of bytes, then reads an image and + returns the pixel array (byte, short, int or float). Returns + null if there was an IO exception. Does not close the InputStream. + */ + public Object readPixels(InputStream in, long skipCount) { + this.skipCount = skipCount; + showProgressBar = false; + Object pixels = readPixels(in); + if (eofErrorCount>(imageCount==1?1:0)) + return null; + else + return pixels; + } + + /** + Reads the image from a URL and returns the pixel array (byte, + short, int or float). Returns null if there was an IO exception. + */ + public Object readPixels(String url) { + java.net.URL theURL; + InputStream is; + try {theURL = new URL(url);} + catch (MalformedURLException e) {IJ.log(""+e); return null;} + try {is = theURL.openStream();} + catch (IOException e) {IJ.log(""+e); return null;} + return readPixels(is); + } + + private byte[] uncompress(byte[] input) { + if (fi.compression==FileInfo.PACK_BITS) + return packBitsUncompress(input, fi.rowsPerStrip*fi.width*fi.getBytesPerPixel()); + else if (fi.compression==FileInfo.LZW || fi.compression==FileInfo.LZW_WITH_DIFFERENCING) + return lzwUncompress(input); + else if (fi.compression==FileInfo.ZIP) + return zipUncompress(input); + else + return input; + } + + /** TIFF Adobe ZIP support contributed by Jason Newton. */ + public byte[] zipUncompress(byte[] input) { + ByteArrayOutputStream imageBuffer = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + Inflater decompressor = new Inflater(); + decompressor.setInput(input); + try { + while(!decompressor.finished()) { + int rlen = decompressor.inflate(buffer); + imageBuffer.write(buffer, 0, rlen); + } + } catch(DataFormatException e){ + IJ.log(e.toString()); + } + decompressor.end(); + return imageBuffer.toByteArray(); + } + + /** + * Utility method for decoding an LZW-compressed image strip. + * Adapted from the TIFF 6.0 Specification: + * http://partners.adobe.com/asn/developer/pdfs/tn/TIFF6.pdf (page 61) + * Author: Curtis Rueden (ctrueden at wisc.edu) + */ + public byte[] lzwUncompress(byte[] input) { + if (input==null || input.length==0) + return input; + byte[][] symbolTable = new byte[16384][1]; + int bitsToRead = 9; + int nextSymbol = 258; + int code; + int oldCode = -1; + ByteVector out = new ByteVector(8192); + BitBuffer bb = new BitBuffer(input); + byte[] byteBuffer1 = new byte[16]; + byte[] byteBuffer2 = new byte[16]; + + while (out.size()=0) { // 0 <= n <= 127 + byte[] b = new byte[n+1]; + for (int i=0; isize) + count = (int)(size-bytesWritten); + int j = (int)(bytesWritten/2L); + int value; + if (fi.intelByteOrder) + for (int i=0; i < count; i+=2) { + value = pixels[j]; + buffer[i] = (byte)value; + buffer[i+1] = (byte)(value>>>8); + j++; + } + else + for (int i=0; i < count; i+=2) { + value = pixels[j]; + buffer[i] = (byte)(value>>>8); + buffer[i+1] = (byte)value; + j++; + } + out.write(buffer, 0, count); + bytesWritten += count; + showProgress((double)bytesWritten/size); + } + } + + void write16BitStack(OutputStream out, Object[] stack) throws IOException { + showProgressBar = false; + for (int i=0; i>>8); + value = g[index1]; + buffer[index2++] = (byte)value; + buffer[index2++] = (byte)(value>>>8); + value = b[index1]; + buffer[index2++] = (byte)value; + buffer[index2++] = (byte)(value>>>8); + index1++; + } + } else { + for (int i=0; i>>8); + buffer[index2++] = (byte)value; + value = g[index1]; + buffer[index2++] = (byte)(value>>>8); + buffer[index2++] = (byte)value; + value = b[index1]; + buffer[index2++] = (byte)(value>>>8); + buffer[index2++] = (byte)value; + index1++; + } + } + out.write(buffer, 0, count); + } + } + + void writeFloatImage(OutputStream out, float[] pixels) throws IOException { + long bytesWritten = 0L; + long size = 4L*fi.width*fi.height; + int count = getCount(size); + byte[] buffer = new byte[count]; + int tmp; + + while (bytesWrittensize) + count = (int)(size-bytesWritten); + int j = (int)(bytesWritten/4L); + if (fi.intelByteOrder) + for (int i=0; i < count; i+=4) { + tmp = Float.floatToRawIntBits(pixels[j]); + buffer[i] = (byte)tmp; + buffer[i+1] = (byte)(tmp>>8); + buffer[i+2] = (byte)(tmp>>16); + buffer[i+3] = (byte)(tmp>>24); + j++; + } + else + for (int i=0; i < count; i+=4) { + tmp = Float.floatToRawIntBits(pixels[j]); + buffer[i] = (byte)(tmp>>24); + buffer[i+1] = (byte)(tmp>>16); + buffer[i+2] = (byte)(tmp>>8); + buffer[i+3] = (byte)tmp; + j++; + } + out.write(buffer, 0, count); + bytesWritten += count; + showProgress((double)bytesWritten/size); + } + } + + private int getCount(long imageSize) { + if (savingStack || imageSize<4L) + return (int)imageSize; + int count = (int)(imageSize/50L); + if (count<65536) + count = 65536; + if (count>imageSize) + count = (int)imageSize; + count = (count/4)*4; + if (IJ.debugMode) IJ.log("ImageWriter: "+imageSize+" "+count+" "+imageSize/50); + return count; + } + + void writeFloatStack(OutputStream out, Object[] stack) throws IOException { + showProgressBar = false; + for (int i=0; isize) + count = (int)(size-bytesWritten); + int j = (int)(bytesWritten/3L); + for (int i=0; i>16); //red + buffer[i+1] = (byte)(pixels[j]>>8); //green + buffer[i+2] = (byte)pixels[j]; //blue + j++; + } + out.write(buffer, 0, count); + bytesWritten += count; + showProgress((double)bytesWritten/size); + } + } + + void writeRGBStack(OutputStream out, Object[] stack) throws IOException { + showProgressBar = false; + for (int i=0; i1 + then fi.pixels must be a 2D array, for example an + array of images returned by ImageStack.getImageArray()). + The fi.offset field is ignored. */ + public void write(OutputStream out) throws IOException { + if (fi.pixels==null && fi.virtualStack==null) + throw new IOException("ImageWriter: fi.pixels==null"); + if (fi.nImages>1 && fi.virtualStack==null && !(fi.pixels instanceof Object[])) + throw new IOException("ImageWriter: fi.pixels not a stack"); + if (fi.width*fi.height*fi.getBytesPerPixel()<26214400) + showProgressBar = false; // don't show progress bar if image<25MB + switch (fi.fileType) { + case FileInfo.GRAY8: + case FileInfo.COLOR8: + if (fi.nImages>1 && fi.virtualStack!=null) + write8BitVirtualStack(out, fi.virtualStack); + else if (fi.nImages>1) + write8BitStack(out, (Object[])fi.pixels); + else + write8BitImage(out, (byte[])fi.pixels); + break; + case FileInfo.GRAY16_SIGNED: + case FileInfo.GRAY16_UNSIGNED: + if (fi.nImages>1 && fi.virtualStack!=null) + write16BitVirtualStack(out, fi.virtualStack); + else if (fi.nImages>1) + write16BitStack(out, (Object[])fi.pixels); + else + write16BitImage(out, (short[])fi.pixels); + break; + case FileInfo.RGB48: + writeRGB48Image(out, (Object[])fi.pixels); + break; + case FileInfo.GRAY32_FLOAT: + if (fi.nImages>1 && fi.virtualStack!=null) + writeFloatVirtualStack(out, fi.virtualStack); + else if (fi.nImages>1) + writeFloatStack(out, (Object[])fi.pixels); + else + writeFloatImage(out, (float[])fi.pixels); + break; + case FileInfo.RGB: + if (fi.nImages>1 && fi.virtualStack!=null) + writeRGBVirtualStack(out, fi.virtualStack); + else if (fi.nImages>1) + writeRGBStack(out, (Object[])fi.pixels); + else + writeRGBImage(out, (int[])fi.pixels); + break; + default: + } + savingStack = false; + } + +} + diff --git a/src/ij/io/ImportDialog.java b/src/ij/io/ImportDialog.java new file mode 100644 index 0000000..d5e328e --- /dev/null +++ b/src/ij/io/ImportDialog.java @@ -0,0 +1,362 @@ +package ij.io; + +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.util.*; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.util.*; +import ij.plugin.frame.Recorder; +import ij.plugin.*; +import ij.measure.Calibration; + + +/** This is a dialog box used to imports raw 8, 16, 24 and 32-bit images. */ +public class ImportDialog { + private String fileName; + private String directory; + static final String TYPE = "raw.type"; + static final String WIDTH = "raw.width"; + static final String HEIGHT = "raw.height"; + static final String OFFSET = "raw.offset"; + static final String N = "raw.n"; + static final String GAP = "raw.gap"; + static final String OPTIONS = "raw.options"; + static final int WHITE_IS_ZERO = 1; + static final int INTEL_BYTE_ORDER = 2; + static final int OPEN_ALL = 4; + + // default settings + private static int sChoiceSelection = Prefs.getInt(TYPE,0); + private static int sWidth = Prefs.getInt(WIDTH,512); + private static int sHeight = Prefs.getInt(HEIGHT,512); + private static long sOffset = Prefs.getInt(OFFSET,0); + private static int sNImages = Prefs.getInt(N,1); + private static long sGapBetweenImages = Prefs.getInt(GAP,0); + private static boolean sWhiteIsZero; + private static boolean sIntelByteOrder; + private static boolean sVirtual; + private int choiceSelection = sChoiceSelection; + private int width = sWidth; + private int height = sHeight; + private long offset = sOffset; + private int nImages = sNImages; + private long gapBetweenImages = sGapBetweenImages; + private boolean whiteIsZero = sWhiteIsZero; + private boolean intelByteOrder = sIntelByteOrder; + private boolean virtual = sVirtual; + + private static int options; + private static FileInfo lastFileInfo; + private boolean openAll; + private static String[] types = {"8-bit", "16-bit Signed", "16-bit Unsigned", + "32-bit Signed", "32-bit Unsigned", "32-bit Real", "64-bit Real", "24-bit RGB", + "24-bit RGB Planar", "24-bit BGR", "24-bit Integer", "32-bit ARGB", "32-bit ABGR", "1-bit Bitmap"}; + + static { + options = Prefs.getInt(OPTIONS, 0); + sWhiteIsZero = (options&WHITE_IS_ZERO)!=0; + sIntelByteOrder = (options&INTEL_BYTE_ORDER)!=0; + } + + public ImportDialog(String fileName, String directory) { + this.fileName = fileName; + this.directory = directory; + IJ.showStatus("Importing: " + fileName); + } + + public ImportDialog() { + } + + boolean showDialog() { + boolean macro = Macro.getOptions()!=null; + if (macro) { + width = height = 512; + offset = gapBetweenImages = 0; + nImages = 1; + whiteIsZero = intelByteOrder = virtual = false; + } + if (choiceSelection>=types.length) + choiceSelection = 0; + getDimensionsFromName(fileName); + GenericDialog gd = new GenericDialog("Import>Raw..."); + gd.addChoice("Image type:", types, types[choiceSelection]); + gd.addNumericField("Width:", width, 0, 8, "pixels"); + gd.addNumericField("Height:", height, 0, 8, "pixels"); + gd.addNumericField("Offset to first image:", offset, 0, 8, "bytes"); + gd.addNumericField("Number of images:", nImages, 0, 8, null); + gd.addNumericField("Gap between images:", gapBetweenImages, 0, 8, "bytes"); + gd.addCheckbox("White is zero", whiteIsZero); + gd.addCheckbox("Little-endian byte order", intelByteOrder); + gd.addCheckbox("Open all files in folder", openAll); + gd.addCheckbox("Use virtual stack", virtual); + gd.addHelp(IJ.URL+"/docs/menus/file.html#raw"); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + choiceSelection = gd.getNextChoiceIndex(); + width = (int)gd.getNextNumber(); + height = (int)gd.getNextNumber(); + gd.setSmartRecording(offset==0); + offset = (long)gd.getNextNumber(); + gd.setSmartRecording(nImages==1); + nImages = (int)gd.getNextNumber(); + gd.setSmartRecording(gapBetweenImages==0); + gapBetweenImages = (long)gd.getNextNumber(); + gd.setSmartRecording(false); + whiteIsZero = gd.getNextBoolean(); + intelByteOrder = gd.getNextBoolean(); + openAll = gd.getNextBoolean(); + virtual = gd.getNextBoolean(); + IJ.register(ImportDialog.class); + if (!macro) { + sChoiceSelection = choiceSelection; + sWidth = width; + sHeight = height; + sOffset = offset; + sNImages = nImages; + sGapBetweenImages = gapBetweenImages; + sWhiteIsZero = whiteIsZero; + sIntelByteOrder = intelByteOrder; + sVirtual = virtual; + } + return true; + } + + /** Opens all the images in the directory. */ + void openAll(String[] list, FileInfo fi) { + FolderOpener fo = new FolderOpener(); + list = fo.trimFileList(list); + list = fo.sortFileList(list); + if (list==null) return; + ImageStack stack=null; + ImagePlus imp=null; + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + int digits = 0; + for (int i=0; i99) digits=3; + if (slices>999) digits=4; + if (slices>9999) digits=5; + } + for (int n=1; n<=slices; n++) { + ImageProcessor ip = stack2.getProcessor(n); + if (ip.getMin()max) + max = ip.getMax(); + String label = list[i]; + if (slices>1) label += "-" + IJ.pad(n,digits); + stack.addSlice(label, ip); + } + } catch(OutOfMemoryError e) { + IJ.outOfMemory("OpenAll"); + stack.trim(); + break; + } + IJ.showStatus((stack.size()+1) + ": " + list[i]); + } + } + String dir = Recorder.fixPath(fi.directory); + Recorder.recordCall(fi.getCode()+"imp = Raw.openAll(\""+ dir+"\", fi);"); + if (stack!=null) { + imp = new ImagePlus("Imported Stack", stack); + if (imp.getBitDepth()==16 || imp.getBitDepth()==32) + imp.getProcessor().setMinAndMax(min, max); + Calibration cal = imp.getCalibration(); + if (fi.fileType==FileInfo.GRAY16_SIGNED) + cal.setSigned16BitCalibration(); + imp.show(); + } + } + + /** Displays the dialog and opens the specified image or images. + Does nothing if the dialog is canceled. */ + public void openImage() { + FileInfo fi = getFileInfo(); + if (fi==null) + return; + if (openAll) { + if (virtual) { + ImagePlus imp = Raw.openAllVirtual(directory, fi); + String dir = Recorder.fixPath(directory); + Recorder.recordCall(fi.getCode()+"imp = Raw.openAllVirtual(\""+dir+"\", fi);"); + if (imp!=null) { + imp.setSlice(imp.getStackSize()/2); + imp.show(); + imp.setSlice(1); + } + return; + } + String[] list = new File(directory).list(); + if (list==null) return; + openAll(list, fi); + } else if (virtual) + new FileInfoVirtualStack(fi); + else { + FileOpener fo = new FileOpener(fi); + ImagePlus imp = fo.openImage(); + String filePath = fi.getFilePath(); + filePath = Recorder.fixPath(filePath); + Recorder.recordCall(fi.getCode()+"imp = Raw.open(\""+filePath+"\", fi);"); + if (imp!=null) { + imp.show(); + int n = imp.getStackSize(); + if (n>1) { + imp.setSlice(n/2); + ImageProcessor ip = imp.getProcessor(); + ip.resetMinAndMax(); + imp.setDisplayRange(ip.getMin(),ip.getMax()); + } + } else + IJ.error("File>Import>Raw", "File not found: "+filePath); + } + } + + /** Displays the dialog and returns a FileInfo object that can be used to + open the image. Returns null if the dialog is canceled. The fileName + and directory fields are null if the no argument constructor was used. */ + public FileInfo getFileInfo() { + if (!showDialog()) + return null; + String imageType = types[choiceSelection]; + FileInfo fi = new FileInfo(); + fi.fileFormat = fi.RAW; + fi.fileName = fileName; + directory = IJ.addSeparator(directory); + fi.directory = directory; + fi.width = width; + fi.height = height; + if (offset>2147483647) + fi.longOffset = offset; + else + fi.offset = (int)offset; + fi.nImages = nImages; + fi.gapBetweenImages = (int)gapBetweenImages; + fi.longGap = gapBetweenImages; + fi.intelByteOrder = intelByteOrder; + fi.whiteIsZero = whiteIsZero; + if (imageType.equals("8-bit")) + fi.fileType = FileInfo.GRAY8; + else if (imageType.equals("16-bit Signed")) + fi.fileType = FileInfo.GRAY16_SIGNED; + else if (imageType.equals("16-bit Unsigned")) + fi.fileType = FileInfo.GRAY16_UNSIGNED; + else if (imageType.equals("32-bit Signed")) + fi.fileType = FileInfo.GRAY32_INT; + else if (imageType.equals("32-bit Unsigned")) + fi.fileType = FileInfo.GRAY32_UNSIGNED; + else if (imageType.equals("32-bit Real")) + fi.fileType = FileInfo.GRAY32_FLOAT; + else if (imageType.equals("64-bit Real")) + fi.fileType = FileInfo.GRAY64_FLOAT; + else if (imageType.equals("24-bit RGB")) + fi.fileType = FileInfo.RGB; + else if (imageType.equals("24-bit RGB Planar")) + fi.fileType = FileInfo.RGB_PLANAR; + else if (imageType.equals("24-bit BGR")) + fi.fileType = FileInfo.BGR; + else if (imageType.equals("24-bit Integer")) + fi.fileType = FileInfo.GRAY24_UNSIGNED; + else if (imageType.equals("32-bit ARGB")) + fi.fileType = FileInfo.ARGB; + else if (imageType.equals("32-bit ABGR")) + fi.fileType = FileInfo.ABGR; + else if (imageType.equals("1-bit Bitmap")) + fi.fileType = FileInfo.BITMAP; + else + fi.fileType = FileInfo.GRAY8; + if (IJ.debugMode) IJ.log("ImportDialog: "+fi); + lastFileInfo = (FileInfo)fi.clone(); + return fi; + } + + /** Called once when ImageJ quits. */ + public static void savePreferences(Properties prefs) { + prefs.put(TYPE, Integer.toString(sChoiceSelection)); + prefs.put(WIDTH, Integer.toString(sWidth)); + prefs.put(HEIGHT, Integer.toString(sHeight)); + prefs.put(OFFSET, Integer.toString(sOffset>2147483647?0:(int)sOffset)); + prefs.put(N, Integer.toString(sNImages)); + prefs.put(GAP, Integer.toString(sGapBetweenImages>2147483647?0:(int)sGapBetweenImages)); + int options = 0; + if (sWhiteIsZero) + options |= WHITE_IS_ZERO; + if (sIntelByteOrder) + options |= INTEL_BYTE_ORDER; + prefs.put(OPTIONS, Integer.toString(options)); + } + + /** Returns the FileInfo object used to import the last raw image, + or null if a raw image has not been imported. */ + public static FileInfo getLastFileInfo() { + return lastFileInfo; + } + + private void getDimensionsFromName(String name) { + if (name==null) + return; + if (!name.matches(".*[0-9]+x[0-9]+.*")) + return; // must have 'x' seperator + int lastUnderscore = name.lastIndexOf("_"); + String name2 = name; + if (lastUnderscore>=0) + name2 = name.substring(lastUnderscore); + char[] chars = new char[name2.length()]; + for (int i=0; i2) { + int d = (int)Tools.parseDouble(numbers[2],0); + if (d>0) + nImages = d; + } + guessFormat(directory, name); + } + + private void guessFormat(String dir, String name) { + if (dir==null) return; + File file = new File(dir+name); + long imageSize = (long)width*height*nImages; + long fileSize = file.length(); + if (fileSize==4*imageSize) + choiceSelection = 5; // 32-bit real + else if (fileSize==2*imageSize) + choiceSelection = 2; // 16-bit unsigned + else if (fileSize==3*imageSize) + choiceSelection = 7; // 24-bit RGB + else if (fileSize==imageSize) + choiceSelection = 0; // 8-bit + if (name.endsWith("be.raw")) // big-endian + intelByteOrder = false; + else if (name.endsWith("le.raw")) // little-endian + intelByteOrder = true; + } + +} diff --git a/src/ij/io/LogStream.java b/src/ij/io/LogStream.java new file mode 100644 index 0000000..33d6cac --- /dev/null +++ b/src/ij/io/LogStream.java @@ -0,0 +1,203 @@ +package ij.io; +import ij.IJ; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/** + * This class provides the functionality to divert output sent to the System.out + * and System.err streams to ImageJ's log console. The purpose is to allow + * use of existing Java classes or writing new generic Java classes that only + * output to System.out and are thus less dependent on ImageJ. + * See the ImageJ plugin Redirect_System_Streams at + * http://staff.fh-hagenberg.at/burger/imagej/ + * for usage examples. + * + * @author Wilhelm Burger (wilbur at ieee.org) + * See Also: Redirect_System_Streams (http://staff.fh-hagenberg.at/burger/imagej/) + */ +public class LogStream extends PrintStream { + + private static String outPrefix = "out> "; // prefix string for System.out + private static String errPrefix = "err >"; // prefix string for System.err + + private static PrintStream originalSystemOut = null; + private static PrintStream originalSystemErr = null; + private static PrintStream temporarySystemOut = null; + private static PrintStream temporarySystemErr = null; + + /** + * Redirects all output sent to System.out and System.err to ImageJ's log console + * using the default prefixes. + */ + public static void redirectSystem(boolean redirect) { + if (redirect) + redirectSystem(); + else + revertSystem(); + } + + /** + * Redirects all output sent to System.out and System.err to ImageJ's log console + * using the default prefixes. + * Alternatively use + * {@link #redirectSystemOut(String)} and {@link #redirectSystemErr(String)} + * to redirect the streams separately and to specify individual prefixes. + */ + public static void redirectSystem() { + redirectSystemOut(outPrefix); + redirectSystemErr(errPrefix); + } + + /** + * Redirects all output sent to System.out to ImageJ's log console. + * @param prefix The prefix string inserted at the start of each output line. + * Pass null to use the default prefix or an empty string to + * remove the prefix. + */ + public static void redirectSystemOut(String prefix) { + if (originalSystemOut == null) { // has no effect if System.out is already replaced + originalSystemOut = System.out; // remember the original System.out stream + temporarySystemOut = new LogStream(prefix); + System.setOut(temporarySystemOut); + } + } + + /** + * Redirects all output sent to System.err to ImageJ's log console. + * @param prefix The prefix string inserted at the start of each output line. + * Pass null to use the default prefix or an empty string to + * remove the prefix. + */ + public static void redirectSystemErr(String prefix) { + if (originalSystemErr == null) { // has no effect if System.out is already replaced + originalSystemErr = System.err; // remember the original System.out stream + temporarySystemErr = new LogStream(prefix); + System.setErr(temporarySystemErr); + } + } + + /** + * Returns the redirection stream for {@code System.out} if it exists. + * Note that a reference to the current output stream can also be obtained directly from + * the {@code System.out} field. + * @return A reference to the {@code PrintStream} object currently substituting {@code System.out} + * or {@code null} of if {@code System.out} is currently not redirected. + */ + public static PrintStream getCurrentOutStream() { + return temporarySystemOut; + } + + /** + * Returns the redirection stream for {@code System.err} if it exists. + * Note that a reference to the current output stream can also be obtained directly from + * the {@code System.err} field. + * @return A reference to the {@code PrintStream} object currently substituting {@code System.err} + * or {@code null} of if {@code System.err} is currently not redirected. + */ + public static PrintStream getCurrentErrStream() { + return temporarySystemErr; + } + + /** + * Use this method to revert both System.out and System.err + * to their original output streams. + */ + public static void revertSystem() { + revertSystemOut(); + revertSystemErr(); + } + + /** + * Use this method to revertSystem.out + * to the original output stream. + */ + public static void revertSystemOut() { + if (originalSystemOut != null && temporarySystemOut != null) { + temporarySystemOut.flush(); + temporarySystemOut.close(); + System.setOut(originalSystemOut); + originalSystemOut = null; + temporarySystemOut = null; + } + } + + /** + * Use this method to revertSystem.err + * to the original output stream. + */ + public static void revertSystemErr() { + if (originalSystemErr != null && temporarySystemErr != null) { + temporarySystemErr.flush(); + temporarySystemErr.close(); + System.setErr(originalSystemErr); + originalSystemErr = null; + temporarySystemErr = null; + } + } + + // ---------------------------------------------------------------- + + private final String endOfLineSystem = System.getProperty("line.separator"); + private final String endOfLineShort = String.format("\n"); + private final ByteArrayOutputStream byteStream; + private final String prefix; + + public LogStream() { + super(new ByteArrayOutputStream()); + this.byteStream = (ByteArrayOutputStream) this.out; + this.prefix = ""; + } + + private LogStream(String prefix) { + super(new ByteArrayOutputStream()); + this.byteStream = (ByteArrayOutputStream) this.out; + this.prefix = (prefix == null) ? "" : prefix; + } + + @Override + // ever called? + public void write(byte[] b) { + this.write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) { + String msg = new String(b, off, len); + if (msg.equals(endOfLineSystem) || msg.equals(endOfLineShort)) { // this is a newline sequence only + ejectBuffer(); + } else { + byteStream.write(b, off, len); // append message to buffer + if (msg.endsWith(endOfLineSystem) || msg.endsWith(endOfLineShort)) { // line terminated by Newline + // note that this does not seem to happen ever (even with format)!? + ejectBuffer(); + } + } + } + + @Override + // ever called? + public void write(int b) { + byteStream.write(b); + } + + @Override + public void flush() { + if (byteStream.size() > 0) { + String msg = byteStream.toString(); + if (msg.endsWith(endOfLineSystem) || msg.endsWith(endOfLineShort)) + ejectBuffer(); + } + super.flush(); + } + + @Override + public void close() { + super.close(); + } + + private void ejectBuffer() { + IJ.log(prefix + byteStream.toString()); + byteStream.reset(); + } + +} diff --git a/src/ij/io/OpenDialog.java b/src/ij/io/OpenDialog.java new file mode 100644 index 0000000..33090a3 --- /dev/null +++ b/src/ij/io/OpenDialog.java @@ -0,0 +1,266 @@ +package ij.io; +import ij.*; +import ij.gui.*; +import ij.plugin.frame.Recorder; +import ij.util.Java2; +import ij.macro.Interpreter; +import java.awt.*; +import java.io.*; +import javax.swing.*; +import javax.swing.filechooser.*; + +/** This class displays a dialog window from + * which the user can select an input file. +*/ +public class OpenDialog { + + private String dir; + private String name; + private boolean recordPath; + private static String defaultDirectory; + private static Frame sharedFrame; + private String title; + private static String lastDir, lastName; + private static boolean defaultDirectorySet; + + + /** Displays a file open dialog with 'title' as the title. */ + public OpenDialog(String title) { + this(title, null); + } + + /** Displays a file open dialog with 'title' as + the title. If 'path' is non-blank, it is + used and the dialog is not displayed. Uses + and updates the ImageJ default directory. */ + public OpenDialog(String title, String path) { + String macroOptions = Macro.getOptions(); + if (macroOptions!=null && (path==null||path.equals(""))) { + path = Macro.getValue(macroOptions, title, path); + if (path==null || path.equals("")) + path = Macro.getValue(macroOptions, "path", path); + if ((path==null || path.equals("")) && title!=null && title.equals("Open As String")) + path = Macro.getValue(macroOptions, "OpenAsString", path); + path = lookupPathVariable(path); + } + if (path==null || path.equals("")) { + if (Prefs.useJFileChooser) + jOpen(title, getDefaultDirectory(), null); + else + open(title, getDefaultDirectory(), null); + if (name!=null) + setDefaultDirectory(dir); + this.title = title; + recordPath = true; + } else { + decodePath(path); + recordPath = IJ.macroRunning(); + } + IJ.register(OpenDialog.class); + } + + /** Displays a file open dialog, using the specified + default directory and file name. */ + public OpenDialog(String title, String defaultDir, String defaultName) { + String path = null; + String macroOptions = Macro.getOptions(); + if (macroOptions!=null) + path = Macro.getValue(macroOptions, title, path); + if (path!=null) + decodePath(path); + else { + if (Prefs.useJFileChooser) + jOpen(title, defaultDir, defaultName); + else + open(title, defaultDir, defaultName); + this.title = title; + recordPath = true; + } + } + + public static String lookupPathVariable(String path) { + if (path!=null && path.indexOf(".")==-1 && !((new File(path)).exists())) { + if (path.startsWith("&")) path=path.substring(1); + Interpreter interp = Interpreter.getInstance(); + String path2 = interp!=null?interp.getStringVariable(path):null; + if (path2!=null) path = path2; + } + return path; + } + + // Uses JFileChooser to display file open dialog box. + void jOpen(String title, String path, String fileName) { + Java2.setSystemLookAndFeel(); + if (EventQueue.isDispatchThread()) + jOpenDispatchThread(title, path, fileName); + else + jOpenInvokeAndWait(title, path, fileName); + } + + // Uses the JFileChooser class to display the dialog box. + // Assumes we are running on the event dispatch thread + void jOpenDispatchThread(String title, String path, final String fileName) { + JFileChooser fc = new JFileChooser(); + fc.setDialogTitle(title); + fc.setDragEnabled(true); + fc.setTransferHandler(new DragAndDropHandler(fc)); + File fdir = null; + if (path!=null) + fdir = new File(path); + if (fdir!=null) + fc.setCurrentDirectory(fdir); + if (fileName!=null) + fc.setSelectedFile(new File(fileName)); + int returnVal = fc.showOpenDialog(IJ.getInstance()); + if (returnVal!=JFileChooser.APPROVE_OPTION) + {Macro.abort(); return;} + File file = fc.getSelectedFile(); + if (file==null) + {Macro.abort(); return;} + name = file.getName(); + dir = fc.getCurrentDirectory().getPath()+File.separator; + } + + // Run JFileChooser on event dispatch thread to avoid deadlocks + void jOpenInvokeAndWait(final String title, final String path, final String fileName) { + final boolean isMacro = Thread.currentThread().getName().endsWith("Macro$"); + try { + EventQueue.invokeAndWait(new Runnable() { + public void run() { + JFileChooser fc = new JFileChooser(); + fc.setDialogTitle(title); + fc.setDragEnabled(true); + fc.setTransferHandler(new DragAndDropHandler(fc)); + File fdir = null; + if (path!=null) + fdir = new File(path); + if (fdir!=null) + fc.setCurrentDirectory(fdir); + if (fileName!=null) + fc.setSelectedFile(new File(fileName)); + int returnVal = fc.showOpenDialog(IJ.getInstance()); + if (returnVal!=JFileChooser.APPROVE_OPTION && isMacro) + {Interpreter.abort(); return;} + File file = fc.getSelectedFile(); + if (file==null && isMacro) + {Interpreter.abort(); return;} + name = file.getName(); + dir = fc.getCurrentDirectory().getPath()+File.separator; + } + }); + } catch (Exception e) {} + } + + // Uses the AWT FileDialog class to display the dialog box + void open(String title, String path, String fileName) { + Frame parent = IJ.getInstance(); + if (parent==null) { + if (sharedFrame==null) sharedFrame = new Frame(); + parent = sharedFrame; + } + if (IJ.isMacOSX() && IJ.isJava18()) { + ImageJ ij = IJ.getInstance(); + if (ij!=null && ij.isActive()) + parent = ij; + else + parent = null; + } + FileDialog fd = new FileDialog(parent, title); + if (path!=null) { + if (IJ.isWindows() && path.contains("/")) + path = path.replaceAll("/","\\\\"); // work around FileDialog.setDirectory() bug + fd.setDirectory(path); + } + if (fileName!=null) + fd.setFile(fileName); + fd.show(); + name = fd.getFile(); + if (name==null) { + if (IJ.isMacOSX()) + System.setProperty("apple.awt.fileDialogForDirectories", "false"); + Macro.abort(); + } else + dir = fd.getDirectory(); + } + + void decodePath(String path) { + int i = path.lastIndexOf('/'); + if (i==-1) + i = path.lastIndexOf('\\'); + if (i>0) { + dir = path.substring(0, i+1); + name = path.substring(i+1); + } else { + dir = ""; + name = path; + } + } + + /** Returns the selected directory. */ + public String getDirectory() { + lastDir = dir; + return dir; + } + + /** Returns the selected file name. */ + public String getFileName() { + if (name!=null) { + if (Recorder.record && recordPath && dir!=null) + Recorder.recordPath(title, dir+name); + lastName = name; + } + return name; + } + + /** Returns the selected file path or null if the dialog was canceled. */ + public String getPath() { + if (getFileName()==null) + return null; + else return + getDirectory() + getFileName(); + } + + /** Returns the current working directory as a string + ending in the separator character ("/" or "\"), or + an empty or null string. */ + public static String getDefaultDirectory() { + if (Prefs.commandLineMacro() && !defaultDirectorySet) + return IJ.getDir("cwd"); + if (defaultDirectory==null) + defaultDirectory = Prefs.getDefaultDirectory(); + return defaultDirectory; + } + + /** Sets the current working directory. + * @see ij.plugin.frame.Editor#setDefaultDirectory + */ + public static void setDefaultDirectory(String dir) { + dir = IJ.addSeparator(dir); + defaultDirectory = dir; + defaultDirectorySet = true; + } + + /** Returns the path to the directory that contains the last file + opened or saved, or null if a file has not been opened or saved. */ + public static String getLastDirectory() { + return lastDir; + } + + /** Sets the path to the directory containing the last file opened by the user. */ + public static void setLastDirectory(String dir) { + lastDir = dir; + } + + /** Returns the name of the last file opened by the user + using a file open or file save dialog, or using drag and drop. + Returns null if the users has not opened a file. */ + public static String getLastName() { + return lastName; + } + + /** Sets the name of the last file opened by the user. */ + public static void setLastName(String name) { + lastName = name; + } + +} diff --git a/src/ij/io/Opener.java b/src/ij/io/Opener.java new file mode 100644 index 0000000..3503c3b --- /dev/null +++ b/src/ij/io/Opener.java @@ -0,0 +1,1388 @@ +package ij.io; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.plugin.frame.*; +import ij.plugin.*; +import ij.text.TextWindow; +import ij.util.Java2; +import ij.measure.ResultsTable; +import ij.macro.Interpreter; +import ij.util.Tools; +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.net.URL; +import java.net.*; +import java.util.*; +import java.util.zip.*; +import javax.swing.*; +import javax.swing.filechooser.*; +import java.awt.event.KeyEvent; +import javax.imageio.ImageIO; +import java.lang.reflect.Method; + +/** Opens tiff (and tiff stacks), dicom, fits, pgm, jpeg, bmp or + gif images, and look-up tables, using a file open dialog or a path. + Calls HandleExtraFileTypes plugin if the file type is unrecognised. */ +public class Opener { + + public static final int UNKNOWN=0,TIFF=1,DICOM=2,FITS=3,PGM=4,JPEG=5, + GIF=6,LUT=7,BMP=8,ZIP=9,JAVA_OR_TEXT=10,ROI=11,TEXT=12,PNG=13, + TIFF_AND_DICOM=14,CUSTOM=15, AVI=16, OJJ=17, TABLE=18, RAW=19; // don't forget to also update 'types' + public static final String[] types = {"unknown","tif","dcm","fits","pgm", + "jpg","gif","lut","bmp","zip","java/txt","roi","txt","png","t&d","custom","ojj","table","raw"}; + private static String defaultDirectory = null; + private int fileType; + private boolean error; + private boolean isRGB48; + private boolean silentMode; + private String omDirectory; + private File[] omFiles; + private static boolean openUsingPlugins; + private static boolean bioformats; + private String url; + + static { + Hashtable commands = Menus.getCommands(); + bioformats = commands!=null && commands.get("Bio-Formats Importer")!=null; + } + + public Opener() { + } + + /** + * Displays a file open dialog box and then opens the tiff, dicom, + * fits, pgm, jpeg, bmp, gif, lut, roi, or text file selected by + * the user. Displays an error message if the selected file is not + * in a supported format. This is the method that + * ImageJ's File/Open command uses to open files. + * @see ij.IJ#open() + * @see ij.IJ#open(String) + * @see ij.IJ#openImage() + * @see ij.IJ#openImage(String) + */ + public void open() { + OpenDialog od = new OpenDialog("Open", ""); + String directory = od.getDirectory(); + String name = od.getFileName(); + if (name!=null) { + String path = directory+name; + error = false; + open(path); + if (!error) Menus.addOpenRecentItem(path); + } + } + + /** + * Opens and displays the specified tiff, dicom, fits, pgm, jpeg, + * bmp, gif, lut, roi, or text file. Displays an error message if + * the file is not in a supported format. + * @see ij.IJ#open(String) + * @see ij.IJ#openImage(String) + */ + public void open(String path) { + boolean isURL = path.contains("://") || path.contains("file:/"); + if (isURL && isText(path)) { + openTextURL(path); + return; + } + if (path.endsWith(".jar") || path.endsWith(".class")) { + (new PluginInstaller()).install(path); + return; + } + path = makeFullPath(path); + if (!silentMode) + IJ.showStatus("Opening: " + path); + long start = System.currentTimeMillis(); + ImagePlus imp = null; + if (path.endsWith(".txt")) + this.fileType = JAVA_OR_TEXT; + else + imp = openImage(path); + if (imp==null && isURL) + return; + if (imp!=null) { + WindowManager.checkForDuplicateName = true; + if (isRGB48) + openRGB48(imp); + else + imp.show(getLoadRate(start,imp)); + } else { + switch (this.fileType) { + case LUT: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.LutLoader", path); + if (imp.getWidth()!=0) + imp.show(); + break; + case ROI: + IJ.runPlugIn("ij.plugin.RoiReader", path); + break; + case JAVA_OR_TEXT: case TEXT: + if (IJ.altKeyDown()) { // open in TextWindow if alt key down + new TextWindow(path,400,450); + IJ.setKeyUp(KeyEvent.VK_ALT); + break; + } + File file = new File(path); + int maxSize = 250000; + long size = file.length(); + if (size>=28000) { + String osName = System.getProperty("os.name"); + if (osName.equals("Windows 95") || osName.equals("Windows 98") || osName.equals("Windows Me")) + maxSize = 60000; + } + if (size64) + path = (new File(path)).getName(); + if (path.length()<=64) + msg += " \n"+path; + } + if (openUsingPlugins && msg.length()>20) + msg += "\n \nNOTE: The \"OpenUsingPlugins\" option is set."; + IJ.wait(IJ.isMacro()?500:100); // work around for OS X thread deadlock problem + IJ.error("Opener", msg); + error = true; + break; + } + } + } + + /** Displays a JFileChooser and then opens the tiff, dicom, + fits, pgm, jpeg, bmp, gif, lut, roi, or text files selected by + the user. Displays error messages if one or more of the selected + files is not in one of the supported formats. This is the method + that ImageJ's File/Open command uses to open files if + "Open/Save Using JFileChooser" is checked in EditOptions/Misc. */ + public void openMultiple() { + Java2.setSystemLookAndFeel(); + // run JFileChooser in a separate thread to avoid possible thread deadlocks + try { + EventQueue.invokeAndWait(new Runnable() { + public void run() { + JFileChooser fc = new JFileChooser(); + fc.setMultiSelectionEnabled(true); + fc.setDragEnabled(true); + fc.setTransferHandler(new DragAndDropHandler(fc)); + File dir = null; + String sdir = OpenDialog.getDefaultDirectory(); + if (sdir!=null) + dir = new File(sdir); + if (dir!=null) + fc.setCurrentDirectory(dir); + if (IJ.debugMode) IJ.log("Opener.openMultiple: "+sdir+" "+dir); + int returnVal = fc.showOpenDialog(IJ.getInstance()); + if (returnVal!=JFileChooser.APPROVE_OPTION) + return; + omFiles = fc.getSelectedFiles(); + if (omFiles.length==0) { // getSelectedFiles does not work on some JVMs + omFiles = new File[1]; + omFiles[0] = fc.getSelectedFile(); + } + omDirectory = fc.getCurrentDirectory().getPath()+File.separator; + } + }); + } catch (Exception e) {} + if (omDirectory==null) return; + OpenDialog.setDefaultDirectory(omDirectory); + for (int i=0; i6) + return true; // no extension + else + return false; + } + + /** Opens the specified file and adds it to the File/Open Recent menu. + Returns true if the file was opened successfully. */ + public boolean openAndAddToRecent(String path) { + open(path); + if (!error) + Menus.addOpenRecentItem(path); + return error; + } + + /** + * Attempts to open the specified file as a tiff, bmp, dicom, fits, + * pgm, gif or jpeg image. Returns an ImagePlus object if successful. + * Modified by Gregory Jefferis to call HandleExtraFileTypes plugin if + * the file type is unrecognised. + * @see ij.IJ#openImage(String) + */ + public ImagePlus openImage(String directory, String name) { + ImagePlus imp; + FileOpener.setSilentMode(silentMode); + if (directory.length()>0 && !(directory.endsWith("/")||directory.endsWith("\\"))) + directory += Prefs.separator; + OpenDialog.setLastDirectory(directory); + OpenDialog.setLastName(name); + String path = directory+name; + this.fileType = getFileType(path); + if (IJ.debugMode) IJ.log("openImage: \""+types[this.fileType]+"\", "+path); + switch (this.fileType) { + case TIFF: + imp = openTiff(directory, name); + return imp; + case DICOM: case TIFF_AND_DICOM: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.DICOM", path); + if (imp.getWidth()!=0) return imp; else return null; + case FITS: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.FITS_Reader", path); + if (imp.getWidth()!=0) return imp; else return null; + case PGM: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.PGM_Reader", path); + if (imp.getWidth()!=0) { + if (imp.getStackSize()==3 && imp.getBitDepth()==16) + imp = new CompositeImage(imp, IJ.COMPOSITE); + return imp; + } else + return null; + case JPEG: + imp = openJpegOrGif(directory, name); + if (imp!=null&&imp.getWidth()!=0) return imp; else return null; + case GIF: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.GIF_Reader", path); + if (imp!=null&&imp.getWidth()!=0) return imp; else return null; + case PNG: + imp = openUsingImageIO(directory+name); + if (imp!=null&&imp.getWidth()!=0) return imp; else return null; + case BMP: + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.BMP_Reader", path); + if (imp.getWidth()!=0) return imp; else return null; + case ZIP: + return openZip(path); + case AVI: + AVI_Reader reader = new AVI_Reader(); + reader.setVirtual(true); + reader.displayDialog(!IJ.macroRunning()); + reader.run(path); + return reader.getImagePlus(); + case JAVA_OR_TEXT: + if (name.endsWith(".txt")) + return openTextImage(directory,name); + else + return null; + case UNKNOWN: case TEXT: + return openUsingHandleExtraFileTypes(path); + default: + return null; + } + } + + public ImagePlus openTempImage(String directory, String name) { + ImagePlus imp = openImage(directory, name); + if (imp!=null) + imp.setTemporary(); + return imp; + } + + // Call HandleExtraFileTypes plugin to see if it can handle unknown formats + // or files in TIFF format that the built in reader is unable to open. + private ImagePlus openUsingHandleExtraFileTypes(String path) { + File f = new File(path); + if (!f.exists()) + return null; + int[] wrap = new int[] {this.fileType}; + ImagePlus imp = openWithHandleExtraFileTypes(path, wrap); + if (imp!=null && imp.getNChannels()>1) + imp = new CompositeImage(imp, IJ.COLOR); + this.fileType = wrap[0]; + return imp; + } + + String getPath() { + OpenDialog od = new OpenDialog("Open", ""); + String dir = od.getDirectory(); + String name = od.getFileName(); + if (name==null) + return null; + else + return dir+name; + } + + /** Opens the specified text file as a float image. */ + public ImagePlus openTextImage(String dir, String name) { + String path = dir+name; + TextReader tr = new TextReader(); + ImageProcessor ip = tr.open(path); + return ip!=null?new ImagePlus(name,ip):null; + } + + /** + * Attempts to open the specified url as a tiff, zip compressed tiff, + * dicom, gif or jpeg. Tiff file names must end in ".tif", ZIP file names + * must end in ".zip" and dicom file names must end in ".dcm". Returns an + * ImagePlus object if successful. + * @see ij.IJ#openImage(String) + */ + public ImagePlus openURL(String url) { + url = updateUrl(url); + if (IJ.debugMode) IJ.log("OpenURL: "+url); + ImagePlus imp = openCachedImage(url); + if (imp!=null) + return imp; + try { + String name = ""; + int index = url.lastIndexOf('/'); + if (index==-1) + index = url.lastIndexOf('\\'); + if (index>0) + name = url.substring(index+1); + else + throw new MalformedURLException("Invalid URL: "+url); + if (url.indexOf(" ")!=-1) + url = url.replaceAll(" ", "%20"); + URL u = new URL(url); + IJ.showStatus(""+url); + String lurl = url.toLowerCase(Locale.US); + if (lurl.endsWith(".tif")) { + this.url = url; + imp = openTiff(u.openStream(), name); + } else if (lurl.endsWith(".zip")) + imp = openZipUsingUrl(u); + else if (lurl.endsWith(".jpg") || lurl.endsWith(".jpeg") || lurl.endsWith(".gif")) + imp = openJpegOrGifUsingURL(name, u); + else if (lurl.endsWith(".dcm") || lurl.endsWith(".ima")) { + imp = (ImagePlus)IJ.runPlugIn("ij.plugin.DICOM", url); + if (imp!=null && imp.getWidth()==0) imp = null; + } else if (lurl.endsWith(".png")) + imp = openPngUsingURL(name, u); + else { + URLConnection uc = u.openConnection(); + String type = uc.getContentType(); + if (type!=null && (type.equals("image/jpeg")||type.equals("image/gif"))) + imp = openJpegOrGifUsingURL(name, u); + else if (type!=null && type.equals("image/png")) + imp = openPngUsingURL(name, u); + else + imp = openWithHandleExtraFileTypes(url, new int[]{0}); + } + IJ.showStatus(""); + return imp; + } catch (Exception e) { + String msg = e.getMessage(); + if (msg==null || msg.equals("")) + msg = "" + e; + msg += "\n"+url; + IJ.error("Open URL", msg); + return null; + } + } + + /** Can't open imagej.nih.gov URLs due to encryption so redirect to imagej.net mirror. */ + public static String updateUrl(String url) { + if (url==null || !url.contains("nih.gov")) + return url; + if (IJ.isJava18()) + url = url.replace("http:", "https:"); + else { + url = url.replace("imagej.nih.gov/ij", "imagej.net"); + url = url.replace("rsb.info.nih.gov/ij", "imagej.net"); + url = url.replace("rsbweb.nih.gov/ij", "imagej.net"); + } + return url; + } + + private ImagePlus openCachedImage(String url) { + if (url==null || !url.contains("/images")) + return null; + String ijDir = IJ.getDirectory("imagej"); + if (ijDir==null) + return null; + int slash = url.lastIndexOf('/'); + File file = new File(ijDir + "samples", url.substring(slash+1)); + if (!file.exists()) + return null; + if (url.endsWith(".gif")) // ij.plugin.GIF_Reader does not correctly handle inverting LUTs + return openJpegOrGif(file.getParent()+File.separator, file.getName()); + return IJ.openImage(file.getPath()); + } + + /** Used by open() and IJ.open() to open text URLs. */ + void openTextURL(String url) { + if (url.endsWith(".pdf")||url.endsWith(".zip")) + return; + String text = IJ.openUrlAsString(url); + if (text!=null && text.startsWith("

+ Macros contained in a file named "StartupMacros.txt", in the same directory as the HTML file + containing the applet tag, will be installed on startup. +*/ +public class ImageJApplet extends Applet { + + /** Starts ImageJ if it's not already running. */ + public void init() { + ImageJ ij = IJ.getInstance(); + if (ij==null || (ij!=null && !ij.isShowing())) + new ImageJ(this); + for (int i=1; i<=9; i++) { + String url = getParameter("url"+i); + if (url==null) break; + ImagePlus imp = new ImagePlus(url); + if (imp!=null) imp.show(); + } + } + + public void destroy() { + ImageJ ij = IJ.getInstance(); + if (ij!=null) ij.quit(); + } + +} + diff --git a/src/ij/ImageListener.java b/src/ij/ImageListener.java new file mode 100644 index 0000000..ae8faac --- /dev/null +++ b/src/ij/ImageListener.java @@ -0,0 +1,17 @@ +package ij; + + /** Plugins that implement this interface are notified when + an image is opened, closed or updated. The + Plugins/Utilities/Monitor Events command uses this interface. + */ + public interface ImageListener { + + public void imageOpened(ImagePlus imp); + + public void imageClosed(ImagePlus imp); + + public void imageUpdated(ImagePlus imp); + + //default void imageSaved(ImagePlus imp) { } + +} diff --git a/src/ij/ImagePlus.java b/src/ij/ImagePlus.java new file mode 100644 index 0000000..231609e --- /dev/null +++ b/src/ij/ImagePlus.java @@ -0,0 +1,3438 @@ +package ij; +import java.awt.*; +import java.awt.image.*; +import java.net.URL; +import java.util.*; +import ij.process.*; +import ij.io.*; +import ij.gui.*; + +import ij.measure.*; +import ij.plugin.filter.Analyzer; +import ij.util.*; +import ij.macro.Interpreter; +import ij.plugin.*; +import ij.plugin.frame.*; + + +/** +An ImagePlus contain an ImageProcessor (2D image) or an ImageStack (3D, 4D or 5D image). +It also includes metadata (spatial calibration and possibly the directory/file where +it was read from). The ImageProcessor contains the pixel data (8-bit, 16-bit, float or RGB) +of the 2D image and some basic methods to manipulate it. An ImageStack is essentually +a list ImageProcessors of same type and size. +@see ij.process.ImageProcessor +@see ij.ImageStack +@see ij.gui.ImageWindow +@see ij.gui.ImageCanvas +*/ + +public class ImagePlus implements ImageObserver, Measurements, Cloneable { + + /** 8-bit grayscale (unsigned)*/ + public static final int GRAY8 = 0; + + /** 16-bit grayscale (unsigned) */ + public static final int GRAY16 = 1; + + /** 32-bit floating-point grayscale */ + public static final int GRAY32 = 2; + + /** 8-bit indexed color */ + public static final int COLOR_256 = 3; + + /** 32-bit RGB color */ + public static final int COLOR_RGB = 4; + + /** Title of image used by Flatten command */ + public static final String flattenTitle = "flatten~canvas"; + + /** True if any changes have been made to this image. */ + public boolean changes; + + protected Image img; + protected ImageProcessor ip; + protected ImageWindow win; + protected Roi roi; + protected int currentSlice; // current stack index (one-based) + protected static final int OPENED=0, CLOSED=1, UPDATED=2, SAVED=3; + protected boolean compositeImage; + protected int width; + protected int height; + protected boolean locked; + private int lockedCount; + private Thread lockingThread; + protected int nChannels = 1; + protected int nSlices = 1; + protected int nFrames = 1; + protected boolean dimensionsSet; + + private ImageJ ij = IJ.getInstance(); + private String title; + private String url; + private FileInfo fileInfo; + private int imageType = GRAY8; + private boolean typeSet; + private ImageStack stack; + private static int currentID = -1; + private int ID; + private static Component comp; + private boolean imageLoaded; + private int imageUpdateY, imageUpdateW; + private Properties properties; + private long startTime; + private Calibration calibration; + private static Calibration globalCalibration; + private boolean activated; + private boolean ignoreFlush; + private boolean errorLoadingImage; + private static ImagePlus clipboard; + private static Vector listeners = new Vector(); + private boolean openAsHyperStack; + private int[] position = {1,1,1}; + private boolean noUpdateMode; + private ImageCanvas flatteningCanvas; + private Overlay overlay; + private boolean compositeChanges; + private boolean hideOverlay; + private static int default16bitDisplayRange; + private boolean antialiasRendering = true; + private boolean ignoreGlobalCalibration; + private boolean oneSliceStack; + public boolean setIJMenuBar = Prefs.setIJMenuBar; + private Plot plot; + private Properties imageProperties; + private Color borderColor; + private boolean temporary; + + + /** Constructs an uninitialized ImagePlus. */ + public ImagePlus() { + title = (this instanceof CompositeImage)?"composite":"null"; + setID(); + } + + /** Constructs an ImagePlus from an Image or BufferedImage. The first + argument will be used as the title of the window that displays the image. + Throws an IllegalStateException if an error occurs while loading the image. */ + public ImagePlus(String title, Image image) { + this.title = title; + if (image!=null) + setImage(image); + setID(); + } + + /** Constructs an ImagePlus from an ImageProcessor. */ + public ImagePlus(String title, ImageProcessor ip) { + setProcessor(title, ip); + setID(); + } + + /** Constructs an ImagePlus from a TIFF, BMP, DICOM, FITS, + PGM, GIF or JPRG specified by a path or from a TIFF, DICOM, + GIF or JPEG specified by a URL. */ + public ImagePlus(String pathOrURL) { + Opener opener = new Opener(); + ImagePlus imp = null; + boolean isURL = pathOrURL.indexOf("://")>0; + if (isURL) + imp = opener.openURL(pathOrURL); + else + imp = opener.openImage(pathOrURL); + if (imp!=null) { + if (imp.getStackSize()>1) + setStack(imp.getTitle(), imp.getStack()); + else + setProcessor(imp.getTitle(), imp.getProcessor()); + setCalibration(imp.getCalibration()); + properties = imp.getProperties(); + setFileInfo(imp.getOriginalFileInfo()); + setDimensions(imp.getNChannels(), imp.getNSlices(), imp.getNFrames()); + setOverlay(imp.getOverlay()); + setRoi(imp.getRoi()); + if (isURL) + this.url = pathOrURL; + setID(); + } + } + + /** Constructs an ImagePlus from a stack. */ + public ImagePlus(String title, ImageStack stack) { + setStack(title, stack); + setID(); + } + + private void setID() { + ID = --currentID; + } + + public void setTemporary() { + if (!temporary) { + temporary = true; + currentID++; + ID = -Integer.MAX_VALUE; + } + } + + /** Locks the image so other threads can test to see if it is in use. + * One thread can lock an image multiple times, then it has to unlock + * it as many times until it is unlocked. This allows nested locking + * within a thread. + * Returns true if the image was successfully locked. + * Beeps, displays a message in the status bar, and returns + * false if the image is already locked by another thread. + */ + public synchronized boolean lock() { + return lock(true); + } + + /** Similar to lock, but doesn't beep and display an error + * message if the attempt to lock the image fails. + */ + public synchronized boolean lockSilently() { + return lock(false); + } + + private synchronized boolean lock(boolean loud) { + if (locked) { + if (Thread.currentThread()==lockingThread) { + lockedCount++; //allow locking multiple times by the same thread + return true; + } else { + if (loud) { + IJ.beep(); + IJ.showStatus("\"" + title + "\" is locked"); + if (IJ.debugMode) IJ.log(title + " is locked by " + lockingThread + "; refused locking by " + Thread.currentThread().getName()); + if (IJ.macroRunning()) + IJ.wait(500); + } + return false; + } + } else { + locked = true; //we could use 'lockedCount instead, but subclasses might use + lockedCount = 1; + lockingThread = Thread.currentThread(); + if (win instanceof StackWindow) + ((StackWindow)win).setSlidersEnabled(false); + if (IJ.debugMode) IJ.log(title + ": locked" + (loud ? "" : "silently") + " by " + Thread.currentThread().getName()); + return true; + } + } + + /** Unlocks the image. + * In case the image had been locked several times by the current thread, + * it gets unlocked only after as many unlock operations as there were + * previous lock operations. + */ + public synchronized void unlock() { + if (Thread.currentThread()==lockingThread && lockedCount>1) + lockedCount--; + else { + locked = false; + lockedCount = 0; + lockingThread = null; + if (win instanceof StackWindow) + ((StackWindow)win).setSlidersEnabled(true); + if (IJ.debugMode) IJ.log(title + ": unlocked"); + } + } + + /** Returns 'true' if the image is locked. */ + public boolean isLocked() { + return locked; + } + + /** Returns 'true' if the image was locked on another thread. */ + public boolean isLockedByAnotherThread() { + return locked && Thread.currentThread()!=lockingThread; + } + + private void waitForImage(Image image) { + if (comp==null) { + comp = IJ.getInstance(); + if (comp==null) + comp = new Canvas(); + } + imageLoaded = false; + if (!comp.prepareImage(image, this)) { + double progress; + waitStart = System.currentTimeMillis(); + while (!imageLoaded && !errorLoadingImage) { + IJ.wait(30); + if (imageUpdateW>1) { + progress = (double)imageUpdateY/imageUpdateW; + if (!(progress<1.0)) { + progress = 1.0 - (progress-1.0); + if (progress<0.0) progress = 0.9; + } + showProgress(progress); + } + } + showProgress(1.0); + } + } + + long waitStart; + private void showProgress(double percent) { + if ((System.currentTimeMillis()-waitStart)>500L) + IJ.showProgress(percent); + } + + /** Draws the image. If there is an ROI, its + outline is also displayed. Does nothing if there + is no window associated with this image (i.e. show() + has not been called).*/ + public void draw() { + if (win!=null) + win.getCanvas().repaint(); + } + + /** Draws image and roi outline using a clip rect. */ + public void draw(int x, int y, int width, int height){ + if (win!=null) { + ImageCanvas ic = win.getCanvas(); + double mag = ic.getMagnification(); + x = ic.screenX(x); + y = ic.screenY(y); + width = (int)(width*mag); + height = (int)(height*mag); + ic.repaint(x, y, width, height); + if (listeners.size()>0 && roi!=null && roi.getPasteMode()!=Roi.NOT_PASTING) + notifyListeners(UPDATED); + } + } + + /** Updates this image from the pixel data in its + associated ImageProcessor, then displays it. Does + nothing if there is no window associated with + this image (i.e. show() has not been called).*/ + public synchronized void updateAndDraw() { + if (win==null) { + img = null; + return; + } + if (stack!=null && !stack.isVirtual() && currentSlice>=1 && currentSlice<=stack.size()) { + if (stack.size()>1 && win!=null && !(win instanceof StackWindow)) { + setStack(stack); //adds scroll bar if stack size has changed to >1 + return; + } + Object pixels = stack.getPixels(currentSlice); + if (ip!=null && pixels!=null && pixels!=ip.getPixels()) { // was stack updated? + try { + ip.setPixels(pixels); + ip.setSnapshotPixels(null); + } catch(Exception e) {} + } + } + if (win!=null) { + win.getCanvas().setImageUpdated(); + if (listeners.size()>0) notifyListeners(UPDATED); + } + draw(); + } + + /** Use to update the image when the underlying virtual stack changes. */ + public void updateVirtualSlice() { + ImageStack vstack = getStack(); + if (vstack.isVirtual()) { + double min=getDisplayRangeMin(), max=getDisplayRangeMax(); + setProcessor(vstack.getProcessor(getCurrentSlice())); + setDisplayRange(min,max); + } else + throw new IllegalArgumentException("Virtual stack required"); + } + + /** Sets the display mode of composite color images, where 'mode' + should be IJ.COMPOSITE, IJ.COLOR or IJ.GRAYSCALE. */ + public void setDisplayMode(int mode) { + if (this instanceof CompositeImage) { + ((CompositeImage)this).setMode(mode); + updateAndDraw(); + } + } + + /** Returns the display mode (IJ.COMPOSITE, IJ.COLOR + or IJ.GRAYSCALE) if this is a composite color + image, or 0 if it not. */ + public int getDisplayMode() { + if (this instanceof CompositeImage) + return ((CompositeImage)this).getMode(); + else + return 0; + } + + /** Controls which channels in a composite color image are displayed, + where 'channels' is a list of ones and zeros that specify the channels to + display. For example, "101" causes channels 1 and 3 to be displayed. */ + public void setActiveChannels(String channels) { + if (!(this instanceof CompositeImage)) + return; + boolean[] active = ((CompositeImage)this).getActiveChannels(); + for (int i=0; ii && channels.charAt(i)=='1') + b = true; + active[i] = b; + } + updateAndDraw(); + Channels.updateChannels(); + } + + /** Updates this image from the pixel data in its + associated ImageProcessor, then displays it. + The CompositeImage class overrides this method + to only update the current channel. */ + public void updateChannelAndDraw() { + updateAndDraw(); + } + + /** Returns a reference to the current ImageProcessor. The + CompositeImage class overrides this method to return + the processor associated with the current channel. */ + public ImageProcessor getChannelProcessor() { + return getProcessor(); + } + + /** Returns an array containing the lookup tables used by this image, + * one per channel, or an empty array if this is an RGB image. + * @see #getNChannels + * @see #isComposite + * @see #getCompositeMode + */ + public LUT[] getLuts() { + ImageProcessor ip2 = getProcessor(); + if (ip2==null) + return new LUT[0]; + LUT lut = ip2.getLut(); + if (lut==null) + return new LUT[0]; + LUT[] luts = new LUT[1]; + luts[0] = lut; + return luts; + } + + /** Calls draw to draw the image and also repaints the + image window to force the information displayed above + the image (dimension, type, size) to be updated. */ + public void repaintWindow() { + if (win!=null) { + draw(); + win.repaint(); + } + } + + /** Calls updateAndDraw to update from the pixel data + and draw the image, and also repaints the image + window to force the information displayed above + the image (dimension, type, size) to be updated. */ + public void updateAndRepaintWindow() { + if (win!=null) { + updateAndDraw(); + win.repaint(); + } + } + + /** ImageCanvas.paint() calls this method when the + ImageProcessor has generated a new image. */ + public void updateImage() { + if (win==null) { + img = null; + return; + } + if (ip!=null) + img = ip.createImage(); + } + + /** Closes the window, if any, that is displaying this image. */ + public void hide() { + if (win==null) { + img = null; + Interpreter.removeBatchModeImage(this); + return; + } + boolean unlocked = lockSilently(); + Overlay overlay2 = getOverlay(); + changes = false; + win.close(); + win = null; + setOverlay(overlay2); + if (unlocked) unlock(); + } + + /** Closes this image and sets the ImageProcessor to null. To avoid the + "Save changes?" dialog, first set the public 'changes' variable to false. */ + public void close() { + ImageWindow win = getWindow(); + if (win!=null) + win.close(); + else { + if (WindowManager.getCurrentImage()==this) + WindowManager.setTempCurrentImage(null); + deleteRoi(); //save any ROI so it can be restored later + Interpreter.removeBatchModeImage(this); + } + } + + /** Opens a window to display this image and clears the status bar. */ + public void show() { + show(""); + } + + /** Opens a window to display this image and displays + 'statusMessage' in the status bar. */ + public void show(String statusMessage) { + if (isVisible() || temporary) + return; + win = null; + if ((IJ.isMacro() && ij==null) || Interpreter.isBatchMode()) { + if (isComposite()) ((CompositeImage)this).reset(); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) imp.saveRoi(); + WindowManager.setTempCurrentImage(this); + Interpreter.addBatchModeImage(this); + return; + } + if (Prefs.useInvertingLut && getBitDepth()==8 && ip!=null && !ip.isInvertedLut()&& !ip.isColorLut()) + invertLookupTable(); + img = getImage(); + if ((img!=null) && (width>=0) && (height>=0)) { + activated = false; + int stackSize = getStackSize(); + if (stackSize>1) + win = new StackWindow(this); // displays the window and (if macro) waits for window to be activated + else if (getProperty(Plot.PROPERTY_KEY) != null) + win = new PlotWindow(this, (Plot)(getProperty(Plot.PROPERTY_KEY))); + else + win = new ImageWindow(this); + if (roi!=null) roi.setImage(this); + if (overlay!=null && getCanvas()!=null) + getCanvas().setOverlay(overlay); + IJ.showStatus(statusMessage); + if (IJ.isMacro() && stackSize==1) // for non-stacks, wait for window to be activated + waitTillActivated(); + if (imageType==GRAY16 && default16bitDisplayRange!=0) { + resetDisplayRange(); + updateAndDraw(); + } + if (stackSize>1) { + int c = getChannel(); + int z = getSlice(); + int t = getFrame(); + if (c>1 || z>1 || t>1) + setPosition(c, z, t); + } + if (setIJMenuBar) + IJ.wait(25); + notifyListeners(OPENED); + } + } + + void invertLookupTable() { + int nImages = getStackSize(); + ip.invertLut(); + if (nImages==1) + ip.invert(); + else { + ImageStack stack2 = getStack(); + for (int i=1; i<=nImages; i++) + stack2.getProcessor(i).invert(); + stack2.setColorModel(ip.getColorModel()); + } + } + + /** Waits until the image window becomes activated. This is necessary in + * macros or other programs if an ImagePlus is shown on the screen, + * because displaying the window is asynchronous (happens later) + * and will make the image the active one. Without waiting, in the + * meanwhile another window could be already the active one and would + * become deactivated. + * If the ImagePlus may have been displayed previously, first call + * setDeactivated(). + * ImagePlus.show() and new StackWindow(ImagePlus) + * call this method if IJ.isMacro() is true, i.e., when running a macro or + * executing an IJ.run(...) call. + */ + public void waitTillActivated() { + if (win == null) return; + if (EventQueue.isDispatchThread()) { //'activated' is set in the EventQueue, we can't wait for it in the EventQueue + WindowManager.setTempCurrentImage(this); + return; + } + long start = System.currentTimeMillis(); + while (!activated) { + IJ.wait(5); + if (ij != null && ij.quitting()) return; + if ((System.currentTimeMillis()-start)>2000) { + WindowManager.setTempCurrentImage(this); + break; // 2 second timeout + } + } + } + + /** Called by ImageWindow.windowActivated(); to end waiting in waitTillActivated. */ + public void setActivated() { + activated = true; + if (borderColor!=null && win!=null) + win.setBackground(borderColor); + } + + /** Called by new StackWindow(ImagePlus) + * before showing the StackWindow, to prepare for + * waitTillActivated(). + */ + public void setDeactivated() { + activated = false; + } + + /** Returns this image as a AWT image. */ + public Image getImage() { + if (img==null && ip!=null) + img = ip.createImage(); + return img; + } + + /** Returns a copy of this image as an 8-bit or RGB BufferedImage. + * @see ij.process.ShortProcessor#get16BitBufferedImage + */ + public BufferedImage getBufferedImage() { + if (isComposite()) + return (new ColorProcessor(getImage())).getBufferedImage(); + else + return ip.getBufferedImage(); + } + + /** Returns this image's unique numeric ID. */ + public int getID() { + return ID; + } + + /** Replaces the image, if any, with the one specified. + * Throws an IllegalStateException if an error occurs + * while loading the image. + */ + public void setImage(Image image) { + if (image instanceof BufferedImage) { + BufferedImage bi = (BufferedImage)image; + int nBands = bi.getSampleModel().getNumBands(); + int type = bi.getType(); + boolean rgb = type==BufferedImage.TYPE_3BYTE_BGR || type==BufferedImage.TYPE_INT_RGB || type==BufferedImage.TYPE_4BYTE_ABGR; + if (nBands>1 && !rgb) { + ImageStack biStack = new ImageStack(bi.getWidth(), bi.getHeight()); + for (int b=0; b0?GRAY8:COLOR_RGB; + if (image!=null && type==COLOR_RGB) + ip = new ColorProcessor(image); + if (ip==null && image!=null) + ip = new ByteProcessor(image); + setType(type); + this.img = ip.createImage(); + if (win!=null) { + if (dimensionsChanged) + win = new ImageWindow(this); + else + repaintWindow(); + } + } + + /** + * Extract pixels as an an ImageProcessor from a single band of a BufferedImage. + * @param img + * @param band + * @return + */ + public static ImageProcessor convertToImageProcessor(BufferedImage img, int band) { + int w = img.getWidth(); + int h = img.getHeight(); + int dataType = img.getSampleModel().getDataType(); + // Read data as float (no matter what it is - it's the most accuracy ImageJ can provide) + FloatProcessor fp = new FloatProcessor(w, h); + float[] pixels = (float[])fp.getPixels(); + img.getRaster().getSamples(0, 0, w, h, band, pixels); + // Convert to 8 or 16-bit, if appropriate + if (dataType == DataBuffer.TYPE_BYTE) { + ByteProcessor bp = new ByteProcessor(w, h); + bp.setPixels(0, fp); + return bp; + } else if (dataType == DataBuffer.TYPE_USHORT) { + ShortProcessor sp = new ShortProcessor(w, h); + sp.setPixels(0, fp); + return sp; + } else + return fp; + } + + /** Replaces this image with the specified ImagePlus. May + not work as expected if 'imp' is a CompositeImage + and this image is not. */ + public void setImage(ImagePlus imp) { + Properties newProperties = imp.getProperties(); + if (newProperties!=null) + newProperties = (Properties)(newProperties.clone()); + if (imp.getWindow()!=null) + imp = imp.duplicate(); + ImageStack stack2 = imp.getStack(); + if (imp.isHyperStack()) + setOpenAsHyperStack(true); + LUT[] luts = null; + if (imp.isComposite() && (this instanceof CompositeImage)) { + if (((CompositeImage)imp).getMode()!=((CompositeImage)this).getMode()) + ((CompositeImage)this).setMode(((CompositeImage)imp).getMode()); + luts = ((CompositeImage)imp).getLuts(); + } + LUT lut = !imp.isComposite()?imp.getProcessor().getLut():null; + setStack(stack2, imp.getNChannels(), imp.getNSlices(), imp.getNFrames()); + compositeImage = imp.isComposite(); + if (luts!=null) { + ((CompositeImage)this).setLuts(luts); + ((CompositeImage)this).setMode(((CompositeImage)imp).getMode()); + updateAndRepaintWindow(); + } else if (lut!=null) { + getProcessor().setLut(lut); + updateAndRepaintWindow(); + } + setTitle(imp.getTitle()); + setCalibration(imp.getCalibration()); + setOverlay(imp.getOverlay()); + properties = newProperties; + if (getProperty(Plot.PROPERTY_KEY)!=null && win instanceof PlotWindow) { + Plot plot = (Plot)(getProperty(Plot.PROPERTY_KEY)); + ((PlotWindow)win).setPlot(plot); + plot.setImagePlus(this); + } + setFileInfo(imp.getOriginalFileInfo()); + setProperty ("Info", imp.getProperty ("Info")); + setProperties(imp.getPropertiesAsArray()); + } + + /** Replaces the ImageProcessor with the one specified and updates the + display. With stacks, the ImageProcessor must be the same type as the + other images in the stack and it must be the same width and height. */ + public void setProcessor(ImageProcessor ip) { + setProcessor(null, ip); + } + + /** Replaces the ImageProcessor with the one specified and updates the display. With + stacks, the ImageProcessor must be the same type as other images in the stack and + it must be the same width and height. Set 'title' to null to leave the title unchanged. */ + public void setProcessor(String title, ImageProcessor ip) { + if (ip==null || ip.getPixels()==null) + throw new IllegalArgumentException("ip null or ip.getPixels() null"); + if (getStackSize()>1) { + if (ip.getWidth()!=width || ip.getHeight()!=height) + throw new IllegalArgumentException("Wrong dimensions for this stack"); + int stackBitDepth = stack!=null?stack.getBitDepth():0; + if (stackBitDepth>0 && getBitDepth()!=stackBitDepth) + throw new IllegalArgumentException("Wrong type for this stack"); + } else { + setStackNull(); + setCurrentSlice(1); + } + setProcessor2(title, ip, null); + } + + void setProcessor2(String title, ImageProcessor ip, ImageStack newStack) { + if (title!=null) setTitle(title); + if (ip==null) + return; + this.ip = ip; + if (this.ip!=null && getWindow()!=null) + notifyListeners(UPDATED); + if (ij!=null) + ip.setProgressBar(ij.getProgressBar()); + int stackSize = 1; + boolean dimensionsChanged = width>0 && height>0 && (width!=ip.getWidth() || height!=ip.getHeight()); + if (stack!=null) { + stackSize = stack.size(); + if (currentSlice>stackSize) + setCurrentSlice(stackSize); + if (currentSlice>=1 && currentSlice<=stackSize && !dimensionsChanged) + stack.setPixels(ip.getPixels(),currentSlice); + } + img = null; + if (dimensionsChanged) roi = null; + int type; + if (ip instanceof ByteProcessor) + type = GRAY8; + else if (ip instanceof ColorProcessor) + type = COLOR_RGB; + else if (ip instanceof ShortProcessor) + type = GRAY16; + else + type = GRAY32; + if (width==0) + imageType = type; + else + setType(type); + width = ip.getWidth(); + height = ip.getHeight(); + if (win!=null) { + if (dimensionsChanged && stackSize==1) + win.updateImage(this); + else if (newStack==null) + repaintWindow(); + draw(); + } + } + + /** Replaces the image with the specified stack and updates the display. */ + public void setStack(ImageStack stack) { + setStack(null, stack); + } + + /** Replaces the image with the specified stack and updates + the display. Set 'title' to null to leave the title unchanged. */ + public void setStack(String title, ImageStack newStack) { + int bitDepth1 = getBitDepth(); + int previousStackSize = getStackSize(); + int newStackSize = newStack.getSize(); + if (newStackSize==0) + throw new IllegalArgumentException("Stack is empty"); + if (!newStack.isVirtual()) { + Object[] arrays = newStack.getImageArray(); + if (arrays==null || (arrays.length>0&&arrays[0]==null)) + throw new IllegalArgumentException("Stack pixel array null"); + } + boolean sliderChange = false; + if (win!=null && (win instanceof StackWindow)) { + int nScrollbars = ((StackWindow)win).getNScrollbars(); + if (nScrollbars>0 && newStackSize==1) + sliderChange = true; + else if (nScrollbars==0 && newStackSize>1) + sliderChange = true; + } + if (currentSlice<1) setCurrentSlice(1); + boolean resetCurrentSlice = currentSlice>newStackSize; + if (resetCurrentSlice) setCurrentSlice(newStackSize); + ImageProcessor ip = newStack.getProcessor(currentSlice); + boolean dimensionsChanged = width>0 && height>0 && (width!=ip.getWidth()||height!=ip.getHeight()); + if (this.stack==null) + newStack.viewers(+1); + this.stack = newStack; + oneSliceStack = false; + setProcessor2(title, ip, newStack); + if (bitDepth1!=0 && bitDepth1!=getBitDepth()) + compositeChanges = true; + if (compositeChanges && (this instanceof CompositeImage)) { + this.compositeImage = getStackSize()!=getNSlices(); + ((CompositeImage)this).completeReset(); + if (bitDepth1!=0 && bitDepth1!=getBitDepth()) + ((CompositeImage)this).resetDisplayRanges(); + } + compositeChanges = false; + if (win==null) { + if (resetCurrentSlice) setSlice(currentSlice); + return; + } + boolean invalidDimensions = (isDisplayedHyperStack()||(this instanceof CompositeImage)) && (win instanceof StackWindow) && !((StackWindow)win).validDimensions(); + if (newStackSize>1 && !(win instanceof StackWindow)) { + if (isDisplayedHyperStack()) + setOpenAsHyperStack(true); + activated = false; + win = new StackWindow(this, dimensionsChanged?null:getCanvas()); // replaces this window + if (IJ.isMacro()) waitTillActivated(); // wait for stack window to be activated + setPosition(1, 1, 1); + } else if (newStackSize>1 && invalidDimensions) { + if (isDisplayedHyperStack()) + setOpenAsHyperStack(true); + win = new StackWindow(this); // replaces this window + setPosition(1, 1, 1); + } else if (dimensionsChanged || sliderChange) { + win.updateImage(this); + } else { + if (win!=null && win instanceof StackWindow) + ((StackWindow)win).updateSliceSelector(); + if (isComposite()) { + ((CompositeImage)this).reset(); + updateAndDraw(); + } + repaintWindow(); + } + if (resetCurrentSlice) + setSlice(currentSlice); + } + + public void setStack(ImageStack newStack, int channels, int slices, int frames) { + if (newStack==null || channels*slices*frames!=newStack.getSize()) + throw new IllegalArgumentException("channels*slices*frames!=stackSize"); + if (IJ.debugMode) IJ.log("setStack: "+newStack.getSize()+" "+channels+" "+slices+" "+frames+" "+isComposite()); + compositeChanges = channels!=this.nChannels; + this.nChannels = channels; + this.nSlices = slices; + this.nFrames = frames; + setStack(null, newStack); + } + + private synchronized void setStackNull() { + if (oneSliceStack && stack!=null && stack.size()>0) { + String label = stack.getSliceLabel(1); + setProp("Slice_Label", label); + } + stack = null; + oneSliceStack = false; + } + + /** Saves this image's FileInfo so it can be later + retieved using getOriginalFileInfo(). */ + public void setFileInfo(FileInfo fi) { + if (fi!=null) { + fi.pixels = null; + if (fi.imageSaved) { + notifyListeners(SAVED); + fi.imageSaved = false; + } + } + fileInfo = fi; + } + + /** Returns the ImageWindow that is being used to display + this image. Returns null if show() has not be called + or the ImageWindow has been closed. */ + public ImageWindow getWindow() { + return win; + } + + /** Returns true if this image is currently being displayed in a window. */ + public boolean isVisible() { + return win!=null && win.isVisible(); + } + + /** This method should only be called from an ImageWindow. */ + public void setWindow(ImageWindow win) { + this.win = win; + if (roi!=null) + roi.setImage(this); // update roi's 'ic' field + } + + /** Returns the ImageCanvas being used to + display this image, or null. */ + public ImageCanvas getCanvas() { + return win!=null?win.getCanvas():flatteningCanvas; + } + + /** Sets current foreground color. */ + public void setColor(Color c) { + if (ip!=null) + ip.setColor(c); + } + + void setupProcessor() { + } + + public boolean isProcessor() { + return ip!=null; + } + + /** Returns a reference to the current ImageProcessor. If there + is no ImageProcessor, it creates one. Returns null if this + ImagePlus contains no ImageProcessor and no AWT Image. + Sets the line width to the current line width and sets the + calibration table if the image is density calibrated. */ + public ImageProcessor getProcessor() { + if (ip==null) + return null; + if (roi!=null && roi.isArea()) + ip.setRoi(roi.getBounds()); + else + ip.resetRoi(); + if (!compositeImage) + ip.setLineWidth(Line.getWidth()); + if (ij!=null) + ip.setProgressBar(ij.getProgressBar()); + Calibration cal = getCalibration(); + if (cal.calibrated()) + ip.setCalibrationTable(cal.getCTable()); + else + ip.setCalibrationTable(null); + if (Recorder.record) { + Recorder recorder = Recorder.getInstance(); + if (recorder!=null) recorder.imageUpdated(this); + } + return ip; + } + + /** Frees RAM by setting the snapshot (undo) buffer in + the current ImageProcessor to null. */ + public void trimProcessor() { + ImageProcessor ip2 = ip; + if (!locked && ip2!=null) { + if (IJ.debugMode) IJ.log(title + ": trimProcessor"); + Roi roi2 = getRoi(); + if (roi2!=null && roi2.getPasteMode()!=Roi.NOT_PASTING) + roi2.endPaste(); + ip2.setSnapshotPixels(null); + } + } + + /** For images with irregular ROIs, returns a byte mask, otherwise, returns + * null. Mask pixels have a non-zero value.and the dimensions of the + * mask are equal to the width and height of the ROI. + * @see ij.ImagePlus#createRoiMask + * @see ij.ImagePlus#createThresholdMask + */ + public ImageProcessor getMask() { + if (roi==null) { + if (ip!=null) ip.resetRoi(); + return null; + } + ImageProcessor mask = roi.getMask(); + if (mask==null) + return null; + if (ip!=null && roi!=null) { + ip.setMask(mask); + ip.setRoi(roi.getBounds()); + } + return mask; + } + + /** Returns an 8-bit binary (foreground=255, background=0) + * ROI or overlay mask that has the same dimensions + * as this image. Creates an ROI mask If the image has both + * both an ROI and an overlay. Set the threshold of the mask to 255. + * @see #createThresholdMask + * @see ij.gui.Roi#getMask + */ + public ByteProcessor createRoiMask() { + Roi roi2 = getRoi(); + Overlay overlay2 = getOverlay(); + if (roi2==null && overlay2==null) + throw new IllegalArgumentException("ROI or overlay required"); + ByteProcessor mask = new ByteProcessor(getWidth(),getHeight()); + mask.setColor(255); + if (roi2!=null) + mask.fill(roi2); + else if (overlay2!=null) { + if (overlay2.size()==1 && (overlay2.get(0) instanceof ImageRoi)) { + ImageRoi iRoi = (ImageRoi)overlay2.get(0); + ImageProcessor ip = iRoi.getProcessor(); + if (ip.getWidth()!=mask.getWidth() || ip.getHeight()!=mask.getHeight()) + return mask; + for (int i=0; i + imp = IJ.getImage(); + stats = imp.getStatistics(); + IJ.log("Area: "+stats.area); + IJ.log("Mean: "+stats.mean); + IJ.log("Max: "+stats.max); + + @return an {@link ij.process.ImageStatistics} object + @see #getAllStatistics + @see #getRawStatistics + @see ij.process.ImageProcessor#getStats + */ + public ImageStatistics getStatistics() { + return getStatistics(AREA+MEAN+STD_DEV+MODE+MIN_MAX+RECT); + } + + /** This method returns complete calibrated statistics for this + * image or ROI (with "Limit to threshold"), but it is up to 70 times + * slower than getStatistics(). + * @return an {@link ij.process.ImageStatistics} object + * @see #getStatistics + * @see ij.process.ImageProcessor#getStatistics + */ + public ImageStatistics getAllStatistics() { + return getStatistics(ALL_STATS+LIMIT); + } + + /* Returns uncalibrated statistics for this image or ROI, including + 256 bin histogram, pixelCount, mean, mode, min and max. */ + public ImageStatistics getRawStatistics() { + if (roi!=null && roi.isArea()) + ip.setRoi(roi); + else + ip.resetRoi(); + return ImageStatistics.getStatistics(ip, AREA+MEAN+MODE+MIN_MAX, null); + } + + /** Returns an ImageStatistics object generated using the + specified measurement options. + @see ij.measure.Measurements + */ + public ImageStatistics getStatistics(int mOptions) { + return getStatistics(mOptions, 256, 0.0, 0.0); + } + + /** Returns an ImageStatistics object generated using the + specified measurement options and histogram bin count. */ + public ImageStatistics getStatistics(int mOptions, int nBins) { + return getStatistics(mOptions, nBins, 0.0, 0.0); + } + + /** Returns an ImageStatistics object generated using the + specified measurement options, histogram bin count + and histogram range. */ + public ImageStatistics getStatistics(int mOptions, int nBins, double histMin, double histMax) { + ImageProcessor ip2 = ip; + int bitDepth = getBitDepth(); + if (nBins!=256 && (bitDepth==8||bitDepth==24)) + ip2 =ip.convertToShort(false); + Roi roi2 = roi; + if (roi2==null) + ip2.resetRoi(); + else if (roi2.isArea()) + ip2.setRoi(roi2); + else if ((roi2 instanceof PointRoi) && roi2.size()==1) { + // needed to be consistent with ImageProcessor.getStatistics() + FloatPolygon p = roi2.getFloatPolygon(); + ip2.setRoi((int)p.xpoints[0], (int)p.ypoints[0], 1, 1); + } + ip2.setHistogramSize(nBins); + Calibration cal = getCalibration(); + if (getType()==GRAY16&& !(histMin==0.0&&histMax==0.0)) { + histMin = cal.getRawValue(histMin); + histMax=cal.getRawValue(histMax); + } + ip2.setHistogramRange(histMin, histMax); + ImageStatistics stats = ImageStatistics.getStatistics(ip2, mOptions, cal); + ip2.setHistogramSize(256); + ip2.setHistogramRange(0.0, 0.0); + return stats; + } + + /** Returns the image name. */ + public String getTitle() { + if (title==null) + return ""; + else + return title; + } + + /** If the image title is a file name, returns the name + without the extension and with spaces removed, + otherwise returns the title shortened to the first space. + */ + public String getShortTitle() { + String title = getTitle().trim(); + int index = title.lastIndexOf('.'); + boolean fileName = index>0; + if (fileName) { + title = title.substring(0, index); + title = title.replaceAll(" ",""); + } else { + index = title.indexOf(' '); + if (index>-1 && !fileName) + title = title.substring(0, index); + } + return title; + } + + /** Sets the image name. */ + public void setTitle(String title) { + if (title==null) + return; + if (win!=null) { + if (ij!=null) + Menus.updateWindowMenuItem(this, this.title, title); + String virtual = stack!=null && stack.isVirtual()?" (V)":""; + String global = getGlobalCalibration()!=null?" (G)":""; + String scale = ""; + double magnification = win.getCanvas().getMagnification(); + if (magnification!=1.0) { + double percent = magnification*100.0; + int digits = percent>100.0||percent==(int)percent?0:1; + scale = " (" + IJ.d2s(percent,digits) + "%)"; + } + win.setTitle(title+virtual+global+scale); + } + boolean titleChanged = !title.equals(this.title); + this.title = title; + if (titleChanged && listeners.size()>0) + notifyListeners(UPDATED); + } + + /** Returns the width of this image in pixels. */ + public int getWidth() { + return width; + } + + /** Returns the height of this image in pixels. */ + public int getHeight() { + return height; + } + + /** Returns the size of this image in bytes. */ + public double getSizeInBytes() { + double size = ((double)getWidth()*getHeight()*getStackSize()); + int type = getType(); + switch (type) { + case ImagePlus.GRAY16: size *= 2.0; break; + case ImagePlus.GRAY32: size *= 4.0; break; + case ImagePlus.COLOR_RGB: size *= 4.0; break; + } + return size; + } + + /** If this is a stack, returns the number of slices, else returns 1. */ + public int getStackSize() { + if (stack==null || oneSliceStack) + return 1; + else { + int slices = stack.size(); + if (slices<=0) slices = 1; + return slices; + } + } + + /** If this is a stack, returns the actual number of images in the stack, else returns 1. */ + public int getImageStackSize() { + if (stack==null) + return 1; + else { + int slices = stack.size(); + if (slices==0) slices = 1; + return slices; + } + } + + /** Sets the 3rd, 4th and 5th dimensions, where + nChannels*nSlices*nFrames + must be equal to the stack size. */ + public void setDimensions(int nChannels, int nSlices, int nFrames) { + //IJ.log("setDimensions: "+nChannels+" "+nSlices+" "+nFrames+" "+getImageStackSize()); + if (nChannels*nSlices*nFrames!=getImageStackSize() && ip!=null) { + //throw new IllegalArgumentException("channels*slices*frames!=stackSize"); + nChannels = 1; + nSlices = getImageStackSize(); + nFrames = 1; + if (isDisplayedHyperStack()) { + setOpenAsHyperStack(false); + new StackWindow(this); + setSlice(1); + } + } + boolean updateWin = isDisplayedHyperStack() && (this.nChannels!=nChannels||this.nSlices!=nSlices||this.nFrames!=nFrames); + boolean newSingleImage = win!=null && (win instanceof StackWindow) && nChannels==1&&nSlices==1&&nFrames==1; + if (newSingleImage) updateWin = true; + this.nChannels = nChannels; + this.nSlices = nSlices; + this.nFrames = nFrames; + if (updateWin) { + if (nSlices!=getImageStackSize()) + setOpenAsHyperStack(true); + ip = null; + img = null; + setPositionWithoutUpdate(getChannel(), getSlice(), getFrame()); + if (isComposite()) ((CompositeImage)this).reset(); + new StackWindow(this); + } + dimensionsSet = true; + } + + /** Returns 'true' if this image is a hyperstack. */ + public boolean isHyperStack() { + return isDisplayedHyperStack() || (openAsHyperStack&&getNDimensions()>3); + } + + /** Returns the number of dimensions (2, 3, 4 or 5). */ + public int getNDimensions() { + int dimensions = 2; + int[] dim = getDimensions(true); + if (dim[2]>1) dimensions++; + if (dim[3]>1) dimensions++; + if (dim[4]>1) dimensions++; + return dimensions; + } + + /** Returns 'true' if this is a hyperstack currently being displayed in a StackWindow. */ + public boolean isDisplayedHyperStack() { + return win!=null && win instanceof StackWindow && ((StackWindow)win).isHyperStack(); + } + + /** Returns the number of channels. */ + public int getNChannels() { + verifyDimensions(); + return nChannels; + } + + /** Returns the image depth (number of z-slices). */ + public int getNSlices() { + verifyDimensions(); + return nSlices; + } + + /** Returns the number of frames (time-points). */ + public int getNFrames() { + verifyDimensions(); + return nFrames; + } + + /** Returns the dimensions of this image (width, height, nChannels, + nSlices, nFrames) as a 5 element int array. */ + public int[] getDimensions() { + return getDimensions(true); + } + + public int[] getDimensions(boolean varify) { + if (varify) + verifyDimensions(); + int[] d = new int[5]; + d[0] = width; + d[1] = height; + d[2] = nChannels; + d[3] = nSlices; + d[4] = nFrames; + return d; + } + + void verifyDimensions() { + int stackSize = getImageStackSize(); + if (nSlices==1) { + if (nChannels>1 && nFrames==1) + nChannels = stackSize; + else if (nFrames>1 && nChannels==1) + nFrames = stackSize; + } + if (nChannels*nSlices*nFrames!=stackSize) { + nSlices = stackSize; + nChannels = 1; + nFrames = 1; + } + } + + /** Returns the current image type (ImagePlus.GRAY8, ImagePlus.GRAY16, + ImagePlus.GRAY32, ImagePlus.COLOR_256 or ImagePlus.COLOR_RGB). + @see #getBitDepth + */ + public int getType() { + return imageType; + } + + /** Returns the bit depth, 8, 16, 24 (RGB) or 32, or 0 if the bit depth + is unknown. RGB images actually use 32 bits per pixel. */ + public int getBitDepth() { + ImageProcessor ip2 = ip; + if (ip2==null) { + int bitDepth = 0; + switch (imageType) { + case GRAY8: bitDepth=typeSet?8:0; break; + case COLOR_256: bitDepth=8; break; + case GRAY16: bitDepth=16; break; + case GRAY32: bitDepth=32; break; + case COLOR_RGB: bitDepth=24; break; + } + return bitDepth; + } + if (ip2 instanceof ByteProcessor) + return 8; + else if (ip2 instanceof ShortProcessor) + return 16; + else if (ip2 instanceof ColorProcessor) + return 24; + else if (ip2 instanceof FloatProcessor) + return 32; + return 0; + } + + /** Returns the number of bytes per pixel. */ + public int getBytesPerPixel() { + switch (imageType) { + case GRAY16: return 2; + case GRAY32: case COLOR_RGB: return 4; + default: return 1; + } + } + + protected void setType(int type) { + if ((type<0) || (type>COLOR_RGB)) + return; + int previousType = imageType; + imageType = type; + if (imageType!=previousType) { + if (win!=null) + Menus.updateMenus(); + getLocalCalibration().setImage(this); + } + typeSet = true; + } + + public void setTypeToColor256() { + if (imageType==ImagePlus.GRAY8) { + ImageProcessor ip2 = getProcessor(); + if (ip2!=null && ip2.getMinThreshold()==ImageProcessor.NO_THRESHOLD && ip2.isColorLut() && !ip2.isPseudoColorLut()) { + imageType = COLOR_256; + typeSet = true; + } + } + } + + + /** Returns the string value from the "Info" property string + * associated with 'key', or null if the key is not found. + * Works with DICOM tags and Bio-Formats metadata. + * @see #getNumericProperty + * @see #getInfoProperty + * @see #getProp + * @see #setProp + */ + public String getStringProperty(String key) { + if (key==null) + return null; + if (isDicomTag(key)) + return DicomTools.getTag(this, key); + if (getStackSize()>1) { + ImageStack stack2 = getStack(); + String label = stack2.getSliceLabel(getCurrentSlice()); + if (label!=null && label.indexOf('\n')>0) { + String value = getStringProperty(key, label); + if (value!=null) + return value; + } + } + Object obj = getProperty("Info"); + if (obj==null || !(obj instanceof String)) + return null; + String info = (String)obj; + return getStringProperty(key, info); + } + + private boolean isDicomTag(String key) { + if (key.length()!=9 || key.charAt(4)!=',') + return false; + key = key.toLowerCase(); + for (int i=0; i<9; i++) { + char c = i!=4?key.charAt(i):'0'; + if (!(Character.isDigit(c)||(c=='a'||c=='b'||c=='c'||c=='d'||c=='e'||c=='f'))) + return false; + } + return true; + } + + /** Returns the numeric value from the "Info" property string + * associated with 'key', or NaN if the key is not found or the + * value associated with the key is not numeric. Works with + * DICOM tags and Bio-Formats metadata. + * @see #getStringProperty + * @see #getInfoProperty + */ + public double getNumericProperty(String key) { + return Tools.parseDouble(getStringProperty(key)); + } + + private String getStringProperty(String key, String info) { + int index1 = -1; + index1 = findKey(info, key+": "); // standard 'key: value' pair? + if (index1<0) // Bio-Formats metadata? + index1 = findKey(info, key+" = "); + if (index1<0) // '=' with no spaces + index1 = findKey(info, key+"="); + if (index1<0) // otherwise not found + return null; + if (index1==info.length()) + return ""; //empty value at the end + int index2 = info.indexOf("\n", index1); + if (index2==-1) + index2=info.length(); + String value = info.substring(index1, index2); + return value; + } + + /** Find a key in a String (words merely ending with 'key' don't qualify). + * @return index of first character after the key, or -1 if not found + */ + private int findKey(String s, String key) { + int i = s.indexOf(key); + if (i<0) + return -1; //key not found + while (i>0 && Character.isLetterOrDigit(s.charAt(i-1))) + i = s.indexOf(key, i+key.length()); + if (i>=0) + return i + key.length(); + else + return -1; + } + + /** Adds a key-value pair to this image's string properties. + * The key-value pair is removed if 'value' is null. The + * properties persist if the image is saved in TIFF format. + * Add a "HideInfo" property (e.g. set("HideInfo","true")) to + * prevent the properties from being displayed by the + * Image/Show Info command. + */ + public void setProp(String key, String value) { + if (key==null) + return; + if (imageProperties==null) + imageProperties = new Properties(); + if (value==null || value.length()==0) + imageProperties.remove(key); + else + imageProperties.setProperty(key, value); + } + + /** Saves a persistent numeric propery. The property is + * removed if 'value' is NaN. + * @see #getNumericProp + */ + public void setProp(String key, double value) { + String svalue = ""+value; + if (svalue.endsWith(".0")) + svalue = svalue.substring(0,svalue.length()-2); + setProp(key, Double.isNaN(value)?null:svalue); + } + + /** Returns as a string the image property associated with the + * specified key or null if the property is not found. + * @see #setProp + * @see #getNumericProp + * @see #getStringProperty + */ + public String getProp(String key) { + if (imageProperties==null) + return getStringProperty(key); + else { + String value = imageProperties.getProperty(key); + if (value==null) + value = getStringProperty(key); + return value; + } + } + + /** Returns the numeric property associated with the specified key + * or NaN if the property is not found. + * @see #setProp(String,double) + * @see #getProp + */ + public double getNumericProp(String key) { + if (imageProperties==null) + return Double.NaN; + else + return Tools.parseDouble(getProp(key), Double.NaN); + } + + /** Used for saving string properties in TIFF header. */ + public String[] getPropertiesAsArray() { + if (imageProperties==null || imageProperties.size()==0) + return null; + String props[] = new String[imageProperties.size()*2]; + int index = 0; + for (Enumeration en=imageProperties.keys(); en.hasMoreElements();) { + String key = (String)en.nextElement(); + String value = imageProperties.getProperty(key); + props[index++] = key; + props[index++] = value; + } + return props; + } + + /** Returns information displayed by Image/Show Info command. */ + public String getPropsInfo() { + if (imageProperties==null || imageProperties.size()==0) + return "0"; + String info2 = ""; + for (Enumeration en=imageProperties.keys(); en.hasMoreElements();) { + String key = (String)en.nextElement(); + if (info2.length()>50) { + info2 += "..."; + break; + } else + info2 += " " + key; + } + if (info2.length()>1) + info2 = " (" + info2.substring(1) + ")"; + return imageProperties.size() + info2; + } + + /** Creates a set of image properties from an array of strings. + * @see #getPropertiesAsArray + * @see #getProp(String) + * @see #setProp(String,String) + */ + public void setProperties(String[] props) { + imageProperties = null; + if (props==null) + return; + //IJ.log("setProperties: "+props.length+" "+getTitle()); + for (int i=0; i>16; + int g = (c&0xff00)>>8; + int b = c&0xff; + pvalue[0] = r; + pvalue[1] = g; + pvalue[2] = b; + break; + case GRAY16: case GRAY32: + if (ip!=null) pvalue[0] = ip.getPixel(x, y); + break; + } + return pvalue; + } + + /** Returns an empty image stack that has the same + width, height and color table as this image. */ + public ImageStack createEmptyStack() { + ColorModel cm; + if (ip!=null) + cm = ip.getColorModel(); + else + cm = createLut().getColorModel(); + return new ImageStack(width, height, cm); + } + + /** Returns the image stack. The stack may have only + one slice. After adding or removing slices, call + setStack() to update the image and + the window that is displaying it. + @see #setStack + */ + public ImageStack getStack() { + ImageStack s; + if (stack==null) { + s = createEmptyStack(); + ImageProcessor ip2 = getProcessor(); + if (ip2==null) + return s; + String label = getProp("Slice_Label"); + if (label==null) { + String info = (String)getProperty("Info"); + label = info!=null?getTitle()+"\n"+info:null; // DICOM metadata + } + s.addSlice(label, ip2); + s.update(ip2); + this.stack = s; + ip = ip2; + oneSliceStack = true; + setCurrentSlice(1); + } else { + s = stack; + if (ip!=null) { + Calibration cal = getCalibration(); + if (cal.calibrated()) + ip.setCalibrationTable(cal.getCTable()); + else + ip.setCalibrationTable(null); + } + s.update(ip); + } + if (roi!=null) + s.setRoi(roi.getBounds()); + else + s.setRoi(null); + return s; + } + + /** Returns the base image stack. */ + public ImageStack getImageStack() { + if (stack==null) + return getStack(); + else { + stack.update(ip); + return stack; + } + } + + /** Returns the current stack index (one-based) or 1 if this is a single image. */ + public int getCurrentSlice() { + if (currentSlice<1) setCurrentSlice(1); + if (currentSlice>getStackSize()) + setCurrentSlice(getStackSize()); + return currentSlice; + } + + final void setCurrentSlice(int slice) { + currentSlice = slice; + int stackSize = getStackSize(); + if (nChannels==stackSize) updatePosition(currentSlice, 1, 1); + if (nSlices==stackSize) updatePosition(1, currentSlice, 1); + if (nFrames==stackSize) updatePosition(1, 1, currentSlice); + } + + public int getChannel() { + return position[0]; + } + + public int getSlice() { + return position[1]; + } + + public int getFrame() { + return position[2]; + } + + public void killStack() { + setStackNull(); + trimProcessor(); + } + + /** Sets the current hyperstack position and updates the display, + where 'channel', 'slice' and 'frame' are one-based indexes. */ + public void setPosition(int channel, int slice, int frame) { + //IJ.log("setPosition: "+channel+" "+slice+" "+frame+" "+noUpdateMode); + verifyDimensions(); + if (channel<0) channel=0; + if (slice<0) slice=0; + if (frame<0) frame=0; + if (channel==0) channel=getC(); + if (slice==0) slice=getZ(); + if (frame==0) frame=getT(); + if (channel>nChannels) channel=nChannels; + if (slice>nSlices) slice=nSlices; + if (frame>nFrames) frame=nFrames; + if (isDisplayedHyperStack()) + ((StackWindow)win).setPosition(channel, slice, frame); + else { + boolean channelChanged = channel!=getChannel(); + setSlice((frame-1)*nChannels*nSlices + (slice-1)*nChannels + channel); + updatePosition(channel, slice, frame); + if (channelChanged && isComposite() && !noUpdateMode) + updateImage(); + } + } + + /** Sets the current hyperstack position without updating the display, + where 'channel', 'slice' and 'frame' are one-based indexes. */ + public void setPositionWithoutUpdate(int channel, int slice, int frame) { + noUpdateMode = true; + setPosition(channel, slice, frame); + noUpdateMode = false; + } + + /** Sets the hyperstack channel position (one based). */ + public void setC(int channel) { + setPosition(channel, getZ(), getT()); + } + + /** Sets the hyperstack slice position (one based). */ + public void setZ(int slice) { + setPosition(getC(), slice, getT()); + } + + /** Sets the hyperstack frame position (one based). */ + public void setT(int frame) { + setPosition(getC(), getZ(), frame); + } + + /** Returns the current hyperstack channel position. */ + public int getC() { + return position[0]; + } + + /** Returns the current hyperstack slice position. */ + public int getZ() { + return position[1]; + } + + /** Returns the current hyperstack frame position. */ + public int getT() { + return position[2]; + } + + /** Returns that stack index (one-based) corresponding to the specified position. */ + public int getStackIndex(int channel, int slice, int frame) { + if (channel<1) channel = 1; + if (channel>nChannels) channel = nChannels; + if (slice<1) slice = 1; + if (slice>nSlices) slice = nSlices; + if (frame<1) frame = 1; + if (frame>nFrames) frame = nFrames; + return (frame-1)*nChannels*nSlices + (slice-1)*nChannels + channel; + } + + /* Hack needed to make the HyperStackReducer work. */ + public void resetStack() { + if (currentSlice==1 && stack!=null && stack.size()>0) { + ColorModel cm = ip.getColorModel(); + double min = ip.getMin(); + double max = ip.getMax(); + ImageProcessor ip2 = stack.getProcessor(1); + if (ip2!=null) { + ip = ip2; + ip.setColorModel(cm); + ip.setMinAndMax(min, max); + } + } + } + + /** Set the current hyperstack position based on the stack index 'n' (one-based). */ + public void setPosition(int n) { + int[] pos = convertIndexToPosition(n); + setPosition(pos[0], pos[1], pos[2]); + } + + /** Converts the stack index 'n' (one-based) into a hyperstack position (channel, slice, frame). */ + public int[] convertIndexToPosition(int n) { + if (n<1 || n>getStackSize()) + throw new IllegalArgumentException("n out of range: "+n); + int[] position = new int[3]; + int[] dim = getDimensions(); + position[0] = ((n-1)%dim[2])+1; + position[1] = (((n-1)/dim[2])%dim[3])+1; + position[2] = (((n-1)/(dim[2]*dim[3]))%dim[4])+1; + return position; + } + + /** Displays the specified stack image, where 1<=n<=stackSize. + * Does nothing if this image is not a stack. + * @see #setPosition + * @see #setC + * @see #setZ + * @see #setT + */ + public synchronized void setSlice(int n) { + if (stack==null || (n==currentSlice&&ip!=null)) { + if (!noUpdateMode) + updateAndRepaintWindow(); + return; + } + if (n>=1 && n<=stack.size()) { + Roi roi = getRoi(); + if (roi!=null) + roi.endPaste(); + if (isProcessor()) { + if (currentSlice==0) currentSlice=1; + stack.setPixels(ip.getPixels(),currentSlice); + } + setCurrentSlice(n); + Object pixels = null; + Overlay overlay2 = null; + if (stack.isVirtual() && !((stack instanceof FileInfoVirtualStack)||(stack instanceof AVI_Reader))) { + ImageProcessor ip2 = stack.getProcessor(currentSlice); + overlay2 = ip2!=null?ip2.getOverlay():null; + if (overlay2!=null) + setOverlay(overlay2); + if (stack instanceof VirtualStack) { + Properties props = ((VirtualStack)stack).getProperties(); + if (props!=null) + setProperty("FHT", props.get("FHT")); + } + if (ip2!=null) pixels=ip2.getPixels(); + } else + pixels = stack.getPixels(currentSlice); + if (ip!=null && pixels!=null) { + try { + ip.setPixels(pixels); + ip.setSnapshotPixels(null); + } catch(Exception e) {} + } else { + ImageProcessor ip2 = stack.getProcessor(n); + if (ip2!=null) ip = ip2; + } + if (compositeImage && getCompositeMode()==IJ.COMPOSITE && ip!=null) { + int channel = getC(); + if (channel>0 && channel<=getNChannels()) + ip.setLut(((CompositeImage)this).getChannelLut(channel)); + } + if (win!=null && win instanceof StackWindow) + ((StackWindow)win).updateSliceSelector(); + if (Prefs.autoContrast && nChannels==1 && imageType!=COLOR_RGB) { + (new ContrastEnhancer()).stretchHistogram(ip,0.35,ip.getStats()); + ContrastAdjuster.update(); + //IJ.showStatus(n+": min="+ip.getMin()+", max="+ip.getMax()); + } + if (imageType==COLOR_RGB) + ContrastAdjuster.update(); + if (!noUpdateMode) + updateAndRepaintWindow(); + else + img = null; + } + } + + /** Displays the specified stack image (1<=n<=stackSize) + without updating the display. */ + public void setSliceWithoutUpdate(int n) { + noUpdateMode = true; + setSlice(n); + noUpdateMode = false; + } + + /** Returns the current selection, or null if there is no selection. */ + public Roi getRoi() { + return roi; + } + + /** Assigns the specified ROI to this image and displays it. Any existing + ROI is deleted if roi is null or its width or height is zero. */ + public void setRoi(Roi newRoi) { + setRoi(newRoi, true); + } + + /** Assigns 'newRoi' to this image and displays it if 'updateDisplay' is true. */ + public void setRoi(Roi newRoi, boolean updateDisplay) { + if (newRoi==null) { + deleteRoi(); + return; + } + if (Recorder.record) { + Recorder recorder = Recorder.getInstance(); + if (recorder!=null) recorder.imageUpdated(this); + } + Rectangle bounds = newRoi.getBounds(); + if (newRoi.isVisible()) { + if ((newRoi instanceof Arrow) && newRoi.getState()==Roi.CONSTRUCTING && bounds.width==0 && bounds.height==0) { + deleteRoi(); + roi = newRoi; + return; + } + if (newRoi==null) { + deleteRoi(); + return; + } + ImagePlus imp = newRoi.getImage(); + if (imp!=null && imp.getID()!=getID()) + newRoi = (Roi)newRoi.clone(); + newRoi.setImage(null); + } + if (bounds.width==0 && bounds.height==0 && !(newRoi.getType()==Roi.POINT||newRoi.getType()==Roi.LINE)) { + deleteRoi(); + return; + } + roi = newRoi; + if (ip!=null) { + ip.setMask(null); + if (roi.isArea()) + ip.setRoi(bounds); + else + ip.resetRoi(); + } + roi.setImage(this); + if ((roi instanceof PointRoi) && ((PointRoi)roi).addToOverlay()) { + IJ.run(this, "Add Selection...", ""); + roi = null; + return; + } + if (updateDisplay) + draw(); + if (roi!=null) + roi.notifyListeners(RoiListener.CREATED); + } + + /** Creates a rectangular selection. */ + public void setRoi(int x, int y, int width, int height) { + setRoi(new Rectangle(x, y, width, height)); + } + + /** Creates a rectangular selection. */ + public void setRoi(Rectangle r) { + setRoi(new Roi(r.x, r.y, r.width, r.height)); + } + + /** Starts the process of creating a new selection, where sx and sy are the + starting screen coordinates. The selection type is determined by which tool in + the tool bar is active. The user interactively sets the selection size and shape. */ + public void createNewRoi(int sx, int sy) { + Roi previousRoi = roi; + deleteRoi(); //also saves the roi as Roi.previousRoi if non-null + Roi prevRoi = Roi.getPreviousRoi(); + if (prevRoi != null) + prevRoi.setImage(previousRoi==null ? null : this); //with 'this' it will be recalled in case of ESC + switch (Toolbar.getToolId()) { + case Toolbar.RECTANGLE: + if (Toolbar.getRectToolType()==Toolbar.ROTATED_RECT_ROI) + roi = new RotatedRectRoi(sx, sy, this); + else + roi = new Roi(sx, sy, this, Toolbar.getRoundRectArcSize()); + break; + case Toolbar.OVAL: + if (Toolbar.getOvalToolType()==Toolbar.ELLIPSE_ROI) + roi = new EllipseRoi(sx, sy, this); + else + roi = new OvalRoi(sx, sy, this); + break; + case Toolbar.POLYGON: + case Toolbar.POLYLINE: + case Toolbar.ANGLE: + roi = new PolygonRoi(sx, sy, this); + break; + case Toolbar.FREEROI: + case Toolbar.FREELINE: + roi = new FreehandRoi(sx, sy, this); + break; + case Toolbar.LINE: + if ("arrow".equals(Toolbar.getToolName())) + roi = new Arrow(sx, sy, this); + else + roi = new Line(sx, sy, this); + break; + case Toolbar.TEXT: + roi = new TextRoi(sx, sy, this); + ((TextRoi)roi).setPreviousTextRoi(previousRoi); + break; + case Toolbar.POINT: + roi = new PointRoi(sx, sy, this); + if (Prefs.pointAddToOverlay) { + int measurements = Analyzer.getMeasurements(); + if (!(Prefs.pointAutoMeasure && (measurements&Measurements.ADD_TO_OVERLAY)!=0)) + IJ.run(this, "Add Selection...", ""); + Overlay overlay2 = getOverlay(); + if (overlay2!=null) + overlay2.drawLabels(!Prefs.noPointLabels); + Prefs.pointAddToManager = false; + } + if (Prefs.pointAutoMeasure || (Prefs.pointAutoNextSlice&&!Prefs.pointAddToManager)) + IJ.run(this, "Measure", ""); + if (Prefs.pointAddToManager) { + IJ.run(this, "Add to Manager ", ""); + ImageCanvas ic = getCanvas(); + if (ic!=null) { + RoiManager rm = RoiManager.getInstance(); + if (rm!=null) { + if (Prefs.noPointLabels) + rm.runCommand("show all without labels"); + else + rm.runCommand("show all with labels"); + } + } + } + if (Prefs.pointAutoNextSlice && getStackSize()>1) { + boolean order = Prefs.reverseNextPreviousOrder; + Prefs.reverseNextPreviousOrder = true; + IJ.run(this, "Next Slice [>]", ""); + Prefs.reverseNextPreviousOrder = order; + deleteRoi(); + } + break; + } + if (roi!=null) + roi.notifyListeners(RoiListener.CREATED); + } + + /** Deletes the current region of interest. Makes a copy of the ROI + so it can be recovered by Edit/Selection/Restore Selection. */ + public void deleteRoi() { + if (roi==null) + return; + saveRoi(); + if (!(IJ.altKeyDown()||IJ.shiftKeyDown())) { + RoiManager rm = RoiManager.getRawInstance(); + if (rm!=null) + rm.deselect(roi); + } + if (roi!=null) + roi.notifyListeners(RoiListener.DELETED); + roi = null; + if (ip!=null) + ip.resetRoi(); + draw(); + } + + public boolean okToDeleteRoi() { + if (roi!=null && (roi instanceof PointRoi) && getWindow()!=null && ((PointRoi)roi).promptBeforeDeleting()) { + int npoints = ((PolygonRoi)roi).getNCoordinates(); + int counters = ((PointRoi)roi).getNCounters(); + String msg = "Delete this multi-point selection ("+npoints+" points, "+counters+" counter"+(counters>1?"s":"")+")?"; + GenericDialog gd=new GenericDialog("Delete Points?"); + gd.addMessage(msg+"\nRestore using Edit>Selection>Restore Selection."); + gd.addHelp(PointToolOptions.help); + gd.setOKLabel("Keep"); + gd.setCancelLabel("Delete"); + gd.showDialog(); + if (gd.wasOKed()) + return false; + } + return true; + } + + /** Deletes the current region of interest. */ + public void killRoi() { + deleteRoi(); + } + + /** Deletes the current region of interest. */ + public void resetRoi() { + deleteRoi(); + } + + public void saveRoi() { + Roi roi2 = roi; + if (roi2!=null) { + roi2.endPaste(); + Rectangle r = roi2.getBounds(); + if ((r.width>0 || r.height>0)) { + Roi.setPreviousRoi(roi2); + if (IJ.debugMode) IJ.log("saveRoi: "+roi2); + } + if ((roi2 instanceof PointRoi) && ((PointRoi)roi2).promptBeforeDeleting()) { + PointRoi.savedPoints = (PointRoi)roi2.clone(); + if (IJ.debugMode) IJ.log("saveRoi: saving multi-point selection"); + } + } + } + + public void restoreRoi() { + if (Toolbar.getToolId()==Toolbar.POINT && PointRoi.savedPoints!=null) { + roi = (Roi)PointRoi.savedPoints.clone(); + draw(); + roi.notifyListeners(RoiListener.MODIFIED); + return; + } + Roi previousRoi = Roi.getPreviousRoi(); + if (previousRoi!=null) { + Roi pRoi = previousRoi; + Rectangle r = pRoi.getBounds(); + if (r.width<=width||r.height<=height||(r.x=width || r.y>=height || (r.x+r.width)<0 || (r.y+r.height)<0) // does it need to be moved? + roi.setLocation((width-r.width)/2, (height-r.height)/2); + else if (r.width==width && r.height==height) // is it the same size as the image + roi.setLocation(0, 0); + draw(); + roi.notifyListeners(RoiListener.MODIFIED); + } + } + } + + boolean isSmaller(Roi r) { + ImageProcessor mask = r.getMask(); + if (mask==null) return false; + mask.setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE); + ImageStatistics stats = ImageStatistics.getStatistics(mask, MEAN+LIMIT, null); + return stats.area<=width*height; + } + + /** Implements the File/Revert command. */ + public void revert() { + if (getStackSize()>1 && getStack().isVirtual()) { + int thisSlice = currentSlice; + currentSlice = 0; + setSlice(thisSlice); + return; + } + FileInfo fi = getOriginalFileInfo(); + boolean isFileInfo = fi!=null && fi.fileFormat!=FileInfo.UNKNOWN; + if (!isFileInfo && url==null) + return; + if (fi.directory==null && url==null) + return; + if (ij!=null && changes && isFileInfo && !Interpreter.isBatchMode() && !IJ.isMacro() && !IJ.altKeyDown()) { + if (!IJ.showMessageWithCancel("Revert?", "Revert to saved version of\n\""+getTitle()+"\"?")) + return; + } + Roi saveRoi = null; + if (roi!=null) { + roi.endPaste(); + saveRoi = (Roi)roi.clone(); + } + trimProcessor(); + new FileOpener(fi).revertToSaved(this); + if (Prefs.useInvertingLut && getBitDepth()==8 && ip!=null && !ip.isInvertedLut()&& !ip.isColorLut()) + invertLookupTable(); + if (getProperty("FHT")!=null) { + properties.remove("FHT"); + if (getTitle().startsWith("FFT of ")) + setTitle(getTitle().substring(7)); + } + ContrastAdjuster.update(); + if (saveRoi!=null) setRoi(saveRoi); + repaintWindow(); + IJ.showStatus(""); + changes = false; + notifyListeners(UPDATED); + } + + void revertStack(FileInfo fi) { + String path = null; + String url2 = null; + if (url!=null && !url.equals("")) { + path = url; + url2 = url; + } else if (fi!=null && !((fi.directory==null||fi.directory.equals("")))) { + path = fi.getFilePath(); + } else if (fi!=null && fi.url!=null && !fi.url.equals("")) { + path = fi.url; + url2 = fi.url; + } else + return; + IJ.showStatus("Loading: " + path); + ImagePlus imp = IJ.openImage(path); + if (imp!=null) { + int n = imp.getStackSize(); + int c = imp.getNChannels(); + int z = imp.getNSlices(); + int t = imp.getNFrames(); + if (z==n || t==n || (c==getNChannels()&&z==getNSlices()&&t==getNFrames())) { + setCalibration(imp.getCalibration()); + setStack(imp.getStack(), c, z, t); + } else { + ImageWindow win = getWindow(); + Point loc = null; + if (win!=null) loc = win.getLocation(); + changes = false; + close(); + FileInfo fi2 = imp.getOriginalFileInfo(); + if (fi2!=null && (fi2.url==null || fi2.url.length()==0)) { + fi2.url = url2; + imp.setFileInfo(fi2); + } + ImageWindow.setNextLocation(loc); + imp.show(); + } + } + } + + /** Returns a FileInfo object containing information, including the + pixel array, needed to save this image. Use getOriginalFileInfo() + to get a copy of the FileInfo object used to open the image. + @see ij.io.FileInfo + @see #getOriginalFileInfo + @see #setFileInfo + */ + public FileInfo getFileInfo() { + FileInfo fi = new FileInfo(); + fi.width = width; + fi.height = height; + fi.nImages = getStackSize(); + if (compositeImage) + fi.nImages = getImageStackSize(); + fi.whiteIsZero = isInvertedLut(); + fi.intelByteOrder = false; + if (fi.nImages==1 && ip!=null) + fi.pixels = ip.getPixels(); + else if (stack!=null) + fi.pixels = stack.getImageArray(); + Calibration cal = getCalibration(); + if (cal.scaled()) { + fi.pixelWidth = cal.pixelWidth; + fi.pixelHeight = cal.pixelHeight; + fi.unit = cal.getUnit(); + } + if (fi.nImages>1) + fi.pixelDepth = cal.pixelDepth; + fi.frameInterval = cal.frameInterval; + if (cal.calibrated()) { + fi.calibrationFunction = cal.getFunction(); + fi.coefficients = cal.getCoefficients(); + fi.valueUnit = cal.getValueUnit(); + } else if (!Calibration.DEFAULT_VALUE_UNIT.equals(cal.getValueUnit())) + fi.valueUnit = cal.getValueUnit(); + + switch (imageType) { + case GRAY8: case COLOR_256: + LookUpTable lut = createLut(); + boolean customLut = !lut.isGrayscale() || (ip!=null&&!ip.isDefaultLut()); + if (imageType==COLOR_256 || customLut) + fi.fileType = FileInfo.COLOR8; + else + fi.fileType = FileInfo.GRAY8; + addLut(lut, fi); + break; + case GRAY16: + if (compositeImage && fi.nImages==3) { + if ("Red".equals(getStack().getSliceLabel(1))) + fi.fileType = fi.RGB48; + else + fi.fileType = fi.GRAY16_UNSIGNED; + } else + fi.fileType = fi.GRAY16_UNSIGNED; + if (!compositeImage) { + lut = createLut(); + if (!lut.isGrayscale() || (ip!=null&&!ip.isDefaultLut())) + addLut(lut, fi); + } + break; + case GRAY32: + fi.fileType = fi.GRAY32_FLOAT; + if (!compositeImage) { + lut = createLut(); + if (!lut.isGrayscale() || (ip!=null&&!ip.isDefaultLut())) + addLut(lut, fi); + } + break; + case COLOR_RGB: + fi.fileType = fi.RGB; + break; + default: + } + return fi; + } + + private void addLut(LookUpTable lut, FileInfo fi) { + fi.lutSize = lut.getMapSize(); + fi.reds = lut.getReds(); + fi.greens = lut.getGreens(); + fi.blues = lut.getBlues(); + } + + /** Returns the FileInfo object that was used to open this image. + Returns null for images created using the File/New command. + @see ij.io.FileInfo + @see #getFileInfo + */ + public FileInfo getOriginalFileInfo() { + if (fileInfo==null & url!=null) { + fileInfo = new FileInfo(); + fileInfo.width = width; + fileInfo.height = height; + fileInfo.url = url; + fileInfo.directory = null; + } + return fileInfo; + } + + /** Used by ImagePlus to monitor loading of images. */ + public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) { + imageUpdateY = y; + imageUpdateW = w; + if ((flags & ERROR) != 0) { + errorLoadingImage = true; + return false; + } + imageLoaded = (flags & (ALLBITS|FRAMEBITS|ABORT)) != 0; + return !imageLoaded; + } + + /** Sets the ImageProcessor, Roi, AWT Image and stack image + arrays to null. Does nothing if the image is locked. */ + public synchronized void flush() { + notifyListeners(CLOSED); + if (locked || ignoreFlush) return; + ip = null; + if (roi!=null) roi.setImage(null); + roi = null; + if (stack!=null && stack.viewers(-1)<=0) { + Object[] arrays = stack.getImageArray(); + if (arrays!=null) { + for (int i=0; istackSize) s2 = stackSize; + if (s1>s2) {s1=1; s2=stackSize;} + return new Duplicator().run(this, (int)s1, (int)s2); + } + } + + /** Returns an array of cropped images based on the provided + * list of rois. 'options' applies with stacks and can be "stack", + * "slice" or a range (e.g., "20-30"). + * @see #crop(ij.gui.Roi[]) + */ + public ImagePlus[] crop(Roi[] rois, String options) { + int nRois = rois.length; + ImagePlus[] cropImps = new ImagePlus[nRois]; + for (int i=0; i1) { + int position = cropRoi.getPosition(); + this.setSlice(position); // no effect if roi position is undefined (=0), ok + } + this.setRoi(cropRoi); + ImagePlus cropped = this.crop(options); + if (cropRoi.getType()!=Roi.RECTANGLE) { + Roi cropRoi2 = (Roi)cropRoi.clone(); + cropRoi2.setLocation(0,0); + cropped.setRoi(cropRoi2); + } + String name2 = IJ.pad(i+1,3)+"_"+this.getTitle(); + cropped.setTitle(name!=null?name:name2); + cropped.setOverlay(null); + cropImps[i] = cropped; + } + return cropImps; + } + + /** Multi-roi cropping with default "slice" option. */ + public ImagePlus[] crop(Roi[] rois) { + return this.crop(rois, "slice"); + } + + /** Saves the contents of the ROIs in this overlay as separate images, + * where 'directory' is the directory path and 'format' is "tif", "png" or "jpg". + * Set 'format' to "show" and the images will be displayed in a stack + * and not saved. + */ + public void cropAndSave(Roi[] rois, String directory, String format) { + ImagePlus[] images = crop(rois); + if (format==null) format = ""; + if (format.contains("show")) { + ImageStack stack = ImageStack.create(images); + new ImagePlus("CROPPED_"+getTitle(),stack).show(); + return; + } + String fileFormat = "tif"; + if (format.contains("png")) fileFormat = "png"; + if (format.contains("jpg")) fileFormat = "jpg"; + for (int i=0; icut is true. */ + public void copy(boolean cut) { + Roi roi = getRoi(); + if (roi!=null && !roi.isArea()) + roi = null; + if (cut && roi==null && !IJ.isMacro()) { + IJ.error("Edit>Cut", "This command requires an area selection"); + return; + } + boolean batchMode = Interpreter.isBatchMode(); + String msg = (cut)?"Cut":"Copy"; + if (!batchMode) IJ.showStatus(msg+ "ing..."); + ImageProcessor ip = getProcessor(); + ImageProcessor ip2; + ip2 = ip.crop(); + clipboard = new ImagePlus("Clipboard", ip2); + if (roi!=null) + clipboard.setRoi((Roi)roi.clone()); + if (cut) { + ip.snapshot(); + ip.setColor(Toolbar.getBackgroundColor()); + ip.fill(); + if (roi!=null && roi.getType()!=Roi.RECTANGLE) { + getMask(); + ip.reset(ip.getMask()); + } setColor(Toolbar.getForegroundColor()); + Undo.setup(Undo.FILTER, this); + updateAndDraw(); + } + int bytesPerPixel = 1; + switch (clipboard.getType()) { + case ImagePlus.GRAY16: bytesPerPixel = 2; break; + case ImagePlus.GRAY32: case ImagePlus.COLOR_RGB: bytesPerPixel = 4; + } + if (!batchMode) { + msg = (cut)?"Cut":"Copy"; + IJ.showStatus(msg + ": " + (clipboard.getWidth()*clipboard.getHeight()*bytesPerPixel)/1024 + "k"); + } + } + + /** Inserts the contents of the internal clipboard into this image. If there + is a selection the same size as the image on the clipboard, the image is inserted + into that selection, otherwise the selection is inserted into the center of the image.*/ + public void paste() { + if (clipboard==null) + return; + int cType = clipboard.getType(); + int iType = getType(); + int w = clipboard.getWidth(); + int h = clipboard.getHeight(); + Roi cRoi = clipboard.getRoi(); + Rectangle r = null; + Rectangle cr = null; + Roi roi = getRoi(); + if (roi!=null) + r = roi.getBounds(); + if (cRoi!=null) + cr = cRoi.getBounds(); + if (cr==null) + cr = new Rectangle(0, 0, w, h); + if (r==null || (cr.width!=r.width || cr.height!=r.height)) { + // Create a new roi centered on visible part of image, or centered on image if clipboard is >= image + ImageCanvas ic = win!=null?ic = win.getCanvas():null; + Rectangle srcRect = ic!=null?ic.getSrcRect():new Rectangle(0,0,width,height); + int xCenter = w>=width ? width/2 : srcRect.x + srcRect.width/2; + int yCenter = h>=height ? height/2 : srcRect.y + srcRect.height/2; + if (cRoi!=null && cRoi.getType()!=Roi.RECTANGLE) { + cRoi.setImage(this); + cRoi.setLocation(xCenter-w/2, yCenter-h/2); + setRoi(cRoi); + } else + setRoi(xCenter-w/2, yCenter-h/2, w, h); + roi = getRoi(); + } + if (IJ.isMacro()) { + //non-interactive paste + int pasteMode = Roi.getCurrentPasteMode(); + boolean nonRect = roi.getType()!=Roi.RECTANGLE; + ImageProcessor ip = getProcessor(); + if (nonRect) ip.snapshot(); + r = roi.getBounds(); + int xoffset = cr.x<0?-cr.x:0; + int yoffset = cr.y<0?-cr.y:0; + ip.copyBits(clipboard.getProcessor(), r.x+xoffset, r.y+yoffset, pasteMode); + if (nonRect) { + ImageProcessor mask = roi.getMask(); + ip.setMask(mask); + ip.setRoi(roi.getBounds()); + ip.reset(ip.getMask()); + } + updateAndDraw(); + } else if (roi!=null) { + roi.startPaste(clipboard); + Undo.setup(Undo.PASTE, this); + } + changes = true; + } + + /** Inserts the contents of the internal clipboard at the + specified location, without updating the display. */ + public void paste(int x, int y) { + paste(x, y, null); + } + + /** Copies the contents of the internal clipboard to the + * specified location using the specified transfer mode + * ("Copy", "Blend", "Average", "Difference", "Transparent", + * "Transparent2", "AND", "OR", "XOR", "Add", "Subtract", + * "Multiply", or "Divide"). The display is not updating. + */ + public void paste(int x, int y, String mode) { + if (clipboard==null) + return; + Roi roi = clipboard.getRoi(); + boolean nonRect = roi!=null && roi.getType()!=Roi.RECTANGLE; + if (nonRect) + ip.snapshot(); + if (mode==null) + ip.insert(clipboard.getProcessor(), x, y); + else { + int pasteMode = IJ.stringToPasteMode(mode); + ip.copyBits(clipboard.getProcessor(), x, y, pasteMode); + } + if (nonRect) { + ImageProcessor mask = roi.getMask(); + ip.setRoi(x, y, mask.getWidth(), mask.getHeight()); + ip.setMask(mask); + ip.reset(ip.getMask()); + } + } + + /** Returns the internal clipboard or null if the internal clipboard is empty. */ + public static ImagePlus getClipboard() { + return clipboard; + } + + /** Clears the internal clipboard. */ + public static void resetClipboard() { + clipboard = null; + } + + /** Copies the contents of the current selection, or the entire + image if there is no selection, to the system clipboard. */ + public void copyToSystem() { + Clipboard.copyToSystem(this); + } + + protected void notifyListeners(final int id) { + if (temporary) + return; + final ImagePlus imp = this; + EventQueue.invokeLater(new Runnable() { + public void run() { + for (int i=0; i=1 && imageType!=COLOR_RGB && (this instanceof CompositeImage); + } + + /** Returns the display mode (IJ.COMPOSITE, IJ.COLOR + or IJ.GRAYSCALE) if this is a CompositeImage, otherwise returns -1. */ + public int getCompositeMode() { + if (isComposite()) + return ((CompositeImage)this).getMode(); + else + return -1; + } + + /** Sets the display range of the current channel. With non-composite + images it is identical to ip.setMinAndMax(min, max). */ + public void setDisplayRange(double min, double max) { + if (ip!=null) + ip.setMinAndMax(min, max); + } + + public double getDisplayRangeMin() { + return ip.getMin(); + } + + public double getDisplayRangeMax() { + return ip.getMax(); + } + + /** Sets the display range of specified channels in an RGB image, where 4=red, + 2=green, 1=blue, 6=red+green, etc. With non-RGB images, this method is + identical to setDisplayRange(min, max). This method is used by the + Image/Adjust/Color Balance tool . */ + public void setDisplayRange(double min, double max, int channels) { + if (ip instanceof ColorProcessor) + ((ColorProcessor)ip).setMinAndMax(min, max, channels); + else + ip.setMinAndMax(min, max); + } + + public void resetDisplayRange() { + if (imageType==GRAY16 && default16bitDisplayRange>=8 && default16bitDisplayRange<=16 && !(getCalibration().isSigned16Bit())) + ip.setMinAndMax(0, Math.pow(2,default16bitDisplayRange)-1); + else + ip.resetMinAndMax(); + } + + /** Returns 'true' if this image is thresholded. */ + public boolean isThreshold() { + return ip!=null && ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD; + } + + /** Set the default 16-bit display range, where 'bitDepth' must be 0 (auto-scaling), + 8 (0-255), 10 (0-1023), 12 (0-4095, 14 (0-16383), 15 (0-32767) or 16 (0-65535). */ + public static void setDefault16bitRange(int bitDepth) { + if (!(bitDepth==8 || bitDepth==10 || bitDepth==12 || bitDepth==14 || bitDepth==15 || bitDepth==16)) + bitDepth = 0; + default16bitDisplayRange = bitDepth; + } + + /** Returns the default 16-bit display range, 0 (auto-scaling), 8, 10, 12, 14, 15 or 16. */ + public static int getDefault16bitRange() { + return default16bitDisplayRange; + } + + public void updatePosition(int c, int z, int t) { + position[0] = c; + position[1] = z; + position[2] = t; + } + + /** Returns a "flattened" version of this image, or stack slice, in RGB format. */ + public ImagePlus flatten() { + if (IJ.debugMode) IJ.log("flatten"); + ImagePlus impCopy = this; + if (getStackSize()>1) + impCopy = crop("whole-slice"); + ImagePlus imp2 = impCopy.createImagePlus(); + imp2.setOverlay(impCopy.getOverlay()); + imp2.setTitle(flattenTitle); + ImageCanvas ic2 = new ImageCanvas(imp2); + imp2.flatteningCanvas = ic2; + imp2.setRoi(getRoi()); + Overlay overlay2 = getOverlay(); + if (overlay2!=null && imp2.getRoi()!=null && !(imp2.getRoi() instanceof PointRoi)) { + imp2.deleteRoi(); + if (getWindow()!=null) IJ.wait(100); + } + setPointScale(imp2.getRoi(), overlay2); + ImageCanvas ic = getCanvas(); + if (ic!=null) + ic2.setShowAllList(ic.getShowAllList()); + BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = (Graphics2D)bi.getGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + antialiasRendering?RenderingHints.VALUE_ANTIALIAS_ON:RenderingHints.VALUE_ANTIALIAS_OFF); + g.drawImage(getImage(), 0, 0, null); + ic2.paint(g); + imp2.flatteningCanvas = null; + ImagePlus imp3 = new ImagePlus("Flat_"+getTitle(), new ColorProcessor(bi)); + imp3.copyScale(this); + imp3.setProperty("Info", getProperty("Info")); + imp3.setProperties(getPropertiesAsArray()); + return imp3; + } + + /** Flattens all slices of this stack or HyperStack.
+ * @throws UnsupportedOperationException if this image
+ * does not have an overlay and the RoiManager overlay is null
+ * or Java version is less than 1.6. + * Copied from OverlayCommands and modified by Marcel Boeglin + * on 2014.01.08 to work with HyperStacks. + */ + public void flattenStack() { + if (IJ.debugMode) IJ.log("flattenStack"); + if (getStackSize()==1) + throw new UnsupportedOperationException("Image stack required"); + boolean composite = isComposite(); + if (getBitDepth()!=24) + new ImageConverter(this).convertToRGB(); + Overlay overlay1 = getOverlay(); + Overlay roiManagerOverlay = null; + boolean roiManagerShowAllMode = !Prefs.showAllSliceOnly; + ImageCanvas ic = getCanvas(); + if (ic!=null) + roiManagerOverlay = ic.getShowAllList(); + setOverlay(null); + if (roiManagerOverlay!=null) { + RoiManager rm = RoiManager.getInstance(); + if (rm!=null) + rm.runCommand("show none"); + } + Overlay overlay2 = overlay1!=null?overlay1:roiManagerOverlay; + if (composite && overlay2==null) + return; + if (overlay2==null || overlay2.size()==0) + throw new UnsupportedOperationException("A non-empty overlay is required"); + ImageStack stack2 = getStack(); + boolean showAll = overlay1!=null?false:roiManagerShowAllMode; + if (isHyperStack()) { + int Z = getNSlices(); + for (int z=1; z<=Z; z++) { + for (int t=1; t<=getNFrames(); t++) { + int s = z + (t-1)*Z; + flattenImage(stack2, s, overlay2.duplicate(), showAll, z, t); + } + } + } else { + for (int s=1; s<=stack2.getSize(); s++) { + flattenImage(stack2, s, overlay2.duplicate(), showAll); + } + } + setStack(stack2); + } + + /** Flattens Overlay 'overlay' on slice 'slice' of ImageStack 'stack'. + * Copied from OverlayCommands by Marcel Boeglin 2014.01.08. + */ + private void flattenImage(ImageStack stack, int slice, Overlay overlay, boolean showAll) { + ImageProcessor ips = stack.getProcessor(slice); + ImagePlus imp1 = new ImagePlus("temp", ips); + int w = imp1.getWidth(); + int h = imp1.getHeight(); + for (int i=0; i=size) { + Object[] tmp1 = new Object[size*2]; + System.arraycopy(stack, 0, tmp1, 0, size); + stack = tmp1; + String[] tmp2 = new String[size*2]; + System.arraycopy(label, 0, tmp2, 0, size); + label = tmp2; + } + stack[nSlices-1] = pixels; + this.label[nSlices-1] = sliceLabel; + if (this.bitDepth==0) + setBitDepth(pixels); + } + + private void setBitDepth(Object pixels) { + if (pixels==null) + return; + if (pixels instanceof byte[]) + this.bitDepth = 8; + else if (pixels instanceof short[]) + this.bitDepth = 16; + else if (pixels instanceof float[]) + this.bitDepth = 32; + else if (pixels instanceof int[]) + this.bitDepth = 24; + } + + /** + * @deprecated + * Short images are always unsigned. + */ + public void addUnsignedShortSlice(String sliceLabel, Object pixels) { + addSlice(sliceLabel, pixels); + } + + /** Adds the image in 'ip' to the end of the stack. */ + public void addSlice(ImageProcessor ip) { + addSlice(null, ip); + } + + /** Adds the image in 'ip' to the end of the stack, setting + the string 'sliceLabel' as the slice metadata. */ + public void addSlice(String sliceLabel, ImageProcessor ip) { + ip = convertType(ip); + if (ip.getWidth()!=this.width || ip.getHeight()!=this.height) { + if (this.width==0 && this.height==0) + init(ip.getWidth(), ip.getHeight()); + else { + ImageProcessor ip2 = ip.createProcessor(this.width,this.height); + ip2.insert(ip, 0, 0); + ip = ip2; + } + } + if (nSlices==0) { + cm = ip.getColorModel(); + min = ip.getMin(); + max = ip.getMax(); + } + addSlice(sliceLabel, ip.getPixels()); + } + + private void init(int width, int height) { + this.width = width; + this.height = height; + stack = new Object[INITIAL_SIZE]; + label = new String[INITIAL_SIZE]; + } + + private ImageProcessor convertType(ImageProcessor ip) { + int newBitDepth = ip.getBitDepth(); + if (this.bitDepth==0) + this.bitDepth = newBitDepth; + if (this.bitDepth!=newBitDepth) { + switch (this.bitDepth) { + case 8: ip=ip.convertToByte(true); break; + case 16: ip=ip.convertToShort(true); break; + case 24: ip=ip.convertToRGB(); break; + case 32: ip=ip.convertToFloat(); break; + } + } + return ip; + } + + /** Adds the image in 'ip' to the stack following slice 'n'. Adds + the slice to the beginning of the stack if 'n' is zero. */ + public void addSlice(String sliceLabel, ImageProcessor ip, int n) { + if (n<0 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + addSlice(sliceLabel, ip); + Object tempSlice = stack[nSlices-1]; + String tempLabel = label[nSlices-1]; + int first = n>0?n:1; + for (int i=nSlices-1; i>=first; i--) { + stack[i] = stack[i-1]; + label[i] = label[i-1]; + } + stack[n] = tempSlice; + label[n] = tempLabel; + } + + /** Deletes the specified slice, were 1<=n<=nslices. */ + public void deleteSlice(int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + if (nSlices<1) + return; + for (int i=n; i0) + deleteSlice(nSlices); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public void setRoi(Rectangle roi) { + this.roi = roi; + } + + public Rectangle getRoi() { + if (roi==null) + return new Rectangle(0, 0, width, height); + else + return(roi); + } + + /** Updates this stack so its attributes, such as min, max, + calibration table and color model, are the same as 'ip'. */ + public void update(ImageProcessor ip) { + if (ip!=null) { + min = ip.getMin(); + max = ip.getMax(); + cTable = ip.getCalibrationTable(); + cm = ip.getColorModel(); + } + } + + /** Returns the pixel array for the specified slice, were 1<=n<=nslices. */ + public Object getPixels(int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + return stack[n-1]; + } + + /** Assigns a pixel array to the specified slice, + were 1<=n<=nslices. */ + public void setPixels(Object pixels, int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + stack[n-1] = pixels; + if (this.bitDepth==0) + setBitDepth(pixels); + } + + /** Returns the stack as an array of 1D pixel arrays. Note + that the size of the returned array may be greater than + the number of slices currently in the stack, with + unused elements set to null. */ + public Object[] getImageArray() { + return stack; + } + + /** Returns the number of slices in this stack. */ + public int size() { + return getSize(); + } + + public int getSize() { + return nSlices; + } + + /** Returns the slice labels as an array of Strings. Note + that the size of the returned array may be greater than + the number of slices currently in the stack. Returns null + if the stack is empty or the label of the first slice is null. */ + public String[] getSliceLabels() { + if (nSlices==0) + return null; + else + return label; + } + + /** Returns the label of the specified slice, were 1<=n<=nslices. + Returns null if the slice does not have a label or 'n'; + is out of range. For DICOM and FITS stacks, labels may + contain header information. + */ + public String getSliceLabel(int n) { + if (n<1 || n>nSlices) + return null; + else + return label[n-1]; + } + + /** Returns a shortened version (up to the first 60 characters + * or first newline), with the extension removed, of the specified + * slice label, or null if the slice does not have a label. + */ + public String getShortSliceLabel(int n) { + return getShortSliceLabel(n, 60); + } + + /** Returns a shortened version (up to the first 'max' characters + * or first newline), with the extension removed, of the specified + * slice label, or null if the slice does not have a label. + */ + public String getShortSliceLabel(int n, int max) { + String shortLabel = getSliceLabel(n); + if (shortLabel==null) return null; + int newline = shortLabel.indexOf('\n'); + if (newline==0) return null; + if (newline>0) + shortLabel = shortLabel.substring(0, newline); + int len = shortLabel.length(); + if (len>4 && shortLabel.charAt(len-4)=='.' && !Character.isDigit(shortLabel.charAt(len-1))) + shortLabel = shortLabel.substring(0,len-4); + if (shortLabel.length()>max) + shortLabel = shortLabel.substring(0, max); + return shortLabel; + } + + /** Sets the label of the specified slice, were 1<=n<=nslices. */ + public void setSliceLabel(String label, int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + this.label[n-1] = label; + } + + /** Returns an ImageProcessor for the specified slice, + were 1<=n<=nslices. Returns null if the stack is empty. + */ + public ImageProcessor getProcessor(int n) { + ImageProcessor ip; + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + if (nSlices==0) + return null; + if (stack[n-1]==null) + throw new IllegalArgumentException("Pixel array is null"); + if (stack[n-1] instanceof byte[]) + ip = new ByteProcessor(width, height, null, cm); + else if (stack[n-1] instanceof short[]) + ip = new ShortProcessor(width, height, null, cm); + else if (stack[n-1] instanceof int[]) { + if (signedInt) + ip = new IntProcessor(width, height); + else + ip = new ColorProcessor(width, height, null); + } else if (stack[n-1] instanceof float[]) + ip = new FloatProcessor(width, height, null, cm); + else + throw new IllegalArgumentException("Unknown stack type"); + ip.setPixels(stack[n-1]); + if (min!=Double.MAX_VALUE && ip!=null && !(ip instanceof ColorProcessor)) + ip.setMinAndMax(min, max); + if (cTable!=null) + ip.setCalibrationTable(cTable); + return ip; + } + + /** Assigns the pixel array of an ImageProcessor to the + specified slice, were 1<=n<=nslices. */ + public void setProcessor(ImageProcessor ip, int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException(outOfRange+n); + ip = convertType(ip); + if (ip.getWidth()!=width || ip.getHeight()!=height) + throw new IllegalArgumentException("Wrong dimensions for this stack"); + stack[n-1] = ip.getPixels(); + } + + /** Assigns a new color model to this stack. */ + public void setColorModel(ColorModel cm) { + this.cm = cm; + } + + /** Returns this stack's color model. May return null. */ + public ColorModel getColorModel() { + return cm; + } + + /** Returns true if this is a 3-slice, 8-bit RGB stack. */ + public boolean isRGB() { + return nSlices==3 && (stack[0] instanceof byte[]) && getSliceLabel(1)!=null && getSliceLabel(1).equals("Red"); + } + + /** Returns true if this is a 3-slice HSB stack. */ + public boolean isHSB() { + return nSlices==3 && bitDepth==8 && getSliceLabel(1)!=null && getSliceLabel(1).equals("Hue"); + } + + /** Returns true if this is a 3-slice 32-bit HSB stack. */ + public boolean isHSB32() { + return nSlices==3 && bitDepth==32 && getSliceLabel(1)!=null && getSliceLabel(1).equals("Hue"); + } + + /** Returns true if this is a Lab stack. */ + public boolean isLab() { + return nSlices==3 && getSliceLabel(1)!=null && getSliceLabel(1).equals("L*"); + } + + /** Returns true if this is a virtual (disk resident) stack. + This method is overridden by the VirtualStack subclass. */ + public boolean isVirtual() { + return false; + } + + /** Frees memory by deleting a few slices from the end of the stack. */ + public void trim() { + int n = (int)Math.round(Math.log(nSlices)+1.0); + for (int i=0; i=0 && x=0 && y=0 && z=0 && x=0 && y=0 && z255.0) + value = 255.0; + else if (value<0.0) + value = 0.0; + bytes[y*width+x] = (byte)(value+0.5); + break; + case 16: + short[] shorts = (short[])stack[z]; + if (value>65535.0) + value = 65535.0; + else if (value<0.0) + value = 0.0; + shorts[y*width+x] = (short)(value+0.5); + break; + case 32: + float[] floats = (float[])stack[z]; + floats[y*width+x] = (float)value; + break; + case 24: + int[] ints = (int[])stack[z]; + ints[y*width+x] = (int)value; + break; + } + } + } + + public float[] getVoxels(int x0, int y0, int z0, int w, int h, int d, float[] voxels) { + boolean inBounds = x0>=0 && x0+w<=width && y0>=0 && y0+h<=height && z0>=0 && z0+d<=nSlices; + if (voxels==null || voxels.length!=w*h*d) + voxels = new float[w*h*d]; + int i = 0; + int offset; + for (int z=z0; z=0 && x0+w<=width && y0>=0 && y0+h<=height && z0>=0 && z0+d<=nSlices; + if (voxels==null || voxels.length!=w*h*d) + voxels = new float[w*h*d]; + int i = 0; + for (int z=z0; z>16; break; + case 1: value=(value&0xff00)>>8; break; + case 2: value=value&0xff;; break; + } + voxels[i++] = (float)value; + } + } + } + return voxels; + } + + /** Experimental */ + public void setVoxels(int x0, int y0, int z0, int w, int h, int d, float[] voxels) { + boolean inBounds = x0>=0 && x0+w<=width && y0>=0 && y0+h<=height && z0>=0 && z0+d<=nSlices; + if (voxels==null || voxels.length!=w*h*d) + ; + int i = 0; + float value; + for (int z=z0; z255f) + value = 255f; + else if (value<0f) + value = 0f; + bytes[y*width+x] = (byte)(value+0.5f); + } + break; + case 16: + short[] shorts = (short[])stack[z]; + for (int x=x0; x65535f) + value = 65535f; + else if (value<0f) + value = 0f; + shorts[y*width+x] = (short)(value+0.5f); + } + break; + case 32: + float[] floats = (float[])stack[z]; + for (int x=x0; x=0 && x0+w<=width && y0>=0 && y0+h<=height && z0>=0 && z0+d<=nSlices; + if (voxels==null || voxels.length!=w*h*d) + ; + int i = 0; + for (int z=z0; z0) + setBitDepth(stack[0]); + return this.bitDepth; + } + + /** Sets the bit depth (8=byte, 16=short, 24=RGB, 32=float). */ + public void setBitDepth(int depth) { + if (size()==0 && (depth==8||depth==16||depth==24||depth==32)) + this.bitDepth = depth; + } + + /** Creates a new ImageStack. + * @param width width in pixels + * @param height height in pixels + * @param depth number of images + * @param bitdepth 8, 16, 32 (float) or 24 (RGB) + */ + public static ImageStack create(int width, int height, int depth, int bitdepth) { + ImageStack stack = IJ.createImage("", width, height, depth, bitdepth).getStack(); + if (bitdepth==16 || bitdepth==32) { + stack.min = Double.MAX_VALUE; + stack.max = 0.0; + } + return stack; + } + + /** Creates an ImageStack from an ImagePlus array. */ + public static ImageStack create(ImagePlus[] images) { + int w = 0; + int h = 0; + for (int i=0; iw) w=images[i].getWidth(); + if (images[i].getHeight()>h) h=images[i].getHeight(); + } + ImageStack stack = new ImageStack(w, h); + stack.init(w, h); + for (int i=0; ithis.width||y+height>this.height||z+depth>size()) + throw new IllegalArgumentException("Argument out of range"); + ImageStack stack2 = new ImageStack(width, height, getColorModel()); + for (int i=z; itrue if this is a 256 entry grayscale LUT. + @see ij.process.ImageProcessor#isColorLut + */ + public boolean isGrayscale() { + boolean isGray = true; + + if (mapSize < 256) + return false; + for (int i=0; i1) + return fs.saveAsTiffStack(path); + else + return fs.saveAsTiff(path); + } + + public static String getName(String path) { + int i = path.lastIndexOf('/'); + if (i==-1) + i = path.lastIndexOf('\\'); + if (i>0) + return path.substring(i+1); + else + return path; + } + + public static String getDir(String path) { + int i = path.lastIndexOf('/'); + if (i==-1) + i = path.lastIndexOf('\\'); + if (i>0) + return path.substring(0, i+1); + else + return ""; + } + + /** Aborts the currently running macro or any plugin using IJ.run(). */ + public static void abort() { + //IJ.log("Abort: "+Thread.currentThread().getName()); + abort = true; + if (Thread.currentThread().getName().endsWith("Macro$")) { + table.remove(Thread.currentThread()); + throw new RuntimeException(MACRO_CANCELED); + } + } + + /** If a command started using run(name, options) is running, + and the current thread is the same thread, + returns the options string, otherwise, returns null. + @see ij.gui.GenericDialog + @see ij.io.OpenDialog + */ + public static String getOptions() { + String threadName = Thread.currentThread().getName(); + //IJ.log("getOptions: "+threadName+" "+Thread.currentThread().hashCode()); //ts + if (threadName.startsWith("Run$_")||threadName.startsWith("RMI TCP")) { + Object options = table.get(Thread.currentThread()); + return options==null?null:options+" "; + } else + return null; + } + + /** Define a set of Macro options for the current Thread. */ + public static void setOptions(String options) { + //IJ.log("setOptions: "+Thread.currentThread().getName()+" "+Thread.currentThread().hashCode()+" "+options); //ts + if (options==null || options.equals("")) + table.remove(Thread.currentThread()); + else + table.put(Thread.currentThread(), options); + } + + /** Define a set of Macro options for a Thread. */ + public static void setOptions(Thread thread, String options) { + if (null==thread) + throw new RuntimeException("Need a non-null thread instance"); + if (null==options) + table.remove(thread); + else + table.put(thread, options); + } + + public static String getValue(String options, String key, String defaultValue) { + key = trimKey(key); + if (!options.endsWith(" ")) + options = options + " "; + key += '='; + int index=-1; + do { // Require that key not be preceded by a letter + index = options.indexOf(key, ++index); + if (index<0) return defaultValue; + } while (index!=0&&Character.isLetter(options.charAt(index-1))); + options = options.substring(index+key.length(), options.length()); + if (options.charAt(0)=='\'') { + index = options.indexOf("'",1); + if (index<0) + return defaultValue; + else + return options.substring(1, index); + } else if (options.charAt(0)=='[') { + int count = 1; + index = -1; + for (int i=1; i-1) + key = key.substring(0,index); + index = key.indexOf(":"); + if (index>-1) + key = key.substring(0,index); + key = key.toLowerCase(Locale.US); + return key; + } + + /** Evaluates 'code' and returns the output, or any error, + * as a String (e.g., Macro.eval("2+2") returns "4"). + */ + public static String eval(String code) { + return new Interpreter().eval(code); + } + +} + diff --git a/src/ij/Menus.java b/src/ij/Menus.java new file mode 100644 index 0000000..a988703 --- /dev/null +++ b/src/ij/Menus.java @@ -0,0 +1,1720 @@ +package ij; +import ij.process.*; +import ij.util.*; +import ij.gui.ImageWindow; +import ij.plugin.MacroInstaller; +import ij.gui.Toolbar; +import ij.macro.Interpreter; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; +import java.util.*; +import java.io.*; +import java.applet.Applet; +import java.awt.event.*; +import java.util.zip.*; + +/** +This class installs and updates ImageJ's menus. Note that menu labels, +even in submenus, must be unique. This is because ImageJ uses a single +hash table for all menu labels. If you look closely, you will see that +File->Import->Text Image... and File->Save As->Text Image... do not use +the same label. One of the labels has an extra space. + +@see ImageJ +*/ + +public class Menus { + + public static final char PLUGINS_MENU = 'p'; + public static final char IMPORT_MENU = 'i'; + public static final char SAVE_AS_MENU = 's'; + public static final char SHORTCUTS_MENU = 'h'; // 'h'=hotkey + public static final char ABOUT_MENU = 'a'; + public static final char FILTERS_MENU = 'f'; + public static final char TOOLS_MENU = 't'; + public static final char UTILITIES_MENU = 'u'; + + public static final int WINDOW_MENU_ITEMS = 6; // fixed items at top of Window menu + + public static final int NORMAL_RETURN = 0; + public static final int COMMAND_IN_USE = -1; + public static final int INVALID_SHORTCUT = -2; + public static final int SHORTCUT_IN_USE = -3; + public static final int NOT_INSTALLED = -4; + public static final int COMMAND_NOT_FOUND = -5; + + public static final int MAX_OPEN_RECENT_ITEMS = 15; + + private static Menus instance; + private static MenuBar mbar; + private static CheckboxMenuItem gray8Item,gray16Item,gray32Item, + color256Item,colorRGBItem,RGBStackItem,HSBStackItem,LabStackItem,HSB32Item; + private static PopupMenu popup; + + private static ImageJ ij; + private static Applet applet; + private Hashtable demoImagesTable = new Hashtable(); + private static String ImageJPath, pluginsPath, macrosPath; + private static Properties menus; + private static Properties menuSeparators; + private static Menu pluginsMenu, saveAsMenu, shortcutsMenu, utilitiesMenu, macrosMenu; + static Menu window, openRecentMenu; + private static Hashtable pluginsTable; + + private static int nPlugins, nMacros; + private static Hashtable shortcuts; + private static Hashtable macroShortcuts; + private static Vector pluginsPrefs; // commands saved in IJ_Prefs + static int windowMenuItems2; // non-image windows listed in Window menu + separator + private String error; + private String jarError; + private String pluginError; + private boolean isJarErrorHeading; + private static boolean installingJars, duplicateCommand; + private static Vector jarFiles; // JAR files in plugins folder with "_" in their name + private Map menuEntry2jarFile = new HashMap(); + private static Vector macroFiles; // Macros and scripts in the plugins folder + private static int userPluginsIndex; // First user plugin or submenu in Plugins menu + private static boolean addSorted; + private static int defaultFontSize = IJ.isWindows()?15:0; + private static int fontSize = Prefs.getInt(Prefs.MENU_SIZE, defaultFontSize); + private static Font menuFont; + private static double scale = 1.0; + + static boolean jnlp; // true when using Java WebStart + public static int setMenuBarCount; + + Menus(ImageJ ijInstance, Applet appletInstance) { + ij = ijInstance; + String title = ij!=null?ij.getTitle():null; + applet = appletInstance; + instance = this; + } + + String addMenuBar() { + scale = Prefs.getGuiScale(); + if ((scale>=1.5&&scale<2.0) || (scale>=2.5&&scale<3.0)) + scale = (int)Math.round(scale); + nPlugins = nMacros = userPluginsIndex = 0; + addSorted = installingJars = duplicateCommand = false; + error = null; + mbar = null; + menus = new Properties(); + pluginsTable = new Hashtable(); + shortcuts = new Hashtable(); + pluginsPrefs = new Vector(); + macroShortcuts = null; + setupPluginsAndMacrosPaths(); + Menu file = getMenu("File"); + Menu newMenu = getMenu("File>New", true); + addPlugInItem(file, "Open...", "ij.plugin.Commands(\"open\")", KeyEvent.VK_O, false); + addPlugInItem(file, "Open Next", "ij.plugin.NextImageOpener", KeyEvent.VK_O, true); + Menu openSamples = getMenu("File>Open Samples", true); + openSamples.addSeparator(); + addPlugInItem(openSamples, "Cache Sample Images ", "ij.plugin.URLOpener(\"cache\")", 0, false); + addOpenRecentSubMenu(file); + Menu importMenu = getMenu("File>Import", true); + Menu showFolderMenu = new Menu("Show Folder"); + file.add(showFolderMenu); + addPlugInItem(showFolderMenu, "Image", "ij.plugin.SimpleCommands(\"showdirImage\")", 0, false); + addPlugInItem(showFolderMenu, "Plugins", "ij.plugin.SimpleCommands(\"showdirPlugins\")", 0, false); + addPlugInItem(showFolderMenu, "Macros", "ij.plugin.SimpleCommands(\"showdirMacros\")", 0, false); + addPlugInItem(showFolderMenu, "LUTs", "ij.plugin.SimpleCommands(\"showdirLuts\")", 0, false); + addPlugInItem(showFolderMenu, "ImageJ", "ij.plugin.SimpleCommands(\"showdirImageJ\")", 0, false); + addPlugInItem(showFolderMenu, "temp", "ij.plugin.SimpleCommands(\"showdirTemp\")", 0, false); + addPlugInItem(showFolderMenu, "Home", "ij.plugin.SimpleCommands(\"showdirHome\")", 0, false); + file.addSeparator(); + addPlugInItem(file, "Close", "ij.plugin.Commands(\"close\")", KeyEvent.VK_W, false); + addPlugInItem(file, "Close All", "ij.plugin.Commands(\"close-all\")", KeyEvent.VK_W, true); + addPlugInItem(file, "Save", "ij.plugin.Commands(\"save\")", KeyEvent.VK_S, false); + saveAsMenu = getMenu("File>Save As", true); + addPlugInItem(file, "Revert", "ij.plugin.Commands(\"revert\")", KeyEvent.VK_R, true); + file.addSeparator(); + addPlugInItem(file, "Page Setup...", "ij.plugin.filter.Printer(\"setup\")", 0, false); + addPlugInItem(file, "Print...", "ij.plugin.filter.Printer(\"print\")", KeyEvent.VK_P, false); + + Menu edit = getMenu("Edit"); + addPlugInItem(edit, "Undo", "ij.plugin.Commands(\"undo\")", KeyEvent.VK_Z, false); + edit.addSeparator(); + addPlugInItem(edit, "Cut", "ij.plugin.Clipboard(\"cut\")", KeyEvent.VK_X, false); + addPlugInItem(edit, "Copy", "ij.plugin.Clipboard(\"copy\")", KeyEvent.VK_C, false); + addPlugInItem(edit, "Copy to System", "ij.plugin.Clipboard(\"scopy\")", 0, false); + addPlugInItem(edit, "Paste", "ij.plugin.Clipboard(\"paste\")", KeyEvent.VK_V, false); + addPlugInItem(edit, "Paste Control...", "ij.plugin.frame.PasteController", 0, false); + edit.addSeparator(); + addPlugInItem(edit, "Clear", "ij.plugin.filter.Filler(\"clear\")", 0, false); + addPlugInItem(edit, "Clear Outside", "ij.plugin.filter.Filler(\"outside\")", 0, false); + addPlugInItem(edit, "Fill", "ij.plugin.filter.Filler(\"fill\")", KeyEvent.VK_F, false); + addPlugInItem(edit, "Draw", "ij.plugin.filter.Filler(\"draw\")", KeyEvent.VK_D, false); + addPlugInItem(edit, "Invert", "ij.plugin.filter.Filters(\"invert\")", KeyEvent.VK_I, true); + edit.addSeparator(); + getMenu("Edit>Selection", true); + Menu optionsMenu = getMenu("Edit>Options", true); + + Menu image = getMenu("Image"); + Menu imageType = getMenu("Image>Type"); + gray8Item = addCheckboxItem(imageType, "8-bit", "ij.plugin.Converter(\"8-bit\")"); + gray16Item = addCheckboxItem(imageType, "16-bit", "ij.plugin.Converter(\"16-bit\")"); + gray32Item = addCheckboxItem(imageType, "32-bit", "ij.plugin.Converter(\"32-bit\")"); + color256Item = addCheckboxItem(imageType, "8-bit Color", "ij.plugin.Converter(\"8-bit Color\")"); + colorRGBItem = addCheckboxItem(imageType, "RGB Color", "ij.plugin.Converter(\"RGB Color\")"); + imageType.add(new MenuItem("-")); + RGBStackItem = addCheckboxItem(imageType, "RGB Stack", "ij.plugin.Converter(\"RGB Stack\")"); + HSBStackItem = addCheckboxItem(imageType, "HSB Stack", "ij.plugin.Converter(\"HSB Stack\")"); + HSB32Item = addCheckboxItem(imageType, "HSB (32-bit)", "ij.plugin.Converter(\"HSB (32-bit)\")"); + LabStackItem = addCheckboxItem(imageType, "Lab Stack", "ij.plugin.Converter(\"Lab Stack\")"); + image.add(imageType); + + image.addSeparator(); + getMenu("Image>Adjust", true); + addPlugInItem(image, "Show Info...", "ij.plugin.ImageInfo", KeyEvent.VK_I, false); + addPlugInItem(image, "Properties...", "ij.plugin.filter.ImageProperties", KeyEvent.VK_P, true); + getMenu("Image>Color", true); + getMenu("Image>Stacks", true); + getMenu("Image>Stacks>Animation_", true); + getMenu("Image>Stacks>Tools_", true); + Menu hyperstacksMenu = getMenu("Image>Hyperstacks", true); + image.addSeparator(); + addPlugInItem(image, "Crop", "ij.plugin.Resizer(\"crop\")", KeyEvent.VK_X, true); + addPlugInItem(image, "Duplicate...", "ij.plugin.Duplicator", KeyEvent.VK_D, true); + addPlugInItem(image, "Rename...", "ij.plugin.SimpleCommands(\"rename\")", 0, false); + addPlugInItem(image, "Scale...", "ij.plugin.Scaler", KeyEvent.VK_E, false); + getMenu("Image>Transform", true); + getMenu("Image>Zoom", true); + getMenu("Image>Overlay", true); + image.addSeparator(); + getMenu("Image>Lookup Tables", true); + + Menu process = getMenu("Process"); + addPlugInItem(process, "Smooth", "ij.plugin.filter.Filters(\"smooth\")", KeyEvent.VK_S, true); + addPlugInItem(process, "Sharpen", "ij.plugin.filter.Filters(\"sharpen\")", 0, false); + addPlugInItem(process, "Find Edges", "ij.plugin.filter.Filters(\"edge\")", 0, false); + addPlugInItem(process, "Find Maxima...", "ij.plugin.filter.MaximumFinder", 0, false); + addPlugInItem(process, "Enhance Contrast...", "ij.plugin.ContrastEnhancer", 0, false); + getMenu("Process>Noise", true); + getMenu("Process>Shadows", true); + getMenu("Process>Binary", true); + getMenu("Process>Math", true); + getMenu("Process>FFT", true); + Menu filtersMenu = getMenu("Process>Filters", true); + process.addSeparator(); + getMenu("Process>Batch", true); + addPlugInItem(process, "Image Calculator...", "ij.plugin.ImageCalculator", 0, false); + addPlugInItem(process, "Subtract Background...", "ij.plugin.filter.BackgroundSubtracter", 0, false); + addItem(process, "Repeat Command", KeyEvent.VK_R, false); + + Menu analyzeMenu = getMenu("Analyze"); + addPlugInItem(analyzeMenu, "Measure", "ij.plugin.filter.Analyzer", KeyEvent.VK_M, false); + addPlugInItem(analyzeMenu, "Analyze Particles...", "ij.plugin.filter.ParticleAnalyzer", 0, false); + addPlugInItem(analyzeMenu, "Summarize", "ij.plugin.filter.Analyzer(\"sum\")", 0, false); + addPlugInItem(analyzeMenu, "Distribution...", "ij.plugin.Distribution", 0, false); + addPlugInItem(analyzeMenu, "Label", "ij.plugin.filter.Filler(\"label\")", 0, false); + addPlugInItem(analyzeMenu, "Clear Results", "ij.plugin.filter.Analyzer(\"clear\")", 0, false); + addPlugInItem(analyzeMenu, "Set Measurements...", "ij.plugin.filter.Analyzer(\"set\")", 0, false); + analyzeMenu.addSeparator(); + addPlugInItem(analyzeMenu, "Set Scale...", "ij.plugin.filter.ScaleDialog", 0, false); + addPlugInItem(analyzeMenu, "Calibrate...", "ij.plugin.filter.Calibrator", 0, false); + if (IJ.isMacOSX()) { + addPlugInItem(analyzeMenu, "Histogram", "ij.plugin.Histogram", 0, false); + shortcuts.put(new Integer(KeyEvent.VK_H),"Histogram"); + } else + addPlugInItem(analyzeMenu, "Histogram", "ij.plugin.Histogram", KeyEvent.VK_H, false); + addPlugInItem(analyzeMenu, "Plot Profile", "ij.plugin.Profiler(\"plot\")", KeyEvent.VK_K, false); + addPlugInItem(analyzeMenu, "Surface Plot...", "ij.plugin.SurfacePlotter", 0, false); + getMenu("Analyze>Gels", true); + Menu toolsMenu = getMenu("Analyze>Tools", true); + + // the plugins will be added later, after a separator + addPluginsMenu(); + + Menu window = getMenu("Window"); + addPlugInItem(window, "Show All", "ij.plugin.WindowOrganizer(\"show\")", KeyEvent.VK_CLOSE_BRACKET, false); + String key = IJ.isWindows()?"enter":"return"; + addPlugInItem(window, "Main Window ["+key+"]", "ij.plugin.WindowOrganizer(\"imagej\")", 0, false); + addPlugInItem(window, "Put Behind [tab]", "ij.plugin.Commands(\"tab\")", 0, false); + addPlugInItem(window, "Cascade", "ij.plugin.WindowOrganizer(\"cascade\")", 0, false); + addPlugInItem(window, "Tile", "ij.plugin.WindowOrganizer(\"tile\")", 0, false); + window.addSeparator(); + + Menu help = getMenu("Help"); + addPlugInItem(help, "ImageJ Website...", "ij.plugin.BrowserLauncher", 0, false); + help.addSeparator(); + addPlugInItem(help, "Dev. Resources...", "ij.plugin.BrowserLauncher(\""+IJ.URL+"/developer/index.html\")", 0, false); + addPlugInItem(help, "Plugins...", "ij.plugin.BrowserLauncher(\""+IJ.URL+"/plugins\")", 0, false); + addPlugInItem(help, "Macros...", "ij.plugin.BrowserLauncher(\""+IJ.URL+"/macros/\")", 0, false); + addPlugInItem(help, "Macro Functions...", "ij.plugin.BrowserLauncher(\""+IJ.URL+"/developer/macro/functions.html\")", 0, false); + Menu examplesMenu = getExamplesMenu(ij); + addPlugInItem(examplesMenu, "Open as Panel", "ij.plugin.SimpleCommands(\"opencp\")", 0, false); + help.add(examplesMenu); + help.addSeparator(); + addPlugInItem(help, "Update ImageJ...", "ij.plugin.ImageJ_Updater", 0, false); + addPlugInItem(help, "Refresh Menus", "ij.plugin.ImageJ_Updater(\"menus\")", 0, false); + help.addSeparator(); + Menu aboutMenu = getMenu("Help>About Plugins", true); + addPlugInItem(help, "About ImageJ...", "ij.plugin.AboutBox", 0, false); + + if (applet==null) { + menuSeparators = new Properties(); + installPlugins(); + } + + // make sure "Quit" is the last item in the File menu + file.addSeparator(); + addPlugInItem(file, "Quit", "ij.plugin.Commands(\"quit\")", 0, false); + + //System.out.println("MenuBar.setFont: "+fontSize+" "+scale+" "+getFont()); + if (fontSize!=0 || scale>1.0) + mbar.setFont(getFont()); + if (ij!=null) { + ij.setMenuBar(mbar); + Menus.setMenuBarCount++; + } + + // Add deleted sample images to commands table + pluginsTable.put("Lena (68K)", "ij.plugin.URLOpener(\"lena-std.tif\")"); + pluginsTable.put("Bridge (174K)", "ij.plugin.URLOpener(\"bridge.gif\")"); + + if (pluginError!=null) + error = error!=null?error+="\n"+pluginError:pluginError; + if (jarError!=null) + error = error!=null?error+="\n"+jarError:jarError; + return error; + } + + public static Menu getExamplesMenu(ActionListener listener) { + Menu menu = new Menu("Examples"); + Menu submenu = new Menu("Plots"); + addExample(submenu, "Example Plot", "Example_Plot_.ijm"); + addExample(submenu, "Semi-log Plot", "Semi-log_Plot_.ijm"); + addExample(submenu, "Arrow Plot", "Arrow_Plot_.ijm"); + addExample(submenu, "Damped Wave Plot", "Damped_Wave_Plot_.ijm"); + addExample(submenu, "Dynamic Plot", "Dynamic_Plot_.ijm"); + addExample(submenu, "Dynamic Plot 2D", "Dynamic_Plot_2D_.ijm"); + addExample(submenu, "Custom Plot Symbols", "Custom_Plot_Symbols_.ijm"); + addExample(submenu, "Histograms", "Histograms_.ijm"); + addExample(submenu, "Bar Charts", "Bar_Charts_.ijm"); + addExample(submenu, "Shapes", "Plot_Shapes_.ijm"); + addExample(submenu, "Plot Styles", "Plot_Styles_.ijm"); + addExample(submenu, "Random Data", "Random_Data_.ijm"); + addExample(submenu, "Plot Results", "Plot_Results_.ijm"); + submenu.addActionListener(listener); + menu.add(submenu); + + submenu = new Menu("Tools"); + addExample(submenu, "Annular Selection", "Annular_Selection_Tool.ijm"); + addExample(submenu, "Big Cursor", "Big_Cursor_Tool.ijm"); + addExample(submenu, "Circle Tool", "Circle_Tool.ijm"); + addExample(submenu, "Point Picker", "Point_Picker_Tool.ijm"); + addExample(submenu, "Star Tool", "Star_Tool.ijm"); + addExample(submenu, "Animated Icon Tool", "Animated_Icon_Tool.ijm"); + submenu.addActionListener(listener); + menu.add(submenu); + + submenu = new Menu("Macro"); + addExample(submenu, "Sphere", "Sphere.ijm"); + addExample(submenu, "Dialog Box", "Dialog_Box.ijm"); + addExample(submenu, "Process Folder", "Batch_Process_Folder.ijm"); + addExample(submenu, "OpenDialog Demo", "OpenDialog_Demo.ijm"); + addExample(submenu, "Save All Images", "Save_All_Images.ijm"); + addExample(submenu, "Sine/Cosine Table", "Sine_Cosine_Table.ijm"); + addExample(submenu, "Non-numeric Table", "Non-numeric_Table.ijm"); + addExample(submenu, "Overlay", "Overlay.ijm"); + addExample(submenu, "Stack Overlay", "Stack_Overlay.ijm"); + addExample(submenu, "Array Functions", "Array_Functions.ijm"); + addExample(submenu, "Dual Progress Bars", "Dual_Progress_Bars.ijm"); + addExample(submenu, "Grab Viridis Colormap", "Grab_Viridis_Colormap.ijm"); + addExample(submenu, "Custom Measurement", "Custom_Measurement.ijm"); + addExample(submenu, "Synthetic Images", "Synthetic_Images.ijm"); + addExample(submenu, "Spiral Rotation", "Spiral_Rotation.ijm"); + addExample(submenu, "Curve Fitting", "Curve_Fitting.ijm"); + addExample(submenu, "Colors of 2021", "Colors_of_2021.ijm"); + submenu.addActionListener(listener); + menu.add(submenu); + + submenu = new Menu("JavaScript"); + addExample(submenu, "Sphere", "Sphere.js"); + addExample(submenu, "Plasma Cloud", "Plasma_Cloud.js"); + addExample(submenu, "Cloud Debugger", "Cloud_Debugger.js"); + addExample(submenu, "Synthetic Images", "Synthetic_Images.js"); + addExample(submenu, "Points", "Points.js"); + addExample(submenu, "Spiral Rotation", "Spiral_Rotation.js"); + addExample(submenu, "Example Plot", "Example_Plot.js"); + addExample(submenu, "Semi-log Plot", "Semi-log_Plot.js"); + addExample(submenu, "Arrow Plot", "Arrow_Plot.js"); + addExample(submenu, "Dynamic Plot", "Dynamic_Plot.js"); + addExample(submenu, "Plot Styles", "Plot_Styles.js"); + addExample(submenu, "Plot Random Data", "Plot_Random_Data.js"); + addExample(submenu, "Histogram Plots", "Histogram_Plots.js"); + addExample(submenu, "JPEG Quality Plot", "JPEG_Quality_Plot.js"); + addExample(submenu, "Process Folder", "Batch_Process_Folder.js"); + addExample(submenu, "Sine/Cosine Table", "Sine_Cosine_Table.js"); + addExample(submenu, "Non-numeric Table", "Non-numeric_Table.js"); + addExample(submenu, "Overlay", "Overlay.js"); + addExample(submenu, "Stack Overlay", "Stack_Overlay.js"); + addExample(submenu, "Dual Progress Bars", "Dual_Progress_Bars.js"); + addExample(submenu, "Gamma Adjuster", "Gamma_Adjuster.js"); + addExample(submenu, "Custom Measurement", "Custom_Measurement.js"); + addExample(submenu, "Terabyte VirtualStack", "Terabyte_VirtualStack.js"); + addExample(submenu, "Event Listener", "Event_Listener.js"); + addExample(submenu, "FFT Filter", "FFT_Filter.js"); + addExample(submenu, "Curve Fitting", "Curve_Fitting.js"); + addExample(submenu, "Overlay Text", "Overlay_Text.js"); + addExample(submenu, "Crop Multiple Rois", "Crop_Multiple_Rois.js"); + addExample(submenu, "Show all LUTs", "Show_all_LUTs.js"); + addExample(submenu, "Dialog Demo", "Dialog_Demo.js"); + submenu.addActionListener(listener); + menu.add(submenu); + submenu = new Menu("BeanShell"); + addExample(submenu, "Sphere", "Sphere.bsh"); + addExample(submenu, "Example Plot", "Example_Plot.bsh"); + addExample(submenu, "Semi-log Plot", "Semi-log_Plot.bsh"); + addExample(submenu, "Arrow Plot", "Arrow_Plot.bsh"); + addExample(submenu, "Sine/Cosine Table", "Sine_Cosine_Table.bsh"); + submenu.addActionListener(listener); + menu.add(submenu); + submenu = new Menu("Python"); + addExample(submenu, "Sphere", "Sphere.py"); + addExample(submenu, "Animated Gaussian Blur", "Animated_Gaussian_Blur.py"); + addExample(submenu, "Spiral Rotation", "Spiral_Rotation.py"); + addExample(submenu, "Overlay", "Overlay.py"); + submenu.addActionListener(listener); + menu.add(submenu); + submenu = new Menu("Java"); + addExample(submenu, "Sphere", "Sphere_.java"); + addExample(submenu, "Plasma Cloud", "Plasma_Cloud.java"); + addExample(submenu, "Gamma Adjuster", "Gamma_Adjuster.java"); + addExample(submenu, "Plugin", "My_Plugin.java"); + addExample(submenu, "Plugin Filter", "Filter_Plugin.java"); + addExample(submenu, "Plugin Frame", "Plugin_Frame.java"); + addExample(submenu, "Plugin Tool", "Prototype_Tool.java"); + submenu.addActionListener(listener); + menu.add(submenu); + menu.addSeparator(); + CheckboxMenuItem item = new CheckboxMenuItem("Autorun Examples"); + menu.add(item); + item.addItemListener(ij); + item.setState(Prefs.autoRunExamples); + return menu; + } + + private static void addExample(Menu menu, String label, String command) { + MenuItem item = new MenuItem(label); + menu.add(item); + item.setActionCommand(command); + } + + void addOpenRecentSubMenu(Menu menu) { + openRecentMenu = getMenu("File>Open Recent"); + for (int i=0; i0) + key = key.substring(0, index); + for (int count=1; count<100; count++) { + value = Prefs.getString(key + (count/10)%10 + count%10); + if (value==null) + break; + if (count==1) + menu.add(submenu); + if (value.equals("-")) + submenu.addSeparator(); + else + addPluginItem(submenu, value); + } + if (name.equals("Lookup Tables") && applet==null) + addLuts(submenu); + return submenu; + } + + static void addLuts(Menu submenu) { + String path = IJ.getDirectory("luts"); + if (path==null) return; + File f = new File(path); + String[] list = null; + if (applet==null && f.exists() && f.isDirectory()) + list = f.list(); + if (list==null) return; + if (IJ.isLinux() || IJ.isMacOSX()) + Arrays.sort(list); + submenu.addSeparator(); + for (int i=0; i0) { + String shortcut = command.substring(openBracket+1,command.length()-1); + keyCode = convertShortcutToCode(shortcut); + boolean functionKey = keyCode>=KeyEvent.VK_F1 && keyCode<=KeyEvent.VK_F12; + if (keyCode>0 && !functionKey) + command = command.substring(0,openBracket); + } + } + if (keyCode>=KeyEvent.VK_F1 && keyCode<=KeyEvent.VK_F12) { + shortcuts.put(new Integer(keyCode),command); + keyCode = 0; + } else if (keyCode>=265 && keyCode<=290) { + keyCode -= 200; + shift = true; + } + addItem(submenu,command,keyCode,shift); + while(s.charAt(lastComma+1)==' ' && lastComma+2') { + String submenu = value.substring(2,value.length()-1); + //Menu menu = getMenu("Plugins>" + submenu, true); + Menu menu = addSubMenu(pluginsMenu, submenu); + if (submenu.equals("Shortcuts")) + shortcutsMenu = menu; + else if (submenu.equals("Utilities")) + utilitiesMenu = menu; + else if (submenu.equals("Macros")) + macrosMenu = menu; + } else + addPluginItem(pluginsMenu, value); + } + userPluginsIndex = pluginsMenu.getItemCount(); + if (userPluginsIndex<0) userPluginsIndex = 0; + } + + /** Install plugins using "pluginxx=" keys in IJ_Prefs.txt. + Plugins not listed in IJ_Prefs are added to the end + of the Plugins menu. */ + void installPlugins() { + int nPlugins0 = nPlugins; + String value, className; + char menuCode; + Menu menu; + String[] pluginList = getPlugins(); + String[] pluginsList2 = null; + Hashtable skipList = new Hashtable(); + for (int index=0; index<100; index++) { + value = Prefs.getString("plugin" + (index/10)%10 + index%10); + if (value==null) + break; + menuCode = value.charAt(0); + switch (menuCode) { + case PLUGINS_MENU: default: menu = pluginsMenu; break; + case IMPORT_MENU: menu = getMenu("File>Import"); break; + case SAVE_AS_MENU: menu = getMenu("File>Save As"); break; + case SHORTCUTS_MENU: menu = shortcutsMenu; break; + case ABOUT_MENU: menu = getMenu("Help>About Plugins"); break; + case FILTERS_MENU: menu = getMenu("Process>Filters"); break; + case TOOLS_MENU: menu = getMenu("Analyze>Tools"); break; + case UTILITIES_MENU: menu = utilitiesMenu; break; + } + String prefsValue = value; + value = value.substring(2,value.length()); //remove menu code and coma + className = value.substring(value.lastIndexOf(',')+1,value.length()); + boolean found = className.startsWith("ij."); + if (!found && pluginList!=null) { // does this plugin exist? + if (pluginsList2==null) + pluginsList2 = getStrippedPlugins(pluginList); + for (int i=0; i0) + className = className.substring(0, argStart); + } + skipList.put(className, ""); + } + } + if (pluginList!=null) { + for (int i=0; i0) { + dir = name.substring(0, slashIndex); + name = name.substring(slashIndex+1, name.length()); + menu = getPluginsSubmenu(dir); + slashIndex = name.indexOf('/'); + if (slashIndex>0) { + String dir2 = name.substring(0, slashIndex); + name = name.substring(slashIndex+1, name.length()); + String menuName = "Plugins>"+dir+">"+dir2; + menu = getMenu(menuName); + dir += File.separator+dir2; + } + } + String command = name.replace('_',' '); + if (command.endsWith(".js")||command.endsWith(".py")) + command = command.substring(0, command.length()-3); //remove ".js" or ".py" + else + command = command.substring(0, command.length()-4); //remove ".txt", ".ijm" or ".bsh" + command.trim(); + if (pluginsTable.get(command)!=null) // duplicate command? + command = command + " Macro"; + MenuItem item = new MenuItem(command); + addOrdered(menu, item); + item.addActionListener(ij); + String path = (dir!=null?dir+File.separator:"") + name; + pluginsTable.put(command, "ij.plugin.Macro_Runner(\""+path+"\")"); + nMacros++; + } + + static int addPluginSeparatorIfNeeded(Menu menu) { + if (menuSeparators == null) + return 0; + Integer i = (Integer)menuSeparators.get(menu); + if (i == null) { + if (menu.getItemCount() > 0) + addSeparator(menu); + i = new Integer(menu.getItemCount()); + menuSeparators.put(menu, i); + } + return i.intValue(); + } + + /** Inserts 'item' into 'menu' in alphanumeric order. */ + static void addOrdered(Menu menu, MenuItem item) { + String label = item.getLabel(); + int start = addPluginSeparatorIfNeeded(menu); + for (int i=start; i=3 && !s.startsWith("#")) + entries.add(s); + } + } + catch (IOException e) {} + finally { + try {if (lnr!=null) lnr.close();} + catch (IOException e) {} + } + for (int j=0; j")) { + int firstComma = s.indexOf(','); + if (firstComma==-1 || firstComma<=8) + menu = null; + else { + String name = s.substring(8, firstComma); + menu = getPluginsSubmenu(name); + } + } else if (s.startsWith("\"") || s.startsWith("Plugins")) { + String name = getSubmenuName(jar); + if (name!=null) + menu = getPluginsSubmenu(name); + else + menu = pluginsMenu; + addSorted = true; + } else { + int firstQuote = s.indexOf('"'); + String name = firstQuote<0 ? s : s.substring(0, firstQuote).trim(); + int comma = name.indexOf(','); + if (comma >= 0) + name = name.substring(0, comma); + if (name.startsWith("Help>About")) // for backward compatibility + name = "Help>About Plugins"; + menu = getMenu(name); + } + int firstQuote = s.indexOf('"'); + if (firstQuote==-1) + return; + s = s.substring(firstQuote, s.length()); // remove menu + if (menu!=null) { + addPluginSeparatorIfNeeded(menu); + addPluginItem(menu, s); + addSorted = false; + } + String menuEntry = s; + if (s.startsWith("\"")) { + int quote = s.indexOf('"', 1); + menuEntry = quote<0?s.substring(1):s.substring(1, quote); + } else { + int comma = s.indexOf(','); + if (comma > 0) + menuEntry = s.substring(0, comma); + } + if (duplicateCommand) { + if (jarError==null) jarError = ""; + addJarErrorHeading(jar); + String jar2 = (String)menuEntry2jarFile.get(menuEntry); + if (jar2 != null && jar2.startsWith(pluginsPath)) + jar2 = jar2.substring(pluginsPath.length()); + jarError += " Duplicate command: " + s + + (jar2 != null ? " (already in " + jar2 + ")" + : "") + "\n"; + } else + menuEntry2jarFile.put(menuEntry, jar); + duplicateCommand = false; + } + + void addJarErrorHeading(String jar) { + if (!isJarErrorHeading) { + if (!jarError.equals("")) + jarError += " \n"; + jarError += "Plugin configuration error: " + jar + "\n"; + isJarErrorHeading = true; + } + } + + /** Returns the specified ImageJ menu (e.g., "File>New") or null if it is not found. */ + public static Menu getImageJMenu(String menuPath) { + if (menus==null) + IJ.init(); + if (menus==null) + return null; + if (menus.get(menuPath)!=null) + return getMenu(menuPath, false); + else + return null; + } + + private static Menu getMenu(String menuPath) { + return getMenu(menuPath, false); + } + + private static Menu getMenu(String menuName, boolean readFromProps) { + if (menuName.endsWith(">")) + menuName = menuName.substring(0, menuName.length() - 1); + Menu result = (Menu)menus.get(menuName); + if (result==null) { + int offset = menuName.lastIndexOf('>'); + if (offset < 0) { + result = new Menu(menuName); + if (mbar == null) + mbar = new MenuBar(); + if (menuName.equals("Help")) + mbar.setHelpMenu(result); + else + mbar.add(result); + if (menuName.equals("Window")) + window = result; + else if (menuName.equals("Plugins")) + pluginsMenu = result; + } else { + String parentName = menuName.substring(0, offset); + String menuItemName = menuName.substring(offset + 1); + Menu parentMenu = getMenu(parentName); + result = new Menu(menuItemName); + addPluginSeparatorIfNeeded(parentMenu); + if (readFromProps) + result = addSubMenu(parentMenu, menuItemName); + else if (parentName.startsWith("Plugins") && menuSeparators != null) + addItemSorted(parentMenu, result, parentName.equals("Plugins")?userPluginsIndex:0); + else + parentMenu.add(result); + if (menuName.equals("File>Open Recent")) + openRecentMenu = result; + } + menus.put(menuName, result); + } + return result; + } + + Menu getPluginsSubmenu(String submenuName) { + return getMenu("Plugins>" + submenuName); + } + + String getSubmenuName(String jarPath) { + //IJ.log("getSubmenuName: \n"+jarPath+"\n"+pluginsPath); + if (pluginsPath == null) + return null; + if (jarPath.startsWith(pluginsPath)) + jarPath = jarPath.substring(pluginsPath.length() - 1); + int index = jarPath.lastIndexOf(File.separatorChar); + if (index<0) return null; + String name = jarPath.substring(0, index); + index = name.lastIndexOf(File.separatorChar); + if (index<0) return null; + name = name.substring(index+1); + if (name.equals("plugins")) return null; + return name; + } + + static void addItemSorted(Menu menu, MenuItem item, int startingIndex) { + String itemLabel = item.getLabel(); + int count = menu.getItemCount(); + boolean inserted = false; + for (int i=startingIndex; i0 && name.indexOf("$")==-1 + && name.indexOf("/_")==-1 && !name.startsWith("_")) { + if (Character.isLowerCase(name.charAt(0))&&name.indexOf("/")!=-1) + continue; + if (sb==null) sb = new StringBuffer(); + String className = name.substring(0, name.length()-6); + int slashIndex = className.lastIndexOf('/'); + String plugins = "Plugins"; + if (slashIndex >= 0) { + plugins += ">" + className.substring(0, slashIndex).replace('/', '>').replace('_', ' '); + name = className.substring(slashIndex + 1); + } else + name = className; + name = name.replace('_', ' '); + className = className.replace('/', '.'); + //if (className.indexOf(".")==-1 || Character.isUpperCase(className.charAt(0))) + sb.append(plugins + ", \""+name+"\", "+className+"\n"); + } + } + } + catch (Throwable e) { + IJ.log(jar+": "+e); + } + //IJ.log(""+(sb!=null?sb.toString():"null")); + if (sb==null) + return null; + else + return new ByteArrayInputStream(sb.toString().getBytes()); + } + + /** Returns a list of the plugins with directory names removed. */ + String[] getStrippedPlugins(String[] plugins) { + String[] plugins2 = new String[plugins.length]; + int slashPos; + for (int i=0; i=0) + plugins2[i] = plugins[i].substring(slashPos+1,plugins2[i].length()); + } + return plugins2; + } + + void setupPluginsAndMacrosPaths() { + ImageJPath = pluginsPath = macrosPath = null; + String currentDir = Prefs.getHomeDir(); // "user.dir" + if (currentDir==null) + return; + if (currentDir.endsWith("plugins")) + ImageJPath = pluginsPath = currentDir+File.separator; + else { + String pluginsDir = System.getProperty("plugins.dir"); + if (pluginsDir!=null) { + if (pluginsDir.endsWith("/")||pluginsDir.endsWith("\\")) + pluginsDir = pluginsDir.substring(0, pluginsDir.length()-1); + if (pluginsDir.endsWith("/plugins")||pluginsDir.endsWith("\\plugins")) + pluginsDir = pluginsDir.substring(0, pluginsDir.length()-8); + } + if (pluginsDir==null) + pluginsDir = currentDir; + else if (pluginsDir.equals("user.home")) { + pluginsDir = System.getProperty("user.home"); + if (!(new File(pluginsDir+File.separator+"plugins")).isDirectory()) + pluginsDir = pluginsDir + File.separator + "ImageJ"; + // needed to run plugins when ImageJ launched using Java WebStart + if (applet==null) + System.setSecurityManager(null); + jnlp = true; + } + pluginsPath = pluginsDir+File.separator+"plugins"+File.separator; + macrosPath = pluginsDir+File.separator+"macros"+File.separator; + ImageJPath = pluginsDir+File.separator; + } + File f = pluginsPath!=null?new File(pluginsPath):null; + if (f==null || !f.isDirectory()) { + ImageJPath = currentDir+File.separator; + pluginsPath = ImageJPath+"plugins"+File.separator; + f = new File(pluginsPath); + if (!f.isDirectory()) { + String altPluginsPath = System.getProperty("plugins.dir"); + if (altPluginsPath!=null) { + f = new File(altPluginsPath); + if (!f.isDirectory()) + altPluginsPath = null; + else { + ImageJPath = f.getParent() + File.separator; + pluginsPath = ImageJPath + f.getName() + File.separator; + macrosPath = ImageJPath+"macros"+File.separator; + } + } + if (altPluginsPath==null) + ImageJPath = pluginsPath = null; + } + } + f = macrosPath!=null?new File(macrosPath):null; + if (f!=null && !f.isDirectory()) { + macrosPath = currentDir+File.separator+"macros"+File.separator; + f = new File(macrosPath); + if (!f.isDirectory()) + macrosPath = null; + } + if (IJ.debugMode) { + IJ.log("Menus.setupPluginsAndMacrosPaths"); + IJ.log(" user.dir: "+currentDir); + IJ.log(" plugins.dir: "+System.getProperty("plugins.dir")); + IJ.log(" ImageJPath: "+ImageJPath); + IJ.log(" pluginsPath: "+pluginsPath); + } + } + + /** Returns a list of the plugins in the plugins menu. */ + public static synchronized String[] getPlugins() { + File f = pluginsPath!=null?new File(pluginsPath):null; + if (f==null || (f!=null && !f.isDirectory())) + return null; + String[] list = f.list(); + if (list==null) + return null; + Vector v = new Vector(); + jarFiles = null; + macroFiles = null; + for (int i=0; i=0; + if (hasUnderscore && isClassFile && name.indexOf('$')<0 ) { + name = name.substring(0, name.length()-6); // remove ".class" + v.addElement(name); + } else if (hasUnderscore && (name.endsWith(".jar") || name.endsWith(".zip"))) { + if (jarFiles==null) jarFiles = new Vector(); + jarFiles.addElement(pluginsPath + name); + } else if (validMacroName(name,hasUnderscore)) { + if (macroFiles==null) macroFiles = new Vector(); + macroFiles.addElement(name); + } else { + if (!isClassFile) + checkSubdirectory(pluginsPath, name, v); + } + } + list = new String[v.size()]; + v.copyInto((String[])list); + StringSorter.sort(list); + return list; + } + + /** Looks for plugins and jar files in a subdirectory of the plugins directory. */ + private static void checkSubdirectory(String path, String dir, Vector v) { + if (dir.endsWith(".java")) + return; + File f = new File(path, dir); + if (!f.isDirectory()) + return; + String[] list = f.list(); + if (list==null) + return; + dir += "/"; + int classCount=0, otherCount=0; + String className = null; + for (int i=0; i=0; + if (hasUnderscore && name.endsWith(".class") && name.indexOf('$')<0) { + name = name.substring(0, name.length()-6); // remove ".class" + v.addElement(dir+name); + classCount++; + className = name; + } else if (hasUnderscore && (name.endsWith(".jar") || name.endsWith(".zip"))) { + if (jarFiles==null) jarFiles = new Vector(); + jarFiles.addElement(f.getPath() + File.separator + name); + otherCount++; + } else if (validMacroName(name,hasUnderscore)) { + if (macroFiles==null) macroFiles = new Vector(); + macroFiles.addElement(dir + name); + otherCount++; + } else { + File f2 = new File(f, name); + if (f2.isDirectory()) installSubdirectorMacros(f2, dir+name); + } + } + if (Prefs.moveToMisc && classCount==1 && otherCount==0 && dir.indexOf("_")==-1) + v.setElementAt("Miscellaneous/" + className, + v.size() - 1); + } + + /** Installs macros and scripts located in subdirectories. */ + private static void installSubdirectorMacros(File f2, String dir) { + if (dir.endsWith("Launchers")) return; + String[] list = f2.list(); + if (list==null) return; + for (int i=0; i=0; + if (validMacroName(name,hasUnderscore)) { + if (macroFiles==null) macroFiles = new Vector(); + macroFiles.addElement(dir+"/"+name); + } + } + } + + private static boolean validMacroName(String name, boolean hasUnderscore) { + return (hasUnderscore&&name.endsWith(".txt")) || name.endsWith(".ijm") + || name.endsWith(".js") || name.endsWith(".bsh") || name.endsWith(".py"); + } + + /** Installs a plugin in the Plugins menu using the class name, + with underscores replaced by spaces, as the command. */ + void installUserPlugin(String className) { + installUserPlugin(className, false); + } + + public void installUserPlugin(String className, boolean force) { + int slashIndex = className.indexOf('/'); + String menuName = slashIndex < 0 ? "Plugins" : "Plugins>" + + className.substring(0, slashIndex).replace('/', '>'); + Menu menu = getMenu(menuName); + String command = className; + if (slashIndex>0) { + command = className.substring(slashIndex+1); + } + command = command.replace('_',' '); + command.trim(); + boolean itemExists = (pluginsTable.get(command)!=null); + if(force && itemExists) + return; + + if (!force && itemExists) // duplicate command? + command = command + " Plugin"; + MenuItem item = new MenuItem(command); + if(force) + addItemSorted(menu,item,0); + else + addOrdered(menu, item); + item.addActionListener(ij); + pluginsTable.put(command, className.replace('/', '.')); + nPlugins++; + } + + void installPopupMenu(ImageJ ij) { + String s; + int count = 0; + MenuItem mi; + popup = new PopupMenu(""); + if (fontSize!=0 || scale>1.0) + popup.setFont(getFont()); + while (true) { + count++; + s = Prefs.getString("popup" + (count/10)%10 + count%10); + if (s==null) + break; + if (s.equals("-")) + popup.addSeparator(); + else if (!s.equals("")) { + mi = new MenuItem(s); + mi.addActionListener(ij); + popup.add(mi); + } + } + } + + public static MenuBar getMenuBar() { + return mbar; + } + + public static Menu getMacrosMenu() { + return macrosMenu; + } + + public static Menu getOpenRecentMenu() { + return openRecentMenu; + } + + public int getMacroCount() { + return nMacros; + } + + public int getPluginCount() { + return nPlugins; + } + + static final int RGB_STACK=10, HSB_STACK=11, LAB_STACK=12, HSB32_STACK=13; + + /** Updates the Image/Type and Window menus. */ + public static void updateMenus() { + if (ij==null) return; + gray8Item.setState(false); + gray16Item.setState(false); + gray32Item.setState(false); + color256Item.setState(false); + colorRGBItem.setState(false); + RGBStackItem.setState(false); + HSBStackItem.setState(false); + LabStackItem.setState(false); + HSB32Item.setState(false); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) + return; + int type = imp.getType(); + if (imp.getStackSize()>1) { + ImageStack stack = imp.getStack(); + if (stack.isRGB()) + type = RGB_STACK; + else if (stack.isHSB()) + type = HSB_STACK; + else if (stack.isLab()) + type = LAB_STACK; + else if (stack.isHSB32()) + type = HSB32_STACK; + } + switch (type) { + case ImagePlus.GRAY8: + gray8Item.setState(true); + break; + case ImagePlus.GRAY16: + gray16Item.setState(true); + break; + case ImagePlus.GRAY32: + gray32Item.setState(true); + break; + case ImagePlus.COLOR_256: + color256Item.setState(true); + break; + case ImagePlus.COLOR_RGB: + colorRGBItem.setState(true); + break; + case RGB_STACK: + RGBStackItem.setState(true); + break; + case HSB_STACK: + HSBStackItem.setState(true); + break; + case LAB_STACK: + LabStackItem.setState(true); + break; + case HSB32_STACK: + HSB32Item.setState(true); + break; + } + + //update Window menu + int nItems = window.getItemCount(); + int start = WINDOW_MENU_ITEMS + windowMenuItems2; + int index = start + WindowManager.getCurrentIndex(); + try { // workaround for Linux/Java 5.0/bug + for (int i=start; i=2) + index--; + window.insert(item, index); + windowMenuItems2++; + if (windowMenuItems2==1) { + window.insertSeparator(WINDOW_MENU_ITEMS+windowMenuItems2); + windowMenuItems2++; + } + } + + /** Adds one image to the end of the Window menu. */ + static synchronized void addWindowMenuItem(ImagePlus imp) { + if (ij==null) + return; + String name = imp.getTitle(); + String size = ImageWindow.getImageSize(imp); + CheckboxMenuItem item = new CheckboxMenuItem(name+" "+size); + item.setActionCommand("" + imp.getID()); + window.add(item); + item.addItemListener(ij); + } + + /** Removes the specified item from the Window menu. */ + static synchronized void removeWindowMenuItem(int index) { + //IJ.log("removeWindowMenuItem: "+index+" "+windowMenuItems2+" "+window.getItemCount()); + if (ij==null) + return; + try { + if (index>=0 && index-1) + label = label.substring(0, index); + } + if (item!=null && label.equals(oldLabel) && (imp==null||(""+imp.getID()).equals(cmd))) { + String size = ""; + if (imp!=null) + size = " " + ImageWindow.getImageSize(imp); + item.setLabel(newLabel+size); + return; + } + } + } catch (Exception e) {} + } + + /** Adds a file path to the beginning of the File/Open Recent submenu. */ + public static synchronized void addOpenRecentItem(String path) { + if (ij==null) return; + int count = openRecentMenu.getItemCount(); + for (int i=0; iImport"); break; + case SAVE_AS_MENU: menu = getMenu("File>Save As"); break; + case SHORTCUTS_MENU: menu = shortcutsMenu; break; + case ABOUT_MENU: menu = getMenu("Help>About Plugins"); break; + case FILTERS_MENU: menu = getMenu("Process>Filters"); break; + case TOOLS_MENU: menu = getMenu("Analyze>Tools"); break; + case UTILITIES_MENU: menu = utilitiesMenu; break; + default: return 0; + } + int code = convertShortcutToCode(shortcut); + MenuItem item; + boolean functionKey = code>=KeyEvent.VK_F1 && code<=KeyEvent.VK_F12; + if (code==0) + item = new MenuItem(command); + else if (functionKey) { + command += " [F"+(code-KeyEvent.VK_F1+1)+"]"; + shortcuts.put(new Integer(code),command); + item = new MenuItem(command); + } else { + shortcuts.put(new Integer(code),command); + int keyCode = code; + boolean shift = false; + if (keyCode>=265 && keyCode<=290) { + keyCode -= 200; + shift = true; + } + item = new MenuItem(command, new MenuShortcut(keyCode, shift)); + } + menu.add(item); + item.addActionListener(ij); + pluginsTable.put(command, plugin); + shortcut = code>0 && !functionKey?"["+shortcut+"]":""; + pluginsPrefs.addElement(menuCode+",\""+command+shortcut+"\","+plugin); + return NORMAL_RETURN; + } + + /** Deletes a command installed by Plugins/Shortcuts/Add Shortcut. */ + public static int uninstallPlugin(String command) { + boolean found = false; + for (Enumeration en=pluginsPrefs.elements(); en.hasMoreElements();) { + String cmd = (String)en.nextElement(); + if (cmd.contains(command)) { + boolean ok = pluginsPrefs.removeElement((Object)cmd); + found = true; + break; + } + } + if (found) + return NORMAL_RETURN; + else + return COMMAND_NOT_FOUND; + + } + + public static boolean commandInUse(String command) { + if (pluginsTable.get(command)!=null) + return true; + else + return false; + } + + public static int convertShortcutToCode(String shortcut) { + int code = 0; + int len = shortcut.length(); + if (len==2 && shortcut.charAt(0)=='F') { + code = KeyEvent.VK_F1+(int)shortcut.charAt(1)-49; + if (code>=KeyEvent.VK_F1 && code<=KeyEvent.VK_F9) + return code; + else + return 0; + } + if (len==3 && shortcut.charAt(0)=='F') { + code = KeyEvent.VK_F10+(int)shortcut.charAt(2)-48; + if (code>=KeyEvent.VK_F10 && code<=KeyEvent.VK_F12) + return code; + else + return 0; + } + if (len==2 && shortcut.charAt(0)=='N') { // numeric keypad + code = KeyEvent.VK_NUMPAD0+(int)shortcut.charAt(1)-48; + if (code>=KeyEvent.VK_NUMPAD0 && code<=KeyEvent.VK_NUMPAD9) + return code; + switch (shortcut.charAt(1)) { + case '/': return KeyEvent.VK_DIVIDE; + case '*': return KeyEvent.VK_MULTIPLY; + case '-': return KeyEvent.VK_SUBTRACT; + case '+': return KeyEvent.VK_ADD; + case '.': return KeyEvent.VK_DECIMAL; + default: return 0; + } + } + if (len!=1) + return 0; + int c = (int)shortcut.charAt(0); + if (c>=65&&c<=90) //A-Z + code = KeyEvent.VK_A+c-65 + 200; + else if (c>=97&&c<=122) //a-z + code = KeyEvent.VK_A+c-97; + else if (c>=48&&c<=57) //0-9 + code = KeyEvent.VK_0+c-48; + return code; + } + + void installStartupMacroSet() { + if (macrosPath==null) { + MacroInstaller.installFromJar("/macros/StartupMacros.txt"); + return; + } + String path = macrosPath + "StartupMacros.txt"; + File f = new File(path); + if (!f.exists()) { + path = macrosPath + "StartupMacros.ijm"; + f = new File(path); + if (!f.exists()) { + (new MacroInstaller()).installFromIJJar("/macros/StartupMacros.txt"); + return; + } + } else { + if ("StartupMacros.fiji.ijm".equals(f.getName())) + path = f.getPath(); + } + String libraryPath = macrosPath + "Library.txt"; + f = new File(libraryPath); + boolean isLibrary = f.exists(); + try { + MacroInstaller mi = new MacroInstaller(); + if (isLibrary) mi.installLibrary(libraryPath); + mi.installStartupMacros(path); + nMacros += mi.getMacroCount(); + } catch (Exception e) {} + } + + static boolean validShortcut(String shortcut) { + int len = shortcut.length(); + if (shortcut.equals("")) + return true; + else if (len==1) + return true; + else if (shortcut.startsWith("F") && (len==2 || len==3)) + return true; + else + return false; + } + + /** Returns 'true' if this keyboard shortcut is in use. */ + public static boolean shortcutInUse(String shortcut) { + int code = convertShortcutToCode(shortcut); + if (shortcuts.get(new Integer(code))!=null) + return true; + else + return false; + } + + /** Set the size (in points) used for the fonts in ImageJ menus. + Set the size to 0 to use the Java default size. */ + public static void setFontSize(int size) { + if (size<9 && size!=0) size = 9; + if (size>24) size = 24; + fontSize = size; + } + + /** Returns the size (in points) used for the fonts in ImageJ menus. Returns + 0 if the default font size is being used or if this is a Macintosh. */ + public static int getFontSize() { + return fontSize; + //return IJ.isMacintosh()?0:fontSize; + } + + public static Font getFont() { + if (menuFont==null) { + int size = fontSize==0?13:fontSize; + size = (int)Math.round(size*scale); + if (IJ.isWindows() && scale>1.0 && size>17) + size = 17; // Java resets size to 12 if you try to set it to 18 or greater + menuFont = new Font("SanSerif", Font.PLAIN, size); + } + //System.out.println("Menus.getFont: "+scale+" "+fontSize+" "+menuFont); + return menuFont; + } + + /** Called once when ImageJ quits. */ + public static void savePreferences(Properties prefs) { + if (pluginsPrefs==null) + return; + int index = 0; + for (Enumeration en=pluginsPrefs.elements(); en.hasMoreElements();) { + String key = "plugin" + (index/10)%10 + index%10; + String value = (String)en.nextElement(); + prefs.put(key, value); + index++; + } + int n = openRecentMenu.getItemCount(); + for (int i=0; i"); + if (index==-1 || index==menuPath.length()-1) + return; + String label = menuPath.substring(index+1, menuPath.length()); + menuPath = menuPath.substring(0, index); + pluginsTable.put(label, plugin); + addItem(getMenu(menuPath), label, 0, false); + } + +} diff --git a/src/ij/OtherInstance.java b/src/ij/OtherInstance.java new file mode 100644 index 0000000..17ba5ab --- /dev/null +++ b/src/ij/OtherInstance.java @@ -0,0 +1,241 @@ +package ij; + +import ij.IJ; +import ij.ImageJ; +import ij.Prefs; + +import ij.io.OpenDialog; +import ij.io.Opener; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import java.lang.reflect.Method; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; + +import java.util.Properties; + +/* + * This class tries to contact another instance on the same machine, started + * by the current user. If such an instance is found, the arguments are + * sent to that instance. If no such an instance is found, listen for clients. + * + * No need for extra security, as the stub (and its serialization) contain + * a hard-to-guess hash code. + * + * @author Johannes Schindelin + */ +public class OtherInstance { + private static final String DELIMETER = "~!~"; // Separates macro name and argument + + interface ImageJInstance extends Remote { + void sendArgument(String arg) throws RemoteException; + } + + static class Implementation implements ImageJInstance { + int counter = 0; + + public void sendArgument(String cmd) { + if (IJ.debugMode) IJ.log("SocketServer.sendArgument: \""+ cmd+"\""); + if (cmd.startsWith("open ")) + (new Opener()).openAndAddToRecent(cmd.substring(5)); + else if (cmd.startsWith("macro ")) { + String name = cmd.substring(6); + String name2 = name; + String arg = null; + int index = name2.indexOf(DELIMETER); + if (index!=-1) { + name = name2.substring(0, index); + arg = name2.substring(index+DELIMETER.length(), name2.length()); + } + IJ.runMacroFile(name, arg); + } else if (cmd.startsWith("run ")) + IJ.run(cmd.substring(4)); + else if (cmd.startsWith("eval ")) { + String rtn = IJ.runMacro(cmd.substring(5)); + if (rtn!=null) + System.out.print(rtn); + } else if (cmd.startsWith("user.dir ")) + OpenDialog.setDefaultDirectory(cmd.substring(9)); + } + } + + public static String getStubPath() { + String display = System.getenv("DISPLAY"); + if (display!=null) { + display = display.replace(':', '_'); + display = display.replace('/', '_'); + } + String tmpDir = System.getProperty("java.io.tmpdir"); + tmpDir = IJ.addSeparator(tmpDir); + return tmpDir + "ImageJ-" + + System.getProperty("user.name") + "-" + + (display == null ? "" : display + "-") + + ImageJ.getPort() + ".stub"; + } + + public static void makeFilePrivate(String path) { + try { + File file = new File(path); + file.deleteOnExit(); + + // File.setReadable() is Java 6 + Class[] types = { boolean.class, boolean.class }; + Method m = File.class.getMethod("setReadable", types); + Object[] arguments = { Boolean.FALSE, Boolean.FALSE }; + m.invoke(file, arguments); + arguments = new Object[] { Boolean.TRUE, Boolean.TRUE }; + m.invoke(file, arguments); + types = new Class[] { boolean.class }; + m = File.class.getMethod("setWritable", types); + arguments = new Object[] { Boolean.FALSE }; + m.invoke(file, arguments); + return; + } catch (Exception e) { + if (IJ.debugMode) + System.err.println("Java < 6 detected," + + " trying chmod 0600 " + path); + } + if (!IJ.isWindows()) { + try { + String[] command = { + "chmod", "0600", path + }; + Runtime.getRuntime().exec(command); + } catch (Exception e) { + if (IJ.debugMode) + System.err.println("Even chmod failed."); + } + } + } + + public static boolean sendArguments(String[] args) { + if (!isRMIEnabled()) + return false; + String file = getStubPath(); + try { + FileInputStream in = new FileInputStream(file); + ImageJInstance instance = (ImageJInstance) new ObjectInputStream(in).readObject(); + in.close(); + if (instance==null) + return false; + instance.sendArgument("user.dir "+System.getProperty("user.dir")); + int macros = 0; + for (int i=0; itext in the preferences + * file using the keyword key. The string can be + * retrieved using the appropriate get() method. + * @see #get(String,String) + */ + public static void set(String key, String text) { + if (key.indexOf('.')<1) + throw new IllegalArgumentException("Key must have a prefix"); + if (text==null) + ijPrefs.remove(KEY_PREFIX+key); + else + ijPrefs.put(KEY_PREFIX+key, text); + } + + /** Saves the value of the integer value in the preferences + * file using the keyword key. The value can be + * retrieved using the appropriate get() method. + * @see #get(String,double) + */ + public static void set(String key, int value) { + set(key, Integer.toString(value)); + } + + /** Saves the value of the double value in the preferences + * file using the keyword key. The value can be + * retrieved using the appropriate get() method. + * @see #get(String,double) + */ + public static void set(String key, double value) { + set(key, ""+value); + } + + /** Saves the value of the boolean value in the preferences + * file using the keyword key. The value can be + * retrieved using the appropriate get() method. + * @see #get(String,boolean) + */ + public static void set(String key, boolean value) { + set(key, ""+value); + } + + /** Uses the keyword key to retrieve a string from the + preferences file. Returns defaultValue if the key + is not found. */ + public static String get(String key, String defaultValue) { + String value = ijPrefs.getProperty(KEY_PREFIX+key); + if (value == null) + return defaultValue; + else + return value; + } + + /** Uses the keyword key to retrieve a number from the + preferences file. Returns defaultValue if the key + is not found. */ + public static double get(String key, double defaultValue) { + String s = ijPrefs.getProperty(KEY_PREFIX+key); + Double d = null; + if (s!=null) { + try {d = new Double(s);} + catch (NumberFormatException e) {d = null;} + if (d!=null) + return(d.doubleValue()); + } + return defaultValue; + } + + /** Uses the keyword key to retrieve a boolean from + the preferences file. Returns defaultValue if + the key is not found. */ + public static boolean get(String key, boolean defaultValue) { + String value = ijPrefs.getProperty(KEY_PREFIX+key); + if (value==null) + return defaultValue; + else + return value.equals("true"); + } + + /** Finds and loads the configuration file ("IJ_Props.txt") + * and the preferences file ("IJ_Prefs.txt"). + * @return an error message if "IJ_Props.txt" not found. + */ + public static String load(Object ij, Applet applet) { + if (ImageJDir==null) + ImageJDir = System.getProperty("user.dir"); + if (ij!=null) { + InputStream f = null; + try { // Look for IJ_Props.txt in ImageJ folder + f = new FileInputStream(ImageJDir+"/"+PROPS_NAME); + propertiesPath = ImageJDir+"/"+PROPS_NAME; + } catch (FileNotFoundException e) { + f = null; + } + if (f==null) { + // Look in ij.jar if not found in ImageJ folder + f = ij.getClass().getResourceAsStream("/"+PROPS_NAME); + } + if (applet!=null) + return loadAppletProps(f, applet); + if (f==null) + return PROPS_NAME+" not found in ij.jar or in "+ImageJDir; + f = new BufferedInputStream(f); + try { + props.load(f); + f.close(); + } catch (IOException e) { + return("Error loading "+PROPS_NAME); + } + imagesURL = props.getProperty(IJ.isJava18()?"images.location":"images.location2"); + } + loadPreferences(); + loadOptions(); + guiScale = get(GUI_SCALE, 1.0); + return null; + } + + /* + static void dumpPrefs() { + System.out.println(""); + Enumeration e = ijPrefs.keys(); + while (e.hasMoreElements()) { + String key = (String) e.nextElement(); + System.out.println(key+": "+ijPrefs.getProperty(key)); + } + } + */ + + static String loadAppletProps(InputStream f, Applet applet) { + if (f==null) + return PROPS_NAME+" not found in ij.jar"; + try { + props.load(f); + f.close(); + } + catch (IOException e) {return("Error loading "+PROPS_NAME);} + try { + URL url = new URL(applet.getDocumentBase(), "images/"); + imagesURL = url.toString(); + } + catch (Exception e) {} + return null; + } + + /** Returns the URL of the directory that contains the ImageJ sample images. */ + public static String getImagesURL() { + return imagesURL; + } + + /** Sets the URL of the directory that contains the ImageJ sample images. */ + public static void setImagesURL(String url) { + imagesURL = url; + } + + /** Obsolete, replaced by getImageJDir(), which, unlike this method, + returns a path that ends with File.separator. */ + public static String getHomeDir() { + return ImageJDir; + } + + /** Returns the path, ending in File.separator, to the ImageJ directory. */ + public static String getImageJDir() { + String path = Menus.getImageJPath(); + if (path==null) + return ImageJDir + File.separator; + else + return path; + } + + /** Returns the path to the directory where the + preferences file (IJPrefs.txt) is saved. */ + public static String getPrefsDir() { + if (prefsDir==null) { + if (ImageJDir==null) + ImageJDir = System.getProperty("user.dir"); + File f = new File(ImageJDir+File.separator+PREFS_NAME); + if (f.exists()) { + prefsDir = ImageJDir; + preferencesPath = ImageJDir+"/"+PREFS_NAME; + } + //System.out.println("getPrefsDir: "+f+" "+prefsDir); + if (prefsDir==null) { + String dir = System.getProperty("user.home"); + if (IJ.isMacOSX()) + dir += "/Library/Preferences"; + else + dir += File.separator+".imagej"; + prefsDir = dir; + } + } + return prefsDir; + } + + /** Sets the path to the ImageJ directory. */ + static void setHomeDir(String path) { + if (path.endsWith(File.separator)) + path = path.substring(0, path.length()-1); + ImageJDir = path; + } + + /** Returns the default directory, if any, or null. */ + public static String getDefaultDirectory() { + if (commandLineMacro) + return null; + else + return getString(DIR_IMAGE); + } + + /** Returns the file.separator system property. */ + public static String getFileSeparator() { + return separator; + } + + /** Opens the ImageJ preferences file ("IJ_Prefs.txt") file. */ + static void loadPreferences() { + String path = getPrefsDir()+separator+PREFS_NAME; + boolean ok = loadPrefs(path); + if (!ok) { // not found + if (IJ.isWindows()) + path = ImageJDir +separator+PREFS_NAME; + else + path = System.getProperty("user.home")+separator+PREFS_NAME; //User's home dir + ok = loadPrefs(path); + if (ok) + new File(path).delete(); + } + + } + + static boolean loadPrefs(String path) { + try { + InputStream is = new BufferedInputStream(new FileInputStream(path)); + ijPrefs.load(is); + is.close(); + return true; + } catch (Exception e) { + return false; + } + } + + /** Saves user preferences in the IJ_Prefs.txt properties file. */ + public static void savePreferences() { + String path = null; + commandLineMacro = false; + try { + Properties prefs = new Properties(); + String dir = OpenDialog.getDefaultDirectory(); + if (dir!=null) + prefs.put(DIR_IMAGE, dir); + prefs.put(ROICOLOR, Tools.c2hex(Roi.getColor())); + prefs.put(SHOW_ALL_COLOR, Tools.c2hex(ImageCanvas.getShowAllColor())); + prefs.put(FCOLOR, Tools.c2hex(Toolbar.getForegroundColor())); + prefs.put(BCOLOR, Tools.c2hex(Toolbar.getBackgroundColor())); + prefs.put(JPEG, Integer.toString(FileSaver.getJpegQuality())); + prefs.put(FPS, Double.toString(Animator.getFrameRate())); + prefs.put(DIV_BY_ZERO_VALUE, Double.toString(FloatBlitter.divideByZeroValue)); + prefs.put(NOISE_SD, Double.toString(Filters.getSD())); + if (threads>1) prefs.put(THREADS, Integer.toString(threads)); + if (IJ.isMacOSX()) useJFileChooser = false; + if (!IJ.isLinux()) dialogCancelButtonOnRight = false; + saveOptions(prefs); + savePluginPrefs(prefs); + ImageJ ij = IJ.getInstance(); + if (ij!=null) + ij.savePreferences(prefs); + Menus.savePreferences(prefs); + ParticleAnalyzer.savePreferences(prefs); + Analyzer.savePreferences(prefs); + ImportDialog.savePreferences(prefs); + PlotWindow.savePreferences(prefs); + NewImage.savePreferences(prefs); + String prefsDir = getPrefsDir(); + path = prefsDir+separator+PREFS_NAME; + if (prefsDir.endsWith(".imagej")) { + File f = new File(prefsDir); + if (!f.exists()) f.mkdir(); // create .imagej directory + } + if (resetPreferences) { + File f = new File(path); + if (!f.exists()) + IJ.error("Edit>Options>Reset", "Unable to reset preferences. File not found at\n"+path); + boolean rtn = f.delete(); + resetPreferences = false; + } else + savePrefs(prefs, path); + } catch (Throwable t) { + String msg = t.getMessage(); + if (msg==null) msg = ""+t; + int delay = 4000; + try { + new TextWindow("Error Saving Preferences:\n"+path, msg, 500, 200); + IJ.wait(delay); + } catch (Throwable t2) {} + } + } + + /** Delete the preferences file when ImageJ quits. */ + public static void resetPreferences() { + resetPreferences = true; + } + + static void loadOptions() { + int defaultOptions = ANTIALIASING+AVOID_RESLICE_INTERPOLATION+ANTIALIASED_TOOLS+MULTI_POINT_MODE + +(!IJ.isMacOSX()?RUN_SOCKET_LISTENER:0)+BLACK_BACKGROUND; + int options = getInt(OPTIONS, defaultOptions); + usePointerCursor = (options&USE_POINTER)!=0; + //antialiasedText = (options&ANTIALIASING)!=0; + antialiasedText = false; + interpolateScaledImages = (options&INTERPOLATE)!=0; + open100Percent = (options&ONE_HUNDRED_PERCENT)!=0; + blackBackground = (options&BLACK_BACKGROUND)!=0; + useJFileChooser = (options&JFILE_CHOOSER)!=0; + weightedColor = (options&WEIGHTED)!=0; + if (weightedColor) + ColorProcessor.setWeightingFactors(0.299, 0.587, 0.114); + blackCanvas = (options&BLACK_CANVAS)!=0; + requireControlKey = (options&REQUIRE_CONTROL)!=0; + useInvertingLut = (options&USE_INVERTING_LUT)!=0; + intelByteOrder = (options&INTEL_BYTE_ORDER)!=0; + noBorder = (options&NO_BORDER)!=0; + showAllSliceOnly = (options&SHOW_ALL_SLICE_ONLY)!=0; + copyColumnHeaders = (options©_HEADERS)!=0; + noRowNumbers = (options&NO_ROW_NUMBERS)!=0; + moveToMisc = (options&MOVE_TO_MISC)!=0; + runSocketListener = (options&RUN_SOCKET_LISTENER)!=0; + multiPointMode = (options&MULTI_POINT_MODE)!=0; + rotateYZ = (options&ROTATE_YZ)!=0; + flipXZ = (options&FLIP_XZ)!=0; + //dontSaveHeaders = (options&DONT_SAVE_HEADERS)!=0; + //dontSaveRowNumbers = (options&DONT_SAVE_ROW_NUMBERS)!=0; + noClickToGC = (options&NO_CLICK_TO_GC)!=0; + avoidResliceInterpolation = (options&AVOID_RESLICE_INTERPOLATION)!=0; + keepUndoBuffers = (options&KEEP_UNDO_BUFFERS)!=0; + + defaultOptions = (!IJ.isMacOSX()?USE_FILE_CHOOSER:0); + int options2 = getInt(OPTIONS2, defaultOptions); + useSystemProxies = (options2&USE_SYSTEM_PROXIES)!=0; + useFileChooser = (options2&USE_FILE_CHOOSER)!=0; + subPixelResolution = (options2&SUBPIXEL_RESOLUTION)!=0; + enhancedLineTool = (options2&ENHANCED_LINE_TOOL)!=0; + skipRawDialog = (options2&SKIP_RAW_DIALOG)!=0; + reverseNextPreviousOrder = (options2&REVERSE_NEXT_PREVIOUS_ORDER)!=0; + autoRunExamples = (options2&AUTO_RUN_EXAMPLES)!=0; + showAllPoints = (options2&SHOW_ALL_POINTS)!=0; + doNotSaveWindowLocations = (options2&DO_NOT_SAVE_WINDOW_LOCS)!=0; + jFileChooserSettingChanged = (options2&JFILE_CHOOSER_CHANGED)!=0; + dialogCancelButtonOnRight = (options2&CANCEL_BUTTON_ON_RIGHT)!=0; + ignoreRescaleSlope = (options2&IGNORE_RESCALE_SLOPE)!=0; + nonBlockingFilterDialogs = (options2&NON_BLOCKING_DIALOGS)!=0; + fixedDicomScaling = (options2&FIXED_DICOM_SCALINGg)!=0; + } + + static void saveOptions(Properties prefs) { + int options = (usePointerCursor?USE_POINTER:0) + (antialiasedText?ANTIALIASING:0) + + (interpolateScaledImages?INTERPOLATE:0) + (open100Percent?ONE_HUNDRED_PERCENT:0) + + (blackBackground?BLACK_BACKGROUND:0) + (useJFileChooser?JFILE_CHOOSER:0) + + (blackCanvas?BLACK_CANVAS:0) + (weightedColor?WEIGHTED:0) + + (requireControlKey?REQUIRE_CONTROL:0) + + (useInvertingLut?USE_INVERTING_LUT:0) + + (intelByteOrder?INTEL_BYTE_ORDER:0) + (doubleBuffer?DOUBLE_BUFFER:0) + + (noPointLabels?NO_POINT_LABELS:0) + (noBorder?NO_BORDER:0) + + (showAllSliceOnly?SHOW_ALL_SLICE_ONLY:0) + (copyColumnHeaders?COPY_HEADERS:0) + + (noRowNumbers?NO_ROW_NUMBERS:0) + (moveToMisc?MOVE_TO_MISC:0) + + (runSocketListener?RUN_SOCKET_LISTENER:0) + + (multiPointMode?MULTI_POINT_MODE:0) + (rotateYZ?ROTATE_YZ:0) + + (flipXZ?FLIP_XZ:0) + (dontSaveHeaders?DONT_SAVE_HEADERS:0) + + (dontSaveRowNumbers?DONT_SAVE_ROW_NUMBERS:0) + (noClickToGC?NO_CLICK_TO_GC:0) + + (avoidResliceInterpolation?AVOID_RESLICE_INTERPOLATION:0) + + (keepUndoBuffers?KEEP_UNDO_BUFFERS:0); + prefs.put(OPTIONS, Integer.toString(options)); + + int options2 = (useSystemProxies?USE_SYSTEM_PROXIES:0) + + (useFileChooser?USE_FILE_CHOOSER:0) + (subPixelResolution?SUBPIXEL_RESOLUTION:0) + + (enhancedLineTool?ENHANCED_LINE_TOOL:0) + (skipRawDialog?SKIP_RAW_DIALOG:0) + + (reverseNextPreviousOrder?REVERSE_NEXT_PREVIOUS_ORDER:0) + + (autoRunExamples?AUTO_RUN_EXAMPLES:0) + (showAllPoints?SHOW_ALL_POINTS:0) + + (doNotSaveWindowLocations?DO_NOT_SAVE_WINDOW_LOCS:0) + + (jFileChooserSettingChanged?JFILE_CHOOSER_CHANGED:0) + + (dialogCancelButtonOnRight?CANCEL_BUTTON_ON_RIGHT:0) + + (ignoreRescaleSlope?IGNORE_RESCALE_SLOPE:0) + + (nonBlockingFilterDialogs?NON_BLOCKING_DIALOGS:0) + + (fixedDicomScaling?FIXED_DICOM_SCALINGg:0); + prefs.put(OPTIONS2, Integer.toString(options2)); + } + + /** Saves the Point loc in the preferences + file as a string using the keyword key. */ + public static void saveLocation(String key, Point loc) { + if (!doNotSaveWindowLocations) + set(key, loc!=null?loc.x+","+loc.y:null); + } + + /** Uses the keyword key to retrieve a location + from the preferences file. Returns null if the + key is not found or the location is not valid (e.g., offscreen). */ + public static Point getLocation(String key) { + String value = ijPrefs.getProperty(KEY_PREFIX+key); + if (value==null) return null; + int index = value.indexOf(","); + if (index==-1) return null; + double xloc = Tools.parseDouble(value.substring(0, index)); + if (Double.isNaN(xloc) || index==value.length()-1) return null; + double yloc = Tools.parseDouble(value.substring(index+1)); + if (Double.isNaN(yloc)) return null; + Point p = new Point((int)xloc, (int)yloc); + Rectangle bounds = GUI.getScreenBounds(p); // get bounds of screen that contains p + if (bounds!=null && p.x+100<=bounds.x+bounds.width && p.y+ 40<=bounds.y+bounds.height) { + if (locKeys.get(key)==null) { // first time for this key? + locKeys.setProperty(key, ""); + Rectangle primaryScreen = GUI.getMaxWindowBounds(); + ImageJ ij = IJ.getInstance(); + Point ijLoc = ij!=null?ij.getLocation():null; + //System.out.println("getLoc: "+key+" "+(ijLoc!=null&&primaryScreen.contains(ijLoc)) + " "+!primaryScreen.contains(p)); + if ((ijLoc!=null&&primaryScreen.contains(ijLoc)) && !primaryScreen.contains(p)) + return null; // return null if "ImageJ" window on primary screen and this location is not + } + return p; + } else + return null; + } + + /** Save plugin preferences. */ + static void savePluginPrefs(Properties prefs) { + Enumeration e = ijPrefs.keys(); + while (e.hasMoreElements()) { + String key = (String) e.nextElement(); + if (key.indexOf(KEY_PREFIX) == 0) + prefs.put(key, ijPrefs.getProperty(key)); + } + } + + public static void savePrefs(Properties prefs, String path) throws IOException{ + FileOutputStream fos = new FileOutputStream(path); + BufferedOutputStream bos = new BufferedOutputStream(fos); + prefs.store(bos, "ImageJ "+ImageJ.VERSION+" Preferences"); + bos.close(); + } + + /** Returns the number of threads used by PlugInFilters to process images and stacks. */ + public static int getThreads() { + if (threads==0) { + threads = getInt(THREADS, 0); + int processors = Runtime.getRuntime().availableProcessors(); + if (threads<1 || threads>processors) + threads = processors; + } + return threads; + } + + /** Sets the number of threads (1-32) used by PlugInFilters to process stacks. */ + public static void setThreads(int n) { + if (n<1) n = 1; + threads = n; + } + + /** Sets the transparent index (0-255), or set to -1 to disable transparency. */ + public static void setTransparentIndex(int index) { + if (index<-1 || index>255) index = -1; + transparentIndex = index; + } + + /** Returns the transparent index (0-255), or -1 if transparency is disabled. */ + public static int getTransparentIndex() { + return transparentIndex; + } + + public static Properties getControlPanelProperties() { + return ijPrefs; + } + + public static String defaultResultsExtension() { + return get("options.ext", ".csv"); + } + + /** Sets the GenericDialog and Command Finder text scale (0.5 to 3.0). */ + public static void setGuiScale(double scale) { + if (scale>=0.5 && scale<=3.0) { + guiScale = scale; + set(GUI_SCALE, guiScale); + Roi.resetDefaultHandleSize(); + } + } + + /** Returns the GenericDialog and Command Finder text scale. */ + public static double getGuiScale() { + return guiScale; + } + + /** Returns the custom properties (IJ_Props.txt) file path. */ + public static String getCustomPropsPath() { + return propertiesPath; + } + + /** Returns the custom preferences (IJ_Prefs.txt) file path. */ + public static String getCustomPrefsPath() { + return preferencesPath; + } + + /** Retrieves a string from IJ_Props or IJ_Prefs.txt. + Does not retrieve strings set using Prefs.set(). */ + public static String getString(String key, String defaultString) { + if (props==null) + return defaultString; + String s = props.getProperty(key); + if (s==null) + return defaultString; + else + return s; + } + + /** Retrieves a string from string in IJ_Props or IJ_Prefs.txt. */ + public static String getString(String key) { + return props.getProperty(key); + } + + /** Retrieves a number from IJ_Props or IJ_Prefs.txt. + Does not retrieve numbers set using Prefs.set(). */ + public static int getInt(String key, int defaultValue) { + if (props==null) //workaround for Netscape JIT bug + return defaultValue; + String s = props.getProperty(key); + if (s!=null) { + try { + return Integer.decode(s).intValue(); + } catch (NumberFormatException e) {IJ.log(""+e);} + } + return defaultValue; + } + + /** Retrieves a number from IJ_Props or IJ_Prefs.txt. + Does not retrieve numbers set using Prefs.set(). */ + public static double getDouble(String key, double defaultValue) { + if (props==null) + return defaultValue; + String s = props.getProperty(key); + Double d = null; + if (s!=null) { + try {d = new Double(s);} + catch (NumberFormatException e){d = null;} + if (d!=null) + return(d.doubleValue()); + } + return defaultValue; + } + + /** Retrieves a boolean from IJ_Props or IJ_Prefs.txt. + Does not retrieve boolean set using Prefs.set(). */ + public static boolean getBoolean(String key, boolean defaultValue) { + if (props==null) return defaultValue; + String s = props.getProperty(key); + if (s==null) + return defaultValue; + else + return s.equals("true"); + } + + /** Finds a color in IJ_Props or IJ_Prefs.txt. */ + public static Color getColor(String key, Color defaultColor) { + int i = getInt(key, 0xaaa); + if (i == 0xaaa) + return defaultColor; + return new Color((i >> 16) & 0xFF, (i >> 8) & 0xFF, i & 0xFF); + } + + public static boolean commandLineMacro() { + return commandLineMacro; + } + +} + diff --git a/src/ij/RecentOpener.java b/src/ij/RecentOpener.java new file mode 100644 index 0000000..8f04162 --- /dev/null +++ b/src/ij/RecentOpener.java @@ -0,0 +1,37 @@ +package ij; +import ij.io.*; +import java.awt.*; +import java.io.*; + +/** Opens, in a separate thread, files selected from the File/Open Recent submenu.*/ +public class RecentOpener implements Runnable { + private String path; + + RecentOpener(String path) { + this.path = path; + Thread thread = new Thread(this, "RecentOpener"); + thread.start(); + } + + /** Open the file and move the path to top of the submenu. */ + public void run() { + Opener o = new Opener(); + o.open(path); + Menu menu = Menus.getOpenRecentMenu(); + int n = menu.getItemCount(); + int index = 0; + for (int i=0; i0) { + MenuItem item = menu.getItem(index); + menu.remove(index); + menu.insert(item, 0); + } + } + +} + diff --git a/src/ij/Undo.java b/src/ij/Undo.java new file mode 100644 index 0000000..5bca876 --- /dev/null +++ b/src/ij/Undo.java @@ -0,0 +1,215 @@ +/**Implements the Edit/Undo command.*/ + +package ij; +import ij.process.*; +import ij.gui.*; +import ij.measure.Calibration; +import java.awt.*; +import java.awt.image.*; + +/** This class consists of static methods and + fields that implement ImageJ's Undo command. */ +public class Undo { + + public static final int NOTHING = 0; + /** Undo using ImageProcessor.snapshot. */ + public static final int FILTER = 1; + /** Undo using an ImageProcessor copy. */ + public static final int TYPE_CONVERSION = 2; + public static final int PASTE = 3; + public static final int COMPOUND_FILTER = 4; + public static final int COMPOUND_FILTER_DONE = 5; + /** Undo using a single image, or composite color stack, copy (limited to 200MB). */ + public static final int TRANSFORM = 6; + public static final int OVERLAY_ADDITION = 7; + public static final int ROI = 8; + public static final int MACRO = 9; + + private static int whatToUndo = NOTHING; + private static int imageID; + private static ImageProcessor ipCopy = null; + private static ImagePlus impCopy; + private static Calibration calCopy; + private static Roi roiCopy; + private static double displayRangeMin, displayRangeMax; + private static LUT lutCopy; + private static Overlay overlayCopy; + + public static void setup(int what, ImagePlus imp) { + if (imp==null) { + whatToUndo = NOTHING; + reset(); + return; + } + if (IJ.debugMode) IJ.log("Undo.setup: "+what+" "+imp); + if (what==FILTER && whatToUndo==COMPOUND_FILTER) + return; + if (what==COMPOUND_FILTER_DONE) { + if (whatToUndo==COMPOUND_FILTER) + whatToUndo = what; + return; + } + whatToUndo = what; + imageID = imp.getID(); + if (what==TYPE_CONVERSION) { + ipCopy = imp.getProcessor(); + calCopy = (Calibration)imp.getCalibration().clone(); + } else if (what==TRANSFORM) { + if ((!IJ.macroRunning()||Prefs.supportMacroUndo) && (imp.getStackSize()==1||imp.getDisplayMode()==IJ.COMPOSITE) && imp.getSizeInBytes()<209715200) + impCopy = imp.duplicate(); + else + reset(); + } else if (what==MACRO) { + ipCopy = imp.getProcessor().duplicate(); + calCopy = (Calibration)imp.getCalibration().clone(); + impCopy = null; + } else if (what==COMPOUND_FILTER) { + ImageProcessor ip = imp.getProcessor(); + if (ip!=null) + ipCopy = ip.duplicate(); + else + ipCopy = null; + } else if (what==OVERLAY_ADDITION) { + impCopy = null; + ipCopy = null; + } else if (what==ROI) { + impCopy = null; + ipCopy = null; + Roi roi = imp.getRoi(); + if (roi!=null) { + roiCopy = (Roi)roi.clone(); + roiCopy.setImage(null); + } else + whatToUndo = NOTHING; + } else { + ipCopy = null; + ImageProcessor ip = imp.getProcessor(); + //lutCopy = (LUT)ip.getLut().clone(); + } + } + + public static void saveOverlay(ImagePlus imp) { + Overlay overlay = imp!=null?imp.getOverlay():null; + if (overlay!=null) + overlayCopy = overlay.duplicate(); + else + overlayCopy = null; + } + + public static void reset() { + if (IJ.debugMode) IJ.log("Undo.reset: "+ whatToUndo+" "+impCopy); + if (whatToUndo==COMPOUND_FILTER || whatToUndo==OVERLAY_ADDITION) + return; + whatToUndo = NOTHING; + imageID = 0; + ipCopy = null; + impCopy = null; + calCopy = null; + roiCopy = null; + lutCopy = null; + overlayCopy = null; + } + + public static void undo() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (IJ.debugMode) IJ.log("Undo.undo: "+ whatToUndo+" "+imp+" "+impCopy); + if (imp==null || imageID!=imp.getID()) { + if (imp!=null && !IJ.macroRunning()) { // does image still have an undo buffer? + ImageProcessor ip2 = imp.getProcessor(); + ip2.swapPixelArrays(); + imp.updateAndDraw(); + } else + reset(); + return; + } + switch (whatToUndo) { + case FILTER: + if (overlayCopy!=null) { + Overlay overlay = imp.getOverlay(); + if (overlay!=null) { + imp.setOverlay(overlayCopy); + overlayCopy = overlay.duplicate(); + } + } + ImageProcessor ip = imp.getProcessor(); + if (ip!=null) { + if (!IJ.macroRunning()) { + ip.swapPixelArrays(); + imp.updateAndDraw(); + return; // don't reset + } else { + ip.reset(); + imp.updateAndDraw(); + } + } + break; + case TYPE_CONVERSION: + case COMPOUND_FILTER: + case COMPOUND_FILTER_DONE: + if (ipCopy!=null) { + if (whatToUndo==TYPE_CONVERSION && calCopy!=null) + imp.setCalibration(calCopy); + if (swapImages(new ImagePlus("",ipCopy), imp)) { + imp.updateAndDraw(); + return; + } else + imp.setProcessor(null, ipCopy); + } + break; + case TRANSFORM: + if (impCopy!=null) + imp.setStack(impCopy.getStack()); + break; + case PASTE: + Roi roi = imp.getRoi(); + if (roi!=null) + roi.abortPaste(); + break; + case ROI: + Roi roiCopy2 = roiCopy; + setup(ROI, imp); // setup redo + imp.setRoi(roiCopy2); + return; //don't reset + case MACRO: + if (ipCopy!=null) { + imp.setProcessor(ipCopy); + if (calCopy!=null) imp.setCalibration(calCopy); + } + break; + case OVERLAY_ADDITION: + Overlay overlay = imp.getOverlay(); + if (overlay==null) + {IJ.beep(); return;} + int size = overlay.size(); + if (size>0) { + overlay.remove(size-1); + imp.draw(); + } else { + IJ.beep(); + return; + } + return; //don't reset + } + reset(); + } + + static boolean swapImages(ImagePlus imp1, ImagePlus imp2) { + if (imp1.getWidth()!=imp2.getWidth() || imp1.getHeight()!=imp2.getHeight() + || imp1.getBitDepth()!=imp2.getBitDepth() || IJ.macroRunning()) + return false; + ImageProcessor ip1 = imp1.getProcessor(); + ImageProcessor ip2 = imp2.getProcessor(); + double min1 = ip1.getMin(); + double max1 = ip1.getMax(); + double min2 = ip2.getMin(); + double max2 = ip2.getMax(); + ip2.setSnapshotPixels(ip1.getPixels()); + ip2.swapPixelArrays(); + ip1.setPixels(ip2.getSnapshotPixels()); + ip2.setSnapshotPixels(null); + ip1.setMinAndMax(min2, max2); + ip2.setMinAndMax(min1, max1); + return true; + } + +} diff --git a/src/ij/VirtualStack.java b/src/ij/VirtualStack.java new file mode 100644 index 0000000..658badf --- /dev/null +++ b/src/ij/VirtualStack.java @@ -0,0 +1,335 @@ +package ij; +import ij.process.*; +import ij.io.*; +import ij.gui.ImageCanvas; +import ij.util.Tools; +import ij.plugin.FolderOpener; +import java.io.*; +import java.awt.*; +import java.awt.image.ColorModel; +import java.util.Properties; + +/** This class represents an array of disk-resident images. */ +public class VirtualStack extends ImageStack { + private static final int INITIAL_SIZE = 100; + private String path; + private int nSlices; + private String[] names; + private String[] labels; + private int bitDepth; + private int delay; + private Properties properties; + private boolean generateData; + private int[] indexes; // used to translate non-CZT hyperstack slice numbers + + + /** Default constructor. */ + public VirtualStack() { } + + public VirtualStack(int width, int height) { + super(width, height); + } + + /** Creates an empty virtual stack. + * @param width image width + * @param height image height + * @param cm ColorModel or null + * @param path file path of directory containing the images + * @see #addSlice(String) + * @see
OpenAsVirtualStack.js + */ + public VirtualStack(int width, int height, ColorModel cm, String path) { + super(width, height, cm); + path = IJ.addSeparator(path); + this.path = path; + names = new String[INITIAL_SIZE]; + labels = new String[INITIAL_SIZE]; + } + + /** Creates a virtual stack with no backing storage.
+ * See: Help>Examples>JavaScript>Terabyte VirtualStack + */ + public VirtualStack(int width, int height, int slices) { + this(width, height, slices, "8-bit"); + } + + /** Creates a virtual stack with no backing storage.
+ * See: Help>Examples>JavaScript>Terabyte VirtualStack + */ + public VirtualStack(int width, int height, int slices, String options) { + super(width, height, null); + nSlices = slices; + int depth = 8; + if (options.contains("16-bit")) depth=16; + if (options.contains("RGB")) depth=24; + if (options.contains("32-bit")) depth=32; + if (options.contains("delay")) delay=250; + this.generateData = options.contains("fill"); + this.bitDepth = depth; + } + + /** Adds an image to the end of a virtual stack created using the + * VirtualStack(w,h,cm,path) constructor. The argument + * can be a full file path (e.g., "C:/Users/wayne/dir1/image.tif") + * if the 'path' argument in the constructor is "". File names + * that start with '.' are ignored. + */ + public void addSlice(String fileName) { + if (fileName==null) + throw new IllegalArgumentException("'fileName' is null!"); + if (fileName.startsWith(".")) + return; + if (names==null) + throw new IllegalArgumentException("VirtualStack(w,h,cm,path) constructor not used"); + nSlices++; + if (nSlices==names.length) { + String[] tmp = new String[nSlices*2]; + System.arraycopy(names, 0, tmp, 0, nSlices); + names = tmp; + tmp = new String[nSlices*2]; + System.arraycopy(labels, 0, tmp, 0, nSlices); + labels = tmp; + } + names[nSlices-1] = fileName; + } + + /** Does nothing. */ + public void addSlice(String sliceLabel, Object pixels) { + } + + /** Does nothing.. */ + public void addSlice(String sliceLabel, ImageProcessor ip) { + } + + /** Does noting. */ + public void addSlice(String sliceLabel, ImageProcessor ip, int n) { + } + + /** Deletes the specified slice, were 1<=n<=nslices. */ + public void deleteSlice(int n) { + if (n<1 || n>nSlices) + throw new IllegalArgumentException("Argument out of range: "+n); + if (nSlices<1) + return; + for (int i=n; i0) + deleteSlice(n); + } + + /** Returns the pixel array for the specified slice, were 1<=n<=nslices. */ + public Object getPixels(int n) { + ImageProcessor ip = getProcessor(n); + if (ip!=null) + return ip.getPixels(); + else + return null; + } + + /** Assigns a pixel array to the specified slice, + were 1<=n<=nslices. */ + public void setPixels(Object pixels, int n) { + } + + /** Returns an ImageProcessor for the specified slice, + were 1<=n<=nslices. Returns null if the stack is empty. + */ + public ImageProcessor getProcessor(int n) { + if (path==null) { //Help>Examples?JavaScript>Terabyte VirtualStack + ImageProcessor ip = null; + int w=getWidth(), h=getHeight(); + switch (bitDepth) { + case 8: ip = new ByteProcessor(w,h); break; + case 16: ip = new ShortProcessor(w,h); break; + case 24: ip = new ColorProcessor(w,h); break; + case 32: ip = new FloatProcessor(w,h); break; + } + String hlabel = null; + if (generateData) { + int value = 0; + ImagePlus img = WindowManager.getCurrentImage(); + if (img!=null && img.getStackSize()==nSlices) + value = img.getCurrentSlice()-1; + if (bitDepth==16) + value *= 256; + if (bitDepth!=32) { + for (int i=0; i0) + IJ.wait(delay); + return ip; + } + n = translate(n); // update n for hyperstacks not in the default CZT order + Opener opener = new Opener(); + opener.setSilentMode(true); + IJ.redirectErrorMessages(true); + ImagePlus imp = opener.openTempImage(path, names[n-1]); + IJ.redirectErrorMessages(false); + ImageProcessor ip = null; + int depthThisImage = 0; + if (imp!=null) { + int w = imp.getWidth(); + int h = imp.getHeight(); + int type = imp.getType(); + ColorModel cm = imp.getProcessor().getColorModel(); + String info = (String)imp.getProperty("Info"); + if (info!=null) { + if (FolderOpener.useInfo(info)) + labels[n-1] = info; + } else { + String sliceLabel = imp.getStack().getSliceLabel(1); + if (FolderOpener.useInfo(sliceLabel)) + labels[n-1] = "Label: "+sliceLabel; + } + depthThisImage = imp.getBitDepth(); + ip = imp.getProcessor(); + ip.setOverlay(imp.getOverlay()); + properties = imp.getProperty("FHT")!=null?imp.getProperties():null; + } else { + File f = new File(path, names[n-1]); + String msg = f.exists()?"Error opening ":"File not found: "; + ip = new ByteProcessor(getWidth(), getHeight()); + ip.invert(); + label(ip, msg+names[n-1], Color.black); + depthThisImage = 8; + } + if (depthThisImage!=bitDepth) { + switch (bitDepth) { + case 8: ip=ip.convertToByte(true); break; + case 16: ip=ip.convertToShort(true); break; + case 24: ip=ip.convertToRGB(); break; + case 32: ip=ip.convertToFloat(); break; + } + } + if (ip.getWidth()!=getWidth() || ip.getHeight()!=getHeight()) { + ImageProcessor ip2 = ip.createProcessor(getWidth(), getHeight()); + ip2.insert(ip, 0, 0); + ip = ip2; + } + return ip; + } + + private void label(ImageProcessor ip, String msg, Color color) { + int size = getHeight()/20; + if (size<9) size=9; + Font font = new Font("Helvetica", Font.PLAIN, size); + ip.setFont(font); + ip.setAntialiasedText(true); + ip.setColor(color); + ip.drawString(msg, size, size*2); + } + + /** Currently not implemented */ + public int saveChanges(int n) { + return -1; + } + + /** Returns the number of slices in this stack. */ + public int size() { + return getSize(); + } + + public int getSize() { + return nSlices; + } + + /** Returns the label of the Nth image. */ + public String getSliceLabel(int n) { + if (labels==null) + return null; + String label = labels[n-1]; + if (label==null) + return names[n-1]; + else { + if (label.startsWith("Label: ")) // slice label + return label.substring(7,label.length()); + else + return names[n-1]+"\n"+label; + } + } + + /** Returns null. */ + public Object[] getImageArray() { + return null; + } + + /** Does nothing. */ + public void setSliceLabel(String label, int n) { + } + + /** Always return true. */ + public boolean isVirtual() { + return true; + } + + /** Does nothing. */ + public void trim() { + } + + /** Returns the path to the directory containing the images. */ + public String getDirectory() { + return IJ.addSeparator(path); + } + + /** Returns the file name of the specified slice, were 1<=n<=nslices. */ + public String getFileName(int n) { + return names[n-1]; + } + + /** Sets the bit depth (8, 16, 24 or 32). */ + public void setBitDepth(int bitDepth) { + this.bitDepth = bitDepth; + } + + /** Returns the bit depth (8, 16, 24 or 32), or 0 if the bit depth is not known. */ + public int getBitDepth() { + return bitDepth; + } + + public ImageStack sortDicom(String[] strings, String[] info, int maxDigits) { + int n = size(); + String[] names2 = new String[n]; + for (int i=0; i0) { + ImagePlus imp = getFocusManagerActiveImage(); + if (imp!=null) + return imp; + ImageWindow win = (ImageWindow)imageList.get(imageList.size()-1); + return win.getImagePlus(); + } else + return Interpreter.getLastBatchModeImage(); + } + + private static ImagePlus getFocusManagerActiveImage() { + if (IJ.isMacro()) + return null; + KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); + Window win = kfm.getActiveWindow(); + ImagePlus imp = null; + if (win!=null && (win instanceof ImageWindow)) + imp = ((ImageWindow)win).getImagePlus(); + return imp; + } + + /** Returns the number of open image windows. */ + public static int getWindowCount() { + int count = imageList.size(); + return count; + } + + /** Returns the number of open images. */ + public static int getImageCount() { + int count = imageList.size(); + count += Interpreter.getBatchModeImageCount(); + if (count==0 && getCurrentImage()!=null) + count = 1; + return count; + } + + /** Returns the front most window or null. */ + public static Window getActiveWindow() { + return frontWindow; + } + + /** Returns the Window containing the active table, or null. + * @see ij.measure.ResultsTable#getActiveTable + */ + public static Window getActiveTable() { + return frontTable; + } + + /** Obsolete; replaced by getActiveWindow. */ + public static Frame getFrontWindow() { + return frontFrame; + } + + /** Returns a list of the IDs of open images. Returns + null if no image windows are open. */ + public synchronized static int[] getIDList() { + int nWindows = imageList.size(); + int[] batchModeImages = Interpreter.getBatchModeImageIDs(); + int nBatchImages = batchModeImages.length; + if ((nWindows+nBatchImages)==0) + return null; + int[] list = new int[nWindows+nBatchImages]; + for (int i=0; i0) + imageID = getNthImageID(imageID); + if (imageID==0 || getImageCount()==0) + return null; + ImagePlus imp2 = Interpreter.getBatchModeImage(imageID); + if (imp2!=null) + return imp2; + ImagePlus imp = null; + for (int i=0; ilist.length) + return 0; + else + return list[n-1]; + } else { + if (n>imageList.size()) return 0; + ImageWindow win = (ImageWindow)imageList.get(n-1); + if (win!=null) + return win.getImagePlus().getID(); + else + return 0; + } + } + + + /** Returns the first image that has the specified title or null if it is not found. */ + public synchronized static ImagePlus getImage(String title) { + int[] wList = getIDList(); + if (wList==null) return null; + for (int i=0; i=0) { + Menus.removeWindowMenuItem(index); + nonImageList.removeElement(win); + } + if (win!=null && win==frontTable) + frontTable = null; + } + setWindow(null); + } + + /** Removes the specified Frame from the Window menu. */ + public static void removeWindow(Frame win) { + removeWindow((Window)win); + } + + private static void removeImageWindow(ImageWindow win) { + int index = imageList.indexOf(win); + if (index==-1) + return; // not on the window list + try { + synchronized(WindowManager.class) { + imageList.remove(win); + } + activations.remove(win); + if (imageList.size()>1 && !Prefs.closingAll) { + ImageWindow win2 = activations.size()>0?(ImageWindow)activations.get(activations.size()-1):null; + setCurrentWindow(win2); + } else + currentWindow = null; + setTempCurrentImage(null); //??? + int nonImageCount = nonImageList.size(); + if (nonImageCount>0) + nonImageCount++; + Menus.removeWindowMenuItem(nonImageCount+index); + Menus.updateMenus(); + Undo.reset(); + } catch (Exception e) { } + } + + /** The specified Window becomes the front window. */ + public static void setWindow(Window win) { + //System.out.println("setWindow(W): "+win); + frontWindow = win; + if (win instanceof Frame) + frontFrame = (Frame)win; + } + + /** The specified frame becomes the front window, the one returnd by getFrontWindow(). */ + public static void setWindow(Frame win) { + frontWindow = win; + frontFrame = win; + if (win!=null && win instanceof TextWindow && !(win instanceof Editor) && !"Log".equals(((TextWindow)win).getTitle())) + frontTable = win; + //System.out.println("Set window(F): "+(win!=null?win.getTitle():"null")); + } + + /** Closes all windows. Stops and returns false if an image or Editor "save changes" dialog is canceled. */ + public synchronized static boolean closeAllWindows() { + Prefs.closingAll = true; + while (imageList.size()>0) { + if (!((ImageWindow)imageList.get(0)).close()) { + Prefs.closingAll = false; + return false; + } + if (!quittingViaMacro()) + IJ.wait(100); + } + Prefs.closingAll = false; + Frame[] nonImages = getNonImageWindows(); + for (int i=0; i0) // remove image size (e.g., " 90K") + menuItemLabel = menuItemLabel.substring(0, lastSpace); + String idString = item.getActionCommand(); + int id = (int)Tools.parseDouble(idString, 0); + ImagePlus imp = WindowManager.getImage(id); + if (imp==null) return; + ImageWindow win1 = imp.getWindow(); + if (win1==null) return; + setCurrentWindow(win1); + toFront(win1); + int index = imageList.indexOf(win1); + int n = Menus.window.getItemCount(); + int start = Menus.WINDOW_MENU_ITEMS+Menus.windowMenuItems2; + for (int j=start; jHEADLESS) + defaultStyle = FILLED; + } + + public Arrow(double ox1, double oy1, double ox2, double oy2) { + super(ox1, oy1, ox2, oy2); + setStrokeWidth(2); + } + + public Arrow(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + setStrokeWidth(defaultWidth); + style = defaultStyle; + headSize = defaultHeadSize; + doubleHeaded = defaultDoubleHeaded; + outline = defaultOutline; + setStrokeColor(Toolbar.getForegroundColor()); + } + + /** Draws this arrow on the image. */ + public void draw(Graphics g) { + Shape shape2 = null; + if (doubleHeaded) { + flipEnds(); + shape2 = getShape(); + flipEnds(); + } + Shape shape = getShape(); + Color color = strokeColor!=null? strokeColor:ROIColor; + if (fillColor!=null) color = fillColor; + g.setColor(color); + Graphics2D g2 = (Graphics2D)g; + setRenderingHint(g2); + AffineTransform at = g2.getDeviceConfiguration().getDefaultTransform(); + double mag = getMagnification(); + int xbase=0, ybase=0; + if (ic!=null) { + Rectangle r = ic.getSrcRect(); + xbase = r.x; ybase = r.y; + } + at.setTransform(mag, 0.0, 0.0, mag, (-xbase+0.5)*mag, (-ybase+0.5)*mag); //0.5: int coordinate at pixel center + if (outline) { + float lineWidth = (float)(getOutlineWidth()*mag); + g2.setStroke(new BasicStroke(lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND)); + g2.draw(at.createTransformedShape(shape)); + if (doubleHeaded) g2.draw(at.createTransformedShape(shape2)); + g2.setStroke(defaultStroke); + } else { + g2.fill(at.createTransformedShape(shape)); + if (doubleHeaded) g2.fill(at.createTransformedShape(shape2)); + } + if (!overlay) { + handleColor=Color.white; + drawHandle(g, screenXD(x1d), screenYD(y1d)); + drawHandle(g, screenXD(x2d), screenYD(y2d)); + drawHandle(g, screenXD(x1d+(x2d-x1d)/2.0), screenYD(y1d+(y2d-y1d)/2.0)); + } + if (state!=NORMAL && imp!=null && imp.getRoi()!=null) + showStatus(); + if (updateFullWindow) + {updateFullWindow = false; imp.draw();} + } + + private void flipEnds() { + double tmp = x1R; + x1R=x2R; + x2R=tmp; + tmp=y1R; + y1R=y2R; + y2R=tmp; + } + + private Shape getPath() { + path.reset(); + path = new GeneralPath(); + calculatePoints(); + float tailx = points[0]; + float taily = points[1]; + float headbackx = points[2*1]; + float headbacky = points[2*1+1]; + float headtipx = points[2*3]; + float headtipy = points[2*3+1]; + if (outline) { + double dx = headtipx - tailx; + double dy = headtipy - taily; + double shaftLength = Math.sqrt(dx*dx+dy*dy); + dx = headtipx - headbackx; + dy = headtipy- headbacky; + double headLength = Math.sqrt(dx*dx+dy*dy); + headShaftRatio = headLength/shaftLength; + if (headShaftRatio>1.0) + headShaftRatio = 1.0; + //IJ.log(headShaftRatio+" "+(int)shaftLength+" "+(int)headLength+" "+(int)tailx+" "+(int)taily+" "+(int)headtipx+" "+(int)headtipy); + } + path.moveTo(tailx, taily); // tail + path.lineTo(headbackx, headbacky); // head back + path.moveTo(headbackx, headbacky); // head back + if (style==OPEN) + path.moveTo(points[2 * 2], points[2 * 2 + 1]); + else + path.lineTo(points[2 * 2], points[2 * 2 + 1]); // left point + path.lineTo(headtipx, headtipy); // head tip + path.lineTo(points[2 * 4], points[2 * 4 + 1]); // right point + path.lineTo(headbackx, headbacky); // back to the head back + return path; + } + + /** Based on the method with the same name in Fiji's Arrow plugin, + written by Jean-Yves Tinevez and Johannes Schindelin. */ + private void calculatePoints() { + double tip = 0.0; + double base; + double shaftWidth = getStrokeWidth(); + double length = 8+10*shaftWidth*0.5; + length = length*(headSize/10.0); + length -= shaftWidth*1.42; + if (style==NOTCHED) length*=0.74; + if (style==OPEN) length*=1.32; + if (length<0.0 || style==HEADLESS) length=0.0; + double x = getXBase(); + double y = getYBase(); + x1d=x+x1R; y1d=y+y1R; x2d=x+x2R; y2d=y+y2R; + x1=(int)x1d; y1=(int)y1d; x2=(int)x2d; y2=(int)y2d; + double dx=x2d-x1d, dy=y2d-y1d; + double arrowLength = Math.sqrt(dx*dx+dy*dy); + dx=dx/arrowLength; dy=dy/arrowLength; + if (doubleHeaded && style!=HEADLESS) { + points[0] = (float)(x1d+dx*shaftWidth*2.0); + points[1] = (float)(y1d+dy*shaftWidth*2.0); + } else { + points[0] = (float)x1d; + points[1] = (float)y1d; + } + if (length>0) { + double factor = style==OPEN?1.3:1.42; + points[2*3] = (float)(x2d-dx*shaftWidth*factor); + points[2*3+1] = (float)(y2d-dy*shaftWidth*factor); + if (style==BAR) { + points[2*3] = (float)(x2d-dx*shaftWidth*0.5); + points[2*3+1] = (float)(y2d-dy*shaftWidth*0.5); + } + } else { + points[2*3] = (float)x2d; + points[2*3+1] = (float)y2d; + } + final double alpha = Math.atan2(points[2*3+1]-points[1], points[2*3]-points[0]); + double SL = 0.0; + switch (style) { + case FILLED: case HEADLESS: + tip = Math.toRadians(20.0); + base = Math.toRadians(90.0); + points[1*2] = (float) (points[2*3] - length*Math.cos(alpha)); + points[1*2+1] = (float) (points[2*3+1] - length*Math.sin(alpha)); + SL = length*Math.sin(base)/Math.sin(base+tip);; + break; + case NOTCHED: + tip = Math.toRadians(20); + base = Math.toRadians(120); + points[1*2] = (float) (points[2*3] - length*Math.cos(alpha)); + points[1*2+1] = (float) (points[2*3+1] - length*Math.sin(alpha)); + SL = length*Math.sin(base)/Math.sin(base+tip);; + break; + case OPEN: + tip = Math.toRadians(25); //30 + points[1*2] = points[2*3]; + points[1*2+1] = points[2*3+1]; + SL = length; + break; + case BAR: + tip = Math.toRadians(90); //30 + points[1*2] = points[2*3]; + points[1*2+1] = points[2*3+1]; + SL = length; + updateFullWindow = true; + break; + } + // P2 = P3 - SL*alpha+tip + points[2*2] = (float) (points[2*3] - SL*Math.cos(alpha+tip)); + points[2*2+1] = (float) (points[2*3+1] - SL*Math.sin(alpha+tip)); + // P4 = P3 - SL*alpha-tip + points[2*4] = (float) (points[2*3] - SL*Math.cos(alpha-tip)); + points[2*4+1] = (float) (points[2*3+1] - SL*Math.sin(alpha-tip)); + } + + private Shape getShape() { + Shape arrow = getPath(); + BasicStroke stroke = new BasicStroke((float)getStrokeWidth(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + Shape outlineShape = stroke.createStrokedShape(arrow); + Area a1 = new Area(arrow); + Area a2 = new Area(outlineShape); + try {a1.add(a2);} catch(Exception e) {}; + return a1; + } + + private ShapeRoi getShapeRoi() { + Shape arrow = getPath(); + BasicStroke stroke = new BasicStroke(getStrokeWidth(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + ShapeRoi sroi = new ShapeRoi(arrow); + Shape outlineShape = stroke.createStrokedShape(arrow); + sroi.or(new ShapeRoi(outlineShape)); + return sroi; + } + + public ImageProcessor getMask() { + if (width==0 && height==0) + return null; + else + return getShapeRoi().getMask(); + } + + private double getOutlineWidth() { + double width = getStrokeWidth()/8.0; + double head = headSize/7.0; + double lineWidth = width + head + headShaftRatio; + if (lineWidth<1.0) lineWidth = 1.0; + //if (width<1) width=1; + //if (head<1) head=1; + //IJ.log(getStrokeWidth()+" "+IJ.d2s(width,2)+" "+IJ.d2s(head,2)+" "+IJ.d2s(headShaftRatio,2)+" "+IJ.d2s(lineWidth,2)+" "+IJ.d2s(width*head,2)); + return lineWidth; + } + + public void drawPixels(ImageProcessor ip) { + ShapeRoi shapeRoi = getShapeRoi(); + ShapeRoi shapeRoi2 = null; + if (doubleHeaded) { + flipEnds(); + shapeRoi2 = getShapeRoi(); + flipEnds(); + } + if (outline) { + int lineWidth = ip.getLineWidth(); + ip.setLineWidth((int)Math.round(getOutlineWidth())); + shapeRoi.drawPixels(ip); + if (doubleHeaded) shapeRoi2.drawPixels(ip); + ip.setLineWidth(lineWidth); + } else { + ip.fill(shapeRoi); + if (doubleHeaded) ip.fill(shapeRoi2); + } + } + + public boolean contains(int x, int y) { + return getShapeRoi().contains(x, y); + } + + /** Return the bounding rectangle of this arrow. */ + public Rectangle getBounds() { + return getShapeRoi().getBounds(); + } + + protected void handleMouseDown(int sx, int sy) { + super.handleMouseDown(sx, sy); + startxd = ic!=null?ic.offScreenXD(sx):sx; + startyd = ic!=null?ic.offScreenYD(sy):sy; + } + + protected int clipRectMargin() { + double mag = getMagnification(); + double arrowWidth = getStrokeWidth(); + double size = 8+10*arrowWidth*mag*0.5; + return (int)Math.max(size*2.0, headSize); + } + + public boolean isDrawingTool() { + return true; + } + + public static void setDefaultWidth(double width) { + defaultWidth = (float)width; + } + + public static double getDefaultWidth() { + return defaultWidth; + } + + public void setStyle(int style) { + this.style = style; + } + + /* Set the style, where 'style' is "filled", "notched", "open", "headless" or "bar", + plus optionial modifiers of "outline", "double", "small", "medium" and "large". */ + public void setStyle(String style) { + style = style.toLowerCase(); + int newStyle = Arrow.FILLED; + if (style.contains("notched")) + newStyle = Arrow.NOTCHED; + else if (style.contains("open")) + newStyle = Arrow.OPEN; + else if (style.contains("headless")) + newStyle = Arrow.HEADLESS; + else if (style.contains("bar")) + newStyle = Arrow.BAR; + setStyle(newStyle); + setOutline(style.contains("outline")); + setDoubleHeaded(style.contains("double")); + if (style.contains("small")) + setHeadSize(5); + else if (style.contains("large")) + setHeadSize(15); + } + + public int getStyle() { + return style; + } + + public static void setDefaultStyle(int style) { + defaultStyle = style; + } + + public static int getDefaultStyle() { + return defaultStyle; + } + + public void setHeadSize(double headSize) { + this.headSize = headSize; + } + + public double getHeadSize() { + return headSize; + } + + public static void setDefaultHeadSize(double size) { + defaultHeadSize = size; + } + + public static double getDefaultHeadSize() { + return defaultHeadSize; + } + + public void setDoubleHeaded(boolean b) { + doubleHeaded = b; + } + + public boolean getDoubleHeaded() { + return doubleHeaded; + } + + public static void setDefaultDoubleHeaded(boolean b) { + defaultDoubleHeaded = b; + } + + public static boolean getDefaultDoubleHeaded() { + return defaultDoubleHeaded; + } + + public void setOutline(boolean b) { + outline = b; + } + + public boolean getOutline() { + return outline; + } + + public static void setDefaultOutline(boolean b) { + defaultOutline = b; + } + + public static boolean getDefaultOutline() { + return defaultOutline; + } + +} diff --git a/src/ij/gui/ColorChooser.java b/src/ij/gui/ColorChooser.java new file mode 100644 index 0000000..d5764bf --- /dev/null +++ b/src/ij/gui/ColorChooser.java @@ -0,0 +1,121 @@ +package ij.gui; +import ij.*; +import ij.process.*; +import ij.util.*; +import ij.plugin.Colors; +import java.awt.*; +import java.util.Vector; +import java.awt.event.*; + + + /** Displays a dialog that allows the user to select a color using three sliders. */ +public class ColorChooser implements TextListener, AdjustmentListener { + Vector colors, sliders; + ColorPanel panel; + Color initialColor; + int red, green, blue; + boolean useHSB; + String title; + Frame frame; + double scale = Prefs.getGuiScale(); + + /** Constructs a ColorChooser using the specified title and initial color. */ + public ColorChooser(String title, Color initialColor, boolean useHSB) { + this(title, initialColor, useHSB, null); + } + + public ColorChooser(String title, Color initialColor, boolean useHSB, Frame frame) { + this.title = title; + if (initialColor==null) initialColor = Color.black; + this.initialColor = initialColor; + red = initialColor.getRed(); + green = initialColor.getGreen(); + blue = initialColor.getBlue(); + this.useHSB = useHSB; + this.frame = frame; + } + + /** Displays a color selection dialog and returns the color selected by the user. */ + public Color getColor() { + GenericDialog gd = frame!=null?new GenericDialog(title, frame):new GenericDialog(title); + gd.addSlider("Red:", 0, 255, red); + gd.addSlider("Green:", 0, 255, green); + gd.addSlider("Blue:", 0, 255, blue); + panel = new ColorPanel(initialColor, scale); + gd.addPanel(panel, GridBagConstraints.CENTER, new Insets(10, 0, 0, 0)); + colors = gd.getNumericFields(); + for (int i=0; i255) red=255; + if (green<0) green=0; if (green>255) green=255; + if (blue<0) blue=0; if (blue>255) blue=255; + panel.setColor(new Color(red, green, blue)); + panel.repaint(); + } + + public synchronized void adjustmentValueChanged(AdjustmentEvent e) { + Object source = e.getSource(); + for (int i=0; ie is null if the + * dialogItemChanged method is called after the user has pressed the + * OK button or if the GenericDialog has read its parameters from a + * macro. + * @param gd A reference to the GenericDialog. + * @return Should be true if the dialog input is valid. False disables the + * OK button and preview (if any). + */ + boolean dialogItemChanged(GenericDialog gd, AWTEvent e); +} diff --git a/src/ij/gui/EllipseRoi.java b/src/ij/gui/EllipseRoi.java new file mode 100644 index 0000000..9796a32 --- /dev/null +++ b/src/ij/gui/EllipseRoi.java @@ -0,0 +1,269 @@ +package ij.gui; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; +import ij.*; +import ij.plugin.frame.Recorder; +import ij.process.FloatPolygon; +import ij.measure.Calibration; + +/** This class implements the ellipse selection tool. */ +public class EllipseRoi extends PolygonRoi { + private static final int vertices = 72; + private static double defaultRatio = 0.6; + private double xstart, ystart; + private double aspectRatio = defaultRatio; + private int[] handle = {0, vertices/4, vertices/2, vertices/2+vertices/4}; + + public EllipseRoi(double x1, double y1, double x2, double y2, double aspectRatio) { + super(new float[vertices], new float[vertices], vertices, FREEROI); + if (aspectRatio<0.0) aspectRatio = 0.0; + if (aspectRatio>1.0) aspectRatio = 1.0; + this.aspectRatio = aspectRatio; + makeEllipse(x1, y1, x2, y2); + state = NORMAL; + bounds = null; + } + + public EllipseRoi(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + type = FREEROI; + xstart = offScreenXD(sx); + ystart = offScreenYD(sy); + setDrawOffset(false); + bounds = null; + } + + public void draw(Graphics g) { + super.draw(g); + if (!overlay) { + for (int i=0; i1.0) aspectRatio = 1.0; + defaultRatio = aspectRatio; + } + + public int isHandle(int sx, int sy) { + int size = getHandleSize()+5; + int halfSize = size/2; + int index = -1; + for (int i=0; i=sx2 && sx<=sx2+size && sy>=sy2 && sy<=sy2+size) { + index = i; + break; + } + } + return index; + } + + /** Returns the perimeter of this ellipse. */ + public double getLength() { + double length = 0.0; + double dx, dy; + double w2=1.0, h2=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + w2 = cal.pixelWidth*cal.pixelWidth; + h2 = cal.pixelHeight*cal.pixelHeight; + } + for (int i=0; i<(nPoints-1); i++) { + dx = xpf[i+1]-xpf[i]; + dy = ypf[i+1]-ypf[i]; + length += Math.sqrt(dx*dx*w2+dy*dy*h2); + } + dx = xpf[0]-xpf[nPoints-1]; + dy = ypf[0]-ypf[nPoints-1]; + length += Math.sqrt(dx*dx*w2+dy*dy*h2); + return length; + } + + /** Returns x1, y1, x2, y2 and aspectRatio as a 5 element array. */ + public double[] getParams() { + double[] params = new double[5]; + params[0] = xpf[handle[2]]+x; + params[1] = ypf[handle[2]]+y; + params[2] = xpf[handle[0]]+x; + params[3] = ypf[handle[0]]+y; + params[4] = aspectRatio; + return params; + } + + public double[] getFeretValues() { + double a[] = super.getFeretValues(); + double pw=1.0, ph=1.0; + if (imp!=null) { + Calibration cal = imp.getCalibration(); + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } + if (pw != ph) //the following calculation holds only for pixel aspect ratio == 1 (otherwise different axes in distorted ellipse) + return a; + double[] p = getParams(); + double dx = p[2] - p[0]; //this is always major axis; aspect ratio p[4] is limited to <= 1 + double dy = p[3] - p[1]; + double major = Math.sqrt(dx*dx + dy*dy); + double minor = major*p[4]; + a[0] = major*pw; //Feret from convex hull should be accurate anyhow + a[2] = minor*pw; //here our own calculation is better + System.arraycopy(p, 0, a, 8, 4); //MaxFeret endpoints + double xCenter = 0.5*(p[2] + p[0]); + double yCenter = 0.5*(p[3] + p[1]); + double semiMinorX = dy * 0.5 * p[4]; + double semiMinorY = dx * (-0.5) * p[4]; + a[12] = xCenter + semiMinorX; a[14] = xCenter - semiMinorX; + a[13] = yCenter + semiMinorY; a[15] = yCenter - semiMinorY; + return a; + } + + /** Always returns true. */ + public boolean subPixelResolution() { + return true; + } + +} diff --git a/src/ij/gui/FreehandRoi.java b/src/ij/gui/FreehandRoi.java new file mode 100644 index 0000000..db844b2 --- /dev/null +++ b/src/ij/gui/FreehandRoi.java @@ -0,0 +1,98 @@ +package ij.gui; + +import java.awt.*; +import java.awt.image.*; +import ij.*; + +/** Freehand region of interest or freehand line of interest*/ +public class FreehandRoi extends PolygonRoi { + + public FreehandRoi(int sx, int sy, ImagePlus imp) { + super(sx, sy, imp); + if (Toolbar.getToolId()==Toolbar.FREEROI) + type = FREEROI; + else + type = FREELINE; + if (nPoints==2) nPoints--; + } + + protected void grow(int sx, int sy) { + if (subPixelResolution() && xpf!=null) { + growFloat(sx, sy); + return; + } + int ox = offScreenX(sx); + int oy = offScreenY(sy); + if (ox<0) ox = 0; + if (oy<0) oy = 0; + if (ox>xMax) ox = xMax; + if (oy>yMax) oy = yMax; + if (ox!=xp[nPoints-1]+x || oy!=yp[nPoints-1]+y) { + xp[nPoints] = ox-x; + yp[nPoints] = oy-y; + nPoints++; + if (IJ.altKeyDown()) + wipeBack(); + if (nPoints==xp.length) + enlargeArrays(); + drawLine(); + } + } + + private void growFloat(int sx, int sy) { + double ox = offScreenXD(sx); + double oy = offScreenYD(sy); + if (ox<0.0) ox = 0.0; + if (oy<0.0) oy = 0.0; + if (ox>xMax) ox = xMax; + if (oy>yMax) oy = yMax; + double xbase = getXBase(); + double ybase = getYBase(); + if (ox!=xpf[nPoints-1]+xbase || oy!=ypf[nPoints-1]+ybase) { + xpf[nPoints] = (float)(ox-xbase); + ypf[nPoints] = (float)(oy-ybase); + nPoints++; + if (nPoints==xpf.length) + enlargeArrays(); + drawLine(); + } + } + + void drawLine() { + int x1, y1, x2, y2; + if (xpf!=null) { + x1 = (int)Math.round(xpf[nPoints-2]+x); + y1 = (int)Math.round(ypf[nPoints-2]+y); + x2 = (int)Math.round(xpf[nPoints-1]+x); + y2 = (int)Math.round(ypf[nPoints-1]+y); + } else { + x1 = xp[nPoints-2]+x; + y1 = yp[nPoints-2]+y; + x2 = xp[nPoints-1]+x; + y2 = yp[nPoints-1]+y; + } + int xmin = Math.min(x1, x2); + int xmax = Math.max(x1, x2); + int ymin = Math.min(y1, y2); + int ymax = Math.max(y1, y2); + int margin = 4; + if (lineWidth>margin && isLine()) + margin = lineWidth; + if (ic!=null) { + double mag = ic.getMagnification(); + if (mag<1.0) margin = (int)(margin/mag); + } + if (IJ.altKeyDown()) + margin += 20; // for wipeBack + imp.draw(xmin-margin, ymin-margin, (xmax-xmin)+margin*2, (ymax-ymin)+margin*2); + } + + protected void handleMouseUp(int screenX, int screenY) { + if (state==CONSTRUCTING) { + addOffset(); + finishPolygon(); + } + state = NORMAL; + } + +} diff --git a/src/ij/gui/GUI.java b/src/ij/gui/GUI.java new file mode 100644 index 0000000..6bb4921 --- /dev/null +++ b/src/ij/gui/GUI.java @@ -0,0 +1,288 @@ +package ij.gui; +import ij.*; +import java.awt.*; +import javax.swing.JComponent; +import javax.swing.JList; +import javax.swing.JTable; +import javax.swing.UIManager; + +/** This class consists of static GUI utility methods. */ +public class GUI { + private static final Font DEFAULT_FONT = IJ.font12; + private static Color lightGray = new Color(240,240,240); + private static boolean isWindows8; + private static Color scrollbarBackground = new Color(245,245,245); + + static { + if (IJ.isWindows()) { + String osname = System.getProperty("os.name"); + isWindows8 = osname.contains("unknown") || osname.contains("8"); + } + } + + /** Positions the specified window in the center of the screen that contains target. */ + public static void center(Window win, Component target) { + if (win == null) + return; + Rectangle bounds = getMaxWindowBounds(target); + Dimension window = win.getSize(); + if (window.width == 0) + return; + int left = bounds.x + Math.max(0, (bounds.width - window.width) / 2); + int top = bounds.y + Math.max(0, (bounds.height - window.height) / 4); + win.setLocation(left, top); + } + + /** Positions the specified window in the center of the + screen containing the "ImageJ" window. */ + public static void centerOnImageJScreen(Window win) { + center(win, IJ.getInstance()); + } + + public static void center(Window win) { + center(win, win); + } + + private static java.util.List getScreenConfigs() { + java.util.ArrayList configs = new java.util.ArrayList(); + for (GraphicsDevice device : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { + configs.add(device.getDefaultConfiguration()); + } + return configs; + } + + /** + * Get maximum bounds for the screen that contains a given point. + * @param point Coordinates of point. + * @param accountForInsets Deduct the space taken up by menu and status bars, etc. (after point is found to be inside bonds) + * @return Rectangle of bounds or null if point not inside of any screen. + */ + public static Rectangle getScreenBounds(Point point, boolean accountForInsets) { + if (GraphicsEnvironment.isHeadless()) + return new Rectangle(0,0,0,0); + for (GraphicsConfiguration config : getScreenConfigs()) { + Rectangle bounds = config.getBounds(); + if (bounds != null && bounds.contains(point)) { + Insets insets = accountForInsets ? Toolkit.getDefaultToolkit().getScreenInsets(config) : null; + return shrinkByInsets(bounds, insets); + } + } + return null; + } + + /** + * Get maximum bounds for the screen that contains a given component. + * @param component An AWT component located on the desired screen. + * If null is provided, the default screen is used. + * @param accountForInsets Deduct the space taken up by menu and status bars, etc. + * @return Rectangle of bounds. + */ + public static Rectangle getScreenBounds(Component component, boolean accountForInsets) { + if (GraphicsEnvironment.isHeadless()) + return new Rectangle(0,0,0,0); + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsConfiguration gc = component == null ? ge.getDefaultScreenDevice().getDefaultConfiguration() : + component.getGraphicsConfiguration(); + Insets insets = accountForInsets ? Toolkit.getDefaultToolkit().getScreenInsets(gc) : null; + return shrinkByInsets(gc.getBounds(), insets); + } + + public static Rectangle getScreenBounds(Point point) { + return getScreenBounds(point, false); + } + + public static Rectangle getScreenBounds(Component component) { + return getScreenBounds(component, false); + } + + public static Rectangle getScreenBounds() { + return getScreenBounds((Component)null); + } + + public static Rectangle getMaxWindowBounds(Point point) { + return getScreenBounds(point, true); + } + + public static Rectangle getMaxWindowBounds(Component component) { + return getScreenBounds(component, true); + } + + public static Rectangle getMaxWindowBounds() { + return getMaxWindowBounds((Component)null); + } + + private static Rectangle shrinkByInsets(Rectangle bounds, Insets insets) { + Rectangle shrunk = new Rectangle(bounds); + if (insets == null) return shrunk; + shrunk.x += insets.left; + shrunk.y += insets.top; + shrunk.width -= insets.left + insets.right; + shrunk.height -= insets.top + insets.bottom; + return shrunk; + } + + public static Rectangle getZeroBasedMaxBounds() { + for (GraphicsConfiguration config : getScreenConfigs()) { + Rectangle bounds = config.getBounds(); + if (bounds != null && bounds.x == 0 && bounds.y == 0) + return bounds; + } + return null; + } + + public static Rectangle getUnionOfBounds() { + Rectangle unionOfBounds = new Rectangle(); + for (GraphicsConfiguration config : getScreenConfigs()) { + unionOfBounds = unionOfBounds.union(config.getBounds()); + } + return unionOfBounds; + } + + static private Frame frame; + + /** Obsolete */ + public static Image createBlankImage(int width, int height) { + if (width==0 || height==0) + throw new IllegalArgumentException(""); + if (frame==null) { + frame = new Frame(); + frame.pack(); + frame.setBackground(Color.white); + } + Image img = frame.createImage(width, height); + return img; + } + + /** Lightens overly dark scrollbar background on Windows 8. */ + public static void fix(Scrollbar sb) { + } + + public static boolean showCompositeAdvisory(ImagePlus imp, String title) { + if (imp==null || imp.getCompositeMode()!=IJ.COMPOSITE || imp.getNChannels()==1 || IJ.macroRunning()) + return true; + String msg = "Channel "+imp.getC()+" of this color composite image will be processed."; + GenericDialog gd = new GenericDialog(title); + gd.addMessage(msg); + gd.showDialog(); + return !gd.wasCanceled(); + } + + /** + * Scales an AWT component according to {@link Prefs#getGuiScale()}. + * @param component the AWT component to be scaled. If a container, scaling is applied to all its child components + */ + public static void scale(final Component component) { + final float scale = (float)Prefs.getGuiScale(); + if (scale==1f) + return; + if (component instanceof Container) + scaleComponents((Container)component, scale); + else + scaleComponent(component, scale); + } + + private static void scaleComponents(final Container container, final float scale) { + for (final Component child : container.getComponents()) { + if (child instanceof Container) + scaleComponents((Container) child, scale); + else + scaleComponent(child, scale); + } + } + + private static void scaleComponent(final Component component, final float scale) { + Font font = component.getFont(); + if (font == null) + font = DEFAULT_FONT; + font = font.deriveFont(scale*font.getSize()); + component.setFont(font); + } + + public static void scalePopupMenu(final PopupMenu popup) { + final float scale = (float)Prefs.getGuiScale(); + if (scale==1f) + return; + Font font = popup.getFont(); + if (font == null) + font = DEFAULT_FONT; + font = font.deriveFont(scale*font.getSize()); + popup.setFont(font); + } + + /** + * Tries to detect if a Swing component is unscaled and scales it it according + * to {@link #getGuiScale()}. + *

+	0-3		"Iout"
+	4-5		version (>=217)
+	6-7		roi type (encoded as one byte)
+	8-9		top
+	10-11	left
+	12-13	bottom
+	14-15	right
+	16-17	NCoordinates
+	18-33	x1,y1,x2,y2 (straight line) | x,y,width,height (double rect) | size (npoints)
+	34-35	stroke width (v1.43i or later)
+	36-39   ShapeRoi size (type must be 1 if this value>0)
+	40-43   stroke color (v1.43i or later)
+	44-47   fill color (v1.43i or later)
+	48-49   subtype (v1.43k or later)
+	50-51   options (v1.43k or later)
+	52-52   arrow style or aspect ratio (v1.43p or later)
+	53-53   arrow head size (v1.43p or later)
+	54-55   rounded rect arc size (v1.43p or later)
+	56-59   position
+	60-63   header2 offset
+	64-       x-coordinates (short), followed by y-coordinates
+	
+	@see DecodeRoiFile.js
+*/
+
+public class RoiDecoder {
+	// offsets
+	public static final int VERSION_OFFSET = 4;
+	public static final int TYPE = 6;
+	public static final int TOP = 8;
+	public static final int LEFT = 10;
+	public static final int BOTTOM = 12;
+	public static final int RIGHT = 14;
+	public static final int N_COORDINATES = 16;
+	public static final int X1 = 18;
+	public static final int Y1 = 22;
+	public static final int X2 = 26;
+	public static final int Y2 = 30;
+	public static final int XD = 18;
+	public static final int YD = 22;
+	public static final int WIDTHD = 26;
+	public static final int HEIGHTD = 30;
+	public static final int SIZE = 18;
+	public static final int STROKE_WIDTH = 34;
+	public static final int SHAPE_ROI_SIZE = 36;
+	public static final int STROKE_COLOR = 40;
+	public static final int FILL_COLOR = 44;
+	public static final int SUBTYPE = 48;
+	public static final int OPTIONS = 50;
+	public static final int ARROW_STYLE = 52;
+	public static final int FLOAT_PARAM = 52; //ellipse ratio or rotated rect width
+	public static final int POINT_TYPE= 52;
+	public static final int ARROW_HEAD_SIZE = 53;
+	public static final int ROUNDED_RECT_ARC_SIZE = 54;
+	public static final int POSITION = 56;
+	public static final int HEADER2_OFFSET = 60;
+	public static final int COORDINATES = 64;
+	// header2 offsets
+	public static final int C_POSITION = 4;
+	public static final int Z_POSITION = 8;
+	public static final int T_POSITION = 12;
+	public static final int NAME_OFFSET = 16;
+	public static final int NAME_LENGTH = 20;
+	public static final int OVERLAY_LABEL_COLOR = 24;
+	public static final int OVERLAY_FONT_SIZE = 28; //short
+	public static final int GROUP = 30;  //byte
+	public static final int IMAGE_OPACITY = 31;  //byte
+	public static final int IMAGE_SIZE = 32;  //int
+	public static final int FLOAT_STROKE_WIDTH = 36;  //float
+	public static final int ROI_PROPS_OFFSET = 40;
+	public static final int ROI_PROPS_LENGTH = 44;
+	public static final int COUNTERS_OFFSET = 48;
+
+	// subtypes
+	public static final int TEXT = 1;
+	public static final int ARROW = 2;
+	public static final int ELLIPSE = 3;
+	public static final int IMAGE = 4;
+	public static final int ROTATED_RECT = 5;
+	
+	// options
+	public static final int SPLINE_FIT = 1;
+	public static final int DOUBLE_HEADED = 2;
+	public static final int OUTLINE = 4;
+	public static final int OVERLAY_LABELS = 8;
+	public static final int OVERLAY_NAMES = 16;
+	public static final int OVERLAY_BACKGROUNDS = 32;
+	public static final int OVERLAY_BOLD = 64;
+	public static final int SUB_PIXEL_RESOLUTION = 128;
+	public static final int DRAW_OFFSET = 256;
+	public static final int ZERO_TRANSPARENT = 512;
+	public static final int SHOW_LABELS = 1024;
+	public static final int SCALE_LABELS = 2048;
+	public static final int PROMPT_BEFORE_DELETING = 4096; //points
+	public static final int SCALE_STROKE_WIDTH = 8192;
+	
+	// types
+	private final int polygon=0, rect=1, oval=2, line=3, freeline=4, polyline=5, noRoi=6,
+		freehand=7, traced=8, angle=9, point=10;
+	
+	private byte[] data;
+	private String path;
+	private InputStream is;
+	private String name;
+	private int size;
+
+	/** Constructs an RoiDecoder using a file path. */
+	public RoiDecoder(String path) {
+		this.path = path;
+	}
+
+	/** Constructs an RoiDecoder using a byte array. */
+	public RoiDecoder(byte[] bytes, String name) {
+		is = new ByteArrayInputStream(bytes);	
+		this.name = name;
+		this.size = bytes.length;
+	}
+
+	/** Opens the Roi at the specified path. Returns null if there is an error. */
+	public static Roi open(String path) {
+		Roi roi = null;
+		RoiDecoder rd = new RoiDecoder(path);
+		try {
+			roi = rd.getRoi();
+		} catch (IOException e) { }
+		return roi;
+	}
+
+	/** Returns the ROI. */
+	public Roi getRoi() throws IOException {
+		if (path!=null) {
+			File f = new File(path);
+			size = (int)f.length();
+			if (!path.endsWith(".roi") && size>5242880)
+				throw new IOException("This is not an ROI or file size>5MB)");
+			name = f.getName();
+			is = new FileInputStream(path);
+		}
+		data = new byte[size];
+
+		int total = 0;
+		while (total=222;
+		boolean drawOffset = subPixelResolution && (options&DRAW_OFFSET)!=0;
+		boolean scaleStrokeWidth = true;
+		if (version>=228)
+			scaleStrokeWidth = (options&SCALE_STROKE_WIDTH)!=0;
+		
+		boolean subPixelRect = version>=223 && subPixelResolution && (type==rect||type==oval);
+		double xd=0.0, yd=0.0, widthd=0.0, heightd=0.0;
+		if (subPixelRect) {
+			xd = getFloat(XD);
+			yd = getFloat(YD);
+			widthd = getFloat(WIDTHD);
+			heightd = getFloat(HEIGHTD);
+		}
+		
+		if (hdr2Offset>0 && hdr2Offset+IMAGE_SIZE+4<=size) {
+			channel = getInt(hdr2Offset+C_POSITION);
+			slice = getInt(hdr2Offset+Z_POSITION);
+			frame = getInt(hdr2Offset+T_POSITION);
+			overlayLabelColor = getInt(hdr2Offset+OVERLAY_LABEL_COLOR);
+			overlayFontSize = getShort(hdr2Offset+OVERLAY_FONT_SIZE);
+			imageOpacity = getByte(hdr2Offset+IMAGE_OPACITY);
+			imageSize = getInt(hdr2Offset+IMAGE_SIZE);
+			group = getByte(hdr2Offset+GROUP);
+		}
+		
+		if (name!=null && name.endsWith(".roi"))
+			name = name.substring(0, name.length()-4);
+		boolean isComposite = getInt(SHAPE_ROI_SIZE)>0;
+		
+		Roi roi = null;
+		if (isComposite) {
+			roi = getShapeRoi();
+			if (version>=218)
+				getStrokeWidthAndColor(roi, hdr2Offset, scaleStrokeWidth);
+			roi.setPosition(position);
+			if (channel>0 || slice>0 || frame>0)
+				roi.setPosition(channel, slice, frame);
+			decodeOverlayOptions(roi, version, options, overlayLabelColor, overlayFontSize);
+			if (version>=224) {
+				String props = getRoiProps();
+				if (props!=null)
+					roi.setProperties(props);
+			}
+			if (version>=228 && group>0)
+				roi.setGroup(group);
+			return roi;
+		}
+
+		switch (type) {
+			case rect:
+				if (subPixelRect)
+					roi = new Roi(xd, yd, widthd, heightd);
+				else
+					roi = new Roi(left, top, width, height);
+				int arcSize = getShort(ROUNDED_RECT_ARC_SIZE);
+				if (arcSize>0)
+					roi.setCornerDiameter(arcSize);
+				break;
+			case oval:
+				if (subPixelRect)
+					roi = new OvalRoi(xd, yd, widthd, heightd);
+				else
+					roi = new OvalRoi(left, top, width, height);
+				break;
+			case line:
+				double x1 = getFloat(X1);		
+				double y1 = getFloat(Y1);		
+				double x2 = getFloat(X2);		
+				double y2 = getFloat(Y2);
+				if (subtype==ARROW) {
+					roi = new Arrow(x1, y1, x2, y2);		
+					((Arrow)roi).setDoubleHeaded((options&DOUBLE_HEADED)!=0);
+					((Arrow)roi).setOutline((options&OUTLINE)!=0);
+					int style = getByte(ARROW_STYLE);
+					if (style>=Arrow.FILLED && style<=Arrow.BAR)
+						((Arrow)roi).setStyle(style);
+					int headSize = getByte(ARROW_HEAD_SIZE);
+					if (headSize>=0 && style<=30)
+						((Arrow)roi).setHeadSize(headSize);
+				} else {
+					roi = new Line(x1, y1, x2, y2);
+					roi.setDrawOffset(drawOffset);
+				}
+				break;
+			case polygon: case freehand: case traced: case polyline: case freeline: case angle: case point:
+					//IJ.log("type: "+type);
+					//IJ.log("n: "+n);
+					//IJ.log("rect: "+left+","+top+" "+width+" "+height);
+					if (n==0 || n<0) break;
+					int[] x = new int[n];
+					int[] y = new int[n];
+					float[] xf = null;
+					float[] yf = null;
+					int base1 = COORDINATES;
+					int base2 = base1+2*n;
+					int xtmp, ytmp;
+					for (int i=0; i=226) {
+							((PointRoi)roi).setPointType(getByte(POINT_TYPE));
+							((PointRoi)roi).setSize(getShort(STROKE_WIDTH));
+						}
+						if ((options&SHOW_LABELS)!=0 && !ij.Prefs.noPointLabels)
+							((PointRoi)roi).setShowLabels(true);
+						if ((options&PROMPT_BEFORE_DELETING)!=0)
+							((PointRoi)roi).promptBeforeDeleting(true);
+						break;
+					}
+					int roiType;
+					if (type==polygon)
+						roiType = Roi.POLYGON;
+					else if (type==freehand) {
+						roiType = Roi.FREEROI;
+						if (subtype==ELLIPSE || subtype==ROTATED_RECT) {
+							double ex1 = getFloat(X1);		
+							double ey1 = getFloat(Y1);		
+							double ex2 = getFloat(X2);		
+							double ey2 = getFloat(Y2);
+							double param = getFloat(FLOAT_PARAM);
+							if (subtype==ROTATED_RECT)
+								roi = new RotatedRectRoi(ex1,ey1,ex2,ey2,param);
+							else
+								roi = new EllipseRoi(ex1,ey1,ex2,ey2,param);
+							break;
+						}
+					} else if (type==traced)
+						roiType = Roi.TRACED_ROI;
+					else if (type==polyline)
+						roiType = Roi.POLYLINE;
+					else if (type==freeline)
+						roiType = Roi.FREELINE;
+					else if (type==angle)
+						roiType = Roi.ANGLE;
+					else
+						roiType = Roi.FREEROI;
+					if (subPixelResolution) {
+						roi = new PolygonRoi(xf, yf, n, roiType);
+						roi.setDrawOffset(drawOffset);
+					} else
+						roi = new PolygonRoi(x, y, n, roiType);
+					break;
+			default:
+				throw new IOException("Unrecognized ROI type: "+type);
+		}
+		if (roi==null)
+			return null;
+		roi.setName(getRoiName());
+		
+		// read stroke width, stroke color and fill color (1.43i or later)
+		if (version>=218) {
+			getStrokeWidthAndColor(roi, hdr2Offset, scaleStrokeWidth);
+			if (type==point)
+				roi.setStrokeWidth(0);
+			boolean splineFit = (options&SPLINE_FIT)!=0;
+			if (splineFit && roi instanceof PolygonRoi)
+				((PolygonRoi)roi).fitSpline();
+		}
+		
+		if (version>=218 && subtype==TEXT)
+			roi = getTextRoi(roi, version);
+
+		if (version>=221 && subtype==IMAGE)
+			roi = getImageRoi(roi, imageOpacity, imageSize, options);
+
+		if (version>=224) {
+			String props = getRoiProps();
+			if (props!=null)
+				roi.setProperties(props);
+		}
+
+		if (version>=227) {
+			int[] counters = getPointCounters(n);
+			if (counters!=null && (roi instanceof PointRoi))
+				((PointRoi)roi).setCounters(counters);
+		}
+		
+		// set group (1.52t or later)
+		if (version>=228 && group>0)
+			roi.setGroup(group);
+
+		roi.setPosition(position);
+		if (channel>0 || slice>0 || frame>0)
+			roi.setPosition(channel, slice, frame);
+		decodeOverlayOptions(roi, version, options, overlayLabelColor, overlayFontSize);
+		return roi;
+	}
+	
+	void decodeOverlayOptions(Roi roi, int version, int options, int color, int fontSize) {
+		Overlay proto = new Overlay();
+		proto.drawLabels((options&OVERLAY_LABELS)!=0);
+		proto.drawNames((options&OVERLAY_NAMES)!=0);
+		proto.drawBackgrounds((options&OVERLAY_BACKGROUNDS)!=0);
+		if (version>=220 && color!=0)
+			proto.setLabelColor(new Color(color));
+		boolean bold = (options&OVERLAY_BOLD)!=0;
+		boolean scalable = (options&SCALE_LABELS)!=0;
+		if (fontSize>0 || bold || scalable) {
+			proto.setLabelFont(new Font("SansSerif", bold?Font.BOLD:Font.PLAIN, fontSize), scalable);
+		}
+		roi.setPrototypeOverlay(proto);
+	}
+
+	void getStrokeWidthAndColor(Roi roi, int hdr2Offset, boolean scaleStrokeWidth) {
+		double strokeWidth = getShort(STROKE_WIDTH);
+		if (hdr2Offset>0) {
+			double strokeWidthD = getFloat(hdr2Offset+FLOAT_STROKE_WIDTH);
+			if (strokeWidthD>0.0)
+				strokeWidth = strokeWidthD;
+		}
+		if (strokeWidth>0.0) {
+			if (scaleStrokeWidth)
+				roi.setStrokeWidth(strokeWidth);
+			else
+				roi.setUnscalableStrokeWidth(strokeWidth);
+		}
+		int strokeColor = getInt(STROKE_COLOR);
+		if (strokeColor!=0) {
+			int alpha = (strokeColor>>24)&0xff;
+			roi.setStrokeColor(new Color(strokeColor, alpha!=255));
+		}
+		int fillColor = getInt(FILL_COLOR);
+		if (fillColor!=0) {
+			int alpha = (fillColor>>24)&0xff;
+			roi.setFillColor(new Color(fillColor, alpha!=255));
+		}
+	}
+
+	public Roi getShapeRoi() throws IOException {
+		int type = getByte(TYPE);
+		if (type!=rect)
+			throw new IllegalArgumentException("Invalid composite ROI type");
+		int top= getShort(TOP);
+		int left = getShort(LEFT);
+		int bottom = getShort(BOTTOM);
+		int right = getShort(RIGHT);
+		int width = right-left;
+		int height = bottom-top;
+		int n = getInt(SHAPE_ROI_SIZE);
+
+		ShapeRoi roi = null;
+		float[] shapeArray = new float[n];
+		int base = COORDINATES;
+		for(int i=0; i>8) & 3;
+		boolean drawStringMode = (styleAndJustification&1024)!=0;
+		int nameLength = getInt(hdrSize+8);
+		int textLength = getInt(hdrSize+12);
+		char[] name = new char[nameLength];
+		char[] text = new char[textLength];
+		for (int i=0; i=225?getFloat(hdrSize+16+nameLength*2+textLength*2):0f;
+		Font font = new Font(new String(name), style, size);
+		TextRoi roi2 = null;
+		if (roi.subPixelResolution()) {
+			Rectangle2D fb = roi.getFloatBounds();
+			roi2 = new TextRoi(fb.getX(), fb.getY(), fb.getWidth(), fb.getHeight(), new String(text), font);
+		} else
+			roi2 = new TextRoi(r.x, r.y, r.width, r.height, new String(text), font);
+		roi2.setStrokeColor(roi.getStrokeColor());
+		roi2.setFillColor(roi.getFillColor());
+		roi2.setName(getRoiName());
+		roi2.setJustification(justification);
+		roi2.setDrawStringMode(drawStringMode);
+		roi2.setAngle(angle);
+		return roi2;
+	}
+	
+	Roi getImageRoi(Roi roi, int opacity, int size, int options) {
+		if (size<=0)
+			return roi;
+		Rectangle r = roi.getBounds();
+		byte[] bytes = new byte[size];
+		for (int i=0; isize)
+			return fileName;
+		char[] name = new char[length];
+		for (int i=0; isize)
+			return null;
+		char[] props = new char[length];
+		for (int i=0; idata.length)
+			return null;
+		int[] counters = new int[n];
+		for (int i=0; i32767 and unsigned
+		return n;		
+	}
+	
+	int getUnsignedShort(int base) {
+		int b0 = data[base]&255;
+		int b1 = data[base+1]&255;
+		return (b0<<8) + b1;	
+	}
+
+	int getInt(int base) {
+		int b0 = data[base]&255;
+		int b1 = data[base+1]&255;
+		int b2 = data[base+2]&255;
+		int b3 = data[base+3]&255;
+		return ((b0<<24) + (b1<<16) + (b2<<8) + b3);
+	}
+
+	float getFloat(int base) {
+		return Float.intBitsToFloat(getInt(base));
+	}
+	
+	/** Opens an ROI from a byte array. */
+	public static Roi openFromByteArray(byte[] bytes) {
+		Roi roi = null;
+		if (bytes==null || bytes.length==0)
+			return roi;
+		try {
+			RoiDecoder decoder = new RoiDecoder(bytes, null);
+			roi = decoder.getRoi();
+		} catch (IOException e) {
+			return null;
+		}
+		return roi;
+	}
+
+}
diff --git a/src/ij/io/RoiEncoder.java b/src/ij/io/RoiEncoder.java
new file mode 100644
index 0000000..7e31123
--- /dev/null
+++ b/src/ij/io/RoiEncoder.java
@@ -0,0 +1,469 @@
+package ij.io;
+import ij.gui.*;
+import ij.process.FloatPolygon;
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import java.net.*;
+import java.awt.geom.*;
+
+
+/** Saves an ROI to a file or stream. RoiDecoder.java has a description of the file format.
+	@see ij.io.RoiDecoder
+	@see ij.plugin.RoiReader
+*/
+public class RoiEncoder {
+	static final int HEADER_SIZE = 64;
+	static final int HEADER2_SIZE = 64;
+	static final int VERSION = 228; // v1.52t (roi groups, scale stroke width)
+	private String path;
+	private OutputStream f;
+	private final int polygon=0, rect=1, oval=2, line=3, freeline=4, polyline=5, noRoi=6, freehand=7, 
+		traced=8, angle=9, point=10;
+	private byte[] data;
+	private String roiName;
+	private int roiNameSize;
+	private String roiProps;
+	private int roiPropsSize;
+	private int countersSize;
+	private int[] counters;
+
+	
+	/** Creates an RoiEncoder using the specified path. */
+	public RoiEncoder(String path) {
+		this.path = path;
+	}
+
+	/** Creates an RoiEncoder using the specified OutputStream. */
+	public RoiEncoder(OutputStream f) {
+		this.f = f;
+	}
+	
+	/** Saves the specified ROI as a file, returning 'true' if successful. */
+	public static boolean save(Roi roi, String path) {
+		RoiEncoder re = new RoiEncoder(path);
+		try {
+			re.write(roi);
+		} catch (IOException e) {
+			return false;
+		}
+		return true;
+	}
+
+	/** Save the Roi to the file of stream. */
+	public void write(Roi roi) throws IOException {
+		if (f!=null) {
+			write(roi, f);
+		} else {
+			f = new FileOutputStream(path);
+			write(roi, f);
+			f.close();
+		}
+	}
+	
+	/** Saves the specified ROI as a byte array. */
+	public static byte[] saveAsByteArray(Roi roi) {
+		if (roi==null) return null;
+		byte[] bytes = null;
+		try {
+			ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
+			RoiEncoder encoder = new RoiEncoder(out);
+			encoder.write(roi);
+			out.close();
+			bytes = out.toByteArray(); 
+		} catch (IOException e) {
+			return null;
+		}
+		return bytes;
+	}
+
+	void write(Roi roi, OutputStream f) throws IOException {
+		Rectangle r = roi.getBounds();
+		if (r.width>65535||r.height>65535||r.x>65535||r.y>65535)
+			roi.enableSubPixelResolution();
+		int roiType = roi.getType();
+		int type = rect;
+		int options = 0;
+		if (roi.getScaleStrokeWidth())
+			options |= RoiDecoder.SCALE_STROKE_WIDTH;
+		roiName = roi.getName();
+		if (roiName!=null)
+			roiNameSize = roiName.length()*2;
+		else
+			roiNameSize = 0;
+		
+		roiProps = roi.getProperties();
+		if (roiProps!=null)
+			roiPropsSize = roiProps.length()*2;
+		else
+			roiPropsSize = 0;
+
+		switch (roiType) {
+			case Roi.POLYGON: type=polygon; break;
+			case Roi.FREEROI: type=freehand; break;
+			case Roi.TRACED_ROI: type=traced; break;
+			case Roi.OVAL: type=oval; break;
+			case Roi.LINE: type=line; break;
+			case Roi.POLYLINE: type=polyline; break;
+			case Roi.FREELINE: type=freeline; break;
+			case Roi.ANGLE: type=angle; break;
+			case Roi.COMPOSITE: type=rect; break; // shape array size (36-39) will be >0 to indicate composite type
+			case Roi.POINT: type=point; break;
+			default: type = rect; break;
+		}
+		
+		if (roiType==Roi.COMPOSITE) {
+			saveShapeRoi(roi, type, f, options);
+			return;
+		}
+
+		int n=0;
+		int[] x=null, y=null;
+		float[] xf=null, yf=null;
+		int floatSize = 0;
+		if (roi instanceof PolygonRoi) {
+			PolygonRoi proi = (PolygonRoi)roi;
+			Polygon p = proi.getNonSplineCoordinates();
+			n = p.npoints;
+			x = p.xpoints;
+			y = p.ypoints;
+			if (roi.subPixelResolution()) {
+				FloatPolygon fp = null;
+				if (proi.isSplineFit())
+					fp = proi.getNonSplineFloatPolygon();
+				else
+					fp = roi.getFloatPolygon();
+				if (n==fp.npoints) {
+					options |= RoiDecoder.SUB_PIXEL_RESOLUTION;
+					if (roi.getDrawOffset())
+						options |= RoiDecoder.DRAW_OFFSET;
+					xf = fp.xpoints;
+					yf = fp.ypoints;
+					floatSize = n*8;
+				}
+			}
+		}
+		
+		countersSize = 0;
+		if (roi instanceof PointRoi) {
+			counters = ((PointRoi)roi).getCounters();
+			if (counters!=null && counters.length>=n)
+				countersSize = n*4;
+		}
+		
+		data = new byte[HEADER_SIZE+HEADER2_SIZE+n*4+floatSize+roiNameSize+roiPropsSize+countersSize];
+		data[0]=73; data[1]=111; data[2]=117; data[3]=116; // "Iout"
+		putShort(RoiDecoder.VERSION_OFFSET, VERSION);
+		data[RoiDecoder.TYPE] = (byte)type;
+		putShort(RoiDecoder.TOP, r.y);
+		putShort(RoiDecoder.LEFT, r.x);
+		putShort(RoiDecoder.BOTTOM, r.y+r.height);
+		putShort(RoiDecoder.RIGHT, r.x+r.width);	
+		if (roi.subPixelResolution() && (type==rect||type==oval)) {
+			FloatPolygon p = roi.getFloatPolygon();
+			if (p.npoints==4) {
+				putFloat(RoiDecoder.XD, p.xpoints[0]);
+				putFloat(RoiDecoder.YD, p.ypoints[0]);
+				putFloat(RoiDecoder.WIDTHD, p.xpoints[1]-p.xpoints[0]);
+				putFloat(RoiDecoder.HEIGHTD, p.ypoints[2]-p.ypoints[1]);	
+				options |= RoiDecoder.SUB_PIXEL_RESOLUTION;
+				putShort(RoiDecoder.OPTIONS, options);
+			}
+		}
+		if (n>65535 && type!=point) {
+			if (type==polygon || type==freehand || type==traced) {
+				String name = roi.getName();
+				roi = new ShapeRoi(roi);
+				if (name!=null) roi.setName(name);
+				saveShapeRoi(roi, rect, f, options);
+				return;
+			}
+			ij.IJ.beep();
+			ij.IJ.log("Non-polygonal selections with more than 65k points cannot be saved.");
+			n = 65535;
+		}
+		if (type==point && n>65535)
+			putInt(RoiDecoder.SIZE, n);
+		else 
+			putShort(RoiDecoder.N_COORDINATES, n);
+		putInt(RoiDecoder.POSITION, roi.getPosition());
+		
+		if (type==rect) {
+			int arcSize = roi.getCornerDiameter();
+			if (arcSize>0)
+				putShort(RoiDecoder.ROUNDED_RECT_ARC_SIZE, arcSize);
+		}
+		
+		if (roi instanceof Line) {
+			Line line = (Line)roi;
+			putFloat(RoiDecoder.X1, (float)line.x1d);
+			putFloat(RoiDecoder.Y1, (float)line.y1d);
+			putFloat(RoiDecoder.X2, (float)line.x2d);
+			putFloat(RoiDecoder.Y2, (float)line.y2d);
+			if (roi instanceof Arrow) {
+				putShort(RoiDecoder.SUBTYPE, RoiDecoder.ARROW);
+				if (((Arrow)roi).getDoubleHeaded())
+					options |= RoiDecoder.DOUBLE_HEADED;
+				if (((Arrow)roi).getOutline())
+					options |= RoiDecoder.OUTLINE;
+				putShort(RoiDecoder.OPTIONS, options);
+				putByte(RoiDecoder.ARROW_STYLE, ((Arrow)roi).getStyle());
+				putByte(RoiDecoder.ARROW_HEAD_SIZE, (int)((Arrow)roi).getHeadSize());
+			} else {
+				if (roi.getDrawOffset())
+					options |= RoiDecoder.SUB_PIXEL_RESOLUTION+RoiDecoder.DRAW_OFFSET;
+			}
+		}
+		
+		if (roi instanceof PointRoi) {
+			PointRoi point = (PointRoi)roi;
+			putByte(RoiDecoder.POINT_TYPE, point.getPointType());
+			putShort(RoiDecoder.STROKE_WIDTH, point.getSize());
+			if (point.getShowLabels())
+				options |= RoiDecoder.SHOW_LABELS;
+			if (point.promptBeforeDeleting())
+				options |= RoiDecoder.PROMPT_BEFORE_DELETING;
+		}
+
+		if (roi instanceof RotatedRectRoi || roi instanceof EllipseRoi) {
+			double[] p = null;
+			if (roi instanceof RotatedRectRoi) {
+				putShort(RoiDecoder.SUBTYPE, RoiDecoder.ROTATED_RECT);
+				p = ((RotatedRectRoi)roi).getParams();
+			} else {
+				putShort(RoiDecoder.SUBTYPE, RoiDecoder.ELLIPSE);
+				p = ((EllipseRoi)roi).getParams();
+			}
+			putFloat(RoiDecoder.X1, (float)p[0]);
+			putFloat(RoiDecoder.Y1, (float)p[1]);
+			putFloat(RoiDecoder.X2, (float)p[2]);
+			putFloat(RoiDecoder.Y2, (float)p[3]);
+			putFloat(RoiDecoder.FLOAT_PARAM, (float)p[4]);
+		}
+				
+		// save stroke width, stroke color and fill color (1.43i or later)
+		if (VERSION>=218) {
+			saveStrokeWidthAndColor(roi);
+			if ((roi instanceof PolygonRoi) && ((PolygonRoi)roi).isSplineFit()) {
+				options |= RoiDecoder.SPLINE_FIT;
+				putShort(RoiDecoder.OPTIONS, options);
+			}
+		}
+		
+		if (n==0 && roi instanceof TextRoi)
+			saveTextRoi((TextRoi)roi);
+		else if (n==0 && roi instanceof ImageRoi)
+			options = saveImageRoi((ImageRoi)roi, options);
+		else
+			putHeader2(roi, HEADER_SIZE+n*4+floatSize);
+			
+		if (n>0) {
+			int base1 = 64;
+			int base2 = base1+2*n;
+			for (int i=0; i=218) saveStrokeWidthAndColor(roi);
+		saveOverlayOptions(roi, options);
+
+		// handle the actual data: data are stored segment-wise, i.e.,
+		// the type of the segment followed by 0-6 control point coordinates.
+		int base = 64;
+		for (int i=0; i0)
+			putName(roi, hdr2Offset);
+		double strokeWidth = roi.getStrokeWidth();
+		if (roi.getStroke()==null)
+			strokeWidth = 0.0;
+		putFloat(hdr2Offset+RoiDecoder.FLOAT_STROKE_WIDTH, (float)strokeWidth);
+		if (roiPropsSize>0)
+			putProps(roi, hdr2Offset);
+		if (countersSize>0)
+			putPointCounters(roi, hdr2Offset);
+		putByte(hdr2Offset+RoiDecoder.GROUP, roi.getGroup());
+	}
+
+	void putName(Roi roi, int hdr2Offset) {
+		int offset = hdr2Offset+HEADER2_SIZE;
+		int nameLength = roiNameSize/2;
+		putInt(hdr2Offset+RoiDecoder.NAME_OFFSET, offset);
+		putInt(hdr2Offset+RoiDecoder.NAME_LENGTH, nameLength);
+		for (int i=0; i>>8);
+		data[base+1] = (byte)v;
+    }
+
+	void putFloat(int base, float v) {
+		int tmp = Float.floatToIntBits(v);
+		data[base]   = (byte)(tmp>>24);
+		data[base+1] = (byte)(tmp>>16);
+		data[base+2] = (byte)(tmp>>8);
+		data[base+3] = (byte)tmp;
+	}
+
+	void putInt(int base, int i) {
+		data[base]   = (byte)(i>>24);
+		data[base+1] = (byte)(i>>16);
+		data[base+2] = (byte)(i>>8);
+		data[base+3] = (byte)i;
+	}
+	
+	
+}
\ No newline at end of file
diff --git a/src/ij/io/SaveDialog.java b/src/ij/io/SaveDialog.java
new file mode 100644
index 0000000..7101f15
--- /dev/null
+++ b/src/ij/io/SaveDialog.java
@@ -0,0 +1,265 @@
+package ij.io;
+import ij.gui.GenericDialog;
+import java.awt.*;
+import java.io.*;
+import javax.swing.*;
+import javax.swing.filechooser.*;
+import ij.*;
+import ij.plugin.frame.Recorder;
+import ij.util.Java2;
+import ij.macro.Interpreter;
+
+/** This class displays a dialog window from 
+	which the user can save a file. */ 
+public class SaveDialog {
+
+	private String dir;
+	private String name;
+	private String title;
+	private String ext;
+	
+	/** Displays a file save dialog with 'title' as the 
+		title, 'defaultName' as the initial file name, and
+		'extension' (e.g. ".tif") as the default extension.
+	*/
+	public SaveDialog(String title, String defaultName, String extension) {
+		this.title = title;
+		ext = extension;
+		if (isMacro())
+			return;
+		String defaultDir = OpenDialog.getDefaultDirectory();
+		defaultName = setExtension(defaultName, extension);
+		if (Prefs.useJFileChooser)
+			jSave(title, defaultDir, defaultName);
+		else
+			save(title, defaultDir, defaultName);
+		if (name!=null && dir!=null)
+			OpenDialog.setDefaultDirectory(dir);
+		IJ.showStatus(title+": "+dir+name);
+	}
+	
+	/** Displays a file save dialog, using the specified 
+		default directory and file name and extension. */
+	public SaveDialog(String title, String defaultDir, String defaultName, String extension) {
+		this.title = title;
+		ext = extension;
+		if (isMacro())
+			return;
+		defaultName = setExtension(defaultName, extension);
+		if (Prefs.useJFileChooser)
+			jSave(title, defaultDir, defaultName);
+		else
+			save(title, defaultDir, defaultName);
+		IJ.showStatus(title+": "+dir+name);
+	}
+	
+	boolean isMacro() {
+		String macroOptions = Macro.getOptions();
+		if (macroOptions!=null) {
+			String path = Macro.getValue(macroOptions, title, null);
+			if (path==null)
+				path = Macro.getValue(macroOptions, "path", null);
+			if (path!=null && path.indexOf(".")==-1 && !((new File(path)).exists())) {
+				// Is 'path' a macro variable?
+				if (path.startsWith("&")) path=path.substring(1);
+				Interpreter interp = Interpreter.getInstance();
+				String path2 = interp!=null?interp.getStringVariable(path):null;
+				if (path2!=null) path = path2;
+			}
+			if (path!=null) {
+				Opener o = new Opener();
+				dir = o.getDir(path);
+				name = o.getName(path);
+				return true;
+			}
+		}
+		return false;
+	}
+	
+	public static String setExtension(String name, String extension) {
+		if (name==null || extension==null || extension.length()==0)
+			return name;
+		int dotIndex = name.lastIndexOf(".");
+		if (dotIndex>=0 && (name.length()-dotIndex)<=5) {
+			if (dotIndex+1"+name);
+		dir = fd.getDirectory();
+		if (name==null)
+			Macro.abort();
+		fd.dispose();
+		if (ij==null)
+			parent.dispose();
+	}
+	
+	private boolean noExtension(String name) {
+		if (name==null) return false;
+		int dotIndex = name.indexOf(".");
+		return dotIndex==-1 || (name.length()-dotIndex)>5;
+	}
+	
+	/** Returns the selected directory. */
+	public String getDirectory() {
+		OpenDialog.setLastDirectory(dir);
+		return dir;
+	}
+	
+	/** Returns the selected file name. */
+	public String getFileName() {
+		if (name!=null) {
+			if (Recorder.record && dir!=null)
+				Recorder.recordPath(title, dir+name);
+			OpenDialog.setLastName(name);
+		}
+		return name;
+	}
+	
+	public static String getPath(ImagePlus imp, String extension) {
+		String title = imp!=null?imp.getTitle():"Untitled";
+		SaveDialog sd = new SaveDialog("Save As", title, extension);
+		if (sd.getFileName()==null)
+			return null;
+		else
+			return sd.getDirectory()+sd.getFileName();
+	}
+			
+}
diff --git a/src/ij/io/TextEncoder.java b/src/ij/io/TextEncoder.java
new file mode 100644
index 0000000..addbee6
--- /dev/null
+++ b/src/ij/io/TextEncoder.java
@@ -0,0 +1,54 @@
+package ij.io;
+import java.io.*;
+import ij.*;
+import ij.process.*;
+import ij.measure.*;
+
+/** Saves an image described by an ImageProcessor object as a tab-delimited text file. */
+public class TextEncoder {
+
+	private ImageProcessor ip;
+	private Calibration cal;
+	private int precision;
+
+	/** Constructs a TextEncoder from an ImageProcessor and optional Calibration. */
+	public TextEncoder (ImageProcessor ip, Calibration cal, int precision) {
+		this.ip = ip;
+		this.cal = cal;
+		this.precision = precision;
+	}
+
+	/** Saves the image as a tab-delimited text file. */
+	public void write(DataOutputStream out) throws IOException {
+		PrintWriter pw = new PrintWriter(out);
+		boolean calibrated = cal!=null && cal.calibrated();
+		if (calibrated)
+			ip.setCalibrationTable(cal.getCTable());
+		else
+			ip.setCalibrationTable(null);
+		boolean intData = !calibrated && ((ip instanceof ByteProcessor) || (ip instanceof ShortProcessor));
+		int width = ip.getWidth();
+		int height = ip.getHeight();
+		int inc = height/20;
+		if (inc<1) inc = 1;
+		//IJ.showStatus("Exporting as text...");
+		double value;
+		for (int y=0; y0 && createdByImageJ && id.charAt(7)!='\n') {
+            int index2 = id.indexOf("\n", index1);
+            if (index2>0) {
+                String images = id.substring(index1+7,index2);
+                int n = (int)Tools.parseDouble(images, 0.0);
+                if (n>1 && fi.compression==FileInfo.COMPRESSION_NONE)
+                	fi.nImages = n;
+            }
+        }
+	}
+
+	public void saveMetadata(String name, String data) {
+		if (data==null) return;
+        String str = name+": "+data+"\n";
+        if (tiffMetadata==null)
+        	tiffMetadata = str;
+        else
+        	tiffMetadata += str;
+	}
+
+	void decodeNIHImageHeader(int offset, FileInfo fi) throws IOException {
+		long saveLoc = in.getLongFilePointer();
+		
+		in.seek(offset+12);
+		int version = in.readShort();
+		
+		in.seek(offset+160);
+		double scale = in.readDouble();
+		if (version>106 && scale!=0.0) {
+			fi.pixelWidth = 1.0/scale;
+			fi.pixelHeight = fi.pixelWidth;
+		} 
+
+		// spatial calibration
+		in.seek(offset+172);
+		int units = in.readShort();
+		if (version<=153) units += 5;
+		switch (units) {
+			case 5: fi.unit = "nanometer"; break;
+			case 6: fi.unit = "micrometer"; break;
+			case 7: fi.unit = "mm"; break;
+			case 8: fi.unit = "cm"; break;
+			case 9: fi.unit = "meter"; break;
+			case 10: fi.unit = "km"; break;
+			case 11: fi.unit = "inch"; break;
+			case 12: fi.unit = "ft"; break;
+			case 13: fi.unit = "mi"; break;
+		}
+
+		// density calibration
+		in.seek(offset+182);
+		int fitType = in.read();
+		int unused = in.read();
+		int nCoefficients = in.readShort();
+		if (fitType==11) {
+			fi.calibrationFunction = 21; //Calibration.UNCALIBRATED_OD
+			fi.valueUnit = "U. OD";
+		} else if (fitType>=0 && fitType<=8 && nCoefficients>=1 && nCoefficients<=5) {
+			switch (fitType) {
+				case 0: fi.calibrationFunction = 0; break; //Calibration.STRAIGHT_LINE
+				case 1: fi.calibrationFunction = 1; break; //Calibration.POLY2
+				case 2: fi.calibrationFunction = 2; break; //Calibration.POLY3
+				case 3: fi.calibrationFunction = 3; break; //Calibration.POLY4
+				case 5: fi.calibrationFunction = 4; break; //Calibration.EXPONENTIAL
+				case 6: fi.calibrationFunction = 5; break; //Calibration.POWER
+				case 7: fi.calibrationFunction = 6; break; //Calibration.LOG
+				case 8: fi.calibrationFunction = 10; break; //Calibration.RODBARD2 (NIH Image)
+			}
+			fi.coefficients = new double[nCoefficients];
+			for (int i=0; i=1 && size<=16) {
+				for (int i=0; i=2 && (fi.fileType==FileInfo.GRAY8||fi.fileType==FileInfo.COLOR8)) {
+			fi.nImages = nImages;
+			fi.pixelDepth = in.readFloat();	//SliceSpacing
+			int skip = in.readShort();		//CurrentSlice
+			fi.frameInterval = in.readFloat();
+		}
+			
+		in.seek(offset+272);
+		float aspectRatio = in.readFloat();
+		if (version>140 && aspectRatio!=0.0)
+			fi.pixelHeight = fi.pixelWidth/aspectRatio;
+		
+		in.seek(saveLoc);
+	}
+	
+	void dumpTag(int tag, int count, int value, FileInfo fi) {
+		long lvalue = ((long)value)&0xffffffffL;
+		String name = getName(tag);
+		String cs = (count==1)?"":", count=" + count;
+		dInfo += "    " + tag + ", \"" + name + "\", value=" + lvalue + cs + "\n";
+		//ij.IJ.log(tag + ", \"" + name + "\", value=" + value + cs + "\n");
+	}
+
+	String getName(int tag) {
+		String name;
+		switch (tag) {
+			case NEW_SUBFILE_TYPE: name="NewSubfileType"; break;
+			case IMAGE_WIDTH: name="ImageWidth"; break;
+			case IMAGE_LENGTH: name="ImageLength"; break;
+			case STRIP_OFFSETS: name="StripOffsets"; break;
+			case ORIENTATION: name="Orientation"; break;
+			case PHOTO_INTERP: name="PhotoInterp"; break;
+			case IMAGE_DESCRIPTION: name="ImageDescription"; break;
+			case BITS_PER_SAMPLE: name="BitsPerSample"; break;
+			case SAMPLES_PER_PIXEL: name="SamplesPerPixel"; break;
+			case ROWS_PER_STRIP: name="RowsPerStrip"; break;
+			case STRIP_BYTE_COUNT: name="StripByteCount"; break;
+			case X_RESOLUTION: name="XResolution"; break;
+			case Y_RESOLUTION: name="YResolution"; break;
+			case RESOLUTION_UNIT: name="ResolutionUnit"; break;
+			case SOFTWARE: name="Software"; break;
+			case DATE_TIME: name="DateTime"; break;
+			case ARTIST: name="Artist"; break;
+			case HOST_COMPUTER: name="HostComputer"; break;
+			case PLANAR_CONFIGURATION: name="PlanarConfiguration"; break;
+			case COMPRESSION: name="Compression"; break; 
+			case PREDICTOR: name="Predictor"; break; 
+			case COLOR_MAP: name="ColorMap"; break; 
+			case SAMPLE_FORMAT: name="SampleFormat"; break; 
+			case JPEG_TABLES: name="JPEGTables"; break; 
+			case NIH_IMAGE_HDR: name="NIHImageHeader"; break; 
+			case META_DATA_BYTE_COUNTS: name="MetaDataByteCounts"; break; 
+			case META_DATA: name="MetaData"; break; 
+			default: name="???"; break;
+		}
+		return name;
+	}
+
+	double getRational(long loc) throws IOException {
+		long saveLoc = in.getLongFilePointer();
+		in.seek(loc);
+		double numerator = getUnsignedInt();
+		double denominator = getUnsignedInt();
+		in.seek(saveLoc);
+		if (denominator!=0.0)
+			return numerator/denominator;
+		else
+			return 0.0;
+	}
+	
+	FileInfo OpenIFD() throws IOException {
+	// Get Image File Directory data
+		int tag, fieldType, count, value;
+		int nEntries = getShort();
+		if (nEntries<1 || nEntries>1000)
+			return null;
+		ifdCount++;
+		if ((ifdCount%50)==0 && ifdCount>0)
+			ij.IJ.showStatus("Opening IFDs: "+ifdCount);
+		FileInfo fi = new FileInfo();
+		fi.fileType = FileInfo.BITMAP;  //BitsPerSample defaults to 1
+		for (int i=0; i0?fi.stripOffsets[0]:value;
+					if (count>1 && (((long)fi.stripOffsets[count-1])&0xffffffffL)<(((long)fi.stripOffsets[0])&0xffffffffL))
+						fi.offset = fi.stripOffsets[count-1];
+					break;
+				case STRIP_BYTE_COUNT:
+					if (count==1)
+						fi.stripLengths = new int[] {value};
+					else {
+						long saveLoc = in.getLongFilePointer();
+						in.seek(lvalue);
+						fi.stripLengths = new int[count];
+						for (int c=0; c1) {
+							long saveLoc = in.getLongFilePointer();
+							in.seek(lvalue);
+							int bitDepth = getShort();
+							if (bitDepth==8)
+								fi.fileType = FileInfo.GRAY8;
+							else if (bitDepth==16)
+								fi.fileType = FileInfo.GRAY16_UNSIGNED;
+							else
+								error("ImageJ cannot open interleaved "+bitDepth+"-bit images.");
+							in.seek(saveLoc);
+						}
+						break;
+				case SAMPLES_PER_PIXEL:
+					fi.samplesPerPixel = value;
+					if (value==3 && fi.fileType==FileInfo.GRAY8)
+						fi.fileType = FileInfo.RGB;
+					else if (value==3 && fi.fileType==FileInfo.GRAY16_UNSIGNED)
+						fi.fileType = FileInfo.RGB48;
+					else if (value==4 && fi.fileType==FileInfo.GRAY8)
+						fi.fileType = photoInterp==5?FileInfo.CMYK:FileInfo.ARGB;
+					else if (value==4 && fi.fileType==FileInfo.GRAY16_UNSIGNED) {
+						fi.fileType = FileInfo.RGB48;
+						if (photoInterp==5)  //assume cmyk
+							fi.whiteIsZero = true;
+					}
+					break;
+				case ROWS_PER_STRIP:
+					fi.rowsPerStrip = value;
+					break;
+				case X_RESOLUTION:
+					double xScale = getRational(lvalue); 
+					if (xScale!=0.0) fi.pixelWidth = 1.0/xScale; 
+					break;
+				case Y_RESOLUTION:
+					double yScale = getRational(lvalue); 
+					if (yScale!=0.0) fi.pixelHeight = 1.0/yScale; 
+					break;
+				case RESOLUTION_UNIT:
+					if (value==1&&fi.unit==null)
+						fi.unit = " ";
+					else if (value==2) {
+						if (fi.pixelWidth==1.0/72.0) {
+							fi.pixelWidth = 1.0;
+							fi.pixelHeight = 1.0;
+						} else
+							fi.unit = "inch";
+					} else if (value==3)
+						fi.unit = "cm";
+					break;
+				case PLANAR_CONFIGURATION:  // 1=chunky, 2=planar
+					if (value==2 && fi.fileType==FileInfo.RGB48)
+							 fi.fileType = FileInfo.RGB48_PLANAR;
+					else if (value==2 && fi.fileType==FileInfo.RGB)
+						fi.fileType = FileInfo.RGB_PLANAR;
+					else if (value!=2 && !(fi.samplesPerPixel==1||fi.samplesPerPixel==3||fi.samplesPerPixel==4)) {
+						String msg = "Unsupported SamplesPerPixel: " + fi.samplesPerPixel;
+						error(msg);
+					}
+					break;
+				case COMPRESSION:
+					if (value==5)  {// LZW compression
+						fi.compression = FileInfo.LZW;
+						if (fi.fileType==FileInfo.GRAY12_UNSIGNED)
+							error("ImageJ cannot open 12-bit LZW-compressed TIFFs");
+					} else if (value==32773)  // PackBits compression
+						fi.compression = FileInfo.PACK_BITS;
+					else if (value==32946 || value==8)
+						fi.compression = FileInfo.ZIP;
+					else if (value!=1 && value!=0 && !(value==7&&fi.width<500)) {
+						// don't abort with Spot camera compressed (7) thumbnails
+						// otherwise, this is an unknown compression type
+						fi.compression = FileInfo.COMPRESSION_UNKNOWN;
+						error("ImageJ cannot open TIFF files " +
+							"compressed in this fashion ("+value+")");
+					}
+					break;
+				case SOFTWARE: case DATE_TIME: case HOST_COMPUTER: case ARTIST:
+					if (ifdCount==1) {
+						byte[] bytes = getString(count, lvalue);
+						String s = bytes!=null?new String(bytes):null;
+						saveMetadata(getName(tag), s);
+					}
+					break;
+				case PREDICTOR:
+					if (value==2 && fi.compression==FileInfo.LZW)
+						fi.compression = FileInfo.LZW_WITH_DIFFERENCING;
+					if (value==3)
+						IJ.log("TiffDecoder: unsupported predictor value of 3");
+					break;
+				case COLOR_MAP: 
+					if (count==768)
+						getColorMap(lvalue, fi);
+					break;
+				case TILE_WIDTH:
+					error("ImageJ cannot open tiled TIFFs.\nTry using the Bio-Formats plugin.");
+					break;
+				case SAMPLE_FORMAT:
+					if (fi.fileType==FileInfo.GRAY32_INT && value==FLOATING_POINT)
+						fi.fileType = FileInfo.GRAY32_FLOAT;
+					if (fi.fileType==FileInfo.GRAY16_UNSIGNED) {
+						if (value==SIGNED)
+							fi.fileType = FileInfo.GRAY16_SIGNED;
+						if (value==FLOATING_POINT)
+							error("ImageJ cannot open 16-bit float TIFFs");
+					}
+					break;
+				case JPEG_TABLES:
+					if (fi.compression==FileInfo.JPEG)
+						error("Cannot open JPEG-compressed TIFFs with separate tables");
+					break;
+				case IMAGE_DESCRIPTION: 
+					if (ifdCount==1) {
+						byte[] s = getString(count, lvalue);
+						if (s!=null) saveImageDescription(s,fi);
+					}
+					break;
+				case ORIENTATION:
+					fi.nImages = 0; // file not created by ImageJ so look at all the IFDs
+					break;
+				case METAMORPH1: case METAMORPH2:
+					if ((name.indexOf(".STK")>0||name.indexOf(".stk")>0) && fi.compression==FileInfo.COMPRESSION_NONE) {
+						if (tag==METAMORPH2)
+							fi.nImages=count;
+						else
+							fi.nImages=9999;
+					}
+					break;
+				case IPLAB: 
+					fi.nImages=value;
+					break;
+				case NIH_IMAGE_HDR: 
+					if (count==256)
+						decodeNIHImageHeader(value, fi);
+					break;
+ 				case META_DATA_BYTE_COUNTS: 
+					long saveLoc = in.getLongFilePointer();
+					in.seek(lvalue);
+					metaDataCounts = new int[count];
+					for (int c=0; c10000 && tag<32768 && ifdCount>1)
+						return null;
+			}
+		}
+		fi.fileFormat = fi.TIFF;
+		fi.fileName = name;
+		fi.directory = directory;
+		if (url!=null)
+			fi.url = url;
+		return fi;
+	}
+
+	void getMetaData(int loc, FileInfo fi) throws IOException {
+		if (metaDataCounts==null || metaDataCounts.length==0)
+			return;
+		int maxTypes = 10;
+		long saveLoc = in.getLongFilePointer();
+		in.seek(loc);
+		int n = metaDataCounts.length;
+		int hdrSize = metaDataCounts[0];
+		if (hdrSize<12 || hdrSize>804) {
+			in.seek(saveLoc);
+			return;
+		}
+		int magicNumber = getInt();
+		if (magicNumber!=MAGIC_NUMBER)  { // "IJIJ"
+			in.seek(saveLoc);
+			return;
+		}
+		int nTypes = (hdrSize-4)/8;
+		int[] types = new int[nTypes];
+		int[] counts = new int[nTypes];		
+		if (debugMode) {
+			dInfo += "Metadata:\n";
+			dInfo += "   Entries: "+(metaDataCounts.length-1)+"\n";
+			dInfo += "   Types: "+nTypes+"\n";
+		}
+		int extraMetaDataEntries = 0;
+		int index = 1;
+		for (int i=0; i=metaDataCounts.length) index=1;
+				String lenstr = count==1?", length=":", length[0]=";
+				dInfo += "   "+i+", type="+id+", count="+count+lenstr+len+"\n";
+			}
+		}
+		fi.metaDataTypes = new int[extraMetaDataEntries];
+		fi.metaData = new byte[extraMetaDataEntries][];
+		int start = 1;
+		int eMDindex = 0;
+		for (int i=0; i0) {
+				if (len>buffer.length)
+					buffer = new byte[len];
+				in.readFully(buffer, len);
+				len /= 2;
+				char[] chars = new char[len];
+				if (littleEndian) {
+					for (int j=0, k=0; jbuffer.length)
+				buffer = new byte[len];
+			in.readFully(buffer, len);
+			len /= 2;
+			char[] chars = new char[len];
+			if (littleEndian) {
+				for (int j=0, k=0; jbuffer.length)
+                buffer = new byte[len];
+            in.readFully(buffer, len);
+		}
+	}
+
+	public void enableDebugging() {
+		debugMode = true;
+	}
+		
+	public FileInfo[] getTiffInfo() throws IOException {
+		long ifdOffset;
+		ArrayList list = new ArrayList();
+		if (in==null)
+			in = new RandomAccessStream(new RandomAccessFile(new File(directory+name), "r"));
+		ifdOffset = OpenImageFileHeader();
+		if (ifdOffset<0L) {
+			in.close();
+			return null;
+		}
+		if (debugMode) dInfo = "\n  " + name + ": opening\n";
+		while (ifdOffset>0L) {
+			in.seek(ifdOffset);
+			FileInfo fi = OpenIFD();
+			if (fi!=null) {
+				list.add(fi);
+				ifdOffset = ((long)getInt())&0xffffffffL;
+			} else
+				ifdOffset = 0L;
+			if (debugMode && ifdCount<10) dInfo += "nextIFD=" + ifdOffset + "\n";
+			if (fi!=null && fi.nImages>1)
+				ifdOffset = 0L;   // ignore extra IFDs in ImageJ and NIH Image stacks
+		}
+		if (list.size()==0) {
+			in.close();
+			return null;
+		} else {
+			FileInfo[] info = (FileInfo[])list.toArray(new FileInfo[list.size()]);
+			if (debugMode) info[0].debugInfo = dInfo;
+			if (url!=null) {
+				in.seek(0);
+				info[0].inputStream = in;
+			} else
+				in.close();
+			if (info[0].info==null)
+				info[0].info = tiffMetadata;
+			FileInfo fi = info[0];
+			if (fi.fileType==FileInfo.GRAY16_UNSIGNED && fi.description==null)
+				fi.lutSize = 0; // ignore troublesome non-ImageJ 16-bit LUTs
+			if (debugMode) {
+				int n = info.length;
+				fi.debugInfo += "number of IFDs: "+ n + "\n";
+				fi.debugInfo += "offset to first image: "+fi.getOffset()+ "\n";
+				fi.debugInfo += "gap between images: "+getGapInfo(info) + "\n";
+				fi.debugInfo += "little-endian byte order: "+fi.intelByteOrder + "\n";
+			}
+			return info;
+		}
+	}
+	
+	String getGapInfo(FileInfo[] fi) {
+		if (fi.length<2) return "0";
+		long minGap = Long.MAX_VALUE;
+		long maxGap = -Long.MAX_VALUE;
+		for (int i=1; imaxGap) maxGap = gap;
+		}
+		long imageSize = fi[0].width*fi[0].height*fi[0].getBytesPerPixel();
+		minGap -= imageSize;
+		maxGap -= imageSize;
+		if (minGap==maxGap)
+			return ""+minGap;
+		else 
+			return "varies ("+minGap+" to "+maxGap+")";
+	}
+
+}
diff --git a/src/ij/io/TiffEncoder.java b/src/ij/io/TiffEncoder.java
new file mode 100644
index 0000000..8989ec8
--- /dev/null
+++ b/src/ij/io/TiffEncoder.java
@@ -0,0 +1,555 @@
+package ij.io;
+import java.io.*;
+
+/**Saves an image described by a FileInfo object as an uncompressed TIFF file.*/
+public class TiffEncoder {
+	static final int HDR_SIZE = 8;
+	static final int MAP_SIZE = 768; // in 16-bit words
+	static final int BPS_DATA_SIZE = 6;
+	static final int SCALE_DATA_SIZE = 16;
+		
+	private FileInfo fi;
+	private int bitsPerSample;
+	private int photoInterp;
+	private int samplesPerPixel;
+	private int nEntries;
+	private int ifdSize;
+	private long imageOffset;
+	private int imageSize;
+	private long stackSize;
+	private byte[] description;
+	private int metaDataSize;
+	private int nMetaDataTypes;
+	private int nMetaDataEntries;
+	private int nSliceLabels;
+	private int extraMetaDataEntries;
+	private int scaleSize;
+	private boolean littleEndian = ij.Prefs.intelByteOrder;
+	private byte buffer[] = new byte[8];
+	private int colorMapSize = 0;
+
+		
+	public TiffEncoder (FileInfo fi) {
+		this.fi = fi;
+		fi.intelByteOrder = littleEndian;
+		bitsPerSample = 8;
+		samplesPerPixel = 1;
+		nEntries = 9;
+		int bytesPerPixel = 1;
+		int bpsSize = 0;
+
+		switch (fi.fileType) {
+			case FileInfo.GRAY8:
+				photoInterp = fi.whiteIsZero?0:1;
+				break;
+			case FileInfo.GRAY16_UNSIGNED:
+			case FileInfo.GRAY16_SIGNED:
+				bitsPerSample = 16;
+				photoInterp = fi.whiteIsZero?0:1;
+				if (fi.lutSize>0) {
+					nEntries = 10;
+					colorMapSize = MAP_SIZE*2;
+				}
+				bytesPerPixel = 2;
+				break;
+			case FileInfo.GRAY32_FLOAT:
+				bitsPerSample = 32;
+				photoInterp = fi.whiteIsZero?0:1;
+				if (fi.lutSize>0) {
+					nEntries = 10;
+					colorMapSize = MAP_SIZE*2;
+				}
+				bytesPerPixel = 4;
+				break;
+			case FileInfo.RGB:
+				photoInterp = 2;
+				samplesPerPixel = 3;
+				bytesPerPixel = 3;
+				bpsSize = BPS_DATA_SIZE;
+				break;
+			case FileInfo.RGB48:
+				bitsPerSample = 16;
+				photoInterp = 2;
+				samplesPerPixel = 3;
+				bytesPerPixel = 6;
+				fi.nImages /= 3;
+				bpsSize = BPS_DATA_SIZE;
+				break;
+			case FileInfo.COLOR8:
+				photoInterp = 3;
+				nEntries = 10;
+				colorMapSize = MAP_SIZE*2;
+				break;
+			default:
+				photoInterp = 0;
+		}
+		if (fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0)
+			nEntries += 3; // XResolution, YResolution and ResolutionUnit
+		if (fi.fileType==fi.GRAY32_FLOAT)
+			nEntries++; // SampleFormat tag
+		makeDescriptionString();
+		if (description!=null)
+			nEntries++;  // ImageDescription tag
+		long size = (long)fi.width*fi.height*bytesPerPixel;
+		imageSize = size<=0xffffffffL?(int)size:0;
+		stackSize = (long)imageSize*fi.nImages;
+		metaDataSize = getMetaDataSize();
+		if (metaDataSize>0)
+			nEntries += 2; // MetaData & MetaDataCounts
+		ifdSize = 2 + nEntries*12 + 4;
+		int descriptionSize = description!=null?description.length:0;
+		scaleSize = fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0?SCALE_DATA_SIZE:0;
+		imageOffset = HDR_SIZE+ifdSize+bpsSize+descriptionSize+scaleSize+colorMapSize + nMetaDataEntries*4 + metaDataSize;
+		fi.offset = (int)imageOffset;
+		//ij.IJ.log(imageOffset+", "+ifdSize+", "+bpsSize+", "+descriptionSize+", "+scaleSize+", "+colorMapSize+", "+nMetaDataEntries*4+", "+metaDataSize);
+	}
+	
+	/** Saves the image as a TIFF file. The OutputStream is not closed.
+		The fi.pixels field must contain the image data. If fi.nImages>1
+		then fi.pixels must be a 2D array. The fi.offset field is ignored. */
+	public void write(OutputStream out) throws IOException {
+		writeHeader(out);
+		long nextIFD = 0L;
+		if (fi.nImages>1)
+			nextIFD = imageOffset+stackSize;
+		boolean bigTiff = nextIFD+fi.nImages*ifdSize>=0xffffffffL;
+		if (bigTiff)
+			nextIFD = 0L;
+		writeIFD(out, (int)imageOffset, (int)nextIFD);
+		if (fi.fileType==FileInfo.RGB||fi.fileType==FileInfo.RGB48)
+			writeBitsPerPixel(out);
+		if (description!=null)
+			writeDescription(out);
+		if (scaleSize>0)
+			writeScale(out);
+		if (colorMapSize>0)
+			writeColorMap(out);
+		if (metaDataSize>0)
+			writeMetaData(out);
+		new ImageWriter(fi).write(out);
+		if (nextIFD>0L) {
+			int ifdSize2 = ifdSize;
+			if (metaDataSize>0) {
+				metaDataSize = 0;
+				nEntries -= 2;
+				ifdSize2 -= 2*12;
+			}
+			for (int i=2; i<=fi.nImages; i++) {
+				if (i==fi.nImages)
+					nextIFD = 0;
+				else
+					nextIFD += ifdSize2;
+				imageOffset += imageSize;
+				writeIFD(out, (int)imageOffset, (int)nextIFD);
+			}
+		} else if (bigTiff)
+				ij.IJ.log("Stack is larger than 4GB. Most TIFF readers will only open the first image. Use this information to open as raw:\n"+fi);
+	}
+	
+	public void write(DataOutputStream out) throws IOException {
+		write((OutputStream)out);
+	}
+
+	int getMetaDataSize() {
+		nSliceLabels = 0;
+		nMetaDataEntries = 0;
+		int size = 0;
+		int nTypes = 0;
+		if (fi.info!=null && fi.info.length()>0) {
+			nMetaDataEntries = 1;
+			size = fi.info.length()*2;
+			nTypes++;
+		}
+		if (fi.sliceLabels!=null) {
+			int max = Math.min(fi.sliceLabels.length, fi.nImages);
+			boolean isNonNullLabel = false;
+			for (int i=0; i0) {
+					isNonNullLabel = true;
+					break;
+				}
+			}
+			if (isNonNullLabel) {
+				for (int i=0; i0) nTypes++;
+				nMetaDataEntries += nSliceLabels;
+			}
+		}
+
+		if (fi.displayRanges!=null) {
+			nMetaDataEntries++;
+			size += fi.displayRanges.length*8;
+			nTypes++;
+		}
+
+		if (fi.channelLuts!=null) {
+			for (int i=0; i0) nMetaDataEntries++; // add entry for header
+		int hdrSize = 4 + nTypes*8;
+		if (size>0) size += hdrSize;
+		nMetaDataTypes = nTypes;
+		return size;
+	}
+	
+	/** Writes the 8-byte image file header. */
+	void writeHeader(OutputStream out) throws IOException {
+		byte[] hdr = new byte[8];
+		if (littleEndian) {
+			hdr[0] = 73; // "II" (Intel byte order)
+			hdr[1] = 73;
+			hdr[2] = 42;  // 42 (magic number)
+			hdr[3] = 0;
+			hdr[4] = 8;  // 8 (offset to first IFD)
+			hdr[5] = 0;
+			hdr[6] = 0;
+			hdr[7] = 0;
+		} else {
+			hdr[0] = 77; // "MM" (Motorola byte order)
+			hdr[1] = 77;
+			hdr[2] = 0;  // 42 (magic number)
+			hdr[3] = 42;
+			hdr[4] = 0;  // 8 (offset to first IFD)
+			hdr[5] = 0;
+			hdr[6] = 0;
+			hdr[7] = 8;
+		}
+		out.write(hdr);
+	}
+	
+	/** Writes one 12-byte IFD entry. */
+	void writeEntry(OutputStream out, int tag, int fieldType, int count, int value) throws IOException {
+		writeShort(out, tag);
+		writeShort(out, fieldType);
+		writeInt(out, count);
+		if (count==1 && fieldType==TiffDecoder.SHORT) {
+			writeShort(out, value);
+			writeShort(out, 0);
+		} else
+			writeInt(out, value); // may be an offset
+	}
+	
+	/** Writes one IFD (Image File Directory). */
+	void writeIFD(OutputStream out, int imageOffset, int nextIFD) throws IOException {	
+		int tagDataOffset = HDR_SIZE + ifdSize;
+		writeShort(out, nEntries);
+		writeEntry(out, TiffDecoder.NEW_SUBFILE_TYPE, 4, 1, 0);
+		writeEntry(out, TiffDecoder.IMAGE_WIDTH, 4, 1, fi.width);
+		writeEntry(out, TiffDecoder.IMAGE_LENGTH, 4, 1, fi.height);
+		if (fi.fileType==FileInfo.RGB||fi.fileType==FileInfo.RGB48) {
+			writeEntry(out, TiffDecoder.BITS_PER_SAMPLE,  3, 3, tagDataOffset);
+			tagDataOffset += BPS_DATA_SIZE;
+		} else
+			writeEntry(out, TiffDecoder.BITS_PER_SAMPLE,  3, 1, bitsPerSample);
+		writeEntry(out, TiffDecoder.PHOTO_INTERP,     3, 1, photoInterp);
+		if (description!=null) {
+			writeEntry(out, TiffDecoder.IMAGE_DESCRIPTION, 2, description.length, tagDataOffset);
+			tagDataOffset += description.length;
+		}
+		writeEntry(out, TiffDecoder.STRIP_OFFSETS,    4, 1, imageOffset);
+		writeEntry(out, TiffDecoder.SAMPLES_PER_PIXEL,3, 1, samplesPerPixel);
+		writeEntry(out, TiffDecoder.ROWS_PER_STRIP,   3, 1, fi.height);
+		writeEntry(out, TiffDecoder.STRIP_BYTE_COUNT, 4, 1, imageSize);
+		if (fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0) {
+			writeEntry(out, TiffDecoder.X_RESOLUTION, 5, 1, tagDataOffset);
+			writeEntry(out, TiffDecoder.Y_RESOLUTION, 5, 1, tagDataOffset+8);
+			tagDataOffset += SCALE_DATA_SIZE;
+			int unit = 1;
+			if (fi.unit.equals("inch"))
+				unit = 2;
+			else if (fi.unit.equals("cm"))
+				unit = 3;
+			writeEntry(out, TiffDecoder.RESOLUTION_UNIT, 3, 1, unit);
+		}
+		if (fi.fileType==fi.GRAY32_FLOAT) {
+			int format = TiffDecoder.FLOATING_POINT;
+			writeEntry(out, TiffDecoder.SAMPLE_FORMAT, 3, 1, format);
+		}
+		if (colorMapSize>0) {
+			writeEntry(out, TiffDecoder.COLOR_MAP, 3, MAP_SIZE, tagDataOffset);
+			tagDataOffset += MAP_SIZE*2;
+		}
+		if (metaDataSize>0) {
+			writeEntry(out, TiffDecoder.META_DATA_BYTE_COUNTS, 4, nMetaDataEntries, tagDataOffset);
+			writeEntry(out, TiffDecoder.META_DATA, 1, metaDataSize, tagDataOffset+4*nMetaDataEntries);
+			tagDataOffset += nMetaDataEntries*4 + metaDataSize;
+		}
+		writeInt(out, nextIFD);
+	}
+	
+	/** Writes the 6 bytes of data required by RGB BitsPerSample tag. */
+	void writeBitsPerPixel(OutputStream out) throws IOException {
+		int bitsPerPixel = fi.fileType==FileInfo.RGB48?16:8;
+		writeShort(out, bitsPerPixel);
+		writeShort(out, bitsPerPixel);
+		writeShort(out, bitsPerPixel);
+	}
+
+	/** Writes the 16 bytes of data required by the XResolution and YResolution tags. */
+	void writeScale(OutputStream out) throws IOException {
+		double xscale = 1.0/fi.pixelWidth;
+		double yscale = 1.0/fi.pixelHeight;
+		double scale = 1000000.0;
+		if (xscale*scale>Integer.MAX_VALUE||yscale*scale>Integer.MAX_VALUE)
+			scale = (int)(Integer.MAX_VALUE/Math.max(xscale,yscale));
+		writeInt(out, (int)(xscale*scale));
+		writeInt(out, (int)scale);
+		writeInt(out, (int)(yscale*scale));
+		writeInt(out, (int)scale);
+	}
+
+	/** Writes the variable length ImageDescription string. */
+	void writeDescription(OutputStream out) throws IOException {
+		out.write(description,0,description.length);
+	}
+
+	/** Writes color palette following the image. */
+	void writeColorMap(OutputStream out) throws IOException {
+		byte[] colorTable16 = new byte[MAP_SIZE*2];
+		int j=littleEndian?1:0;
+		for (int i=0; i0)
+			writeInt(out, fi.info.length()*2);
+		for (int i=0; i0) {
+			writeInt(out, TiffDecoder.LABELS); // type="labl"
+			writeInt(out, nSliceLabels); // count
+		}
+		if (fi.displayRanges!=null) {
+			writeInt(out, TiffDecoder.RANGES); // type="rang"
+			writeInt(out, 1); // count
+		}
+		if (fi.channelLuts!=null) {
+			writeInt(out, TiffDecoder.LUTS); // type="luts"
+			writeInt(out, fi.channelLuts.length); // count
+		}
+		if (fi.plot!=null) {
+			writeInt(out, TiffDecoder.PLOT); // type="plot"
+			writeInt(out, 1); // count
+		}
+		if (fi.roi!=null) {
+			writeInt(out, TiffDecoder.ROI); // type="roi "
+			writeInt(out, 1); // count
+		}
+		if (fi.overlay!=null) {
+			writeInt(out, TiffDecoder.OVERLAY); // type="over"
+			writeInt(out, fi.overlay.length); // count
+		}
+		if (fi.properties!=null) {
+			writeInt(out, TiffDecoder.PROPERTIES); // type="prop"
+			writeInt(out, fi.properties.length); // count
+		}
+		for (int i=0; i>>8)&255);
+ 		} else {
+        	out.write((v>>>8)&255);
+        	out.write(v&255);
+        }
+	}
+
+	final void writeInt(OutputStream out, int v) throws IOException {
+		if (littleEndian) {
+        	out.write(v&255);
+        	out.write((v>>>8)&255);
+        	out.write((v>>>16)&255);
+         	out.write((v>>>24)&255);
+		} else {
+        	out.write((v>>>24)&255);
+        	out.write((v>>>16)&255);
+        	out.write((v>>>8)&255);
+        	out.write(v&255);
+        }
+	}
+
+    final void writeLong(OutputStream out, long v) throws IOException {
+    	if (littleEndian) {
+			buffer[7] = (byte)(v>>>56);
+			buffer[6] = (byte)(v>>>48);
+			buffer[5] = (byte)(v>>>40);
+			buffer[4] = (byte)(v>>>32);
+			buffer[3] = (byte)(v>>>24);
+			buffer[2] = (byte)(v>>>16);
+			buffer[1] = (byte)(v>>> 8);
+			buffer[0] = (byte)v;
+			out.write(buffer, 0, 8);
+        } else {
+			buffer[0] = (byte)(v>>>56);
+			buffer[1] = (byte)(v>>>48);
+			buffer[2] = (byte)(v>>>40);
+			buffer[3] = (byte)(v>>>32);
+			buffer[4] = (byte)(v>>>24);
+			buffer[5] = (byte)(v>>>16);
+			buffer[6] = (byte)(v>>> 8);
+			buffer[7] = (byte)v;
+			out.write(buffer, 0, 8);
+        }
+     }
+
+    final void writeDouble(OutputStream out, double v) throws IOException {
+		writeLong(out, Double.doubleToLongBits(v));
+    }
+    
+	final void writeChars(OutputStream out, String s) throws IOException {
+        int len = s.length();
+        if (littleEndian) {
+			for (int i = 0 ; i < len ; i++) {
+				int v = s.charAt(i);
+				out.write(v&255); 
+				out.write((v>>>8)&255); 
+			}
+        } else {
+			for (int i = 0 ; i < len ; i++) {
+				int v = s.charAt(i);
+				out.write((v>>>8)&255); 
+				out.write(v&255); 
+			}
+        }
+    }
+    
+}
diff --git a/src/ij/macro/Debugger.java b/src/ij/macro/Debugger.java
new file mode 100644
index 0000000..2622c91
--- /dev/null
+++ b/src/ij/macro/Debugger.java
@@ -0,0 +1,10 @@
+package ij.macro;
+
+	public interface Debugger {
+
+		public static final int NOT_DEBUGGING=0, STEP=1, TRACE=2, FAST_TRACE=3,
+			RUN_TO_COMPLETION=4, RUN_TO_CARET=5;
+		
+		public int debug(Interpreter interp, int mode);
+			
+}
diff --git a/src/ij/macro/ExtensionDescriptor.java b/src/ij/macro/ExtensionDescriptor.java
new file mode 100644
index 0000000..c98b011
--- /dev/null
+++ b/src/ij/macro/ExtensionDescriptor.java
@@ -0,0 +1,298 @@
+package ij.macro;
+import ij.IJ;
+import java.util.ArrayList;
+
+public class ExtensionDescriptor {
+  public String name;
+  public int[] argTypes;
+  public MacroExtension handler;
+  
+  public ExtensionDescriptor(String theName, int[] theArgTypes, MacroExtension theHandler) {
+    this.name = theName;
+    this.argTypes = theArgTypes;
+    this.handler = theHandler;
+  }
+  
+  public static ExtensionDescriptor newDescriptor(String theName, MacroExtension theHandler, int[] types) {
+    int[] argTypes = new int[types.length];
+    for (int i=0; i < types.length; ++i) {
+      argTypes[i] = types[i];
+    }
+    
+    return new ExtensionDescriptor(theName, argTypes, theHandler);
+  }
+  
+  public static ExtensionDescriptor newDescriptor(String theName, MacroExtension theHandler) {
+    return newDescriptor(theName, theHandler, new int[0]);
+  }
+  
+  public static ExtensionDescriptor newDescriptor(String theName, MacroExtension theHandler, int type) {
+    return newDescriptor(theName, theHandler, new int[] {type});
+  }
+  
+  public static ExtensionDescriptor newDescriptor(String theName, MacroExtension theHandler, int t1, int t2) {
+    return newDescriptor(theName, theHandler, new int[] {t1, t2});
+  }
+  
+  public static ExtensionDescriptor newDescriptor(String theName, MacroExtension theHandler, int t1, int t2, int t3) {
+    return newDescriptor(theName, theHandler, new int[] {t1, t2, t3});
+  }
+  
+  public static ExtensionDescriptor newDescriptor(String theName, MacroExtension theHandler, int t1, int t2, int t3, int t4) {
+    return newDescriptor(theName, theHandler, new int[] {t1, t2, t3, t4});
+  }  
+    
+  public static ExtensionDescriptor newDescriptor(String theName, MacroExtension theHandler, Integer[] types) {
+    int[] argTypes = new int[types.length];
+    for (int i=0; i < types.length; ++i) {
+      argTypes[i] = types[i].intValue();
+    }
+    
+    return new ExtensionDescriptor(theName, argTypes, theHandler);
+  }
+  
+  public static boolean isOptionalArg(int argType) {
+    return (argType & MacroExtension.ARG_OPTIONAL) == MacroExtension.ARG_OPTIONAL;
+  }
+  
+  public static boolean isOutputArg(int argType) {
+    return (argType & MacroExtension.ARG_OUTPUT) == MacroExtension.ARG_OUTPUT;
+  }
+  
+  public static int getRawType(int argType) {
+    return argType & ~(MacroExtension.ARG_OUTPUT|MacroExtension.ARG_OPTIONAL);
+  }
+  
+  public boolean checkArguments(Object[] args) {
+    for (int i=0; i < argTypes.length; ++i) {
+      boolean optional = isOptionalArg(argTypes[i]);
+      boolean output   = isOutputArg(argTypes[i]);
+      
+      int rawType = getRawType(argTypes[i]);
+      
+      if (args.length < i)
+        return optional ? true : false;
+      
+      switch(rawType) {
+      case MacroExtension.ARG_STRING:
+        if (output) {
+          if (! (args[i] instanceof String[])) return false;
+        } else {
+          if (! (args[i] instanceof String)) return false;
+        }
+      case MacroExtension.ARG_NUMBER:
+        if (output) {
+          if (! (args[i] instanceof Double[])) return false;
+        } else {
+          if (!(args[i] instanceof Double)) return false;
+        }
+      case MacroExtension.ARG_ARRAY:
+        if (!(args[i] instanceof Object[])) return false;
+      }
+    }
+    
+    return true;
+  }
+  
+  public static String getTypeName(int argType) {
+    switch(getRawType(argType)) {
+    case MacroExtension.ARG_STRING:
+      return "string";
+    case MacroExtension.ARG_NUMBER:
+      return "number";
+    case MacroExtension.ARG_ARRAY:
+      return "array";
+    default:
+      return "unknown";
+    }
+  }
+  
+  private static String getVariableTypename(int type) {
+    switch (type) {
+    case Variable.STRING:
+      return "string";
+    case Variable.NUMBER:
+      return "number";
+    case Variable.ARRAY:
+      return "array";
+    default:
+      return "unknown";
+    }
+  }
+  
+  // TODO: this doesn't account for "loops" in the arrays, which will result in an infinite loop
+  private static Object[] convertArray(Variable[] array) {
+    Object[] oArray = new Object[ array.length ];
+    for(int i=0; i < array.length; ++i) {
+      switch(array[i].getType()) {
+      case Variable.STRING:
+        oArray[i] = array[i].getString();
+        break;
+      case Variable.VALUE:
+        oArray[i] = new Double( array[i].getValue() );
+        break;
+      case Variable.ARRAY:
+        oArray[i] = convertArray(array[i].getArray());
+        break;
+      default:
+        oArray[i] = null;
+      }
+    }
+    
+    return oArray;
+  }
+  
+  Variable[] parseArgumentList(Functions func) {
+    Interpreter interp = func.interp;
+
+    Variable[] vArray = new Variable[argTypes.length];
+    int i=0;
+    do {
+      if (i >= argTypes.length) {
+        interp.error("too many arguments (expected "+argTypes.length+")");
+        return null;
+      }
+      boolean output   = isOutputArg(argTypes[i]);
+
+      Variable v;
+      if (output) {
+        v = func.getVariable();
+      } else {
+        v = new Variable();
+        switch (getRawType(argTypes[i])) {
+        case MacroExtension.ARG_STRING:
+          v.setString(func.getString());
+          break;
+        case MacroExtension.ARG_NUMBER:
+          v.setValue(interp.getExpression());
+          break;
+        case MacroExtension.ARG_ARRAY:
+          v.setArray(func.getArray());
+          break;
+        }
+      }
+      vArray[i] = v;
+      ++i;
+      interp.getToken();
+    } while (interp.token == ',');
+    
+    if (interp.token!=')')
+      interp.error("')' expected");
+
+    if (i < argTypes.length && !isOptionalArg(argTypes[i])) {
+      interp.error("too few arguments, expected "+argTypes.length+" but found "+i);
+    }
+    
+    return vArray;
+  }
+ 
+  
+  public static Object convertVariable(Interpreter interp, int rawType, Variable var) {
+    int type = getRawType(rawType);
+    boolean output = isOutputArg(rawType);
+
+    if (var == null)
+      return null;
+
+    switch (type) {
+    case MacroExtension.ARG_STRING:
+      if (!output && var.getType()!=Variable.STRING) {
+        interp.error("Expected string, but variable type is "+getVariableTypename(var.getType()));
+        return null;
+      }
+      if (output) {
+      	String s = var.getString();
+      	if (s==null) s = "";
+        return new String[] { s };
+      } else {
+        return var.getString();
+      }
+    case MacroExtension.ARG_NUMBER:
+      if (var.getType() != Variable.VALUE) {
+        interp.error("Expected number, but variable type is "+getVariableTypename(var.getType()));
+        return null;
+      }
+      if (output) {
+        return new Double[] { new Double(var.getValue()) };
+      } else {
+        return new Double( var.getValue() );
+      }
+    case MacroExtension.ARG_ARRAY:
+      if (var.getType() != Variable.ARRAY) {
+        interp.error("Expected array, but variable type is "+getVariableTypename(var.getType()));
+        return null;
+      }
+      return convertArray(var.getArray());
+    default:
+      interp.error("Unknown descriptor type "+type+" ("+rawType+")");
+      return null;
+    }
+  }
+
+  public static void convertOutputType(Variable variable, Object object) {
+    if (object instanceof String[]) {
+      String[] str = (String[]) object;
+      variable.setString(str[0]);
+    } else if (object instanceof Double[]) {
+      Double[] dbl = (Double[]) object;
+      variable.setValue(dbl[0].doubleValue());
+    } else if (object instanceof Object[]) {
+      Object[] arr = (Object[]) object;
+      Variable[] vArr = new Variable[arr.length];
+      for (int i=0; i < arr.length; ++i) {
+        vArr[i] = new Variable();
+        convertOutputType(vArr[i], arr[i]);
+      }
+      variable.setArray(vArr);
+    }
+  }
+  
+  public String dispatch(Functions func) {
+    Interpreter interp = func.interp;
+
+    if (argTypes.length==0) {
+      interp.getParens();
+      return handler.handleExtension(name, null);
+    }
+    interp.getLeftParen();
+    
+    Variable[] vArgs = null;
+    int next = interp.nextToken();
+    if (next != ')') {
+      vArgs = parseArgumentList(func);
+    }
+    
+    //for (int i=0; i < vArgs.length; ++i) {
+    //  Variable v = vArgs[i];
+    //  System.err.println("variable is "+(v!= null?v.toString():"(null)"));
+    //}
+    
+    Object[] args = new Object[ argTypes.length ];
+    if (vArgs==null && argTypes.length>0) {
+		interp.error("Argument expected");
+		return null;
+    }
+    // check variable types...
+    for (int i=0; i < argTypes.length; ++i) {
+      if (i >= vArgs.length) {
+        if (!ExtensionDescriptor.isOptionalArg(argTypes[i])) {
+          interp.error("Expected argument "+(i+1)+" of type "+ExtensionDescriptor.getTypeName(argTypes[i]));
+          return null;
+        } else {
+          break;
+        }
+      }
+      args[i] = ExtensionDescriptor.convertVariable(interp, argTypes[i], vArgs[i]);
+    }
+    
+    String retVal = handler.handleExtension(name, args);
+    for (int i=0; i < argTypes.length; ++i) {
+      if (i >= vArgs.length) break;
+      if (ExtensionDescriptor.isOutputArg(argTypes[i])) {
+        ExtensionDescriptor.convertOutputType(vArgs[i], args[i]);
+      }
+    }
+    
+    return retVal;
+  }
+}
diff --git a/src/ij/macro/FunctionFinder.java b/src/ij/macro/FunctionFinder.java
new file mode 100644
index 0000000..42ed351
--- /dev/null
+++ b/src/ij/macro/FunctionFinder.java
@@ -0,0 +1,232 @@
+package ij.macro;
+import ij.*;
+import ij.plugin.*;
+import ij.plugin.frame.*;
+import ij.gui.GUI;
+import java.awt.*;
+import java.awt.event.*;
+import java.util.Hashtable;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Set;
+
+/** This class implements the text editor's Macros/Find Functions command.
+	It was written by jerome.mutterer at ibmp.fr, and is based on Mark Longair's CommandFinder plugin.
+*/
+public class FunctionFinder implements TextListener,  WindowListener, KeyListener, ItemListener, ActionListener {
+	private static Dialog dialog;
+	private TextField prompt;
+	private List functions;
+	private Button insertButton, infoButton, closeButton;
+	private String [] commands;
+	private Editor editor;
+
+	public FunctionFinder(Editor editor) {
+
+		this.editor = editor;
+
+		String exists = IJ.runMacro("return File.exists(getDirectory('macros')+'functions.html');");
+		if (exists=="0")	{
+			String installLocalMacroFunctionsFile = "functions = File.openUrlAsString('"+IJ.URL+"/developer/macro/functions.html');\n"+
+			"f = File.open(getDirectory('macros')+'functions.html');\n"+
+			"print (f, functions);\n"+
+			"File.close(f);";
+			try { IJ.runMacro(installLocalMacroFunctionsFile);
+			} catch (Throwable e) { IJ.error("Problem downloading functions.html"); return;}
+		}
+		String f = IJ.runMacro("return File.openAsString(getDirectory('macros')+'functions.html');");
+		f = f.replaceAll(""", "\"");
+		String [] l = f.split("\n");
+		commands= new String [l.length];
+		int c=0;
+		for (int i=0; i")) {
+				commands[c]=line.substring(line.indexOf("")+3,line.indexOf(""));
+				c++;
+			}
+		}
+		if (c==0) {
+			IJ.error("ImageJ/macros/functions.html is corrupted");
+			return;
+		}
+
+		ImageJ imageJ = IJ.getInstance();
+		if (dialog==null) {
+			dialog = new Dialog(imageJ, "Built-in Functions");
+			dialog.setLayout(new BorderLayout());
+			dialog.addWindowListener(this);
+			Panel northPanel = new Panel();
+			prompt = new TextField("", 32);
+			prompt.addTextListener(this);
+			prompt.addKeyListener(this);
+			northPanel.add(prompt);
+			dialog.add(northPanel, BorderLayout.NORTH);
+			functions = new List(12);
+			functions.addKeyListener(this);
+			populateList("");
+			dialog.add(functions, BorderLayout.CENTER);
+			Panel buttonPanel = new Panel();
+			insertButton = new Button("Insert");
+			insertButton.addActionListener(this);
+			buttonPanel.add(insertButton);
+			infoButton = new Button("Info");
+			infoButton.addActionListener(this);
+			buttonPanel.add(infoButton);
+			closeButton = new Button("Close");
+			closeButton.addActionListener(this);
+			buttonPanel.add(closeButton);
+			dialog.add(buttonPanel, BorderLayout.SOUTH);
+			GUI.scale(dialog);
+			dialog.pack();
+		}
+
+		Frame frame = WindowManager.getFrontWindow();
+		if (frame==null) return;
+		java.awt.Point posi=frame.getLocationOnScreen();
+		int initialX = (int)posi.getX() + 38;
+		int initialY = (int)posi.getY() + 84;
+		dialog.setLocation(initialX,initialY);
+		dialog.setVisible(true);
+		dialog.toFront();
+	}
+
+	public FunctionFinder() {
+		this(null);
+	}
+
+	public void populateList(String matchingSubstring) {
+		String substring = matchingSubstring.toLowerCase();
+		functions.removeAll();
+		try {
+			for(int i=0; i= 0 ) {
+					functions.add(commands[i]);
+				}
+			}
+		} catch (Exception e){}
+	}
+
+	public void edPaste(String arg) {
+		Frame frame = WindowManager.getFrontWindow();
+		if (!(frame instanceof Editor))
+			return;
+
+		try {
+			TextArea ta = ((Editor)frame).getTextArea();
+			editor = (Editor)frame;
+			int start = ta.getSelectionStart( );
+			int end = ta.getSelectionEnd( );
+			try {
+				ta.replaceRange(arg.substring(0,arg.length()), start, end);
+			} catch (Exception e) { }
+			if (IJ.isMacOSX())
+				ta.setCaretPosition(start+arg.length());
+		} catch (Exception e) { }
+	}
+
+	public void itemStateChanged(ItemEvent ie) {
+		populateList(prompt.getText());
+	}
+
+	protected void runFromLabel(String listLabel) {
+		edPaste(listLabel);
+		closeAndRefocus();
+	}
+
+	public void close() {
+		closeAndRefocus();
+	}
+
+	public void closeAndRefocus() {
+		if (dialog!=null)
+			dialog.dispose();
+		if (editor!=null)
+			editor.toFront();
+	}
+
+	public void keyPressed(KeyEvent ke) {
+		int key = ke.getKeyCode();
+		int items = functions.getItemCount();
+		Object source = ke.getSource();
+		if (source==prompt) {
+			if (key==KeyEvent.VK_ENTER) {
+				if (1==items) {
+					String selected = functions.getItem(0);
+					edPaste(selected);
+				}
+			} else if (key==KeyEvent.VK_UP) {
+				functions.requestFocus();
+				if(items>0)
+					functions.select(functions.getItemCount()-1);
+			} else if (key==KeyEvent.VK_ESCAPE) {
+				closeAndRefocus();
+			} else if (key==KeyEvent.VK_DOWN)  {
+				functions.requestFocus();
+				if (items>0)
+					functions.select(0);
+			}
+		} else if (source==functions) {
+			if (key==KeyEvent.VK_ENTER) {
+				String selected = functions.getSelectedItem();
+				if (selected!=null)
+					edPaste(selected);
+			}
+			else if (key==KeyEvent.VK_ESCAPE) {
+				closeAndRefocus();
+			}
+			else if (key==KeyEvent.VK_BACK_SPACE || key==KeyEvent.VK_DELETE) {
+			/* If someone presses backspace or delete they probably
+			   want to remove the last letter from the search string, so
+			   switch the focus back to the prompt: */
+			prompt.requestFocus();
+		}
+		}
+	}
+
+	public void keyReleased(KeyEvent ke) { }
+
+	public void keyTyped(KeyEvent ke) { }
+
+	public void textValueChanged(TextEvent te) {
+		populateList(prompt.getText());
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		Object b = e.getSource();
+		if (b==insertButton) {
+			int index = functions.getSelectedIndex();
+			if (index>=0) {
+				String selected = functions.getItem(index);
+				edPaste(selected);
+			}
+		} else if (b==infoButton) {
+			String url = IJ.URL+"/developer/macro/functions.html";
+			int index = functions.getSelectedIndex();
+			if (index>=0) {
+				String selected = functions.getItem(index);
+				int index2 = selected.indexOf("(");
+				if (index2==-1)
+					index2 = selected.length();
+				url = url + "#" + selected.substring(0, index2);
+			}
+			IJ.runPlugIn("ij.plugin.BrowserLauncher", url);
+		} else if (b==closeButton)
+		closeAndRefocus();
+	}
+
+	public void windowClosing(WindowEvent e) {
+		closeAndRefocus();
+	}
+
+	public void windowActivated(WindowEvent e) { }
+	public void windowDeactivated(WindowEvent e) { }
+	public void windowClosed(WindowEvent e) { }
+	public void windowOpened(WindowEvent e) { }
+	public void windowIconified(WindowEvent e) { }
+	public void windowDeiconified(WindowEvent e) { }
+}
diff --git a/src/ij/macro/Functions.java b/src/ij/macro/Functions.java
new file mode 100644
index 0000000..2fe055d
--- /dev/null
+++ b/src/ij/macro/Functions.java
@@ -0,0 +1,8233 @@
+package ij.macro;
+import ij.*;
+import ij.process.*;
+import ij.gui.*;
+import ij.measure.*;
+import ij.plugin.*;
+import ij.plugin.filter.*;
+import ij.plugin.frame.*;
+import ij.text.*;
+import ij.io.*;
+import ij.util.*;
+import java.awt.*;
+import java.awt.image.*;
+import java.util.*;
+import java.io.*;
+import java.awt.event.KeyEvent;
+import java.lang.reflect.*;
+import java.net.URL;
+import java.awt.datatransfer.*;
+import java.awt.geom.*;
+
+
+/** This class implements the built-in macro functions. */
+public class Functions implements MacroConstants, Measurements {
+	Interpreter interp;
+	Program pgm;
+	boolean updateNeeded;
+	boolean autoUpdate = true;
+	ImageProcessor defaultIP;
+	ImagePlus defaultImp;
+	int imageType;
+	boolean fontSet;
+	Color globalColor;
+	double globalValue = Double.NaN;
+	int globalLineWidth;
+	Plot plot;
+	static int plotID;
+	int justification = ImageProcessor.LEFT_JUSTIFY;
+	Font font;
+	GenericDialog gd;
+	PrintWriter writer;
+	boolean altKeyDown, shiftKeyDown;
+	boolean antialiasedText;
+	StringBuffer buffer;
+	RoiManager roiManager;
+	Properties props;
+	CurveFitter fitter;
+	boolean showFitDialog;
+	boolean logFitResults;
+	boolean resultsPending;
+	Overlay offscreenOverlay;
+	Overlay overlayClipboard;
+	Roi roiClipboard;
+	GeneralPath overlayPath;
+	boolean overlayDrawLabels;
+	ResultsTable currentTable;
+	ResultsTable unUpdatedTable;
+
+	// save/restore settings
+	boolean saveSettingsCalled;
+	boolean usePointerCursor, hideProcessStackDialog;
+	float divideByZeroValue;
+	int jpegQuality;
+	int saveLineWidth;
+	boolean doScaling;
+	boolean weightedColor;
+	double[] weights;
+	boolean interpolateScaledImages, open100Percent, blackCanvas;
+	boolean useJFileChooser,debugMode;
+	Color foregroundColor, backgroundColor, roiColor;
+	boolean pointAutoMeasure, requireControlKey, useInvertingLut;
+	boolean disablePopup;
+	int measurements;
+	int decimalPlaces;
+	boolean blackBackground;
+	boolean autoContrast;
+	static WaitForUserDialog waitForUserDialog;
+	int pasteMode;
+	boolean expandableArrays = true;
+	int plotWidth;
+	int plotHeight;
+	int plotFontSize;
+	boolean plotInterpolate;
+	boolean plotNoGridLines;
+	boolean plotNoTicks;
+	boolean profileVerticalProfile;
+	boolean profileSubPixelResolution;
+	boolean waitForCompletion = true;
+
+
+	Functions(Interpreter interp, Program pgm) {
+		this.interp = interp;
+		this.pgm = pgm;
+	}
+
+	void doFunction(int type) {
+		switch (type) {
+			case RUN: doRun(); break;
+			case SELECT: selectWindow(); break;
+			case WAIT: IJ.wait((int)getArg()); break;
+			case BEEP: interp.getParens(); IJ.beep(); break;
+			case RESET_MIN_MAX: interp.getParens(); IJ.resetMinAndMax(); resetImage(); break;
+			case RESET_THRESHOLD: interp.getParens(); IJ.resetThreshold(); resetImage(); break;
+			case PRINT: case WRITE: print(); break;
+			case DO_WAND: doWand(); break;
+			case SET_MIN_MAX: setMinAndMax(); break;
+			case SET_THRESHOLD: setThreshold(); break;
+			case SET_TOOL: setTool(); break;
+			case SET_FOREGROUND: setForegroundColor(); break;
+			case SET_BACKGROUND: setBackgroundColor(); break;
+			case SET_COLOR: setColor(); break;
+			case MAKE_LINE: makeLine(); break;
+			case MAKE_ARROW: makeArrow(); break;
+			case MAKE_OVAL: makeOval(); break;
+			case MAKE_RECTANGLE: makeRectangle(); break;
+			case MAKE_ROTATED_RECT: makeRotatedRectangle(); break;
+			case DUMP: interp.dump(); break;
+			case LINE_TO: lineTo(); break;
+			case MOVE_TO: moveTo(); break;
+			case DRAW_LINE: drawLine(); break;
+			case REQUIRES: requires(); break;
+			case AUTO_UPDATE: autoUpdate = getBooleanArg(); break;
+			case UPDATE_DISPLAY: interp.getParens(); updateDisplay(); break;
+			case DRAW_STRING: drawString(); break;
+			case SET_PASTE_MODE: IJ.setPasteMode(getStringArg()); break;
+			case DO_COMMAND: doCommand(); break;
+			case SHOW_STATUS: showStatus(); break;
+			case SHOW_PROGRESS: showProgress(); break;
+			case SHOW_MESSAGE: showMessage(false); break;
+			case SHOW_MESSAGE_WITH_CANCEL: showMessage(true); break;
+			case SET_PIXEL: case PUT_PIXEL: setPixel(); break;
+			case SNAPSHOT: case RESET: case FILL: doIPMethod(type); break;
+			case SET_LINE_WIDTH: setLineWidth((int)getArg()); break;
+			case CHANGE_VALUES: changeValues(); break;
+			case SELECT_IMAGE: selectImage(); break;
+			case EXIT: exit(); break;
+			case SET_LOCATION: setLocation(); break;
+			case GET_CURSOR_LOC: getCursorLoc(); break;
+			case GET_LINE: getLine(); break;
+			case GET_VOXEL_SIZE: getVoxelSize(); break;
+			case GET_HISTOGRAM: getHistogram(); break;
+			case GET_BOUNDING_RECT: case GET_BOUNDS: getBounds(true); break;
+			case GET_LUT: getLut(); break;
+			case SET_LUT: setLut(); break;
+			case GET_COORDINATES: getCoordinates(); break;
+			case MAKE_SELECTION: makeSelection(); break;
+			case SET_RESULT: setResult(null); break;
+			case UPDATE_RESULTS: updateResults(); break;
+			case SET_BATCH_MODE: setBatchMode(); break;
+			case SET_JUSTIFICATION: setJustification(); break;
+			case SET_Z_COORDINATE: setZCoordinate(); break;
+			case GET_THRESHOLD: getThreshold(); break;
+			case GET_PIXEL_SIZE: getPixelSize(); break;
+			case SETUP_UNDO: interp.getParens(); Undo.setup(Undo.MACRO, getImage()); break;
+			case SAVE_SETTINGS: saveSettings(); break;
+			case RESTORE_SETTINGS: restoreSettings(); break;
+			case SET_KEY_DOWN: setKeyDown(); break;
+			case OPEN: open(); break;
+			case SET_FONT: setFont(); break;
+			case GET_MIN_AND_MAX: getMinAndMax(); break;
+			case CLOSE: close(); break;
+			case SET_SLICE: setSlice(); break;
+			case NEW_IMAGE: newImage(); break;
+			case SAVE: IJ.save(getStringArg()); break;
+			case SAVE_AS: saveAs(); break;
+			case SET_AUTO_THRESHOLD: setAutoThreshold(); break;
+			case RENAME: getImage().setTitle(getStringArg()); break;
+			case GET_STATISTICS: getStatistics(true); break;
+			case GET_RAW_STATISTICS: getStatistics(false); break;
+			case FLOOD_FILL: floodFill(); break;
+			case RESTORE_PREVIOUS_TOOL: restorePreviousTool(); break;
+			case SET_VOXEL_SIZE: setVoxelSize(); break;
+			case GET_LOCATION_AND_SIZE: getLocationAndSize(); break;
+			case GET_DATE_AND_TIME: getDateAndTime(); break;
+			case SET_METADATA: setMetadata(); break;
+			case CALCULATOR: imageCalculator(); break;
+			case SET_RGB_WEIGHTS: setRGBWeights(); break;
+			case MAKE_POLYGON: makePolygon(); break;
+			case SET_SELECTION_NAME: setSelectionName(); break;
+			case DRAW_RECT: case FILL_RECT: case DRAW_OVAL: case FILL_OVAL: drawOrFill(type); break;
+			case SET_OPTION: setOption(); break;
+			case SHOW_TEXT: showText(); break;
+			case SET_SELECTION_LOC: setSelectionLocation(); break;
+			case GET_DIMENSIONS: getDimensions(); break;
+			case WAIT_FOR_USER: waitForUser(); break;
+			case MAKE_POINT: makePoint(); break;
+			case MAKE_TEXT: makeText(); break;
+			case MAKE_ELLIPSE: makeEllipse(); break;
+			case GET_DISPLAYED_AREA: getDisplayedArea(); break;
+			case TO_SCALED: toScaled(); break;
+			case TO_UNSCALED: toUnscaled(); break;
+		}
+	}
+
+	final double getFunctionValue(int type) {
+		double value = 0.0;
+		switch (type) {
+			case GET_PIXEL: value = getPixel(); break;
+			case ABS: case COS: case EXP: case FLOOR: case LOG: case ROUND:
+			case SIN: case SQRT: case TAN: case ATAN: case ASIN: case ACOS:
+				value = math(type);
+				break;
+			case MATH: value = doMath(); break;
+			case MAX_OF: case MIN_OF: case POW: case ATAN2: value=math2(type); break;
+			case GET_TIME: interp.getParens(); value=System.currentTimeMillis(); break;
+			case GET_WIDTH: interp.getParens(); value=getImage().getWidth(); break;
+			case GET_HEIGHT: interp.getParens(); value=getImage().getHeight(); break;
+			case RANDOM: value=random(); break;
+			case GET_COUNT: case NRESULTS: value=getResultsCount(); break;
+			case GET_RESULT: value=getResult(null); break;
+			case GET_NUMBER: value=getNumber(); break;
+			case NIMAGES: value=getImageCount(); break;
+			case NSLICES: value=getStackSize(); break;
+			case LENGTH_OF: value=lengthOf(); break;
+			case GET_ID: interp.getParens(); value=getImage().getID(); break;
+			case BIT_DEPTH: interp.getParens(); value = getImage().getBitDepth(); break;
+			case SELECTION_TYPE: value=getSelectionType(); break;
+			case IS_OPEN: value=isOpen(); break;
+			case IS_ACTIVE: value=isActive(); break;
+			case INDEX_OF: value=indexOf(null); break;
+			case LAST_INDEX_OF: value=getFirstString().lastIndexOf(getLastString()); break;
+			case CHAR_CODE_AT: value=charCodeAt(); break;
+			case GET_BOOLEAN: value=getBoolean(); break;
+			case STARTS_WITH: case ENDS_WITH: value = startsWithEndsWith(type); break;
+			case IS_NAN: value = Double.isNaN(getArg())?1:0; break;
+			case GET_ZOOM: value = getZoom(); break;
+			case PARSE_FLOAT: value = parseDouble(getStringArg()); break;
+			case PARSE_INT: value = parseInt(); break;
+			case IS_KEY_DOWN: value = isKeyDown(); break;
+			case GET_SLICE_NUMBER: interp.getParens(); value=getImage().getCurrentSlice(); break;
+			case SCREEN_WIDTH: case SCREEN_HEIGHT: value = getScreenDimension(type); break;
+			case CALIBRATE: value = getImage().getCalibration().getCValue(getArg()); break;
+			case ROI_MANAGER: value = roiManager(); break;
+			case TOOL_ID: interp.getParens(); value = Toolbar.getToolId(); break;
+			case IS: value = is(); break;
+			case GET_VALUE: value = getValue(); break;
+			case STACK: value = doStack(); break;
+			case MATCHES: value = matches(null); break;
+			case GET_STRING_WIDTH: value = getStringWidth(); break;
+			case FIT: value = fit(); break;
+			case OVERLAY: value = overlay(); break;
+			case SELECTION_CONTAINS: value = selectionContains(); break;
+			case PLOT: value = doPlot(); break;
+			default:
+				interp.error("Numeric function expected");
+		}
+		return value;
+	}
+
+	String getStringFunction(int type) {
+		String str;
+		switch (type) {
+			case D2S: str = d2s(); break;
+			case TO_HEX: str = toString(16); break;
+			case TO_BINARY: str = toString(2); break;
+			case GET_TITLE: interp.getParens(); str=getImage().getTitle(); break;
+			case GET_STRING: str = getStringDialog(); break;
+			case SUBSTRING: str=substring(null); break;
+			case FROM_CHAR_CODE: str = fromCharCode(); break;
+			case GET_INFO: str = getInfo(); break;
+			case GET_IMAGE_INFO: interp.getParens(); str = getImageInfo(); break;
+			case GET_DIRECTORY: case GET_DIR: str = getDirectory(); break;
+			case GET_ARGUMENT: interp.getParens(); str=interp.argument!=null?interp.argument:""; break;
+			case TO_LOWER_CASE: str = getStringArg().toLowerCase(Locale.US); break;
+			case TO_UPPER_CASE: str = getStringArg().toUpperCase(Locale.US); break;
+			case RUN_MACRO: str = runMacro(false); break;
+			case EVAL: str = runMacro(true); break;
+			case TO_STRING: str = doToString(); break;
+			case REPLACE: str = replace(null); break;
+			case DIALOG: str = doDialog(); break;
+			case GET_METADATA: str = getMetadata(); break;
+			case FILE: str = doFile(); break;
+			case SELECTION_NAME: str = selectionName(); break;
+			case GET_VERSION: interp.getParens();  str = IJ.getVersion(); break;
+			case GET_RESULT_LABEL: str = getResultLabel(); break;
+			case CALL: str = call(); break;
+			case STRING: str = doString(); break;
+			case EXT: str = doExt(); break;
+			case EXEC: str = exec(); break;
+			case LIST: str = doList(); break;
+			case DEBUG: str = debug(); break;
+			case IJ_CALL: str = doIJ(); break;
+			case GET_RESULT_STRING: str = getResultString(null); break;
+			case TRIM: str = trim(); break;
+			default:
+				str="";
+				interp.error("String function expected");
+		}
+		return str;
+	}
+
+	Variable[] getArrayFunction(int type) {
+		Variable[] array;
+		switch (type) {
+			case GET_PROFILE: array=getProfile(); break;
+			case NEW_ARRAY: array = newArray(); break;
+			case SPLIT: array = split(); break;
+			case GET_FILE_LIST: array = getFileList(); break;
+			case GET_FONT_LIST: array = getFontList(); break;
+			case NEW_MENU: array = newMenu(); break;
+			case GET_LIST: array = getList(); break;
+			case ARRAY_FUNC: array = doArray(); break;
+			default:
+				array = null;
+				interp.error("Array function expected");
+		}
+		return array;
+	}
+
+	// Functions returning a string must be added
+	// to isStringFunction(String,int).
+	Variable getVariableFunction(int type) {
+		Variable var = null;
+		switch (type) {
+			case TABLE: var = doTable(); break;
+			case ROI: var = doRoi(); break;
+			case ROI_MANAGER2: var = doRoiManager(); break;
+			case PROPERTY: var = doProperty(); break;
+			case IMAGE: var = doImage(); break;
+			case COLOR: var = doColor(); break;
+			default:
+				interp.error("Variable function expected");
+		}
+		if (var==null)
+			var = new Variable(Double.NaN);
+		return var;
+	}
+
+	private void setLineWidth(int width) {
+		if (WindowManager.getCurrentImage()!=null) {
+			if (overlayPath!=null && width!=globalLineWidth)
+				addDrawingToOverlay(getImage());
+			getProcessor().setLineWidth(width);
+		}
+		globalLineWidth = width;
+	}
+
+	private double doMath() {
+		interp.getToken();
+		if (interp.token!='.')
+			interp.error("'.' expected");
+		interp.getToken();
+		if (!(interp.token==WORD||interp.token==NUMERIC_FUNCTION))
+			interp.error("Function name expected: ");
+		String name = interp.tokenString;
+		if (name.equals("min"))
+			return Math.min(getFirstArg(), getLastArg());
+		else if (name.equals("max"))
+			return Math.max(getFirstArg(), getLastArg());
+		else if (name.equals("pow"))
+			return Math.pow(getFirstArg(), getLastArg());
+		else if (name.equals("atan2"))
+			return Math.atan2(getFirstArg(), getLastArg());
+		else if (name.equals("constrain"))
+			return Math.min(Math.max(getFirstArg(), getNextArg()), getLastArg());
+		else if (name.equals("map")) {
+			double value = getFirstArg();
+			double fromLow = getNextArg();
+			double fromHigh = getNextArg();
+			double toLow = getNextArg();
+			double toHigh = getLastArg();
+			return (value-fromLow)*(toHigh-toLow)/(fromHigh-fromLow)+toLow;
+		}
+		double arg = getArg();
+		if (name.equals("ceil"))
+			return Math.ceil(arg);
+		else if (name.equals("abs"))
+			return Math.abs(arg);
+		else if (name.equals("cos"))
+			return Math.cos(arg);
+		else if (name.equals("exp"))
+			return Math.exp(arg);
+		else if (name.equals("floor"))
+			return Math.floor(arg);
+		else if (name.equals("log"))
+			return Math.log(arg);
+		else if (name.equals("log10"))
+			return Math.log10(arg);
+		else if (name.equals("round"))
+			return Math.round(arg);
+		else if (name.equals("sin"))
+			return Math.sin(arg);
+		else if (name.equals("sqr"))
+			return arg*arg;
+		else if (name.equals("sqrt"))
+			return Math.sqrt(arg);
+		else if (name.equals("tan"))
+			return Math.tan(arg);
+		else if (name.equals("atan"))
+			return Math.atan(arg);
+		else if (name.equals("asin"))
+			return Math.asin(arg);
+		else if (name.equals("acos"))
+			return Math.acos(arg);
+		else if (name.equals("erf"))
+			return IJMath.erf(arg);
+		else if (name.equals("toRadians"))
+			return Math.toRadians(arg);
+		else if (name.equals("toDegrees"))
+			return Math.toDegrees(arg);
+		else
+			interp.error("Unrecognized function name");
+		return Double.NaN;
+	}
+
+	final double math(int type) {
+		double arg = getArg();
+		switch (type) {
+			case ABS: return Math.abs(arg);
+			case COS: return Math.cos(arg);
+			case EXP: return Math.exp(arg);
+			case FLOOR: return Math.floor(arg);
+			case LOG: return Math.log(arg);
+			case ROUND: return Math.floor(arg + 0.5);
+			case SIN: return Math.sin(arg);
+			case SQRT: return Math.sqrt(arg);
+			case TAN: return Math.tan(arg);
+			case ATAN: return Math.atan(arg);
+			case ASIN: return Math.asin(arg);
+			case ACOS: return Math.acos(arg);
+			default: return 0.0;
+		}
+	}
+
+	final double math2(int type) {
+		double a1 = getFirstArg();
+		double a2 = getLastArg();
+		switch (type) {
+			case MIN_OF: return Math.min(a1, a2);
+			case MAX_OF: return Math.max(a1, a2);
+			case POW: return Math.pow(a1, a2);
+			case ATAN2: return Math.atan2(a1, a2);
+			default: return 0.0;
+		}
+	}
+
+	final String getString() {
+		String str = interp.getStringTerm();
+		while (true) {
+			interp.getToken();
+			if (interp.token=='+')
+				str += interp.getStringTerm();
+			else {
+				interp.putTokenBack();
+				break;
+			}
+		};
+		return str;
+	}
+
+	final boolean isStringFunction() {
+		Symbol symbol = pgm.table[interp.tokenAddress];
+		return symbol.type==D2S;
+	}
+
+	final double getArg() {
+		interp.getLeftParen();
+		double arg = interp.getExpression();
+		interp.getRightParen();
+		return arg;
+	}
+
+	final double getFirstArg() {
+		interp.getLeftParen();
+		return interp.getExpression();
+	}
+
+	final double getNextArg() {
+		interp.getComma();
+		return interp.getExpression();
+	}
+
+	final double getLastArg() {
+		interp.getComma();
+		double arg = interp.getExpression();
+		interp.getRightParen();
+		return arg;
+	}
+
+	String getStringArg() {
+		interp.getLeftParen();
+		String arg = getString();
+		interp.getRightParen();
+		return arg;
+	}
+
+	final String getFirstString() {
+		interp.getLeftParen();
+		return getString();
+	}
+
+	final String getNextString() {
+		interp.getComma();
+		return getString();
+	}
+
+	final String getLastString() {
+		interp.getComma();
+		String arg = getString();
+		interp.getRightParen();
+		return arg;
+	}
+
+	boolean getBooleanArg() {
+		interp.getLeftParen();
+		double arg = interp.getBooleanExpression();
+		interp.checkBoolean(arg);
+		interp.getRightParen();
+		return arg==0?false:true;
+	}
+
+	final Variable getVariableArg() {
+		interp.getLeftParen();
+		Variable v = getVariable();
+		interp.getRightParen();
+		return v;
+	}
+
+	final Variable getFirstVariable() {
+		interp.getLeftParen();
+		return getVariable();
+	}
+
+	final Variable getNextVariable() {
+		interp.getComma();
+		return getVariable();
+	}
+
+	final Variable getLastVariable() {
+		interp.getComma();
+		Variable v = getVariable();
+		interp.getRightParen();
+		return v;
+	}
+
+	final Variable getVariable() {
+		interp.getToken();
+		if (interp.token!=WORD)
+			interp.error("Variable expected");
+		Variable v = interp.lookupLocalVariable(interp.tokenAddress);
+		if (v==null)
+				v = interp.push(interp.tokenAddress, 0.0, null, interp);
+		Variable[] array = v.getArray();
+		if (array!=null) {
+			int index = interp.getIndex();
+			checkIndex(index, 0, v.getArraySize()-1);
+			v = array[index];
+		}
+		return v;
+	}
+
+	final Variable getFirstArrayVariable() {
+		interp.getLeftParen();
+		return getArrayVariable();
+	}
+
+	final Variable getNextArrayVariable() {
+		interp.getComma();
+		return getArrayVariable();
+	}
+
+	final Variable getLastArrayVariable() {
+		interp.getComma();
+		Variable v = getArrayVariable();
+		interp.getRightParen();
+		return v;
+	}
+
+	final Variable getArrayVariable() {
+		interp.getToken();
+		if (interp.token!=WORD)
+			interp.error("Variable expected");
+		Variable v = interp.lookupLocalVariable(interp.tokenAddress);
+		if (v==null)
+				v = interp.push(interp.tokenAddress, 0.0, null, interp);
+		return v;
+	}
+
+	final double[] getFirstArray() {
+		interp.getLeftParen();
+		return getNumericArray();
+	}
+
+	final double[] getNextArray() {
+		interp.getComma();
+		return getNumericArray();
+	}
+
+	final double[] getLastArray() {
+		interp.getComma();
+		double[] a = getNumericArray();
+		interp.getRightParen();
+		return a;
+	}
+
+	double[] getNumericArray() {
+		Variable[] a1 = getArray();
+		double[] a2 = new double[a1.length];
+		for (int i=0; iupper)
+			interp.error("Index ("+index+") is outside of the "+lower+"-"+upper+" range");
+	}
+
+	void doRun() {
+		interp.getLeftParen();
+		String arg1 = getString();
+		interp.getToken();
+		if (!(interp.token==')' || interp.token==','))
+			interp.error("',' or ')'  expected");
+		String arg2 = null;
+		if (interp.token==',') {
+			arg2 = getString();
+			interp.getRightParen();
+		}
+		IJ.run(this.interp, arg1, arg2);
+		resetImage();
+		IJ.setKeyUp(IJ.ALL_KEYS);
+		shiftKeyDown = altKeyDown = false;
+	}
+
+	private void selectWindow() {
+		String title = getStringArg();
+		if (resultsPending && "Results".equals(title)) {
+			ResultsTable rt = ResultsTable.getResultsTable();
+			if (rt!=null && rt.size()>0)
+				rt.show("Results");
+		}
+		IJ.selectWindow(title);
+		resetImage();
+		interp.selectCount++;
+	}
+
+	void setForegroundColor() {
+		boolean isImage = WindowManager.getCurrentImage()!=null;
+		int lnWidth = 0;
+		if (isImage)
+			lnWidth = getProcessor().getLineWidth();
+		int red=0, green=0, blue=0;
+		int arg1 = (int)getFirstArg();
+		if (interp.nextToken()==')') {
+			interp.getRightParen();
+			red = (arg1&0xff0000)>>16;
+			green = (arg1&0xff00)>>8;
+			blue = arg1&0xff;
+		} else {
+			red = arg1;
+			green = (int)getNextArg();
+			blue = (int)getLastArg();
+		}
+		IJ.setForegroundColor(red, green, blue);
+		resetImage();
+		if (isImage)
+			setLineWidth(lnWidth);
+		globalColor = null;
+		globalValue = Double.NaN;
+	}
+
+	void setBackgroundColor() {
+		int red=0, green=0, blue=0;
+		int arg1 = (int)getFirstArg();
+		if (interp.nextToken()==')') {
+			interp.getRightParen();
+			red = (arg1&0xff0000)>>16;
+			green = (arg1&0xff00)>>8;
+			blue = arg1&0xff;
+		} else {
+			red = arg1;
+			green = (int)getNextArg();
+			blue = (int)getLastArg();
+		}
+		IJ.setBackgroundColor(red, green, blue);
+		resetImage();
+	}
+
+	void setColor() {
+		interp.getLeftParen();
+		if (isStringArg()) {
+			globalColor = getColor();
+			globalValue = Double.NaN;
+			ImagePlus imp = WindowManager.getCurrentImage();
+			if (imp!=null) {
+				if (overlayPath!=null)
+					addDrawingToOverlay(imp);
+				getProcessor().setColor(globalColor);
+			}
+			interp.getRightParen();
+			return;
+		}
+		double arg1 = interp.getExpression();
+		if (interp.nextToken()==')') {
+			interp.getRightParen();
+			setColor(arg1);
+			return;
+		}
+		int red=(int)arg1, green=(int)getNextArg(), blue=(int)getLastArg();
+		if (red<0) red=0; if (green<0) green=0; if (blue<0) blue=0;
+		if (red>255) red=255; if (green>255) green=255; if (blue>255) blue=255;
+		globalColor = new Color(red, green, blue);
+		globalValue = Double.NaN;
+		if (WindowManager.getCurrentImage()!=null)
+			getProcessor().setColor(globalColor);
+	}
+
+	void setColor(double value) {
+		ImageProcessor ip = getProcessor();
+		ImagePlus imp = getImage();
+		switch (imp.getBitDepth()) {
+			case 8:
+				if (value<0 || value>255)
+					interp.error("Argument out of 8-bit range (0-255)");
+				ip.setValue(value);
+				break;
+			case 16:
+				if (imp.getLocalCalibration().isSigned16Bit())
+					value += 32768;
+				if (value<0 || value>65535)
+					interp.error("Argument out of 16-bit range (0-65535)");
+				ip.setValue(value);
+				break;
+			default:
+				ip.setValue(value);
+				break;
+		}
+		globalValue = value;
+		globalColor = null;
+	}
+
+	void makeLine() {
+		double x1d = getFirstArg();
+		double y1d = getNextArg();
+		double x2d = getNextArg();
+		interp.getComma();
+		double y2d = interp.getExpression();
+		interp.getToken();
+		if (interp.token==')')
+			IJ.makeLine(x1d, y1d, x2d, y2d);
+		else {
+			Polygon points = new Polygon();
+			points.addPoint((int)Math.round(x1d),(int)Math.round(y1d));
+			points.addPoint((int)Math.round(x2d),(int)Math.round(y2d));
+			while (interp.token==',') {
+				int x = (int)Math.round(interp.getExpression());
+				if (points.npoints==2 && interp.nextToken()==')') {
+					interp.getRightParen();
+					Roi line = new Line(x1d, y1d, x2d, y2d);
+					line.updateWideLine((float)x);
+					getImage().setRoi(line);
+					return;
+				}
+				interp.getComma();
+				int y = (int)Math.round(interp.getExpression());
+				points.addPoint(x,y);
+				interp.getToken();
+			}
+			getImage().setRoi(new PolygonRoi(points, Roi.POLYLINE));
+		}
+		resetImage();
+	}
+
+	void makeArrow() {
+		String options = "";
+		double x1 = getFirstArg();
+		double y1 = getNextArg();
+		double x2 = getNextArg();
+		double y2 = getNextArg();
+		if (interp.nextToken()==',')
+			options = getNextString();
+		interp.getRightParen();
+		Arrow arrow = new Arrow(x1, y1, x2, y2);
+		arrow.setStyle(options);
+		getImage().setRoi(arrow);
+	}
+
+	void makeOval() {
+		Roi previousRoi = getImage().getRoi();
+		if (shiftKeyDown||altKeyDown) getImage().saveRoi();
+		IJ.makeOval(getFirstArg(), getNextArg(), getNextArg(), getLastArg());
+		Roi roi = getImage().getRoi();
+		if (previousRoi!=null && roi!=null)
+			updateRoi(roi);
+		resetImage();
+		shiftKeyDown = altKeyDown = false;
+		IJ.setKeyUp(IJ.ALL_KEYS);
+	}
+
+	void makeRectangle() {
+		Roi previousRoi = getImage().getRoi();
+		if (shiftKeyDown||altKeyDown) getImage().saveRoi();
+		double x = getFirstArg();
+		double y = getNextArg();
+		double w = getNextArg();
+		double h = getNextArg();
+		int arcSize = 0;
+		if (interp.nextToken()==',') {
+			interp.getComma();
+			arcSize = (int)interp.getExpression();
+		}
+		interp.getRightParen();
+		if (arcSize<1)
+			IJ.makeRectangle(x, y, w, h);
+		else {
+			ImagePlus imp = getImage();
+			imp.setRoi(new Roi(x,y,w,h,arcSize));
+		}
+		Roi roi = getImage().getRoi();
+		if (previousRoi!=null && roi!=null)
+			updateRoi(roi);
+		resetImage();
+		shiftKeyDown = altKeyDown = false;
+		IJ.setKeyUp(IJ.ALL_KEYS);
+	}
+
+	void makeRotatedRectangle() {
+		getImage().setRoi(new RotatedRectRoi(getFirstArg(), getNextArg(), getNextArg(), getNextArg(), getLastArg()));
+		resetImage();
+	}
+
+	ImagePlus getImage() {
+		ImagePlus imp = IJ.getImage(interp);
+		if (imp.getWindow()==null && IJ.getInstance()!=null && !interp.isBatchMode() && WindowManager.getTempCurrentImage()==null)
+			throw new RuntimeException(Macro.MACRO_CANCELED);
+		defaultIP = null;
+		defaultImp = imp;
+		return imp;
+	}
+
+	void resetImage() {
+		defaultImp = null;
+		defaultIP = null;
+		fontSet = false;
+	}
+
+	ImageProcessor getProcessor() {
+		if (defaultIP==null) {
+			defaultIP = getImage().getProcessor();
+			if (globalLineWidth>0)
+				defaultIP.setLineWidth(globalLineWidth);
+			if (globalColor!=null)
+				defaultIP.setColor(globalColor);
+			else if (!Double.isNaN(globalValue))
+				defaultIP.setValue(globalValue);
+			else
+				defaultIP.setColor(Toolbar.getForegroundColor());
+		}
+		return defaultIP;
+	}
+
+	int getType() {
+		imageType = getImage().getType();
+		return imageType;
+	}
+
+	void setPixel() {
+		interp.getLeftParen();
+		int a1 = (int)interp.getExpression();
+		interp.getComma();
+		double a2 = interp.getExpression();
+		interp.getToken();
+		ImageProcessor ip = getProcessor();
+		if (interp.token==',') {
+			double a3 = interp.getExpression();
+			interp.getRightParen();
+			if (ip instanceof FloatProcessor)
+				ip.putPixelValue(a1, (int)a2, a3);
+			else
+				ip.putPixel(a1, (int)a2, (int)a3);
+		} else {
+			if (interp.token!=')') interp.error("')' expected");
+			if (ip instanceof ColorProcessor)
+				ip.set(a1, (int)a2);
+			else
+				ip.setf(a1, (float)a2);
+		}
+		updateNeeded = true;
+	}
+
+	double getPixel() {
+		interp.getLeftParen();
+		double a1 = interp.getExpression();
+		ImageProcessor ip = getProcessor();
+		double value = 0.0;
+		interp.getToken();
+		if (interp.token==',') {
+			double a2 = interp.getExpression();
+			interp.getRightParen();
+			int ia1 = (int)a1;
+			int ia2 = (int)a2;
+			if (a1==ia1 && a2==ia2) {
+				if (ip instanceof FloatProcessor)
+					value = ip.getPixelValue(ia1, ia2);
+				else
+					value = ip.getPixel(ia1, ia2);
+			} else {
+				if (ip instanceof ColorProcessor)
+					value = ip.getPixelInterpolated(a1, a2);
+				else {
+					ImagePlus imp = getImage();
+					Calibration cal = imp.getCalibration();
+					imp.setCalibration(null);
+					ip = imp.getProcessor();
+					value = ip.getInterpolatedValue(a1, a2);
+					imp.setCalibration(cal);
+				}
+			}
+		} else {
+			if (interp.token!=')') interp.error("')' expected");
+			if (ip instanceof ColorProcessor)
+				value = ip.get((int)a1);
+			else
+				value = ip.getf((int)a1);
+		}
+		return value;
+	}
+
+	void setZCoordinate() {
+		int z = (int)getArg();
+		int n = z + 1;
+		ImagePlus imp = getImage();
+		ImageStack stack = imp.getStack();
+		int size = stack.size();
+		if (z<0 || z>=size)
+			interp.error("Z coordinate ("+z+") is out of 0-"+(size-1)+ " range");
+		this.defaultIP = stack.getProcessor(n);
+	}
+
+	void moveTo() {
+		interp.getLeftParen();
+		int a1 = (int)Math.round(interp.getExpression());
+		interp.getComma();
+		int a2 = (int)Math.round(interp.getExpression());
+		interp.getRightParen();
+		getProcessor().moveTo(a1, a2);
+	}
+
+	void lineTo() {
+		interp.getLeftParen();
+		int a1 = (int)Math.round(interp.getExpression());
+		interp.getComma();
+		int a2 = (int)Math.round(interp.getExpression());
+		interp.getRightParen();
+		ImageProcessor ip = getProcessor();
+		ip.lineTo(a1, a2);
+		updateAndDraw();
+	}
+
+	void drawLine() {
+		interp.getLeftParen();
+		int x1 = (int)Math.round(interp.getExpression());
+		interp.getComma();
+		int y1 = (int)Math.round(interp.getExpression());
+		interp.getComma();
+		int x2 = (int)Math.round(interp.getExpression());
+		interp.getComma();
+		int y2 = (int)Math.round(interp.getExpression());
+		interp.getRightParen();
+		ImageProcessor ip = getProcessor();
+		ip.drawLine(x1, y1, x2, y2);
+		updateAndDraw();
+	}
+
+	void doIPMethod(int type) {
+		interp.getParens();
+		ImageProcessor ip = getProcessor();
+		switch (type) {
+			case SNAPSHOT: ip.snapshot(); break;
+			case RESET:
+				ip.reset();
+				updateNeeded = true;
+				break;
+			case FILL:
+				ImagePlus imp = getImage();
+				Roi roi = imp.getRoi();
+				if (roi==null) {
+					ip.resetRoi();
+					ip.fill();
+				} else {
+					ip.setRoi(roi);
+					ip.fill(ip.getMask());
+				}
+				imp.updateAndDraw();
+				break;
+		}
+	}
+
+	void updateAndDraw() {
+		if (autoUpdate) {
+			ImagePlus imp = defaultImp;
+			if (imp==null)
+				imp = getImage();
+			imp.updateChannelAndDraw();
+			imp.changes = true;
+		} else
+			updateNeeded = true;
+	}
+
+	void updateDisplay() {
+		if (updateNeeded && WindowManager.getImageCount()>0) {
+			ImagePlus imp = getImage();
+			imp.updateAndDraw();
+			updateNeeded = false;
+		}
+	}
+
+	void drawString() {
+		interp.getLeftParen();
+		String str = getString();
+		interp.getComma();
+		int x = (int)(interp.getExpression()+0.5);
+		interp.getComma();
+		int y = (int)(interp.getExpression()+0.5);
+		Color background = null;
+		if (interp.nextToken()==',') {
+			interp.getComma();
+			background = getColor();
+		}
+		interp.getRightParen();
+		ImageProcessor ip = getProcessor();
+		setFont(ip);
+		ip.setJustification(justification);
+		ip.setAntialiasedText(antialiasedText);
+		if (background!=null)
+			ip.drawString(str, x, y, background);
+		else
+			ip.drawString(str, x, y);
+		updateAndDraw();
+	}
+
+	void setFont(ImageProcessor ip) {
+		if (font!=null && !fontSet)
+			ip.setFont(font);
+		fontSet = true;
+	}
+
+	void setJustification() {
+		String str = getStringArg().toLowerCase(Locale.US);
+		int just = ImageProcessor.LEFT_JUSTIFY;
+		if (str.equals("center"))
+			just = ImageProcessor.CENTER_JUSTIFY;
+		else if (str.equals("right"))
+			just = ImageProcessor.RIGHT_JUSTIFY;
+		justification = just;
+	}
+
+	void changeValues() {
+		double darg1 = getFirstArg();
+		double darg2 = getNextArg();
+		double darg3 = getLastArg();
+		ImagePlus imp = getImage();
+		ImageProcessor ip = getProcessor();
+		Roi roi = imp.getRoi();
+		ImageProcessor mask = null;
+		if (roi==null || !roi.isArea()) {
+			ip.resetRoi();
+			roi = null;
+		} else {
+			ip.setRoi(roi);
+			mask = ip.getMask();
+			if (mask!=null) ip.snapshot();
+		}
+		int xmin=0, ymin=0, xmax=imp.getWidth(), ymax=imp.getHeight();
+		if (roi!=null) {
+			Rectangle r = roi.getBounds();
+			xmin=r.x; ymin=r.y; xmax=r.x+r.width; ymax=r.y+r.height;
+		}
+		boolean isFloat = getType()==ImagePlus.GRAY32;
+		if (imp.getBitDepth()==24) {
+			darg1 = (int)darg1&0xffffff;
+			darg2 = (int)darg2&0xffffff;
+		}
+		double v;
+		for (int y=ymin; y=darg1 && v<=darg2;
+				if (Double.isNaN(darg1) && Double.isNaN(darg2) && Double.isNaN(v))
+					replace = true;
+				if (replace) {
+					if (isFloat)
+						ip.putPixelValue(x, y, darg3);
+					else
+						ip.putPixel(x, y, (int)darg3);
+				}
+			}
+		}
+		if (mask!=null) ip.reset(mask);
+		imp.updateAndDraw();
+		updateNeeded = false;
+	}
+
+	void requires() {
+		if (IJ.versionLessThan(getStringArg()))
+			interp.done = true;
+	}
+
+	Random ran;
+	double random() {
+		double dseed = Double.NaN;
+		boolean gaussian = false;
+		if (interp.nextToken()=='(') {
+			interp.getLeftParen();
+			if (isStringArg()) {
+				String arg = getString().toLowerCase(Locale.US);
+				if (arg.equals("seed")) {
+					interp.getComma();
+					dseed = interp.getExpression();
+					long seed = (long)dseed;
+					if (seed!=dseed)
+						interp.error("Seed not integer");
+					ran = new Random(seed);
+					ImageProcessor.setRandomSeed(seed);
+				} else if (arg.equals("gaussian"))
+					gaussian = true;
+				else
+					interp.error("'seed' or ''gaussian' expected");
+			}
+			interp.getRightParen();
+			if (!Double.isNaN(dseed)) return Double.NaN;
+		}
+		ImageProcessor.setRandomSeed(Double.NaN);
+		interp.getParens();
+ 		if (ran==null)
+			ran = new Random();
+		if (gaussian)
+			return ran.nextGaussian();
+		else
+			return ran.nextDouble();
+	}
+
+	double getResult(ResultsTable rt) {
+		interp.getLeftParen();
+		String column = getString();
+		int row = -1;
+		if (interp.nextToken()==',') {
+			interp.getComma();
+			row = (int)interp.getExpression();
+		}
+		if (interp.nextToken()==',') {
+			interp.getComma();
+			String title = getString();
+			rt = getResultsTable(title);
+		}
+		if (rt==null)
+			rt = getResultsTable(true);
+		interp.getRightParen();
+		int counter = rt.size();
+		if (row==-1) row = counter-1;
+		if (row<0 || row>=counter)
+			interp.error("Row ("+row+") out of range");
+		int col = rt.getColumnIndex(column);
+		if (!rt.columnExists(col))
+			return Double.NaN;
+		else {
+			double value = rt.getValueAsDouble(col, row);
+			if (Double.isNaN(value)) {
+				String s = rt.getStringValue(col, row);
+				if (s!=null && !s.equals("NaN"))
+					value = Tools.parseDouble(s);
+			}
+			return value;
+		}
+	}
+
+	String getResultString(ResultsTable rt) {
+		interp.getLeftParen();
+		String column = getString();
+		int row = -1;
+		if (interp.nextToken()==',') {
+			interp.getComma();
+			row = (int)interp.getExpression();
+		}
+		if (interp.nextToken()==',') {
+			interp.getComma();
+			String title = getString();
+			rt = getResultsTable(title);
+		}
+		interp.getRightParen();
+		if (rt==null)
+			rt = getResultsTable(true);
+		int counter = rt.size();
+		if (row==-1) row = counter-1;
+		if (row<0 || row>=counter)
+			interp.error("Row ("+row+") out of range");
+		int col = rt.getColumnIndex(column);
+		if (rt.columnExists(col))
+			return rt.getStringValue(col, row);
+		else {
+			String label = null;
+			if ("Label".equals(column))
+				label = rt.getLabel(row);
+			return label!=null?label:"null";
+		}
+	}
+
+	String getResultLabel() {
+		int row = (int)getArg();
+		ResultsTable rt = getResultsTable(true);
+		int counter = rt.size();
+		if (row<0 || row>=counter)
+			interp.error("Row ("+row+") out of range");
+		String label = rt.getLabel(row);
+		if (label!=null)
+			return label;
+		else {
+			label = rt.getStringValue("Label", row);
+			return label!=null?label:"";
+		}
+	}
+
+	private ResultsTable getResultsTable(boolean reportErrors) {
+		ResultsTable rt = Analyzer.getResultsTable();
+		int size = rt.size();
+		if (size==0) {
+			Frame frame = WindowManager.getFrontWindow();
+			if (frame==null || (frame instanceof Editor))
+				frame = WindowManager.getFrame("Results");
+			if (frame!=null && (frame instanceof TextWindow)) {
+				TextPanel tp = ((TextWindow)frame).getTextPanel();
+				rt = tp.getOrCreateResultsTable();
+				size = rt!=null?rt.size():0;
+			}
+		}
+		if (size==0) {
+			Window win = WindowManager.getActiveTable();
+			if (win!=null && (win instanceof TextWindow)) {
+				TextPanel tp = ((TextWindow)win).getTextPanel();
+				rt = tp.getOrCreateResultsTable();
+				size = rt!=null?rt.size():0;
+			}
+		}
+		if (size==0 && reportErrors)
+			interp.error("No results found");
+		return rt;
+	}
+
+	void setResult(ResultsTable rt) {
+		interp.getLeftParen();
+		String column = getString();
+		interp.getComma();
+		int row = (int)interp.getExpression();
+		interp.getComma();
+		double value = 0.0;
+		String stringValue = null;
+		boolean isLabel = column.equals("Label");
+		if (isStringArg() || isLabel)
+			stringValue = getString();
+		else
+			value = interp.getExpression();
+		if (interp.nextToken()==',') {
+			interp.getComma();
+			String title = getString();
+			rt = getResultsTable(title);
+		}
+		interp.getRightParen();
+		if (rt==null) {
+			rt = Analyzer.getResultsTable();
+			resultsPending = true;
+		} else
+			unUpdatedTable = rt;
+		if (row<0 || row>rt.size())
+			interp.error("Row ("+row+") out of range");
+		if (row==rt.size())
+			rt.incrementCounter();
+		try {
+			if (stringValue!=null) {
+				if (isLabel)
+					rt.setLabel(stringValue, row);
+				else
+					rt.setValue(column, row, stringValue);
+			} else
+				rt.setValue(column, row, value);
+		} catch (Exception e) {
+			interp.error(""+e.getMessage());
+		}
+	}
+
+	void updateResults() {
+		interp.getParens();
+		ResultsTable rt = Analyzer.getResultsTable();
+		rt.show("Results");
+		resultsPending = false;
+	}
+
+	double getNumber() {
+		String prompt = getFirstString();
+		double defaultValue = getLastArg();
+		String title = interp.macroName!=null?interp.macroName:"";
+		if (title.endsWith(" Options"))
+			title = title.substring(0, title.length()-8);
+		GenericDialog gd = new GenericDialog(title);
+		int decimalPlaces = (int)defaultValue==defaultValue?0:2;
+		gd.addNumericField(prompt, defaultValue, decimalPlaces);
+		gd.showDialog();
+		if (gd.wasCanceled()) {
+			interp.done = true;
+			return defaultValue;
+		}
+		double v = gd.getNextNumber();
+		if (gd.invalidNumber())
+			return defaultValue;
+		else
+			return v;
+	}
+
+	double getBoolean() {
+		interp.getLeftParen();
+		String prompt = getString();
+		String yesButton = "  Yes  ";
+		String noButton = "  No  ";
+		if (interp.nextToken()==',') {
+			yesButton = getNextString();
+			noButton = getNextString();
+		}
+		interp.getRightParen();
+		String title = interp.macroName!=null?interp.macroName:"";
+		if (title.endsWith(" Options"))
+			title = title.substring(0, title.length()-8);
+		YesNoCancelDialog d = new YesNoCancelDialog(IJ.getInstance(), title, prompt, yesButton, noButton);
+		if (d.cancelPressed()) {
+			interp.done = true;
+			return 0.0;
+		} else if (d.yesPressed())
+			return 1.0;
+		else
+			return 0.0;
+	}
+
+	String getStringDialog() {
+		interp.getLeftParen();
+		String prompt = getString();
+		interp.getComma();
+		String defaultStr = getString();
+		interp.getRightParen();
+
+		String title = interp.macroName!=null?interp.macroName:"";
+		if (title.endsWith(" Options"))
+			title = title.substring(0, title.length()-8);
+		GenericDialog gd = new GenericDialog(title);
+		gd.addStringField(prompt, defaultStr, 20);
+		gd.showDialog();
+		String str = "";
+		if (gd.wasCanceled())
+			interp.done = true;
+		else
+			str = gd.getNextString();
+		return str;
+	}
+
+	String d2s() {
+		return IJ.d2s(getFirstArg(), (int)getLastArg());
+	}
+
+	String toString(int base) {
+		int arg = (int)getArg();
+		if (base==2)
+			return Integer.toBinaryString(arg);
+		else
+			return Integer.toHexString(arg);
+	}
+
+	double getStackSize() {
+		interp.getParens();
+		return getImage().getStackSize();
+	}
+
+	double getImageCount() {
+		interp.getParens();
+		return WindowManager.getImageCount();
+	}
+
+	double getResultsCount() {
+		interp.getParens();
+		return Analyzer.getResultsTable().getCounter();
+	}
+
+	void getCoordinates() {
+		Variable xCoordinates = getFirstArrayVariable();
+		Variable yCoordinates = getLastArrayVariable();
+		ImagePlus imp = getImage();
+		Roi roi = imp.getRoi();
+		if (roi==null)
+			interp.error("Selection required");
+		Variable[] xa, ya;
+		if (roi.getType()==Roi.LINE) {
+			xa = new Variable[2];
+			ya = new Variable[2];
+			Line line = (Line)roi;
+			xa[0] = new Variable(line.x1d);
+			ya[0] = new Variable(line.y1d);
+			xa[1] = new Variable(line.x2d);
+			ya[1] = new Variable(line.y2d);
+		} else {
+			FloatPolygon fp = roi.getFloatPolygon();
+			if (fp!=null) {
+				xa = new Variable[fp.npoints];
+				ya = new Variable[fp.npoints];
+				for (int i=0; i0 && s2!=null && (s2.equals(",")||s2.equals(";")))
+			strings = s1.split(s2,-1);
+		else if (s1.length()>0 && s2!=null && s2.length()>=3 && s2.startsWith("(")&&s2.endsWith(")")) {
+			s2 = s2.substring(1,s2.length()-1);
+			strings = s1.split(s2,-1);
+		} else
+			strings = (s2==null||s2.equals(""))?Tools.split(s1):Tools.split(s1, s2);
+    	Variable[] array = new Variable[strings.length];
+    	for (int i=0; i
+ * For power, exp by linear regression and 'Rodbard NIH Image', this is calculated for the + * fit actually done, not for the residuals of the original data. + */ + public double getRSquared() { + if (Double.isNaN(sumY)) calculateSumYandY2(); + double sumMeanDiffSqr = sumY2 - sumY*sumY/sumWeights; + double rSquared = 0.0; + if (sumMeanDiffSqr > 0.0) + rSquared = 1.0 - getSumResidualsSqr()/sumMeanDiffSqr; + return rSquared; + } + + /** Get a measure of "goodness of fit" where 1.0 is best. + * Approaches R^2 if the number of points is much larger than the number of fit parameters. + * Assumes that the data points are independent (i.e., each point having a different x value). + * For power, exp by linear regression and 'Rodbard NIH Image', this is calculated for the + * fit actually done, not for the residuals of the original data. + */ + public double getFitGoodness() { + if (Double.isNaN(sumY)) calculateSumYandY2(); + double sumMeanDiffSqr = sumY2 - sumY*sumY/sumWeights; + double fitGoodness = 0.0; + int degreesOfFreedom = numPoints - getNumParams(); + if (sumMeanDiffSqr > 0.0 && degreesOfFreedom > 0) + fitGoodness = 1.0 - (getSumResidualsSqr()/ sumMeanDiffSqr) * numPoints / (double)degreesOfFreedom; + + return fitGoodness; + } + + public int getStatus() { + return linearRegressionUsed ? Minimizer.SUCCESS : minimizerStatus; + } + + /** Get a short text with a short description of the status. Should be preferred over + * Minimizer.STATUS_STRING[getMinimizer().getStatus()] because getStatusString() + * better explains the problem in some cases of initialization failure (data not + * compatible with the fit function chosen) */ + public String getStatusString() { + return errorString != null ? errorString : minimizer.STATUS_STRING[getStatus()]; + } + + /** Get a string with detailed description of the curve fitting results (several lines, + * including the fit parameters). + */ + public String getResultString() { + String resultS = "\nFormula: " + getFormula() + + "\nMacro code: "+getMacroCode() + + "\nStatus: "+getStatusString(); + if (getStatus()==Minimizer.INITIALIZATION_FAILURE) + return resultS; + if (!linearRegressionUsed) resultS += "\nNumber of completed minimizations: " + minimizer.getCompletedMinimizations(); + resultS += "\nNumber of iterations: " + getIterations(); + if (!linearRegressionUsed) resultS += " (max: " + minimizer.getMaxIterations() + ")"; + resultS += "\nTime: "+time+" ms" + + "\nSum of residuals squared: " + IJ.d2s(getSumResidualsSqr(),5,9) + + "\nStandard deviation: " + IJ.d2s(getSD(),5,9) + + "\nR^2: " + IJ.d2s(getRSquared(),5) + + "\nParameters:"; + char pChar = 'a'; + double[] pVal = getParams(); + for (int i = 0; i < numParams; i++) { + resultS += "\n " + pChar + " = " + IJ.d2s(pVal[i],5,9); + pChar++; + } + return resultS; + } + + /** Set maximum number of simplex restarts to do. See Minimizer.setMaxRestarts for details. */ + public void setRestarts(int maxRestarts) { + minimizer.setMaxRestarts(maxRestarts); + } + + /** Set the maximum error. by which the sum of residuals may deviate from the true value + * (relative w.r.t. full sum of rediduals). Possible range: 0.1 ... 10^-16 */ + public void setMaxError(double maxRelError) { + if (Double.isNaN(maxRelError)) return; + if (maxRelError > 0.1) maxRelError = 0.1; + if (maxRelError < 1e-16) maxRelError = 1e-16; // can't be less than numerical accuracy + this.maxRelError = maxRelError; + } + + /** Create output on the number of iterations in the ImageJ Status line, e.g. + * " 50 (max 750); ESC to stop" + * @param ijStatusString Displayed in the beginning of the status message. No display if null. + * E.g. "Curve Fit: Iteration " + * @param checkEscape When true, the Minimizer stops if escape is pressed and the status + * becomes ABORTED. Note that checking for ESC does not work in the Event Queue thread. */ + public void setStatusAndEsc(String ijStatusString, boolean checkEscape) { + minimizer.setStatusAndEsc(ijStatusString, checkEscape); + } + + /** Get number of iterations performed. Returns 1 in case the fit was done by linear regression only. */ + public int getIterations() { + return linearRegressionUsed ? 1 : minimizer.getIterations(); + } + + /** Get maximum number of iterations allowed (sum of iteration count for all restarts) */ + public int getMaxIterations() { + return minimizer.getMaxIterations(); + } + + /** Set maximum number of iterations allowed (sum of iteration count for all restarts) */ + public void setMaxIterations(int maxIter) { + minimizer.setMaxIterations(maxIter); + } + + /** Get maximum number of simplex restarts to do. See Minimizer.setMaxRestarts for details. */ + public int getRestarts() { + return minimizer.getMaxRestarts(); + } + + /** Returns the status of the Minimizer after doFit. Minimizer.SUCCESS indicates a + * successful completion. In case of Minimizer.INITIALIZATION_FAILURE, fitting could + * not be performed because the data and/or initial parameters are not compatible + * with the function value. In that case, getStatusString may explain the problem. + * For further status codes indicating problems during fitting, see the status codes + * of the Minimzer class. */ + + /** returns the array with the x data */ + public double[] getXPoints() { + return xData; + } + + /** returns the array with the y data */ + public double[] getYPoints() { + return yData; + } + + /** returns the code of the fit type of the fit performed */ + public int getFit() { + return fitType; + } + + /** returns the name of the fit function of the fit performed */ + public String getName() { + if (fitType==CUSTOM) + return "User-defined"; + if (fitType==GAUSSIAN_INTERNAL) + fitType = GAUSSIAN; + else if (fitType==RODBARD_INTERNAL) + fitType = RODBARD; + return fitList[fitType]; + } + + /** returns a String with the formula of the fit function used */ + public String getFormula() { + if (fitType==CUSTOM) + return customFormula; + if (fitType==GAUSSIAN_INTERNAL) + fitType = GAUSSIAN; + else if (fitType==RODBARD_INTERNAL) + fitType = RODBARD; + return fList[fitType]; + } + + /** Returns macro code of the form "y = ...x" for the fit function used. + * Note that this is not neccessarily the equation acutally used for the fit + * (for the various "linear regression" types and RODBARD2, the fit is done + * differently). Note that no macro code may be avialable for custom fits + * using the UserFunction interface. */ + public String getMacroCode() { + if (fitType==CUSTOM) + return customFormula; + if (fitType==GAUSSIAN_INTERNAL) + fitType = GAUSSIAN; + else if (fitType==RODBARD_INTERNAL) + fitType = RODBARD; + return fMacro[fitType]; + } + + /** Returns an array of fit names with nicer sorting */ + public static String[] getSortedFitList() { + if (sortedFitList == null) { + String[] tmpList = new String[fitList.length]; + for (int i=0; i h = new Hashtable(); + for (int i=0; i= 0) + factor = params[i--]; + if (offsetParam >= 0) + offset = params[i]; + sumResidualsSqr = params[numParams-numRegressionParams]; // sum of squared residuals has been calculated already + params[numParams] = sumResidualsSqr; // ... and is now stored in its new (proper) place + } + // move array elements to position in array with full number of parameters + for (int i=numParams-1, iM=numParams-numRegressionParams-1; i>=0; i--) { + if (i == offsetParam) + params[i] = offset; + else if (i == factorParam) + params[i] = factor; + else if (iM>=0) + params[i] = params[iM--]; + else + params[i] = Double.NaN; + } + params[numParams] = sumResidualsSqr; + } + + /** Determine sum of squared residuals with linear regression. + * The sum of squared residuals is written to the array element with index 'numParams', + * the offset and factor params (if any) are written to their proper positions in the + * params array */ + private void doRegression(double[] params) { + double sumX=0, sumX2=0, sumXY=0; //sums for regression; here 'x' are function values + double sumY=0, sumY2=0; //only calculated for 'slope', otherwise we use the values calculated already + double sumWeights=0; + for (int i=0; i= 0) { + factor = (sumXY-sumX*sumY/sumWeights)/(sumX2-sumX*sumX/sumWeights); + if (restrictPower & factor<=0) // power-law fit with (0,0) point: power must be >0 + factor = 1e-100; + else if (Double.isNaN(factor) || Double.isInfinite(factor)) + factor = 0; // all 'x' values are equal, any factor (slope) will fit + } + double offset = (sumY-factor*sumX)/sumWeights; + params[offsetParam] = offset; + sumResidualsSqr = sqr(factor)*sumX2 + sumWeights*sqr(offset) + sumY2 + + 2*factor*offset*sumX - 2*factor*sumXY - 2*offset*sumY; + // check for accuracy problem: large difference of small numbers? + // Don't report unrealistic or even negative values, otherwise minimization could lead + // into parameters where we have a numeric problem + if (sumResidualsSqr < 2e-15*(sqr(factor)*sumX2 + sumWeights*sqr(offset) + sumY2)) + sumResidualsSqr = 2e-15*(sqr(factor)*sumX2 + sumWeights*sqr(offset) + sumY2); + //if(){IJ.log("sumX="+sumX+" sumX2="+sumX2+" sumXY="+sumXY+" factor="+factor+" offset=="+offset);} + } + params[numParams] = sumResidualsSqr; + if (factorParam >= 0) + params[factorParam] = factor; + } + /** Convert full set of parameters to minimizer parameters and returns the sum of residuals squared. + * The last array elements, not used by the minimizer, are the offset and factor parameters (if any) + */ + private double fullParamsToMinimizerParams(double[] params) { + double offset = offsetParam >=0 ? params[offsetParam] : 0; + double factor = factorParam >=0 ? params[factorParam] : 0; + double sumResidualsSqr = params[numParams]; + + for (int i=0, iNew=0; i= 0) + params[i--] = factor; + if (offsetParam >= 0) + params[i--] = offset; + params[i--] = sumResidualsSqr; + return sumResidualsSqr; + } + + /** In case one or two parameters are calculated by regression and not by the minimizer: + * Make modified initialParams and initialParamVariations for the Minimizer + */ + private void modifyInitialParamsAndVariations() { + minimizerInitialParams = initialParams.clone(); + minimizerInitialParamVariations = initialParamVariations.clone(); + if (numRegressionParams > 0) // convert to shorter arrays with only the parameters used by the minimizer + for (int i=0, iNew=0; ixMax) xMax = xData[i]; + if (xData[i]yMax) { yMax = yData[i]; xOfMax = xData[i]; } + if (yData[i] 0 && fitType==RODBARD) { + errorString = "Cannot fit "+fitList[fitType]+" to mixture of x<0 and x>0"; + return false; + } else if (xMin <= 0 && fitType==LOG) { + errorString = "Cannot fit "+fitList[fitType]+" when x<=0"; + return false; + } + + if (!hasInitialParams) { + switch (fitType) { + //case POLY2: case POLY3: case POLY4: case POLY5: case POLY6: case POLY7: case POLY8: + // offset&slope calculated via regression; leave the others at 0 + + // also for the other cases, some initial parameters are unused; only to show them with 'showSettings' + case EXPONENTIAL: // a*exp(bx) assuming change by factor of e between xMin & xMax + initialParams[1] = 1.0/(xMax-xMin+1e-100) * Math.signum(yMean) * Math.signum(slope); + initialParams[0] = yMean/Math.exp(initialParams[1]*xMean); //don't care, done by regression + break; + case EXP_WITH_OFFSET: // a*exp(-bx) + c assuming b>0, change by factor of e between xMin & xMax + case EXP_RECOVERY: // a*(1-exp(-bx)) + c + initialParams[1] = 1./(xMax-xMin+1e-100); + initialParams[0] = (yMax-yMin+1e-100)/Math.exp(initialParams[1]*xMean) * Math.signum(slope) * + fitType==EXP_RECOVERY ? 1 : -1; // don't care, we will do this via regression + initialParams[2] = 0.5*yMean; // don't care, we will do this via regression + break; + case EXP_RECOVERY_NOOFFSET: // a*(1-exp(-bx)) + initialParams[1] = 1.0/(xMax-xMin+1e-100) * Math.signum(yMean) * Math.signum(slope); + initialParams[0] = yMean/Math.exp(initialParams[1]*xMean); //don't care, done by regression + break; + case POWER: // ax^b, assume linear for the beginning + initialParams[0] = yMean/(Math.abs(xMean+1e-100)); // don't care, we will do this via regression + initialParams[1] = 1.0; + break; + case LOG: // a*ln(bx), assume b=e/xMax + initialParams[0] = yMean; // don't care, we will do this via regression + initialParams[1] = Math.E/(xMax+1e-100); + break; + case LOG2: // y = a+b*ln(x-c) + initialParams[0] = yMean; // don't care, we will do this via regression + initialParams[1] = (yMax-yMin+1e-100)/(xMax-xMin+1e-100); // don't care, we will do this via regression + initialParams[2] = Math.min(0., -xMin-0.1*(xMax-xMin)-1e-100); + break; + case RODBARD: // d+(a-d)/(1+(x/c)^b) + initialParams[0] = firsty; // don't care, we will do this via regression + initialParams[1] = 1.0; + initialParams[2] = xMin < 0 ? xMin : xMax; //better than xMean; + initialParams[3] = lasty; // don't care, we will do this via regression + break; + case INV_RODBARD: case RODBARD2: // c*((x-a)/(d-x))^(1/b) + initialParams[0] = xMin - 0.1 * (xMax-xMin); + initialParams[1] = slope >= 0 ? 1.0 : -1.0; + initialParams[2] = yMax; // don't care, we will do this via regression + initialParams[3] = xMax + (xMax - xMin); + break; + case GAMMA_VARIATE: // // b*(x-a)^c*exp(-(x-a)/d) + // First guesses based on following observations (naming the 'x' coordinate 't'): + // t0 [a] = time of first rise in gamma curve - so use the user specified first x value + // tmax = t0 + c*d where tmX is the time of the peak of the curve + // therefore an estimate for c and d is sqrt(tm-t0) + // K [a] can now be calculated from these estimates + initialParams[0] = xMin; + double cd = xOfMax - xMin; + if (cd < 0.1*(xMax-xMin)) cd = 0.1*(xMax-xMin); + initialParams[2] = Math.sqrt(cd); + initialParams[3] = Math.sqrt(cd); + initialParams[1] = yMax / (Math.pow(cd, initialParams[2]) * Math.exp(-cd/initialParams[3])); // don't care, we will do this via regression + break; + case GAUSSIAN: // a + (b-a)*exp(-(x-c)^2/(2d^2)) + initialParams[0] = yMin; //actually don't care, we will do this via regression + initialParams[1] = yMax; //actually don't care, we will do this via regression + initialParams[2] = xOfMax; + initialParams[3] = 0.39894 * (xMax-xMin) * (yMean-yMin)/(yMax-yMin+1e-100); + break; + case GAUSSIAN_NOOFFSET: // a*exp(-(x-b)^2/(2c^2)) + initialParams[0] = yMax; //actually don't care, we will do this via regression + initialParams[1] = xOfMax; //actually don't care, we will do this via regression + initialParams[2] = 0.39894 * (xMax-xMin) * yMean/(yMax+1e-100); + break; + case CHAPMAN: // a*(1-exp(-b*x))^c + initialParams[0] = yMax; + initialParams[2] = 1.5; // just assuming any reasonable value + for (int i=1; i0.5*yMax) { //approximately (1-exp(-1))^1.5 = 0.5 + initialParams[1] = 1./xData[i]; + break; + } + if(Double.isNaN(initialParams[1]) || initialParams[1]>1000./xMax) //in case an outlier at the beginning has fooled us + initialParams[1] = 10./xMax; + break; + case ERF: // a+b*erf((x-c)/d) + initialParams[0] = 0.5*(yMax+yMin); //actually don't care, we will do this via regression + initialParams[1] = 0.5*(yMax-yMin+1e-100) * (lasty>firsty ? 1 : -1); //actually don't care, we will do this via regression + initialParams[2] = xMin + (xMax-xMin)*(lasty>firsty ? yMax - yMean : yMean - yMin)/(yMax-yMin+1e-100); + initialParams[3] = 0.1 * (xMax-xMin+1e-100); + break; + //no case CUSTOM: here, was done above + } + } + if (!hasInitialParamVariations) { // estimate initial range for parameters + for (int i=0; i=0; i--) + initialParamVariations[i] = initialParamVariations[i+1]*xFactor; + break; + case EXPONENTIAL: // a*exp(bx) a (and c) is calculated by regression + case EXP_WITH_OFFSET: // a*exp(-bx) + c + case EXP_RECOVERY: // a*(1-exp(-bx)) + c + initialParamVariations[1] = 0.1/(xMax-xMin+1e-100); + break; + // case CHAPMAN: // a*(1-exp(-b*x))^c use default (10% of value) for b, c + // case POWER: // ax^b; use default for b + // case LOG: // a*ln(bx); use default for b + // case LOG2: // y = a+b*ln(x-c); use default for c + case RODBARD: // d+(a-d)/(1+(x/c)^b); a and d calculated by regression + initialParamVariations[2] = 0.5*Math.max((xMax-xMin), Math.abs(xMean)); + initialParamVariations[3] = 0.5*Math.max(yMax-yMin, Math.abs(yMax)); + break; + case INV_RODBARD: // c*((x-a)/(d-x))^(1/b); c calculated by regression + initialParamVariations[0] = 0.01*Math.max(xMax-xMin, Math.abs(xMax)); + initialParamVariations[2] = 0.1*Math.max(yMax-yMin, Math.abs(yMax)); + initialParamVariations[3] = 0.1*Math.max((xMax-xMin), Math.abs(xMean)); + break; + case GAMMA_VARIATE: // // b*(x-a)^c*exp(-(x-a)/d); b calculated by regression + // First guesses based on following observations: + // t0 [b] = time of first rise in gamma curve - so use the user specified first limit + // tm = t0 + a*B [c*d] where tm is the time of the peak of the curve + // therefore an estimate for a and B is sqrt(tm-t0) + // K [a] can now be calculated from these estimates + initialParamVariations[0] = 0.1*Math.max(yMax-yMin, Math.abs(yMax)); + double ab = xOfMax - firstx + 0.1*(xMax-xMin+1e-100); + initialParamVariations[2] = 0.1*Math.sqrt(ab); + initialParamVariations[3] = 0.1*Math.sqrt(ab); + break; + case GAUSSIAN: // a + (b-a)*exp(-(x-c)^2/(2d^2)); a,b calculated by regression + initialParamVariations[2] = 0.2*initialParams[3]; //(and default for d) + break; + case GAUSSIAN_NOOFFSET: // a*exp(-(x-b)^2/(2c^2)) + initialParamVariations[1] = 0.2*initialParams[2]; //(and default for c) + break; + case ERF: // a+b*erf((x-c)/d) + initialParamVariations[2] = 0.1 * (xMax-xMin+1e-100); + initialParamVariations[3] = 0.5 * initialParams[3]; + break; + } + } + return true; + } + + /** Set multiplyParams and offsetParam for built-in functions. This allows us to use linear + * regression, reducing the number of parameters used by the Minimizer by up to 2, and + * improving the speed and success rate of the minimization process */ + private void getOffsetAndFactorParams() { + offsetParam = -1; + factorParam = -1; + hasSlopeParam = false; + switch (fitType) { + case STRAIGHT_LINE: + case POLY2: case POLY3: case POLY4: case POLY5: case POLY6: case POLY7: case POLY8: + offsetParam = 0; + factorParam = 1; + hasSlopeParam = true; + break; + case EXPONENTIAL: // a*exp(bx) + factorParam = 0; + break; + case EXP_WITH_OFFSET: // a*exp(-bx) + c + case EXP_RECOVERY: // a*(1-exp(-bx)) + c + offsetParam = 2; + factorParam = 0; + break; + case EXP_RECOVERY_NOOFFSET: // a*(1-exp(-bx)) + factorParam = 0; + break; + case CHAPMAN: // a*(1-exp(-b*x))^c + factorParam = 0; + break; + case POWER: // ax^b + factorParam = 0; + break; + case LOG: // a*ln(bx) + factorParam = 0; + break; + case LOG2: // y = a+b*ln(x-c) + offsetParam = 0; + factorParam = 1; + break; + case RODBARD_INTERNAL: // d+a/(1+(x/c)^b) + offsetParam = 3; + factorParam = 0; + break; + case INV_RODBARD: // c*((x-a)/(d-x))^(1/b) + factorParam = 2; + break; + case GAMMA_VARIATE: // b*(x-a)^c*exp(-(x-a)/d) + factorParam = 1; + break; + case GAUSSIAN_INTERNAL: // a + b*exp(-(x-c)^2/(2d^2)) + offsetParam = 0; + factorParam = 1; + break; + case GAUSSIAN_NOOFFSET: // a*exp(-(x-b)^2/(2c^2)) + factorParam = 0; + break; + case ERF: // a + b*erf((x-c)/d) + offsetParam = 0; + factorParam = 1; + break; + } + numRegressionParams = 0; + if (offsetParam >= 0) numRegressionParams++; + if (factorParam >= 0) numRegressionParams++; + } + + + /** calculates the sum of y and y^2 (weighted sum if we have weights) */ + private void calculateSumYandY2() { + sumY = 0.0; sumY2 = 0.0; sumWeights = 0.0; + double w = 1.0; + for (int i=0; i0) + minimizer.setMaxIterations((int)n); + n = gd.getNextNumber(); + if (n>=0) + minimizer.setMaxRestarts((int)n); + n = gd.getNextNumber(); + setMaxError(Math.pow(10.0, -n)); + } + + /** + * Gets index of highest value in an array. + * + * @param array the array. + * @return Index of highest value. + */ + public static int getMax(double[] array) { + double max = array[0]; + int index = 0; + for(int i = 1; i < array.length; i++) { + if(max < array[i]) { + max = array[i]; + index = i; + } + } + return index; + } + + public Plot getPlot() { + return getPlot(100); + } + + public Plot getPlot(int points) { + int PLOT_WIDTH=600, PLOT_HEIGHT=350; + double[] x = getXPoints(); + double[] y = getYPoints(); + if (getStatus()==Minimizer.INITIALIZATION_FAILURE) { + Plot plot = new Plot(getFormula(),"X","Y"); + plot.setColor(Color.RED, Color.RED); + plot.addPoints(x, y, PlotWindow.CIRCLE); + plot.setColor(Color.BLUE); + plot.setFrameSize(PLOT_WIDTH, PLOT_HEIGHT); + plot.addLabel(0.02, 0.1, getName()); + plot.addLabel(0.02, 0.2, getStatusString()); + return plot; + } + int npoints = points; + if (npoints1000) + npoints = 1000; + double[] a = Tools.getMinMax(x); + double xmin=a[0], xmax=a[1]; + if (points==256) { + npoints = points; + xmin = 0; + xmax = 255; + } + a = Tools.getMinMax(y); + double ymin=a[0], ymax=a[1]; //y range of data points + float[] px = new float[npoints]; + float[] py = new float[npoints]; + double inc = (xmax-xmin)/(npoints-1); + double tmp = xmin; + for (int i=0; i0 + private int randomSeed; // for starting the random number generator + private boolean useSingleThread = // single thread if one processor or set by useSingleThread(true) + Runtime.getRuntime().availableProcessors() <= 1; + private int status; // SUCCESS or error code + private boolean wasInitialized; // initialization was successful at least once + private double[] result; // result data+function value + private Vector resultsVector; // holds several results if multiple tries; the best one is kept. + private String ijStatusString = null; // shown together with iteration count in status, no display if null + private boolean checkEscape; // whether to stop when Escape is pressed + private int nextIterationForStatus = 10;// next iteration when we should display the status + private long startTime; // of the whole minimization process + /*private Hashtable simpTable = + new Hashtable(); //for each thread, holds a reference to its simplex */ + + //----------------------------------------------------------------------------- + + + // We can't set the function in the constructor because the CurveFitter + // allows specifying the fit function after other variables + /** + * Set the the target function, i.e. function whose value should be minimized. + * @param userFunction The class having a function to minimize (implementing + * the UserFunction interface). + * This function must allow simultaneous calls in multiple threads unless + * setMaximumThreads(1); has been called. + * Note that the function will be called with at least numParams+1 array + * elements; the last one is needed to store the function value. Further + * array elements for private use in the user function may be added by + * calling setExtraArrayElements. + * @param numParams Number of independent variables (also called parameters) + * of the function. + */ + public void setFunction(UserFunction userFunction, int numParams) { + if (maxIter<=0) { + maxIter = ITER_FACTOR*numParams*numParams; + if (maxRestarts > 0) + maxIter *= 2; + } + this.userFunction = userFunction; + this.numParams = numParams; + this.numVertices = numParams+1; + } + + /** Perform minimization with the gradient-enhanced simplex method once or a few + * times, depending on the value of 'restarts'. Running it several times helps + * to reduce the probability of finding local minima or accepting one of the rare + * results where the minimizer has got stuck before finding the true minimum. + * We are using two threads and terminate after two equal results. Thus, apart + * from the overhead of starting a new thread (typically < 1 ms), for unproblematic + * minimization problems on a dual-core machine this is almost as fast as running + * it once. + * + * Use 'setFunction' first to define the function and number of parameters. + * Afterwards, use the 'getParams' method to access the result. + * + * @param initialParams Array with starting values of the parameters (variables). + * When null, the starting values are assumed to be 0. + * The target function must be defined (not returning NaN) for + * the values specified as initialParams. + * @param initialParamVariations Parameters (variables) are initially varied by up to +/- + * this value. If not given (null), initial variations are taken as + * 10% of initial parameter value or 0.01 for parameters that are zero. + * When this array is given, all elements must be positive (nonzero). + * If one or several initial parameters are zero, is advisable to set the initialParamVariations + * array to useful values indicating the typical order of magnitude of the parameters. + * For target functions with only one minimum, convergence is fastest with large values of + * initialParamVariations, so that the expected value is within initialParam+/-initialParamVariations. + * If local minima can occur, it is best to use a value close to the expected global minimum, + * and rather small initialParamVariations, much lower than the distance to the nearest local + * minimum. + * + * @return status code; SUCCESS if two attempts have found minima with the + * same value (within the error bounds); so a minimum has been found + * with very high probability. + */ + public int minimize(final double[] initialParams, final double[] initialParamVariations) { + status = SUCCESS; + resultsVector = new Vector(); + int maxLoopCount = maxRestarts+1; + if (useSingleThread) maxLoopCount*=2; // if we have only one thread, loop twice as many times + for (int i=0; i0 && !useSingleThread) { // set up 2nd thread to minimize + final int seed = randomSeed+1000000+i; + final Thread thread = new Thread( + new Runnable() { + final public void run() { + minimizeOnce(initialParams, initialParamVariations, seed); + } + }, "Minimizer-1" + ); + thread.setPriority( Thread.currentThread().getPriority() ); + thread.start(); + secondThread = thread; + } + minimizeOnce(initialParams, initialParamVariations, randomSeed+i); //minimize in main thread + if (secondThread != null) try { + secondThread.join(); // wait until send thread is done + } catch (InterruptedException e) {} + if (resultsVector.size() == 0 && result==null) + return status; + if (result==null) + result = (double[])resultsVector.get(0); + for (double[] r : resultsVector) // find best result so far + if (value(r) < value(result)) + result = r; + if (status != SUCCESS && status != REINITIALIZATION_FAILURE && status != MAX_ITERATIONS_EXCEEDED) + return status; // no more tries if permanent error or aborted + if (totalNumIter >= maxIter) + return MAX_ITERATIONS_EXCEEDED; // no more tries if too many iterations + for (int ir=0; ir= 2) return SUCCESS; // if we have two (almost) equal results, it's enough + } //for i <= maxRestarts + if (ijStatusString != null) + IJ.showStatus(""); // reset status display + return maxRestarts>0 ? + MAX_RESTARTS_EXCEEDED : // number of restarts exceeded without two equal results + status; // if only one run was required, we can't have 2 equal results + } + + /** Perform minimization with the simplex method once, including re-initialization until + * we have a stable solution. + * Use the 'getParams' method to access the result. + * + * @param initialParams Array with starting values of the parameters (variables). + * When null, the starting values are assumed to be 0. + * The target function must be defined (not returning NaN) for + * the values specified as initialParams. + * @param initialParamVariations Parameters (variables) are initially varied by up to +/- + * this value. If not given (null), iniital variations are taken as + * 10% of inial parameter value or 0.01 for parameters that are zero. + * When this array is given, all elements must be positive (nonzero). + * If one or several initial parameters are zero, is advisable to set the initialParamVariations + * array to useful values indicating the typical order of magnitude of the parameters. + * For target functions with only one minimum, convergence is fastest with large values of + * initialParamVariations, so that the expected value is within initialParam+/-initialParamVariations. + * If local minima can occur, it is best to use a value close to the expected global minimum, + * and rather small initialParamVariations, much lower than the distance to the nearest local + * minimum. + * + * @return status code; SUCCESS if it is considered likely that a minimum of the + * target function has been found. + */ + public int minimizeOnce(double[] initialParams, double[] initialParamVariations) { + status = SUCCESS; + minimizeOnce(initialParams, initialParamVariations, randomSeed); + return status; + } + + /** Get the result, i.e., the set of parameter values (i.e., variable values) + * from the best corner of the simplex. Note that the array returned may have more + * elements than numParams; ignore the rest. + * May return an array with only NaN values in case the minimize call has returned + * an INITIALIZATION_FAILURE status or that abort() has been called at the very + * beginning of the minimization. + * Do not call this method before minimization. */ + public double[] getParams() { + if (result == null) { + result = new double[numParams+1+numExtraArrayElements]; + Arrays.fill(result, Double.NaN); + } + return result; + } + + /** Get the value of the minimum, i.e. the value associated with the resulting parameters + * as obtained by getParams(). May return NaN in case the minimize call has returned + * an INITIALIZATION_FAILURE status or that abort() has been called at the very + * beginning of the minimization. + * Do not call this method before minimization. */ + public double getFunctionValue() { + if (result == null) { + result = new double[numParams+1]; + Arrays.fill(result, Double.NaN); + } + return value(result); + } + + /** Get number of iterations performed (includes all restarts). One iteration needs + * between one and numParams+3 calls of the target function (typically two calls + * per iteration) */ + public int getIterations() { + return totalNumIter; + } + + /** Set maximum number of iterations allowed (including all restarts and all threads). + * The number of function calls will be higher, up to about twice the number of + * iterations. + * Note that the number of iterations needed typically scales with the square of + * the dimensions (i.e., numParams^2). + * Default value is 1000 * numParams^2 (half that value if maxRestarts=0), which is + * enough for >99% of all cases (if the maximum number of restarts is set to 2); + * typical number of iterations are below 10 and 20% of this value. + */ + public void setMaxIterations(int x) { + maxIter = x; + } + + /** Get maximum number of iterations allowed. Unless given by 'setMaxIterations', + * this value is defined only after running 'setFunction' */ + public int getMaxIterations() { + return maxIter; + } + + /** Set maximum number of minimization restarts to do. + * With n=0, the minimizer is run once in a single thread. + * With n>0, two threads are used, and if the two results do not agree within + * the error bounds, additional optimizations are done up to n times, each + * with two threads. In any case, if the two best results are within the error + * bounds, the best result is accepted. + * Thus, on dual-core machines running no other jobs, values of n=1 or n=2 (default) + * do not cause a notable increase of computing time for 'easy' optimization problems, + * while greatly reducing the risk of running into spurious local minima or non- + * optimal results due to the minimizer getting stuck. In problematic cases, the + * improved + * The 'n' value does not refer to the restarts within one minimization run + * (there, at least one restart is performed, and restart is repeated until the result + * does not change within the error bounds). + * This value does not affect the 'minimizeOnce' function call. + * When setting the maximum number of restarts to a value much higher than 3, remember + * to adjust the maximum number of iterations (see setMaxIterations). + */ + public void setMaxRestarts(int n) { + maxRestarts = n; + } + + /** Get maximum number of minimization restarts to do */ + public int getMaxRestarts() { + return maxRestarts; + } + /** Get number of minimizations completed (i.e. not aborted or stopped because the + * number of minimization was exceeded). After a minimize(..) call, typically 2 + * for unproblematic cases. Higher number indicate a functin that is difficult to + * minimize or the existence of more than one minimum. + */ + public int getCompletedMinimizations() { + return numCompletedMinimizations; + } + + /** Set a seed to initialize the random number generator, which is used for initialization + * of the simplex. */ + public void setRandomSeed(int n) { + randomSeed = n; + } + + /** Sets the accuracy of convergence. Minimizing is done as long as the + * relative error of the function value is more than this number (Default: 1e-10). + */ + public void setMaxError(double maxRelError) { + this.maxRelError = maxRelError; + } + + /** Sets the accuracy of convergence. Minimizing is done as long as the + * relative error of the function value is more than maxRelError (Default: 1e-10) + * and the maximum absolute error is more than maxAbsError + * (i.e. it is enough to fulfil one of these two criteria) + */ + public void setMaxError(double maxRelError, double maxAbsError) { + this.maxRelError = maxRelError; + this.maxAbsError = maxAbsError; + } + + /** Set the resolution of the parameters, for problems where the target function is not smooth + * but suffers from numerical noise. If all parameters of all vertices are closer to the + * best value than the respective resolution value, minimization is finished, irrespective + * of the difference of the target function values at the vertices */ + public void setParamResolutions(double[] paramResolutions) { + this.paramResolutions = paramResolutions; + } + + /** Call setMaximuThreads(1) to avoid multi-threaded execution (in case the user-provided + * target function does not allow moultithreading). Currently a maximum of 2 thread is used + * irrespective of any higher value. */ + public void setMaximumThreads (int numThreads) { + useSingleThread = numThreads <= 1; + } + + /** Aborts minimization. Calls to getParams() will return the best solution found so far. + * This method may be called from the user-supplied target function. + * If displayStatusAndCheckEsc has been called before, the Minimizer itself checks for the ESC key. + */ + public void abort() { + status = ABORTED; + } + + /** Create output on the number of iterations in the ImageJ Status line, e.g. + * " 50 (max 750); ESC to stop" + * @param ijStatusString Displayed in the beginning of the status message. No display if null. + * E.g. "Optimization: Iteration " + * @param checkEscape When true, the Minimizer stops if escape is pressed and the status + * becomes ABORTED. Note that checking for ESC does not work in the Event Queue thread. */ + public void setStatusAndEsc(String ijStatusString, boolean checkEscape) { + this.ijStatusString = ijStatusString; + this.checkEscape = checkEscape; + } + + /** Add a given number of extra elements to array of parameters (independent vaiables) + * for private use in the user function. Note that the first numParams+1 elements + * should not be touched.*/ + public void setExtraArrayElements(int numExtraArrayElements) { + this.numExtraArrayElements = numExtraArrayElements; + } + /** Get the full simplex of the current thread. This may be useful if the target function + * wants to modify the simplex */ + /* public double[][] getSimplex() { + return simpTable.get(Thread.currentThread()); + } */ + + /** One minimization run (including reinitializations of the simplex until the result is stable) */ + private void minimizeOnce(double[] initialParams, double[] initialParamVariations, int seed) { + Random random = new Random(seed); + double[][] simp = makeSimplex(initialParams, initialParamVariations, random); + if (simp == null) { + status = wasInitialized ? REINITIALIZATION_FAILURE : INITIALIZATION_FAILURE; + return; + } + wasInitialized = true; + if (startTime == 0) + startTime = System.currentTimeMillis(); + //if (IJ.debugMode) showSimplex(simp, seed+" Initialized:"); + int bestVertexNumber = minimize(simp); // first minimization + double bestValueSoFar = value(simp[bestVertexNumber]); + //reinitialize until converged or error/aborted (don't care about reinitialization failure in other thread) + boolean reinitialisationFailure = false; + while (status == SUCCESS || status == REINITIALIZATION_FAILURE) { + double[] paramVariations = + makeNewParamVariations(simp, bestVertexNumber, initialParams, initialParamVariations); + if (!reInitializeSimplex(simp, bestVertexNumber, paramVariations, random)) { + reinitialisationFailure = true; + break; + } + //if (IJ.debugMode) showSimplex(simp, seed+" Reinitialized:"); + bestVertexNumber = minimize(simp); // minimize with reinitialized simplex + if (belowErrorLimit(value(simp[bestVertexNumber]), bestValueSoFar, 2.0)) break; + bestValueSoFar = value(simp[bestVertexNumber]); + } + if (reinitialisationFailure) + status = REINITIALIZATION_FAILURE; + else if (status == SUCCESS || status == REINITIALIZATION_FAILURE) //i.e. not aborted, not max iterations exceeded + numCompletedMinimizations++; // it was a complete minimization + //if (IJ.debugMode) showSimplex(simp, seed+" Final:"); + if (resultsVector != null) synchronized(resultsVector) { + resultsVector.add(simp[bestVertexNumber]); + } else + result = simp[bestVertexNumber]; + } + + /** Minimizes the target function by variation of the simplex. + * Note that one call to this function never does more than 0.4*maxIter iterations. + * @return index of the best value in simp + */ + private int minimize(double[][] simp) { + int[] worstNextBestArray = new int[3]; // used to pass these indices from 'order' function + double[] center = new double[numParams+1+numExtraArrayElements]; // center of all vertices except worst + double[] reflected = new double[numParams+1+numExtraArrayElements]; // the 1st new vertex to try + double[] secondTry = new double[numParams+1+numExtraArrayElements]; // expanded or contracted vertex + + order(simp, worstNextBestArray); + int worst = worstNextBestArray[WORST]; + int nextWorst = worstNextBestArray[NEXT_WORST]; + int best = worstNextBestArray[BEST]; + //showSimplex(simp, "before minimization, value="+value(simp[best])); + + //String operation="ini"; + int thisNumIter=0; + while (true) { + totalNumIter++; // global count over all threads + thisNumIter++; // local count for this minimize call + // THE MINIMIZAION ALGORITHM IS HERE + iteration: { + getCenter(simp, worst, center); // centroid of vertices except worst + // Reflect worst vertex through centriod of not-worst + getVertexAndEvaluate(center, simp[worst], -C_REFLECTION, reflected); + if (value(reflected) <= value(simp[best])) { // if it's better than the best... + // try expanding it + getVertexAndEvaluate(center, simp[worst], -C_EXPANSION, secondTry); + if (value(secondTry) <= value(reflected)) { + copyVertex(secondTry, simp[worst]); // if expanded is better than reflected, keep it + //operation="expa"; + break iteration; + } + } + if (value(reflected) < value(simp[nextWorst])) { + copyVertex(reflected, simp[worst]); // keep reflected if better than 2nd worst + //operation="refl"; + break iteration; + } else if (value(reflected) < value(simp[worst])) { + // try outer contraction + getVertexAndEvaluate(center, simp[worst], -C_CONTRACTION, secondTry); + if (value(secondTry) <= value(reflected)) { + copyVertex(secondTry, simp[worst]); // keep outer contraction + //operation="outC"; + break iteration; + } + } else if (value(reflected) > value(simp[worst]) || Double.isNaN(value(reflected))) { + // else inner contraction + getVertexAndEvaluate(center, simp[worst], C_CONTRACTION, secondTry); + if (value(secondTry) < value(simp[worst])) { + copyVertex(secondTry, simp[worst]); // keep contracted if better than 2nd worst + //operation="innC"; + break iteration; + } + } + // if everything else has failed, contract simplex in on best + shrinkSimplexAndEvaluate(simp, best); + //operation="shri"; + break iteration; + } // iteration: + boolean checkParamResolution = // if new 'worst' is not close to 'best', don't check any further + paramResolutions!=null && belowResolutionLimit(simp[worst], simp[best]); + order(simp, worstNextBestArray); + worst = worstNextBestArray[WORST]; + nextWorst = worstNextBestArray[NEXT_WORST]; + best = worstNextBestArray[BEST]; + + if (checkParamResolution) + if (belowResolutionLimit(simp, best)) // check whether all parameters are within the resolution limit + break; + if (belowErrorLimit(value(simp[best]), value(simp[worst]), 4.0)) { // make sure we are at the minimum: + getCenter(simp, -1, secondTry); // check center of the simplex + evaluate(secondTry); + if (value(secondTry) < value(simp[best])) + copyVertex(secondTry, simp[best]); // better than best: keep + } + if (belowErrorLimit(value(simp[best]), value(simp[worst]), 4.0)) // no large spread of values + break; // looks like we are at the minimum + if (totalNumIter > maxIter || thisNumIter>4*(maxIter/10)) + status = MAX_ITERATIONS_EXCEEDED; + if (status != SUCCESS) + break; + if ((ijStatusString != null || checkEscape) && totalNumIter > nextIterationForStatus) { + long time = System.currentTimeMillis(); + nextIterationForStatus = totalNumIter + (int)(totalNumIter*500L/(time-startTime+1)); //next display 0.5 sec later + if (time - startTime > 1000L) { // display status and check for ESC after the first second + if (checkEscape && IJ.escapePressed()) { + status = ABORTED; + IJ.resetEscape(); + IJ.showStatus(ijStatusString+" ABORTED"); + break; + } + if (ijStatusString != null) { + String statusString = ijStatusString+totalNumIter+" ("+maxIter+" max)"; + if (checkEscape) statusString += " ESC to stop"; + IJ.showStatus(statusString); + } + } + } + } + //showSimplex(simp, "after "+totalNumIter+" iterations: value="+value(simp[best])); + return best; + } + + /** Move along the line between center and worst, where +1 corresponds to worstVertex + * The new point is written to'newVertex' and target function is evaluated there + */ + private void getVertexAndEvaluate(double[] center, double[] worstVertex, double howFar, double[] newVertex) { + for (int i = 0; i < numParams; i++) + newVertex[i] = (1.-howFar)*center[i] + howFar*worstVertex[i]; + evaluate(newVertex); + } + + /** Get center of all vertices except one to exclude + * (may be -1 to exclude none) + * Does not care about function values (i.e., last array elements) */ + private void getCenter(double[][]simp, int excludeVertex, double[] center) { + Arrays.fill(center, 0.); + int nV = 0; + for (int v=0; v= paramResolutions[i]) + return false; + return true; + } + + /** Find initial parameters not yielding a result of NaN in case those given yield NaN + * Called with params containing the initial parameters that have been tried previously */ + private void findValidInitalParams(double[] params, double[] initialParamVariations, Random random) { + final int maxAttempts = 50*numParams*numParams; //max number of attempts to find params that do not lead to NaN + double rangeFactor = 1; // will gradually become larger to handle different orders of magnitude + final double rangeMultiplyLog = Math.log(1e20)/(maxAttempts-1); //will try up to 1e-20 to 1e20*initialParamVariations + double[] firstParams = new double[numParams]; // remember starting params (which may be modified) + double[] variations = new double[numParams]; // new values of parameter variations + for (int i=0; i1e10) + variations[i] = 0.1; + } + for (int attempt=0; attempt 2 && sumSqr > 1e-6) + sumSqr = 1; // don't normalize if attempts with normalized were unsuccessful + for (int i=0; i= numParams ? + initialParamVariations[i] : // relate to initialParamVariations (if given), + Math.max(Math.abs(initialParams[i]), Math.abs(simp[bestVertexNumber][i])); + double logRelativeVariation = paramVariations[i]>relatedTo[i] ? 0 : + Math.log(paramVariations[i]/relatedTo[i]); + logTypicalRelativeVariation += logRelativeVariation; + } + logTypicalRelativeVariation /= numParams; + double typicalRelativeVariation = Math.exp(logTypicalRelativeVariation); + final double WORST_RATIO = 1e-3; //parameter variation should not be lower than typical value by more than this + for (int i=0; i value(simp[worst])) worst = i; + } + nextWorst = best; + for (int i = 0; i < numVertices; i++) + if (i != worst && value(simp[i]) > value(simp[nextWorst])) + nextWorst = i; + worstNextBestArray[WORST] = worst; + worstNextBestArray[NEXT_WORST] = nextWorst; + worstNextBestArray[BEST] = best; + } + + // Display simplex [Iteration: s0(p1, p2....), s1(),....] in Log window + private synchronized void showSimplex(double[][] simp, String heading) { + IJ.log("Minimizer: "+heading); + for (int i = 0; i < numVertices; i++) + showVertex(simp[i], null); + } + private synchronized void showVertex(double[] vertex, String heading) { + if (heading != null) + IJ.log(heading); + String s = ""; + for (int j=0; j < numParams; j++) + s += " " + IJ.d2s(vertex[j], 8,12); + s += " -> " + IJ.d2s(value(vertex), 8,12); + IJ.log(s); + } +} \ No newline at end of file diff --git a/src/ij/measure/ResultsTable.java b/src/ij/measure/ResultsTable.java new file mode 100644 index 0000000..aeef7d3 --- /dev/null +++ b/src/ij/measure/ResultsTable.java @@ -0,0 +1,1637 @@ +package ij.measure; +import ij.*; +import ij.plugin.filter.Analyzer; +import ij.plugin.frame.Editor; +import ij.text.*; +import ij.process.*; +import ij.gui.Roi; +import ij.util.Tools; +import ij.io.*; +import ij.macro.*; +import java.awt.*; +import java.text.*; +import java.util.*; +import java.io.*; +import java.math.RoundingMode; + + +/** This is a table for storing measurement results and strings as columns of values. + Call the static ResultsTable.getResultsTable() method to get a reference to the + ResultsTable used by the Analyze/Measure command. + @see ij.plugin.filter.Analyzer#getResultsTable +*/ +public class ResultsTable implements Cloneable { + + /** Obsolete; use getLastColumn(). */ + public static final int MAX_COLUMNS = 150; + + public static final int COLUMN_NOT_FOUND = -1; + public static final int COLUMN_IN_USE = -2; + public static final int TABLE_FULL = -3; // no longer used + public static final short AUTO_FORMAT = Short.MIN_VALUE; + private static final char commaSubstitute = 0x08B3; + + public static final int AREA=0, MEAN=1, STD_DEV=2, MODE=3, MIN=4, MAX=5, + X_CENTROID=6, Y_CENTROID=7, X_CENTER_OF_MASS=8, Y_CENTER_OF_MASS=9, + PERIMETER=10, ROI_X=11, ROI_Y=12, ROI_WIDTH=13, ROI_HEIGHT=14, + MAJOR=15, MINOR=16, ANGLE=17, CIRCULARITY=18, FERET=19, + INTEGRATED_DENSITY=20, MEDIAN=21, SKEWNESS=22, KURTOSIS=23, + AREA_FRACTION=24, RAW_INTEGRATED_DENSITY=25, CHANNEL=26, SLICE=27, FRAME=28, + FERET_X=29, FERET_Y=30, FERET_ANGLE=31, MIN_FERET=32, ASPECT_RATIO=33, + ROUNDNESS=34, SOLIDITY=35, MIN_THRESHOLD=36, MAX_THRESHOLD=37, LAST_HEADING=37; + private static final String[] defaultHeadings = {"Area","Mean","StdDev","Mode","Min","Max", + "X","Y","XM","YM","Perim.","BX","BY","Width","Height","Major","Minor","Angle", + "Circ.", "Feret", "IntDen", "Median","Skew","Kurt", "%Area", "RawIntDen", "Ch", "Slice", "Frame", + "FeretX", "FeretY", "FeretAngle", "MinFeret", "AR", "Round", "Solidity", "MinThr", "MaxThr"}; + + private int maxRows = 100; // will be increased as needed + private int maxColumns = MAX_COLUMNS; // will be increased as needed + private String[] headings = new String[maxColumns]; + private boolean[] keep = new boolean[maxColumns]; + private short[] decimalPlaces = new short[maxColumns]; + private int counter; + private double[][] columns = new double[maxColumns][]; + private String[] rowLabels; + private int lastColumn = -1; + private StringBuilder sb; + private short precision = 3; + private String rowLabelHeading = ""; + private char delimiter = '\t'; + private boolean headingSet; + private boolean showRowNumbers; + private boolean showRowNumbersSet; + private int baseRowNumber = 1; + private Hashtable stringColumns; + private boolean NaNEmptyCells; + private boolean quoteCommas; + private String title; + private boolean columnDeleted; + private boolean renameWhenSaving; + private boolean saveColumnHeaders = !Prefs.dontSaveHeaders; + + + /** Constructs an empty ResultsTable with the counter=0, no columns + and the precision set to 3 or the "Decimal places" value in + Analyze/Set Measurements if that value is higher than 3. */ + public ResultsTable() { + init(); + } + + /** Constructs a ResultsTable with 'nRows' rows. */ + public ResultsTable(Integer nRows) { + init(); + for (int i=0; iprecision) + precision = (short)p; + for (int i=0; i=maxColumns) + addColumns(); + if (column<0 || column>=maxColumns) + throw new IllegalArgumentException("Column out of range"); + if (counter==0) + incrementCounter(); + if (columns[column]==null) { + columns[column] = new double[maxRows]; + if (NaNEmptyCells) + Arrays.fill(columns[column], Double.NaN); + if (headings[column]==null) + headings[column] = "C"+(column+1); + if (column>lastColumn) lastColumn = column; + } + columns[column][counter-1] = value; + if (counter<25) { + if ((int)value!=value && !Double.isNaN(value)) + decimalPlaces[column] = (short)precision; + } + } + + /** Adds a numeric value to the specified column, on the last + * table row. If the column does not exist, it is created. + * Use addRow() to add another row to the table. + *

JavaScript example: + *

+	 * rt = new ResultsTable();
+	 * for (n=0; n<=2*Math.PI; n+=0.1) {
+	 *    rt.addRow();
+	 *    rt.addValue("n", n);
+	 *    rt.addValue("Sine(n)", Math.sin(n));
+	 *    rt.addValue("Cos(n)", Math.cos(n));
+	 * }
+	 * rt.show("Sine/Cosine Table");
+	 * 
+	 * @see #addRow
+	 * @see #addValue(String,String)
+	 * @see #size
+	 */
+	public void addValue(String column, double value) {
+		if (column==null)
+			throw new IllegalArgumentException("Column is null");
+		int index = getColumnIndex(column);
+		if (index==COLUMN_NOT_FOUND)
+			index = getFreeColumn(column);
+		addValue(index, value);
+		keep[index] = true;
+	}
+	
+	/** Adds a string value to the specified column, on the last
+	 * table row. If the column does not exist, it is created.
+	 * Use addRow() to add another row to the table.
+	 * @see #addRow
+	 * @see #addValue(String,double)
+	 * @see #size
+	 */
+	public void addValue(String column, String value) {
+		if (column==null)
+			throw new IllegalArgumentException("Column is null");
+		int index = getColumnIndex(column);
+		if (index==COLUMN_NOT_FOUND)
+			index = getFreeColumn(column);
+		addValue(index, Double.NaN);
+		setValue(column, size()-1, value);
+		keep[index] = true;
+	}
+
+	/** Adds a label to the beginning of the current row. */
+	public void addLabel(String label) {
+		if (rowLabelHeading.equals(""))
+			rowLabelHeading = "Label";
+		addLabel(rowLabelHeading, label);
+	}
+
+	/**
+	 * @deprecated
+	 * Replaced by setValue(String,int,String)
+	*/
+	public void addLabel(String columnHeading, String label) {
+		if (counter==0)
+			throw new IllegalArgumentException("Counter==0");
+		if (rowLabels==null)
+			rowLabels = new String[maxRows];
+		rowLabels[counter-1] = label;
+		if (columnHeading!=null)
+			rowLabelHeading = columnHeading;
+	}
+	
+	/** Adds a label to the beginning of the specified row, 
+		or updates an existing lable, where 0<=rowshow()
+		to update the window displaying the table. */
+	public void setLabel(String label, int row) {
+		if (row<0||row>=counter)
+			throw new IllegalArgumentException("row>=counter");
+		if (rowLabels==null)
+			rowLabels = new String[maxRows];
+		if (rowLabelHeading.equals(""))
+			rowLabelHeading = "Label";
+		rowLabels[row] = label;
+	}
+	
+	/** Set the row label column to null if the column label is "Label". */
+	public void disableRowLabels() {
+		if (rowLabelHeading.equals("Label"))
+			rowLabels = null;
+	}
+	
+	/** Returns a copy of the given column as a double array,
+		or null if the column is not found. */
+	public double[] getColumn(String column) {
+		int col = getColumnIndex(column);
+		if (col==COLUMN_NOT_FOUND || columns[col]==null)
+			throw new IllegalArgumentException("\""+column+"\" column not found");
+		return getColumnAsDoubles(col);
+	}
+
+	/** Returns a copy of the given column as a String array,
+		or null if the column is not found. */
+	public String[] getColumnAsStrings(String column) {
+		String[] array = new String[size()];
+		if ("Label".equals(column) && rowLabels!=null) {
+			for (int i=0; i=maxColumns))
+			throw new IllegalArgumentException("Index out of range: "+column);
+		if (columns[column]==null)
+			return null;
+		else {
+			float[] data = new float[counter];
+			for (int i=0; i=maxColumns))
+			throw new IllegalArgumentException("Index out of range: "+column);
+		if (columns[column]==null)
+			return null;
+		else {
+			double[] data = new double[counter];
+			for (int i=0; i=maxColumns))
+			return false;
+		else
+			return columns[column]!=null;
+	}
+
+	/** Returns the index of the first column with the given heading.
+		heading. If not found, returns COLUMN_NOT_FOUND. */
+	public int getColumnIndex(String heading) {
+		for (int i=0; ilastColumn) lastColumn = i;
+				return i;
+			}
+			if (headings[i].equals(heading))
+				return COLUMN_IN_USE;
+		}
+		addColumns();
+		lastColumn++;
+		columns[lastColumn] = new double[maxRows];
+		if (NaNEmptyCells)
+			Arrays.fill(columns[lastColumn], Double.NaN);
+		headings[lastColumn] = heading;
+		return lastColumn;
+	}
+	
+	/**	Returns the value of the given column and row, where
+		column must be less than or equal the value returned by
+		getLastColumn() and row must be greater than or equal
+		zero and less than the value returned by size(). */
+	public double getValueAsDouble(int column, int row) {
+		if (column>=maxColumns || row>=counter)
+			throw new IllegalArgumentException("Index out of range: "+column+","+row);
+		if (columns[column]==null)
+			throw new IllegalArgumentException("Column not defined: "+column);
+		return columns[column][row];
+	}
+	
+	/**
+	* @deprecated
+	* replaced by getValueAsDouble
+	*/
+	public float getValue(int column, int row) {
+		return (float)getValueAsDouble(column, row);
+	}
+
+	/**	Returns the value of the specified column and row, where
+		column is the column heading and row is a number greater
+		than or equal zero and less than value returned by size(). 
+		Throws an IllegalArgumentException if this ResultsTable
+		does not have a column with the specified heading. */
+	public double getValue(String column, int row) {
+		if (row<0 || row>=size())
+			throw new IllegalArgumentException("Row out of range");
+		int col = getColumnIndex(column);
+		if (col==COLUMN_NOT_FOUND)
+			throw new IllegalArgumentException("\""+column+"\" column not found");
+		//IJ.log("col: "+col+" "+(col==COLUMN_NOT_FOUND?"not found":""+columns[col]));
+		return getValueAsDouble(col,row);
+	}
+	
+	/** Returns 'true' if the specified column exists and is not emptly. */
+	public boolean columnExists(String column) {
+		int col = getColumnIndex(column);
+		if (col==COLUMN_NOT_FOUND)
+			return false;
+		else
+			return (col=size())
+			throw new IllegalArgumentException("Row out of range");
+		int col = getColumnIndex(column);
+		if (col==COLUMN_NOT_FOUND) {
+			String label = null;
+			if ("Label".equals(column))
+				label = getLabel(row);
+			if (label!=null)
+				return label;
+			else
+				throw new IllegalArgumentException("\""+column+"\" column not found");
+		}
+		return getStringValue(col, row);
+	}
+
+	/** Returns the string value of the given column and row, where
+		column must be less than or equal the value returned by
+		getLastColumn() and row must be greater than or equal
+		zero and less than the value returned by size(). */
+	public String getStringValue(int column, int row) {
+		if (column>=maxColumns || row>=counter)
+			throw new IllegalArgumentException("Index out of range: "+column+","+row);
+		if (columns[column]==null)
+			throw new IllegalArgumentException("Column not defined: "+column);
+		return getValueAsString(column, row);
+	}
+
+	/**	 Returns the label of the specified row. Returns null if the row does not have a label. */
+	public String getLabel(int row) {
+		if (row<0 || row>=size())
+			throw new IllegalArgumentException("Row out of range");
+		String label = null;
+		if (rowLabels!=null && rowLabels[row]!=null)
+				label = rowLabels[row];
+		return label;
+	}
+
+	/** Sets the value of the given column and row, where
+		where 0<=row<size(). If the specified column does 
+		not exist, it is created. When adding columns, 
+		show() must be called to update the 
+		window that displays the table.*/
+	public void setValue(String column, int row, double value) {
+		if (column==null)
+			throw new IllegalArgumentException("Column is null");
+		int col = getColumnIndex(column);
+		if (col==COLUMN_NOT_FOUND)
+			col = getFreeColumn(column);
+		setValue(col, row, value);
+	}
+
+	/** Sets the value of the given column and row, where
+		where 0<=column<=(lastRow+1 and 0<=row<=size(). */
+	public void setValue(int column, int row, double value) {
+		if (column>=maxColumns)
+			addColumns();
+		if (column<0 || column>=maxColumns)
+			throw new IllegalArgumentException("Column out of range");
+		if (row>=counter) {
+			if (row==counter)
+				incrementCounter();
+			else
+				throw new IllegalArgumentException("row>counter");
+		}
+		if (columns[column]==null) {
+			columns[column] = new double[maxRows];
+			if (NaNEmptyCells)
+				Arrays.fill(columns[column], Double.NaN);
+			if (column>lastColumn) lastColumn = column;
+		}
+		columns[column][row] = value;
+		if (headings[column]==null)
+			headings[column] = "C"+(column+1);
+		if ((int)value!=value && !Double.isNaN(value))
+			decimalPlaces[column] = (short)precision;
+	}
+
+	/** Sets the string value of the given column and row, where
+		where 0<=row<size(). If the specified column does 
+		not exist, it is created. When adding columns, 
+		show() must be called to update the 
+		window that displays the table.*/
+	public void setValue(String column, int row, String value) {
+		if (column==null)
+			throw new IllegalArgumentException("Column is null");
+		int col = getColumnIndex(column);
+		if (col==COLUMN_NOT_FOUND)
+			col = getFreeColumn(column);
+		setValue(col, row, value);
+	}
+
+	/** Sets the string value of the given column and row, where
+		where 0<=column<=(lastRow+1 and 0<=row<=size(). */
+	public void setValue(int column, int row, String value) {
+		setValue(column, row, Double.NaN);
+		if (stringColumns==null)
+			stringColumns = new Hashtable();
+		ArrayList stringColumn = (ArrayList)stringColumns.get(new Integer(column));
+		if (stringColumn==null) {
+			stringColumn = new ArrayList();
+			stringColumns.put(new Integer(column), stringColumn);
+		}
+		int size = stringColumn.size();
+		if (row>=size) {
+			for (int i=size; i=maxColumns))
+			throw new IllegalArgumentException("Index out of range: "+column);
+		return headings[column];
+	}
+
+	/** Returns a tab or comma delimited string representing the
+		given row, where 0<=row<=size()-1. */
+	public String getRowAsString(int row) {
+		if ((row<0) || (row>=counter))
+			throw new IllegalArgumentException("Row out of range: "+row);
+		if (sb==null)
+			sb = new StringBuilder(200);
+		else
+			sb.setLength(0);
+		if (showRowNumbers) {
+			sb.append(Integer.toString(row+baseRowNumber));
+			sb.append(delimiter);
+		}
+		if (rowLabels!=null) {
+			if (rowLabels[row]!=null) {
+				String label = rowLabels[row];
+				if (delimiter==',')
+					label = label.replaceAll(",", ";");
+				sb.append(label);
+			}
+			sb.append(delimiter);
+		}
+		for (int i=0; i<=lastColumn; i++) {
+			if (columns[i]!=null) {
+				String value = getValueAsString(i,row);
+				if (quoteCommas) {
+					if (value!=null && value.contains(","))
+						value = "\""+value+"\"";
+				}
+				sb.append(value);
+				if (i!=lastColumn)
+					sb.append(delimiter);
+			}
+		}
+		return new String(sb);
+	}
+	
+	/** Implements the Table.getColumn() macro function. */
+	public Variable[] getColumnAsVariables(String column) {
+		if ("Label".equals(column) && rowLabels!=null) {
+			int n = size();
+			Variable[] labels = new Variable[n];
+			for (int i=0; i=0 && row0 && size()>initialSize) {
+			for (int c=0; c<=lastColumn; c++) {
+				if (c!=col && columns[c]!=null) {
+					String heading = headings[c];
+					if (heading!=null) {
+						for (int i=initialSize; i=0 && row=0)
+			s = d2s(n, 0);
+		else
+			s = d2s(n, precision);
+		return s;
+	}
+		
+	/**
+	* @deprecated
+	* Replaced by addValue(String,double) and setValue(String,int,double)
+	*/
+	public void setHeading(int column, String heading) {
+		if ((column<0) || (column>=headings.length))
+			throw new IllegalArgumentException("Column out of range: "+column);
+		headings[column] = heading;
+		if (columns[column]==null) {
+			columns[column] = new double[maxRows];
+			if (NaNEmptyCells)
+				Arrays.fill(columns[column], Double.NaN);
+		}
+		if (column>lastColumn) lastColumn = column;
+		headingSet = true;
+	}
+	
+	/** Sets the headings used by the Measure command ("Area", "Mean", etc.). */
+	public void setDefaultHeadings() {
+		for(int i=0; i9) precision=9;
+		this.precision = (short)precision;
+		for (int i=0; i=headings.length))
+			throw new IllegalArgumentException("Column out of range: "+column);
+		decimalPlaces[column] = (short)digits;
+	}
+
+	/** Set 'true' to initially fill data arrays with NaNs instead of zeros. */
+	public void setNaNEmptyCells(boolean NaNEmptyCells) {
+		this.NaNEmptyCells = NaNEmptyCells;
+	}
+
+	public void showRowNumbers(boolean showNumbers) {
+		showRowNumbers = showNumbers;
+		baseRowNumber = 1;
+		showRowNumbersSet = true;
+	}
+
+	public boolean showRowNumbers() {
+		return showRowNumbers;
+	}
+
+	public void showRowIndexes(boolean showIndexes) {
+		showRowNumbers = showIndexes;
+		baseRowNumber = showIndexes?0:1;
+	}
+
+	public void saveColumnHeaders(boolean save) {
+		saveColumnHeaders = save;
+	}
+
+	private static DecimalFormat[] df;
+	private static DecimalFormat[] sf;
+	private static DecimalFormatSymbols dfs;
+
+	/** This is a version of IJ.d2s() that uses scientific notation for
+		small numbes that would otherwise display as zero. */
+	public static String d2s(double n, int decimalPlaces) {
+		if (Double.isNaN(n)||Double.isInfinite(n))
+			return ""+n;
+		if (n==Float.MAX_VALUE) // divide by 0 in FloatProcessor
+			return "3.4e38";
+		double np = n;
+		if (n<0.0) np = -n;
+		if ((np<0.001 && np!=0.0 && np<1.0/Math.pow(10,decimalPlaces)) || np>999999999999d || decimalPlaces<0) {
+			if (decimalPlaces<0) {
+				decimalPlaces = -decimalPlaces;
+				if (decimalPlaces>9) decimalPlaces=9;
+			} else
+				decimalPlaces = 3;
+			if (sf==null) {
+				if (dfs==null)
+					dfs = new DecimalFormatSymbols(Locale.US);
+				sf = new DecimalFormat[10];
+				sf[1] = new DecimalFormat("0.0E0",dfs);
+				sf[2] = new DecimalFormat("0.00E0",dfs);
+				sf[3] = new DecimalFormat("0.000E0",dfs);
+				sf[4] = new DecimalFormat("0.0000E0",dfs);
+				sf[5] = new DecimalFormat("0.00000E0",dfs);
+				sf[6] = new DecimalFormat("0.000000E0",dfs);
+				sf[7] = new DecimalFormat("0.0000000E0",dfs);
+				sf[8] = new DecimalFormat("0.00000000E0",dfs);
+				sf[9] = new DecimalFormat("0.000000000E0",dfs);
+			}
+			return sf[decimalPlaces].format(n); // use scientific notation
+		}
+		if (decimalPlaces<0) decimalPlaces = 0;
+		if (decimalPlaces>9) decimalPlaces = 9;
+		if (df==null) {
+			dfs = new DecimalFormatSymbols(Locale.US);
+			df = new DecimalFormat[10];
+			df[0] = new DecimalFormat("0", dfs);
+			df[1] = new DecimalFormat("0.0", dfs);
+			df[2] = new DecimalFormat("0.00", dfs);
+			df[3] = new DecimalFormat("0.000", dfs);
+			df[4] = new DecimalFormat("0.0000", dfs);
+			df[5] = new DecimalFormat("0.00000", dfs);
+			df[6] = new DecimalFormat("0.000000", dfs);
+			df[7] = new DecimalFormat("0.0000000", dfs);
+			df[8] = new DecimalFormat("0.00000000", dfs);
+			df[9] = new DecimalFormat("0.000000000", dfs);
+			df[0].setRoundingMode(RoundingMode.HALF_UP);
+		}
+		return df[decimalPlaces].format(n);
+	}
+
+	/** Deletes the specified row. */
+	public synchronized void deleteRow(int rowIndex) {
+		if (counter==0 || rowIndex<0 || rowIndex>counter-1)
+			return;
+		if (rowLabels!=null) {
+			rowLabels[rowIndex] = null;
+			for (int i=rowIndex; i0)
+				Analyzer.setUnsavedMeasurements(true);
+		} else {
+			Frame frame = WindowManager.getFrame(windowTitle);
+			TextWindow win;
+			if (frame!=null && frame instanceof TextWindow) {
+				win = (TextWindow)frame;
+				if (win!=null) {
+					win.toFront();
+					WindowManager.setWindow(frame);
+				}
+			} else {
+				int chars = Math.max(size()>0?getRowAsString(0).length():15, getColumnHeadings().length());
+				int width = 100 + chars*10;
+				if (width<180) width=180;
+				if (width>700) width=700;
+				if (showRowNumbers)
+					width += 50;
+				int height = 300;
+				if (size()>15) height = 400;
+				if (size()>30 && width>300) height = 500;
+				win = new TextWindow(windowTitle, "", width, height);
+				cloneNeeded = true;
+			}
+			tp = win.getTextPanel();
+			tp.setColumnHeadings(tableHeadings);
+			newWindow = tp.getLineCount()==0;
+		}
+		tp.setResultsTable(cloneNeeded?(ResultsTable)this.clone():this);
+		int n = size();
+		if (n>0) {
+			if (tp.getLineCount()>0) tp.clear();
+			for (int i=0; i=getMaxColumns()) {
+			addColumns();
+		//IJ.log("addColumns: "+getMaxColumns());
+		}
+		if (last=rt2.getMaxColumns())
+				last = rt2.getMaxColumns() - 1;
+		}
+		for (int i=0; i<=last; i++) {
+			//IJ.log(i+"  "+rt2.getColumn(i)+"  "+columns[i]+"  "+rt2.getColumnHeading(i)+"  "+getColumnHeading(i));
+			if (rt2.getColumn(i)!=null && columns[i]==null) {
+				columns[i] = new double[maxRows];
+				if (NaNEmptyCells)
+					Arrays.fill(columns[i], Double.NaN);
+				headings[i] = rt2.getColumnHeading(i);
+				if (i>lastColumn) lastColumn = i;
+			} else if (rt2.getColumn(i)==null && columns[i]!=null && !keep[i])
+				columns[i] = null;
+		}
+		if (rt2.getRowLabels()==null)
+			rowLabels = null;
+		else if (rt2.getRowLabels()!=null && rowLabels==null) {
+			rowLabels = new String[maxRows];
+			rowLabelHeading = "Label";
+		}
+		if (size()>0) show("Results");
+	}
+	
+	int getMaxColumns() {
+		return maxColumns;
+	}
+	
+	String[] getRowLabels() {
+		return rowLabels;
+	}
+	
+	/** Opens a tab or comma delimited text file and returns it 
+	* as a ResultsTable, without requiring a try/catch statement.
+	* Displays a file open dialog if 'path' is empty or null.
+	*/
+	public static ResultsTable open2(String path) {
+		ResultsTable rt = null;
+		try {
+			rt = open(path);
+		} catch (IOException e) {
+			IJ.error("Open Results", e.getMessage());
+			rt = null;
+		}
+		return rt;
+	}
+	
+	/** Opens a tab or comma delimited text file and returns it as a 
+	* ResultsTable. Displays a file open dialog if 'path' is empty or null.
+	* @see #open2(String)
+	*/
+	public static ResultsTable open(String path) throws IOException {
+		final String lineSeparator =  "\n";
+		if (path==null || path.equals("")) {
+			OpenDialog od = new OpenDialog("Open Table", "");
+			String dir = od.getDirectory();
+			String name = od.getFileName();
+			if (name==null)
+				return null;
+			path = dir+name;
+		}
+		String text = IJ.openAsString(path);
+		if (text==null)
+			return null;
+		if (text.length()==0)
+			return new ResultsTable();
+		if (text.startsWith("Error:"))
+			throw new IOException("Error opening "+path);
+		boolean csv = path.endsWith(".csv") || path.endsWith(".CSV");
+		String cellSeparator =  csv?",":"\t";
+		boolean commasReplaced = false;
+		if (csv && text.contains("\"")) {
+			text = replaceQuotedCommas(text);
+			commasReplaced = true;
+		}
+		String commaSubstitute2 = ""+commaSubstitute;
+		String[] lines = text.split(lineSeparator);
+		if (lines.length==0 || (lines.length==1 && lines[0].length()==0))
+			throw new IOException("Table is empty or invalid");
+		String[] headings = lines[0].split(cellSeparator);
+		if (headings.length<1)
+			throw new IOException("This is not a tab or comma delimited text file.");
+		int numbersInHeadings = 0;
+		for (int i=0; i0&&headings[0].equals(" ")?1:0;
+		for (int i=0; i=lines.length) { //empty table?
+			for (int i=0; iResults", ""+"Error saving results:\n   "+e.getMessage());
+			return false;
+		}
+	}
+
+	public boolean saveAndRename(String path) {
+		if (!"Results".equals(title))
+			renameWhenSaving = true;
+		boolean ok = save(path);
+		renameWhenSaving = false;
+		return ok;
+	}
+
+	public void saveAs(String path) throws IOException {
+		boolean emptyTable = size()==0 && lastColumn<0;
+		if (path==null || path.equals("")) {
+			SaveDialog sd = new SaveDialog("Save Table", "Table", Prefs.defaultResultsExtension());
+			String file = sd.getFileName();
+			if (file==null)
+				return;
+			path = sd.getDirectory() + file;
+		}
+		boolean csv = path.endsWith(".csv") || path.endsWith(".CSV");
+		delimiter = csv?',':'\t';
+		PrintWriter pw = null;
+		FileOutputStream fos = new FileOutputStream(path);
+		BufferedOutputStream bos = new BufferedOutputStream(fos);
+		pw = new PrintWriter(bos);
+		boolean saveShowRowNumbers = showRowNumbers;
+		if (Prefs.dontSaveRowNumbers)	
+			showRowNumbers = false;
+		if (saveColumnHeaders && !emptyTable) {
+			String headings = getColumnHeadings();
+			pw.println(headings);
+		}
+		quoteCommas = csv?true:false;
+		for (int i=0; i=0 && index newColumnList = new ArrayList();
+		String[] variables = interp.getVariableNames();
+		for (String variable:variables) {           // check for variables that make a new Column
+			int columnNumber = indexOf(columnNames, variable);
+			if (columnNumber >= 0)                  // variable is a know column
+				columnInUse[columnNumber] = macro.indexOf(variable) >=0;
+			else if (Character.isUpperCase(variable.charAt(0))) {
+				getFreeColumn(variable);            // create new column
+				newColumnList.add(variable);
+			}
+		}
+		String[] newColumns = newColumnList.toArray(new String[0]);
+		int[] newColumnIndices = new int[newColumns.length];
+		for (int i=0; i='0' && names[i].charAt(0)<='9') // variable must not start with digit
+				names[i] = "_"+names[i];
+			names[i] = names[i].replaceAll("[^A-Za-z0-9_]","_");    // replace unsuitable characters with underscores
+			for (int postfix=0; ; postfix++) {
+				boolean isDuplicate = false;
+				for (int j=0; j 0)                                    // remove trailing underscore+postfix
+					names[i] = names[i].substring(0, names[i].lastIndexOf('_'));
+				names[i] += "_"+postfix;                            // add underscore+postfix number
+			}
+		}
+		return names;
+	}
+	
+	public String getTitle() {
+		if (title==null && this==Analyzer.getResultsTable())
+			title = "Results";
+		return title;
+	}
+	
+	public boolean columnDeleted() {
+		return columnDeleted;
+	}
+	
+	/** Selects the row in the "Results" table assocuiated with the specified Roi.
+		The row number is obtained from the roi name..
+	*/
+	public static boolean selectRow(Roi roi) {
+		if (roi==null)
+			return false;	
+		String name = roi.getName();
+		if (name==null || name.length()>8)
+			return false ;
+		Frame frame = WindowManager.getFrame("Results");
+		if (frame==null)
+			return false;
+		if (!(frame instanceof TextWindow))
+			return false ;
+		ResultsTable rt = ((TextWindow)frame).getResultsTable();
+		if (rt==null || rt!=Analyzer.getResultsTable())
+			return false ;
+		double n = Tools.parseDouble(name);
+		if (Double.isNaN(n))
+			return false;
+		int index = (int)n - 1;
+		if (index<0 || index>=rt.size())
+			return false;
+		((TextWindow)frame).getTextPanel().setSelection(index, index);
+    	return true;	
+    }
+    	
+	/** Sorts this table on the specified column, with string support.
+	 * Author: 'mountain_man', 8 April 2019
+	*/
+	public void sort(String column) {
+		int col = getColumnIndex(column);
+		if (col==COLUMN_NOT_FOUND)
+			throw new IllegalArgumentException("Column not found");
+
+		// pad short string columns with "NaN" to avoid "holes" after sorting
+		if (stringColumns!=null) {
+		    for (Object c : stringColumns.values()) {
+			ArrayList sc = (ArrayList) c;
+		        for (int i = sc.size(); i < size(); i++)  sc.add ("NaN");
+		    }
+		}
+		
+		ComparableEntry[] ces = new ComparableEntry[size()];
+		ArrayList stringColumn = null;
+		if (stringColumns!=null)
+		    stringColumn = (ArrayList) stringColumns.get (new Integer (col));
+		for (int i = 0; i < size(); i++) {
+		    ComparableEntry ce = new ComparableEntry();
+		    ce.index = i;
+		    ce.dValue = columns[col][i];
+		    if (stringColumn != null)
+			ce.sValue = (String) stringColumn.get (i);
+		    ces[i] = ce;
+		}
+		Arrays.sort(ces);
+		// copy sorted values back into rt from a duplicate
+		ResultsTable rt2 = (ResultsTable)clone();
+		for (int i = 0; i <= getLastColumn(); i++) {
+			if (columns[i]==null)
+				continue;
+		    for (int j = 0; j < size(); j++)
+				columns[i][j] = rt2.columns[i][ces[j].index];
+		    ArrayList sc = null;
+		    Map scs =  stringColumns;
+		    	if (scs != null)
+			sc = (ArrayList) scs.get (new Integer (i));
+		    if (sc != null) {
+				ArrayList sc2 = (ArrayList) rt2.stringColumns.get (new Integer (i));
+				for (int j = 0; j < size(); j++)
+			    	sc.set (j, sc2.get (ces[j].index));
+		    }
+		}
+		if (rowLabels != null) {
+			for (int i = 0; i < size(); i++)
+				rowLabels[i] =  rt2.rowLabels[ces[i].index];
+		}
+	}
+	
+	class ComparableEntry implements Comparable  {
+		int index;
+		double dValue;
+		String sValue;
+		
+		boolean isStr() {
+			return  Double.isNaN (dValue)  &&  sValue != null  &&  !sValue.equals ("NaN");
+		}
+		
+		public int compareTo (ComparableEntry e) {
+			if (isStr() && e.isStr())
+				return sValue.compareTo (e.sValue);
+			if (isStr())
+				return -1;
+			if (e.isStr())
+				return +1;
+			return  (dValue < e.dValue) ? -1 : ( (dValue > e.dValue) ? 1 : 0 );
+		}
+	}
+		
+}
diff --git a/src/ij/measure/ResultsTableMacros.java b/src/ij/measure/ResultsTableMacros.java
new file mode 100644
index 0000000..958a1b8
--- /dev/null
+++ b/src/ij/measure/ResultsTableMacros.java
@@ -0,0 +1,214 @@
+package ij.measure;
+import ij.plugin.filter.Analyzer;
+import ij.plugin.frame.Recorder;
+import ij.plugin.*;
+import ij.*;
+import ij.gui.*;
+import ij.text.*;
+import java.awt.*;
+import java.awt.event.*;
+
+
+/** This class implements the Apply Macro command in tables.
+* @author Michael Schmid
+*/
+public class ResultsTableMacros implements Runnable, DialogListener, ActionListener, KeyListener {
+	private static String NAME = "TableMacro.ijm";
+	private String defaultMacro = "Sin=sin(row*0.1);\nCos=cos(row*0.1);\nSqr=Sin*Sin+Cos*Cos;";
+	private GenericDialog gd;
+	private ResultsTable rt, rtBackup;
+	private Button runButton, resetButton, openButton, saveButton;
+	private String title;
+	private int runCount;
+	private TextArea ta;
+
+	public ResultsTableMacros(ResultsTable rt) {
+		this.rt = rt;
+		title = rt!=null?rt.getTitle():null;
+		Thread thread = new Thread(this, "ResultTableMacros");
+		thread.start();
+	}
+
+	private void showDialog() {
+		if (rt==null)
+			rt = Analyzer.getResultsTable();
+		if (rt==null || rt.size()==0) {
+			IJ.error("Results Table required");
+			return;
+		}
+		String[] temp = rt.getHeadingsAsVariableNames();
+		String[] variableNames = new String[temp.length+2];
+		variableNames[0] = "Insert...";
+		variableNames[1] = "row";
+		for (int i=2; i

Macro Equations for Results Tables

    "+ + "
  • The macro, or a selection, is applied to each row of the table."+ + "
  • A new variable starting with an Uppercase character creates
    a new column."+ + "
  • A new variable starting with a lowercase character is temporary."+ + "
  • The variable row (row index) is pre-defined.\n"+ + "
  • String operations are supported for the 'Label' column only (if
    enabled"+ + "with Analyze>Set Measurements>Display Label)."+ + "
  • Click \"Run\", or press "+(IJ.isMacOSX()?"cmd":"ctrl") + "-r, to apply the macro code to the table."+ + "
  • Select a line and press "+(IJ.isMacOSX()?"cmd":"ctrl") + "-r to apply a line of macro code."+ + "
  • Click \"Reset\" to revert to the original version of the table."+ + "
  • The code is saved at macros/TableMacro.ijm, and the
    \"Apply Macro\" command is recorded, when you click \"OK\"."+ + "
  • All Table. macro functions (such as Table.size) refer to the
    current (frontmost) table unless the table name is given."+ + "
"); + + gd.addDialogListener(this); + gd.showDialog(); + if (gd.wasCanceled()) { // dialog cancelled? + rt = rtBackup; + updateDisplay(); + return; + } + if (runCount==0) + applyMacro(); + if (Recorder.record) { + String macro = getMacroCode(); + macro = macro.replaceAll("\n", " "); + if (Recorder.scriptMode()) { + Recorder.recordCall("title = \""+title+"\";"); + Recorder.recordCall("frame = WindowManager.getFrame(title);"); + Recorder.recordCall("rt = frame.getResultsTable();"); + Recorder.recordCall("rt.applyMacro(\""+macro+"\");"); + Recorder.recordCall("rt.show(title);"); + } else { + if (title.equals("Results")) + Recorder.record("Table.applyMacro", macro); + else + Recorder.record("Table.applyMacro", macro, title); + } + } + IJ.saveString(ta.getText(), IJ.getDir("macros")+NAME); + } + + private void applyMacro() { + String code = getMacroCode(); + rt.applyMacro(code); + updateDisplay(); + runCount++; + } + + private String getMacroCode() { + int start = ta.getSelectionStart(); + int end = ta.getSelectionEnd(); + return start==end?ta.getText():ta.getSelectedText(); + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + final String variableName = gd.getNextChoice(); + if (e!=null && (e.getSource() instanceof Choice) && !variableName.equals("Insert...")) { + final int pos = ta.getCaretPosition(); + ((Choice)e.getSource()).select(0); + final TextArea textArea = ta; + new Thread(new Runnable() { + public void run() { + IJ.wait(100); + textArea.insert(variableName, pos); + textArea.setCaretPosition(pos+variableName.length()); + textArea.requestFocus(); + }}).start(); + } + return true; + } + + public void actionPerformed(ActionEvent e) { + Object source = e.getSource(); + if (source==runButton) { + applyMacro(); + } else if (source==resetButton) { + rt = (ResultsTable)rtBackup.clone(); + updateDisplay(); + } else if (source==openButton) { + String macro = IJ.openAsString(null); + if (macro==null) + return; + if (macro.startsWith("Error: ")) { + IJ.error(macro); + return; + } else + ta.setText(macro); + } else if (source==saveButton) { + ta.selectAll(); + String macro = ta.getText(); + ta.select(0, 0); + IJ.saveString(macro, null); + } + + } + + public void keyPressed(KeyEvent e) { + int flags = e.getModifiers(); + boolean control = (flags & KeyEvent.CTRL_MASK) != 0; + boolean meta = (flags & KeyEvent.META_MASK) != 0; + int keyCode = e.getKeyCode(); + if (keyCode==KeyEvent.VK_R && (control||meta)) + applyMacro(); + if (keyCode==KeyEvent.VK_Z && (control||meta)) { + rt = (ResultsTable)rtBackup.clone(); + updateDisplay(); + } + } + + private void updateDisplay() { + if (title!=null) + rt.show(title); + } + + public void keyReleased(KeyEvent e) { + } + + public void keyTyped(KeyEvent e) { + } + + private String getMacro() { + String macro = IJ.openAsString(IJ.getDir("macros")+NAME); + if (macro==null || macro.startsWith("Error:")) + return defaultMacro; + else { + macro = macro.replaceAll("rowNumber", "row"); + macro = macro.replaceAll("rowIndex", "row"); + return macro; + } + } + + public void run() { + rtBackup = (ResultsTable)rt.clone(); + showDialog(); + } + +} + diff --git a/src/ij/measure/SplineFitter.java b/src/ij/measure/SplineFitter.java new file mode 100644 index 0000000..b87dfdc --- /dev/null +++ b/src/ij/measure/SplineFitter.java @@ -0,0 +1,159 @@ +package ij.measure; + +/** This class fits a spline function to a set of points. + It is based on the InitSpline() and EvalSine() functions from + XY (http://www.trilon.com/xv/), an interactive image manipulation + program for the X Window System written by John Bradley. Eric Kischell + (keesh@ieee.org) converted these functions to Java and integrated + them into the PolygonRoi class. +*/ +public class SplineFitter { + private double[] y2; + private static int EXTEND_BY = 7; + private int extendBy; + private float[] xpoints, ypoints; + private int npoints; + private int[] ixpoints, iypoints; + + public SplineFitter(int[] x, int[] y, int n) { + initSpline(x, y, n); + } + + /** For closed curves: the first and last y value should be identical; + * internally, a periodic continuation with a few will be used at both + * ends */ + public SplineFitter(float[] x, float[] y, int n, boolean closed) { + initSpline(x, y, n, closed); + } + + public SplineFitter(float[] x, float[] y, int n) { + initSpline(x, y, n, false); + } + + /** Given arrays of data points x[0..n-1] and y[0..n-1], computes the + values of the second derivative at each of the data points + y2[0..n-1] for use in the evalSpline() function. */ + private void initSpline(int[] x, int[] y, int n) { + int i,k; + double p,qn,sig,un; + y2 = new double[n]; // cached + double[] u = new double[n]; + for (i=1; i=0; k--) + y2[k] = y2[k]*y2[k+1]+u[k]; + ixpoints = x; + iypoints = y; + npoints = n; + } + + private void initSpline(float[] x, float[] y, int n, boolean closed) { + if (closed) { //add periodic continuation at both ends + extendBy = EXTEND_BY; + if (extendBy>=n) + extendBy = n - 1; + int n2 = n + 2*extendBy; + float[] xx = new float[n2]; + float[] yy = new float[n2]; + for (int i=0; i=0; k--) + y2[k] = y2[k]*y2[k+1]+u[k]; + xpoints = x; + ypoints = y; + npoints = n; + } + + /** Evalutes spline function at given point */ + public double evalSpline(double xp) { + if (xpoints!=null) + return evalSpline(xpoints, ypoints, npoints, xp); + else + return evalSpline(ixpoints, iypoints, npoints, xp); + } + + public double evalSpline(int x[], int y[], int n, double xp) { + int klo,khi,k; + double h,b,a; + klo = 0; + khi = n-1; + while (khi-klo > 1) { + k = (khi+klo) >> 1; + if (x[k] > xp) khi = k; + else klo = k; + } + h = x[khi] - x[klo]; + /* orig code */ + /* if (h==0.0) FatalError("bad xvalues in splint\n"); */ + if (h==0.0) return (0.0); /* arbitr ret for now */ + a = (x[khi]-xp)/h; + b = (xp-x[klo])/h; + // should have better err checking + if(y2==null) return (0.0); + return (a*y[klo] + b*y[khi] + ((a*a*a-a)*y2[klo] +(b*b*b-b)*y2[khi]) * (h*h) / 6.0); + } + + public double evalSpline(float x[], float y[], int n, double xp) { + int klo,khi,k; + double h,b,a; + klo = 0; + khi = n-1; + while (khi-klo>1) { + k = (khi+klo)>>1; + if (x[k]>xp) + khi = k; + else + klo = k; + } + h = x[khi] - x[klo]; + /* orig code */ + /* if (h==0.0) FatalError("bad xvalues in splint\n"); */ + if (h==0.0) + return (0.0); /* arbitr ret for now */ + a = (x[khi]-xp)/h; + b = (xp-x[klo])/h; + // should have better err checking + if (y2==null) + return (0.0); + return (a*y[klo] + b*y[khi] + ((a*a*a-a)*y2[klo] +(b*b*b-b)*y2[khi]) * (h*h) / 6.0); + } + +} diff --git a/src/ij/measure/UserFunction.java b/src/ij/measure/UserFunction.java new file mode 100644 index 0000000..1beea70 --- /dev/null +++ b/src/ij/measure/UserFunction.java @@ -0,0 +1,23 @@ +package ij.measure; + + +/** + * A plugin should implement this interface for minimizing a single-valued function + * or fitting a curve with a custom fit function. + */ +public interface UserFunction { + /** + * A user-supplied function + * @param params When minimizing, array of variables. + * For curve fit array of fit parameters. + * The array contents should not be modified. + * Note that the function can get an array with more + * elements then needed to specify the parameters. + * Ignore the rest (and don't modify them). + * @param x For a fit function, the independent variable of the function. + * Ignore it when using the minimizer. + * @return The result of the function. + */ + public double userFunction(double[] params, double x); +} + diff --git a/src/ij/plugin/AVI_Reader.java b/src/ij/plugin/AVI_Reader.java new file mode 100644 index 0000000..1316ae3 --- /dev/null +++ b/src/ij/plugin/AVI_Reader.java @@ -0,0 +1,1612 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.io.*; +import ij.plugin.Animator; +import ij.util.Tools; +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.util.*; +import javax.imageio.ImageIO; + +/**
+ * ImageJ Plugin for reading an AVI file into an image stack
+ *	(one slice per video frame)
+ *
+ *
+ * Restrictions and Notes:
+ *		- Only few formats supported:
+ *			- uncompressed 8 bit with palette (=LUT)
+ *			- uncompressed 8 & 16 bit grayscale
+ *			- uncompressed 24 & 32 bit RGB (alpha channel ignored)
+ *			- uncompressed 32 bit AYUV (alpha channel ignored)
+ *			- various YUV 4:2:2 and 4:2:0 compressed formats (i.e., formats with
+ *						  full luminance resolution, but reduced chroma resolution
+ *			- png or jpeg-encoded individual frames.
+ *			  Note that most MJPG (motion-JPEG) formats are not read correctly.
+ *		- Does not read avi formats with more than one frame per chunk
+ *		- Palette changes during the video not supported
+ *		- Out-of-sequence frames (sequence given by index) not supported
+ *		- Different frame sizes in one file (rcFrame) not supported
+ *		- Conversion of (A)YUV formats to grayscale is non-standard:
+ *		  All 255 levels are kept as in the input (i.e. the full dynamic
+ *		  range of data from a frame grabber is preserved).
+ *		  For standard behavior, use "Brightness&Contrast", Press "Set",
+ *		  enter "Min." 16, "Max." 235, and press "Apply".
+ *		- Restrictions for AVIs with blank frames:
+ *		  Currently only supported with AVI-2 type index.
+ *		  Blank frames are ignored.
+ *		  Selection of start and end frames is inconsistent between normal and
+ *		  virtual stacks.
+ *		  Timing in slice info is incorrect unless read as virtual stack.
+ *		- Note: As a last frame, one can enter '0' (= last frame),
+ *		  '-1' (last frame -1), etc.
+ *
+ * Version History:
+ *	 2008-04-29
+ *		  based on a plugin by Daniel Marsh and Wayne Rasband;
+ *		  modifications by Michael Schmid
+ *		- Support for several other formats added, especially some YUV
+ *		  (also named YCbCr) formats
+ *		- Uneven chunk sizes fixed
+ *		- Negative biHeight fixed
+ *		- Audio or second video stream don't cause a problem
+ *		- Can read part of a file (specify start & end frame numbers)
+ *		- Can convert YUV and RGB to grayscale (does not convert 8-bit with palette)
+ *		- Can flip vertically
+ *		- Can create a virtual stack
+ *		- Added slice label: time of the frame in the movie
+ *		- Added a public method 'getStack' that does not create an image window
+ *		- More compact code, especially for reading the header (rewritten)
+ *		- In the code, bitmapinfo items have their canonical names.
+ *	 2008-06-08
+ *		- Support for png and jpeg/mjpg encoded files added
+ *		- Retrieves animation speed from image frame rate
+ *		- Exception handling without multiple error messages
+ *	 2008-07-03
+ *		- Support for 16bit AVIs coded by MIL (Matrox Imaging Library)
+ *	 2009-03-06
+ *		- Jesper Soendergaard Pedersen added support for extended (large) AVI files,
+ *		  also known as 'AVI 2.0' or 'OpenDML 1.02 AVI file format extension'
+ *		  For Virtual stacks, it reads the 'AVI 2.0' index (indx and ix00 tags).
+ *		  This results in a dramatic speed increase in loading of virtual stacks.
+ *		  If indx and ix00 are not found or bIndexType is unsupported, as well as for
+ *		  non-virtual stacks it finds the frames 'the old way', by scanning the whole file.
+ *		- Fixes a bug where it read too many frames.
+ *		  This version was published as external plugin.
+ *	 2011-12-03
+ *		- Minor updates & cleanup for integration into ImageJ again.
+ *		- Multithread-compliant.
+ *	 2011-12-10
+ *		- Based on a plugin by Jesper Soendergaard Pedersen, also reads the 'idx1' index of
+ *		  AVI 1 files, speeding up initial reading of virtual stacks also for smaller files.
+ *		- When the first frame to read is > 1, uses the index to quickly skip the initial frames.
+ *		- Creates a unique window name.
+ *		- Opens MJPG files also if they do not contain Huffman tables
+ *	 2012-02-01
+ *		- added support for YV12, I420, NV12, NV21 (planar formats with 2x2 U and V subsampling)
+ *	 2012-12-04
+ *		- can read AVI-2 files with blank frames into a virtual stack
+ *	 2013-10-29
+ *		- can read MJPG files where the frames don't have the same pixel number as the overall video
+ *	 2015-09-28
+ *		- reads most ImageJ AVI1 files with size>4 GB (incorrectly written by ImageJ versions before 1.50b)
+ *	 2017-04-21
+ *		- bugfix: file was not closed in case of dialog cancelled or some IO errors.
+ *      - Tries to recover data from truncated files.
+ *
+ *
+ * The AVI format looks like this:
+ * RIFF AVI					RIFF HEADER, AVI CHUNK
+ *	 | LIST hdrl			MAIN AVI HEADER
+ *	 | | avih				AVI HEADER
+ *	 | | LIST strl			STREAM LIST(s) (One per stream)
+ *	 | | | strh				STREAM HEADER (Required after above; fourcc type is 'vids' for video stream)
+ *	 | | | strf				STREAM FORMAT (for video: BitMapInfo; may also contain palette)
+ *	 | | | strd				OPTIONAL -- STREAM DATA (ignored in this plugin)
+ *	 | | | strn				OPTIONAL -- STREAM NAME (ignored in this plugin)
+ *	 | | | indx				OPTIONAL -- MAIN 'AVI 2.0' INDEX
+ *	 | LIST movi			MOVIE DATA
+ *	 | | ix00				partial video index of 'AVI 2.0', usually missing in AVI 1 (ix01 would be for audio)
+ *	 | | [rec]				RECORD DATA (one record per frame for interleaved video; optional, unsupported in this plugin)
+ *	 | | |-dataSubchunks	RAW DATA: '??wb' for audio, '??db' and '??dc' for uncompressed and
+ *	 | |					compressed video, respectively. "??" denotes stream number, usually "00" or "01"
+ *	 | idx1					AVI 1 INDEX ('old-style'); may be missing in very old formats
+ * RIFF AVIX				'AVI 2.0' only: further chunks
+ *	 | LIST movi			more movie data, as above, usually with ix00 index
+ *							Any number of further chunks (RIFF tags) may follow
+ *
+ * Items ('chunks') with one fourcc (four-character code such as 'strh') start with two 4-byte words:
+ * the fourcc and the size of the data area.
+ * Items with two fourcc (e.g. 'LIST hdrl') have three 4-byte words: the first fourcc, the size and the
+ * second fourcc. Note that the size includes the 4 bytes needed for the second fourcc.
+ *
+ * Chunks with fourcc 'JUNK' can appear anywhere and should be ignored.
+ *
+ * 
+ */ + +public class AVI_Reader extends VirtualStack implements PlugIn { + + //four-character codes for avi chunk types + //NOTE: byte sequence is reversed - ints in Intel (little endian) byte order! + private final static int FOURCC_RIFF = 0x46464952; //'RIFF' + private final static int FOURCC_AVI = 0x20495641; //'AVI ' + private final static int FOURCC_AVIX = 0x58495641; //'AVIX' // extended AVI + private final static int FOURCC_ix00 = 0x30307869; //'ix00' // index within + private final static int FOURCC_indx = 0x78646e69; //'indx' // main index + private final static int FOURCC_idx1 = 0x31786469; //'idx1' // index of single 'movi' block + private final static int FOURCC_LIST = 0x5453494c; //'LIST' + private final static int FOURCC_hdrl = 0x6c726468; //'hdrl' + private final static int FOURCC_avih = 0x68697661; //'avih' + private final static int FOURCC_strl = 0x6c727473; //'strl' + private final static int FOURCC_strh = 0x68727473; //'strh' + private final static int FOURCC_strf = 0x66727473; //'strf' + private final static int FOURCC_movi = 0x69766f6d; //'movi' + private final static int FOURCC_rec = 0x20636572; //'rec ' + private final static int FOURCC_JUNK = 0x4b4e554a; //'JUNK' + private final static int FOURCC_vids = 0x73646976; //'vids' + private final static int FOURCC_00db = 0x62643030; //'00db' + private final static int FOURCC_00dc = 0x63643030; //'00dc' + + //four-character codes for supported compression formats; see fourcc.org + private final static int NO_COMPRESSION = 0; //uncompressed, also 'RGB ', 'RAW ' + private final static int NO_COMPRESSION_RGB= 0x20424752; //'RGB ' -a name for uncompressed + private final static int NO_COMPRESSION_RAW= 0x20574152; //'RAW ' -a name for uncompressed + private final static int NO_COMPRESSION_Y800=0x30303859; //'Y800' -a name for 8-bit grayscale + private final static int NO_COMPRESSION_Y8 = 0x20203859; //'Y8 ' -another name for Y800 + private final static int NO_COMPRESSION_GREY=0x59455247; //'GREY' -another name for Y800 + private final static int NO_COMPRESSION_Y16= 0x20363159; //'Y16 ' -a name for 16-bit uncompressed grayscale + private final static int NO_COMPRESSION_MIL= 0x204c494d; //'MIL ' - Matrox Imaging Library + private final static int AYUV_COMPRESSION = 0x56555941; //'AYUV' -uncompressed, but alpha, Y, U, V bytes + private final static int UYVY_COMPRESSION = 0x59565955; //'UYVY' - 4:2:2 with byte order u y0 v y1 + private final static int Y422_COMPRESSION = 0x564E5955; //'Y422' -another name for of UYVY + private final static int UYNV_COMPRESSION = 0x32323459; //'UYNV' -another name for of UYVY + private final static int CYUV_COMPRESSION = 0x76757963; //'cyuv' -as UYVY but not top-down + private final static int V422_COMPRESSION = 0x32323456; //'V422' -as UYVY but not top-down + private final static int YUY2_COMPRESSION = 0x32595559; //'YUY2' - 4:2:2 with byte order y0 u y1 v + private final static int YUNV_COMPRESSION = 0x564E5559; //'YUNV' -another name for YUY2 + private final static int YUYV_COMPRESSION = 0x56595559; //'YUYV' -another name for YUY2 + private final static int YVYU_COMPRESSION = 0x55595659; //'YVYU' - 4:2:2 with byte order y0 u y1 v + + private final static int I420_COMPRESSION = 0x30323449; //'I420' - y plane followed by 2x2 subsampled U and V + private final static int IYUV_COMPRESSION = 0x56555949; //'IYUV' - another name for I420 + private final static int YV12_COMPRESSION = 0x32315659; //'YV12' - y plane followed by 2x2 subsampled V and U + private final static int NV12_COMPRESSION = 0x3231564E; //'NV12' - y plane followed by 2x2 subsampled interleaved U, V + private final static int NV21_COMPRESSION = 0x3132564E; //'NV21' - y plane followed by 2x2 subsampled interleaved V, U + + private final static int JPEG_COMPRESSION = 0x6765706a; //'jpeg' JPEG compression of individual frames + private final static int JPEG_COMPRESSION2 = 0x4745504a; //'JPEG' JPEG compression of individual frames + private final static int JPEG_COMPRESSION3 = 0x04; //BI_JPEG: JPEG compression of individual frames + private final static int MJPG_COMPRESSION = 0x47504a4d; //'MJPG' Motion JPEG, also reads compression of individual frames + private final static int PNG_COMPRESSION = 0x20676e70; //'png ' PNG compression of individual frames + private final static int PNG_COMPRESSION2 = 0x20474e50; //'PNG ' PNG compression of individual frames + private final static int PNG_COMPRESSION3 = 0x05; //BI_PNG PNG compression of individual frames + + private final static int BITMASK24 = 0x10000; //for 24-bit (in contrast to 8, 16,... not a bitmask) + private final static long SIZE_MASK = 0xffffffffL; //for conversion of sizes from unsigned int to long + private final static long FOUR_GB = 0x100000000L; //2^32; above this size of data AVI 1 has a problem for sure + + // flags from AVI chunk header + private final static int AVIF_HASINDEX = 0x00000010; // Index at end of file? + private final static int AVIF_MUSTUSEINDEX = 0x00000020; // ignored by this plugin + private final static int AVIF_ISINTERLEAVED= 0x00000100; // ignored by this plugin + + // constants used to read 'AVI 2' index chunks (others than those defined here are not supported) + private final static byte AVI_INDEX_OF_CHUNKS=0x01; //index of frames + private final static byte AVI_INDEX_OF_INDEXES=0x00; //main indx pointing to ix00 etc subindices + + //static versions of dialog parameters that will be remembered + private static boolean staticConvertToGray; + private static boolean staticFlipVertical; + private static boolean staticIsVirtual = true; + + //dialog parameters + private static final String PATH_KEY = "avi.reader.path"; + private String path; // file path + private String fileName; // file name + private String fileDir; // directory + private int firstFrame = 1; //the first frame to read + private int lastFrame = 0; //the last frame to read; 0 means 'read all' + private boolean convertToGray; //whether to convert color video to grayscale + private boolean flipVertical; //whether to flip image vertical + private boolean isVirtual; //whether to open as virtual stack + //the input file + private RandomAccessFile raFile; + private String raFilePath; + private boolean headerOK = false; //whether header has been read + //more avi file properties etc + private int streamNumber; //number of the (first) video stream + private int type0xdb, type0xdc; //video stream chunks must have one of these two types (e.g. '00db' for straem 0) + private long fileSize; //file size + private long aviSize; //size of 'AVI' chunk + private long headerPositionEnd; //'movi' will start somewhere here + private long indexPosition; //position of the main index (indx) + private long indexPositionEnd; //indx seek end + private long moviPosition; //position of 'movi' list + private int paddingGranularity = 2; //tags start at even address + private int frameNumber = 1; //frame currently read; global because distributed over 1st AVi and further RIFF AVIX chunks + private int lastFrameToRead = Integer.MAX_VALUE; + private int totalFramesFromIndex;//number of frames from 'AVI 2.0' indices + private boolean indexForCountingOnly;//don't read the index, only count int totalFramesFromIndex how many entries + private boolean isOversizedAvi1; //AVI-1 file > 4GB + //derived from BitMapInfo + private int dataCompression; //data compression type used + private boolean isPlanarFormat; //I420 & YV12 formats: y frame, then u,v frames + private int scanLineSize; + private boolean dataTopDown; //whether data start at top of image + private ColorModel cm; + private boolean variableLength; //compressed (PNG, JPEG) frames have variable length + //for conversion to ImageJ stack + private Vector frameInfos; //for virtual stack: long[] with frame pos&size in file, time(usec) + private ImageStack stack; + private ImagePlus imp; + //for debug messages and error handling + private boolean verbose = IJ.debugMode; + private long startTime; + private boolean aborting; + private boolean displayDialog = true; + private String errorText; //error occurred during makeStack, or null + + //From AVI Header Chunk + private int dwMicroSecPerFrame; + private int dwMaxBytesPerSec; + private int dwReserved1; + private int dwFlags; + private int dwTotalFrames; //AVI 2.0: will be replaced by number of frames from index + private int dwInitialFrames; + private int dwStreams; + private int dwSuggestedBufferSize; + private int dwWidth; + private int dwHeight; + + //From Stream Header Chunk + private int fccStreamHandler; + private int dwStreamFlags; + private int dwPriorityLanguage; //actually 2 16-bit words: wPriority and wLanguage + private int dwStreamInitialFrames; + private int dwStreamScale; + private int dwStreamRate; + private int dwStreamStart; + private int dwStreamLength; + private int dwStreamSuggestedBufferSize; + private int dwStreamQuality; + private int dwStreamSampleSize; + + //From Stream Format Chunk: BITMAPINFO contents (40 bytes) + private int biSize; // size of this header in bytes (40) + private int biWidth; + private int biHeight; + private short biPlanes; // no. of color planes: for the formats decoded; here always 1 + private short biBitCount; // Bits per Pixel + private int biCompression; + private int biSizeImage; // size of image in bytes (may be 0: if so, calculate) + private int biXPelsPerMeter; // horizontal resolution, pixels/meter (may be 0) + private int biYPelsPerMeter; // vertical resolution, pixels/meter (may be 0) + private int biClrUsed; // no. of colors in palette (if 0, calculate) + private int biClrImportant; // no. of important colors (appear first in palette) (0 means all are important) + + + + /** The plugin is invoked by ImageJ using this method. + * @param arg String 'arg' may be used to select the path. If it is an empty string, + * a file open dialog is shown, and the resulting ImagePlus is displayed thereafter. + * The ImagePlus is not displayed if 'arg' is a non-empty String; it can be + * retrieved with getImagePlus(). + */ + public void run (String arg) { + String options = IJ.isMacro()?Macro.getOptions():null; + if (options!=null && options.contains("select=") && !options.contains("open=")) + Macro.setOptions(options.replaceAll("select=", "open=")); + path = arg; + if (displayDialog && !showDialog()) //ask for parameters + return; + try { + openAndReadHeader(path); //open and read header + } catch (Exception e) { + error(exceptionMessage(e)); + return; + } finally { + closeFile(raFile); + } + errorText = null; + ImageStack stack = makeStack(path, firstFrame, lastFrame, isVirtual, convertToGray, flipVertical); //read data + if (aborting) + return; //error message has been shown already + if (stack==null || stack.size() == 0 || stack.getProcessor(1)==null) { //read nothing? + if (errorText != null) + error(errorText); + else { + String rangeText = ""; + if (firstFrame > 1 || (lastFrame != 0 && lastFrame != dwTotalFrames)) + rangeText = "\nin Range "+firstFrame+ + (lastFrame>0 ? " - "+lastFrame : " - end"); + error("Error: No Frames Found"+rangeText); + } + return; + } else if (errorText != null) + IJ.showMessage("AVI Reader", errorText); //show the error, e.g. we may have an incomplete stack + if (fileName==null) { + File f = new File(path); + fileName = f.getName(); + } + imp = new ImagePlus(WindowManager.makeUniqueName(fileName), stack); + if (imp.getBitDepth()==16) + imp.getProcessor().resetMinAndMax(); + setFramesPerSecond(imp); + FileInfo fi = new FileInfo(); + fi.fileName = fileName; + fi.directory = fileDir; + imp.setFileInfo(fi); + if (arg.equals("")) + imp.show(); + IJ.showTime(imp, startTime, "Read AVI in ", stack.size()); + } + + /** Returns the ImagePlus opened by run(). */ + public ImagePlus getImagePlus() { + return imp; + } + + /** Opens an AVI file as a virtual stack. The ImagePlus is not displayed. */ + public static ImagePlus openVirtual(String path) { + return open(path, true); + } + + /** Opens an AVI file as a stack in memory or a virtual stack. The ImagePlus is not displayed. */ + public static ImagePlus open(String path, boolean virtual) { + AVI_Reader reader = new AVI_Reader(); + ImageStack stack = reader.makeStack (path, 1, 0, virtual, false, false); + if (stack!=null) + return new ImagePlus((new File(path)).getName(), stack); + else + return null; + } + + /** Create an ImageStack from an avi file with given path. + * @param path Directoy+filename of the avi file + * @param firstFrame Number of first frame to read (first frame of the file is 1) + * @param lastFrame Number of last frame to read or 0 for reading all, -1 for all but last... + * @param isVirtual Whether to return a virtual stack + * @param convertToGray Whether to convert color images to grayscale + * @return Returns the stack (may be incomplete on error); null on failure. + * The stack returned may be non-null, but have a length of zero if no suitable frames were found. + * Use getErrorText to determine whether there has been an error reading the file. + * For virtual stacks, not that I/O errors may also occur later, when reading the frames. + */ + public ImageStack makeStack (String path, int firstFrame, int lastFrame, + boolean isVirtual, boolean convertToGray, boolean flipVertical) { + this.firstFrame = firstFrame; + this.lastFrame = lastFrame; + this.isVirtual = isVirtual; + this.convertToGray = convertToGray; + this.flipVertical = flipVertical; + IJ.showProgress(.001); + try { + readAVI(path); + } catch (OutOfMemoryError e) { + stack.trim(); + errorText = "Out of memory. " + stack.size() + " of " + dwTotalFrames + " frames will be opened."; + } catch (Exception e) { + errorText = exceptionMessage(e); + if (isVirtual || stack==null || stack.size()==0) //return null only if we have really nothing + return null; + } finally { + closeFile(raFile); + if (verbose) + IJ.log("File closed."); + IJ.showProgress(1.0); + } + if (isVirtual && frameInfos != null) + stack = this; + if (stack!=null && cm!=null) + stack.setColorModel(cm); + return stack; + } + + /** Returns a description of the error reading the file with makeStack or null if no error */ + public String getErrorText() { + return errorText; + } + + /** Returns an ImageProcessor for the specified slice of this virtual stack (if it is one) + * where 1<=n<=nslices. Returns null if no virtual stack or no slices or error reading the frame. + */ + public synchronized ImageProcessor getProcessor(int n) { + if (frameInfos==null || frameInfos.size()==0 || raFilePath==null) + return null; + n = translate(n); // update n for hyperstacks not in default CZT order + if (n<1 || n>frameInfos.size()) + throw new IllegalArgumentException("Argument out of range: "+n); + Object pixels = null; + RandomAccessFile rFile = null; + try { + rFile = new RandomAccessFile(new File(raFilePath), "r"); + long[] frameInfo = (long[])(frameInfos.get(n-1)); + pixels = readFrame(rFile, frameInfo[0], (int)frameInfo[1]); + } catch (Exception e) { + error(exceptionMessage(e)); + return null; + } finally { + closeFile(rFile); + } + if (pixels == null) return null; //failed + if (pixels instanceof byte[]) + return new ByteProcessor(dwWidth, biHeight, (byte[])pixels, cm); + else if (pixels instanceof short[]) + return new ShortProcessor(dwWidth, biHeight, (short[])pixels, cm); + else + return new ColorProcessor(dwWidth, biHeight, (int[])pixels); + } + + /** Returns the image width of the virtual stack */ + public int getWidth() { + return dwWidth; + } + + /** Returns the image height of the virtual stack */ + public int getHeight() { + return biHeight; + } + + /** Returns the number of images in this virtual stack (if it is one) */ + public int getSize() { + if (frameInfos == null) return 0; + else return frameInfos.size(); + } + + /** Returns the label of the specified slice in this virtual stack (if it is one). */ + public String getSliceLabel(int n) { + if (frameInfos==null || n<1 || n>frameInfos.size()) + throw new IllegalArgumentException("No Virtual Stack or argument out of range: "+n); + return frameLabel(((long[])(frameInfos.get(n-1)))[2]); + } + + /** Deletes the specified image from this virtual stack (if it is one), + * where 1<=n<=nslices. */ + public void deleteSlice(int n) { + if (frameInfos==null || frameInfos.size()==0) return; + if (n<1 || n>frameInfos.size()) + throw new IllegalArgumentException("Argument out of range: "+n); + frameInfos.removeElementAt(n-1); + } + + /** Parameters dialog, returns false on cancel */ + private boolean showDialog () { + if (lastFrame!=-1) + lastFrame = dwTotalFrames; + if (!IJ.isMacro()) { + convertToGray = staticConvertToGray; + flipVertical = staticFlipVertical; + isVirtual = staticIsVirtual; + } + String options = Macro.getOptions(); + if (options!=null) { //macro + if (options.contains("open=")) + Macro.setOptions(options.replace("open=", "avi=")); + } + if (path==null || path.length()==0) + path = Prefs.get(PATH_KEY, IJ.getDir("downloads")+"movie.avi"); + GenericDialog gd = new GenericDialog("AVI Reader"); + gd.setInsets(5, 0, 0); + gd.addFileField("AVI:", path, 29); + gd.setInsets(2, 40, 5); + gd.addMessage("drag and drop target", IJ.font10, Color.darkGray); + gd.addCheckbox("Use Virtual Stack", isVirtual); + gd.addCheckbox("Convert to Grayscale", convertToGray); + gd.addCheckbox("Flip Vertical", flipVertical); + gd.setInsets(15, 0, 3); + gd.addNumericField("First:", firstFrame, 0); + gd.addNumericField("Last:", lastFrame, lastFrame, 6, "*"); + TextField lastField = (TextField)(gd.getNumericFields().lastElement()); + if (lastFrame == 0) + lastField.setText(""); + gd.setInsets(0, 40, 5); + gd.addMessage("* Leave empty or set to 0 for reading to the end.\n Also accepts e.g. -5 to skip last 5 frames.", IJ.font10, Color.darkGray); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + path = gd.getNextString(); + Prefs.set(PATH_KEY, path); + File f = new File(path); + fileName = f.getName(); + fileDir = IJ.addSeparator(f.getParent()); + gd.setSmartRecording(true); + isVirtual = gd.getNextBoolean(); + convertToGray = gd.getNextBoolean(); + flipVertical = gd.getNextBoolean(); + double first = gd.getNextNumber(); + if (!Double.isNaN(first)) firstFrame = (int)first; + if (lastField.getText().length()==0) + lastField.setText("0"); + double last = gd.getNextNumber(); + if (!Double.isNaN(last)) lastFrame = (int)last; + if (!IJ.isMacro()) { + staticConvertToGray = convertToGray; + staticFlipVertical = flipVertical; + staticIsVirtual = isVirtual; + } + IJ.register(this.getClass()); + return true; + } + + /** Read into a (virtual) stack */ + private void readAVI(String path) throws Exception, IOException { + if (!headerOK) // we have not read the header yet? + openAndReadHeader(path); + else { + File file = new File(path); // open if currently not open + raFile = new RandomAccessFile(file, "r"); + } + startTime += System.currentTimeMillis();// taking previously elapsed time into account + /** MOVED UP HERE BY JSP*/ + if (lastFrame > 0) // last frame number to read: from Dialog + lastFrameToRead = lastFrame; + if (lastFrame < 0 && dwTotalFrames > 0) // negative means "end frame minus ..." + lastFrameToRead = dwTotalFrames+lastFrame; + if (lastFrameToRead < firstFrame) // no frames to read + return; + boolean hasIndex = (dwFlags & AVIF_HASINDEX) != 0; + if (isVirtual || firstFrame>1) { // avoid scanning frame-by-frame where we only need the positions + frameInfos = new Vector(100); // holds frame positions, sizes and time since start + long nextPosition = -1; + if (indexPosition > 0) { // attempt to get AVI2.0 index instead of scanning for all frames + raFile.seek(indexPosition); + nextPosition = findFourccAndRead(FOURCC_indx, false, indexPositionEnd, false); + } + if (hasIndex && (frameInfos==null ||frameInfos.size()==0)) { // got nothing from indx, attempt to read AVI 1 index 'idx1' + raFile.seek(headerPositionEnd); + moviPosition = findFourccAndSkip(FOURCC_movi, true, fileSize); // go behind the 'movi' list + if (moviPosition<0) + throw new Exception("AVI File has no movie data"); + long positionBehindMovie = raFile.getFilePointer(); + while (positionBehindMovie < fileSize-8) { + if (verbose) + IJ.log("searching for 'idx1' at 0x"+Long.toHexString(positionBehindMovie)); + raFile.seek(positionBehindMovie); + if (positionBehindMovie > FOUR_GB) + isOversizedAvi1 = true; + nextPosition = findFourccAndRead(FOURCC_idx1, false, fileSize, false); + if (nextPosition >= 0) //AVI-1 index 'idx1' found + break; + positionBehindMovie += FOUR_GB; //maybe position was wrong because it was a 32-bit number, but > 4GB? + } + } + if (verbose) + IJ.log("'frameInfos' has "+frameInfos.size()+" entries"); + } + if (isVirtual && frameInfos.size()>0) // Virtual Stack only needs reading the index + return; + // Read AVI (movie data) frame by frame - if no index tag is present the pointers + // for the virtual AVI stack will be read here + raFile.seek(headerPositionEnd); + if (firstFrame>1 && frameInfos.size()>0) { + long[] frameInfo = (long[])frameInfos.get(0); + raFile.seek(frameInfo[0]-8); // chunk starts 8 bytes before frame data + frameNumber = firstFrame; + if (verbose) + IJ.log("directly go to frame "+firstFrame+" @ 0x"+Long.toHexString(frameInfo[0]-8)); + readMovieData(fileSize); + } else { + frameNumber = 1; + findFourccAndRead(FOURCC_movi, true, fileSize, true); + } + + long pos = raFile.getFilePointer(); + //IJ.log("at 0x"+Long.toHexString(pos)+" filesize=0x"+Long.toHexString(fileSize)); + // extended AVI: try to find further 'RIFF' chunks, where we expect AVIX tags + while (pos>0 && posendPosition || nextPos>fileSize) { + errorText = "AVI File Error: '"+fourccString(type)+"' @ 0x"+Long.toHexString(raFile.getFilePointer()-8)+" has invalid length. File damaged/truncated?"; + IJ.log(errorText); // this text is also remembered as error message for showing in message box + if (fourcc == FOURCC_movi) + nextPos = fileSize; // if movie data truncated, try reading what we can get + else + return -1; // otherwise, nothing to be done + } + if (isList && type == FOURCC_LIST) + type = readInt(); + if (verbose) + IJ.log("Search for '"+fourccString(fourcc)+"', found "+fourccString(type)+"' data "+posSizeString(nextPos-size, size)); + if (type==fourcc) { + contentOk = readContents(fourcc, nextPos); + } else if (verbose) + IJ.log("'"+fourccString(type)+"', ignored"); + raFile.seek(nextPos); + if (contentOk) + return nextPos; //found and read, breaks the loop + } while (!contentOk); + return nextPos; + } + + /** Find the next position of fourcc or LIST fourcc, but does not read it, only + * returns the first position inside the fourcc chunk and puts the file pointer + * behind the fourcc chunk (if successful). + * If not found, returns -1 */ + private long findFourccAndSkip(int fourcc, boolean isList, long endPosition) throws IOException { + while (true) { + int type = readType(endPosition); + if (type == 0) //reached endPosition without finding + return -1; + long size = readInt() & SIZE_MASK; + long chunkPos = raFile.getFilePointer(); + long nextPos = chunkPos + size; //note that 'size' of a list includes the 'type' that follows now + if (isList && type == FOURCC_LIST) + type = readInt(); + if (verbose) + IJ.log("Searching for (to skip) '"+fourccString(fourcc)+"', found "+fourccString(type)+ + "' data "+posSizeString(chunkPos, size)); + raFile.seek(nextPos); + if (type == fourcc) + return chunkPos; //found and skipped, breaks the loop + } + } + + /** read contents defined by four-cc code; returns true if contens ok */ + private boolean readContents (int fourcc, long endPosition) throws Exception, IOException { + switch (fourcc) { + case FOURCC_hdrl: + headerPositionEnd = endPosition; + findFourccAndRead(FOURCC_avih, false, endPosition, true); + findFourccAndRead(FOURCC_strl, true, endPosition, true); + return true; + case FOURCC_avih: + readAviHeader(); + return true; + case FOURCC_strl: + long nextPosition = findFourccAndRead(FOURCC_strh, false, endPosition, false); + if (nextPosition<0) return false; + indexPosition = findFourccAndRead(FOURCC_strf, false, endPosition, true); + indexPositionEnd= endPosition; + indexForCountingOnly = true; //try reading indx for counting number of entries + totalFramesFromIndex = 0; + nextPosition = findFourccAndRead(FOURCC_indx, false, endPosition, false); + if (nextPosition > 0 && totalFramesFromIndex > dwTotalFrames) + dwTotalFrames = totalFramesFromIndex; + indexForCountingOnly = false; + return true; + case FOURCC_strh: + int streamType = readInt(); + if (streamType != FOURCC_vids) { + if (verbose) + IJ.log("Non-video Stream '"+fourccString(streamType)+" skipped"); + streamNumber++; + return false; + } + readStreamHeader(); + return true; + case FOURCC_strf: + readBitMapInfo(endPosition); + return true; + case FOURCC_indx: + case FOURCC_ix00: + readAvi2Index(endPosition); + return true; + case FOURCC_idx1: + readOldFrameIndex(endPosition); + return true; + case FOURCC_RIFF: + readAVIX(endPosition); + return true; + case FOURCC_movi: + readMovieData(endPosition); + return true; + } + throw new Exception("Program error, type "+fourccString(fourcc)); + } + + void readAviHeader() throws Exception, IOException { //'avih' + dwMicroSecPerFrame = readInt(); + dwMaxBytesPerSec = readInt(); + dwReserved1 = readInt(); //in newer avi formats, this is dwPaddingGranularity? + dwFlags = readInt(); + dwTotalFrames = readInt(); + dwInitialFrames = readInt(); + dwStreams = readInt(); + dwSuggestedBufferSize = readInt(); + dwWidth = readInt(); + dwHeight = readInt(); + // dwReserved[4] follows, ignored + + if (verbose) { + IJ.log("AVI HEADER (avih):"+timeString()); + IJ.log(" dwMicroSecPerFrame=" + dwMicroSecPerFrame); + IJ.log(" dwMaxBytesPerSec=" + dwMaxBytesPerSec); + IJ.log(" dwReserved1=" + dwReserved1); + IJ.log(" dwFlags=" + dwFlags); + IJ.log(" dwTotalFrames=" + dwTotalFrames); + IJ.log(" dwInitialFrames=" + dwInitialFrames); + IJ.log(" dwStreams=" + dwStreams); + IJ.log(" dwSuggestedBufferSize=" + dwSuggestedBufferSize); + IJ.log(" dwWidth=" + dwWidth); + IJ.log(" dwHeight=" + dwHeight); + } + } + + void readStreamHeader() throws Exception, IOException { //'strh' + fccStreamHandler = readInt(); + dwStreamFlags = readInt(); + dwPriorityLanguage = readInt(); + dwStreamInitialFrames = readInt(); + dwStreamScale = readInt(); + dwStreamRate = readInt(); + dwStreamStart = readInt(); + dwStreamLength = readInt(); + dwStreamSuggestedBufferSize = readInt(); + dwStreamQuality = readInt(); + dwStreamSampleSize = readInt(); + //rcFrame rectangle follows, ignored + if (verbose) { + IJ.log("VIDEO STREAM HEADER (strh):"); + IJ.log(" fccStreamHandler='" + fourccString(fccStreamHandler)+"'"); + IJ.log(" dwStreamFlags=" + dwStreamFlags); + IJ.log(" wPriority,wLanguage=" + dwPriorityLanguage); + IJ.log(" dwStreamInitialFrames=" + dwStreamInitialFrames); + IJ.log(" dwStreamScale=" + dwStreamScale); + IJ.log(" dwStreamRate=" + dwStreamRate); + IJ.log(" dwStreamStart=" + dwStreamStart); + IJ.log(" dwStreamLength=" + dwStreamLength); + IJ.log(" dwStreamSuggestedBufferSize=" + dwStreamSuggestedBufferSize); + IJ.log(" dwStreamQuality=" + dwStreamQuality); + IJ.log(" dwStreamSampleSize=" + dwStreamSampleSize); + } + if (dwStreamSampleSize > 1) + throw new Exception("Video stream with "+dwStreamSampleSize+" (more than 1) frames/chunk not supported"); + // what the chunks in that stream will be named (we have two possibilites: uncompressed & compressed) + type0xdb = FOURCC_00db + (streamNumber<<8); //'01db' for stream 1, etc. (inverse byte order!) + type0xdc = FOURCC_00dc + (streamNumber<<8); //'01dc' for stream 1, etc. + } + + /** Read 'AVI 2'-type main index 'indx' or an 'ix00' index to frames + * (only the types AVI_INDEX_OF_INDEXES and AVI_INDEX_OF_CHUNKS are supported) */ + private void readAvi2Index(long endPosition) throws Exception, IOException { + short wLongsPerEntry = readShort(); + byte bIndexSubType = raFile.readByte(); + byte bIndexType = raFile.readByte(); + int nEntriesInUse = readInt(); + int dwChunkId = readInt(); + long qwBaseOffset = readLong(); + readInt(); // 3rd dwReserved (first two dwreserved are qwBaseOffset!) + if (verbose) { + String bIndexString = bIndexType == AVI_INDEX_OF_CHUNKS ? ": AVI_INDEX_OF_CHUNKS" : + bIndexType == AVI_INDEX_OF_INDEXES ? ": AVI_INDEX_OF_INDEXES" : ": UNSUPPORTED"; + IJ.log("AVI 2 INDEX:"); + IJ.log(" wLongsPerEntry=" + wLongsPerEntry); + IJ.log(" bIndexSubType=" + bIndexSubType); + IJ.log(" bIndexType=" + bIndexType + bIndexString); + IJ.log(" nEntriesInUse=" + nEntriesInUse); + IJ.log(" dwChunkId='" + fourccString(dwChunkId)+"'"); + if (bIndexType == AVI_INDEX_OF_CHUNKS) + IJ.log(" qwBaseOffset=" + "0x"+Long.toHexString(qwBaseOffset)); + } + if (bIndexType == AVI_INDEX_OF_INDEXES) { // 'indx' points to other indices + if (wLongsPerEntry != 4) return; //badly formed index, ignore it + for (int i=0;ilastFrameToRead) break; + } + } else if (bIndexType == AVI_INDEX_OF_CHUNKS) { + if (verbose) { + IJ.log("readAvi2Index frameNumber="+frameNumber+" firstFrame="+firstFrame); + if (indexForCountingOnly) IJ.log(""); + } + if (wLongsPerEntry != 2) return; //badly formed index, ignore it + if (dwChunkId != type0xdb && dwChunkId != type0xdc) { //not the stream we search for? (should not happen) + if (verbose) + IJ.log("INDEX ERROR: SKIPPED ix00, wrong stream number or type, should be "+ + fourccString(type0xdb)+" or "+fourccString(type0xdc)); + return; + } + if (indexForCountingOnly) { //only count number of entries, don't put into table + totalFramesFromIndex += nEntriesInUse; + return; + } + for (int i=0;i= firstFrame && dwSize>0) { //only valid frames (no blank frames) + frameInfos.add(new long[] {pos, dwSize, (long) frameNumber*dwMicroSecPerFrame}); + if (verbose) + IJ.log("movie data "+frameNumber+" '"+fourccString(dwChunkId)+"' "+posSizeString(pos,dwSize)+timeString()); + } + frameNumber++; + if (frameNumber>lastFrameToRead) break; + } + if (verbose) + IJ.log("Index read up to frame "+(frameNumber-1)); + } + } + + /** Read AVI 1 index 'idx1' */ + private void readOldFrameIndex(long endPosition) throws Exception, IOException { + //IJ.log("READ AVI 1 INDEX, isOversizedAvi1="+isOversizedAvi1); + int offset = -1; //difference between absolute frame address and address given in idx1 + int[] offsetsToTry = new int[] {0, (int)moviPosition}; // dwOffset may be w.r.t. file start or w.r.t. 'movi' list. + long lastFramePos = 0; + while (true) { + if ((raFile.getFilePointer()+16) >endPosition) break; + + int dwChunkId = readInt(); + int dwFlags = readInt(); + int dwOffset = readInt(); + int dwSize = readInt(); + //IJ.log("idx1: dwOffset=0x"+Long.toHexString(dwOffset)); + //IJ.log("moviPosition=0x"+Long.toHexString(moviPosition)); + if ((dwChunkId==type0xdb || dwChunkId==type0xdc) && dwSize>0) { + if (offset < 0) { // find out what the offset refers to + long temp = raFile.getFilePointer(); + for (int i=0; i= firstFrame) { + frameInfos.add(new long[]{framePos+8, dwSize, (long)frameNumber*dwMicroSecPerFrame}); + if (verbose) + IJ.log("idx1 movie data '"+fourccString(dwChunkId)+"' "+posSizeString(framePos,dwSize)+timeString()); + } + frameNumber++; + if (frameNumber>lastFrameToRead) break; + } //if(dwChunkId...) + } //while(true) + if (verbose) + IJ.log("Index read up to frame "+(frameNumber-1)); + + } + + /**Read stream format chunk: starts with BitMapInfo, may contain palette + */ + void readBitMapInfo(long endPosition) throws Exception, IOException { + biSize = readInt(); + biWidth = readInt(); + biHeight = readInt(); + biPlanes = readShort(); + biBitCount = readShort(); + biCompression = readInt(); + biSizeImage = readInt(); + biXPelsPerMeter = readInt(); + biYPelsPerMeter = readInt(); + biClrUsed = readInt(); + biClrImportant = readInt(); + if (verbose) { + IJ.log(" biSize=" + biSize); + IJ.log(" biWidth=" + biWidth); + IJ.log(" biHeight=" + biHeight); + IJ.log(" biPlanes=" + biPlanes); + IJ.log(" biBitCount=" + biBitCount); + IJ.log(" biCompression=0x" + Integer.toHexString(biCompression)+" '"+fourccString(biCompression)+"'"); + IJ.log(" biSizeImage=" + biSizeImage); + IJ.log(" biXPelsPerMeter=" + biXPelsPerMeter); + IJ.log(" biYPelsPerMeter=" + biYPelsPerMeter); + IJ.log(" biClrUsed=" + biClrUsed); + IJ.log(" biClrImportant=" + biClrImportant); + } + + int allowedBitCount = 0; + boolean readPalette = false; + switch (biCompression) { + case NO_COMPRESSION: + case NO_COMPRESSION_RGB: + case NO_COMPRESSION_RAW: + dataCompression = NO_COMPRESSION; + dataTopDown = biHeight<0; //RGB mode is usually bottom-up, negative height signals top-down + allowedBitCount = 8 | BITMASK24 | 32; //we don't support 1, 2 and 4 byte data + readPalette = biBitCount <= 8; + break; + case NO_COMPRESSION_Y8: + case NO_COMPRESSION_GREY: + case NO_COMPRESSION_Y800: + dataTopDown = true; + dataCompression = NO_COMPRESSION; + allowedBitCount = 8; + break; + case NO_COMPRESSION_Y16: + case NO_COMPRESSION_MIL: + dataCompression = NO_COMPRESSION; + allowedBitCount = 16; + break; + case AYUV_COMPRESSION: + dataCompression = AYUV_COMPRESSION; + allowedBitCount = 32; + break; + case UYVY_COMPRESSION: + case UYNV_COMPRESSION: + dataTopDown = true; + case CYUV_COMPRESSION: //same, not top-down + case V422_COMPRESSION: + dataCompression = UYVY_COMPRESSION; + allowedBitCount = 16; + break; + case YUY2_COMPRESSION: + case YUNV_COMPRESSION: + case YUYV_COMPRESSION: + dataTopDown = true; + dataCompression = YUY2_COMPRESSION; + allowedBitCount = 16; + break; + case YVYU_COMPRESSION: + dataTopDown = true; + dataCompression = YVYU_COMPRESSION; + allowedBitCount = 16; + break; + case IYUV_COMPRESSION: + case I420_COMPRESSION: + case YV12_COMPRESSION: + case NV12_COMPRESSION: + case NV21_COMPRESSION: + dataCompression = (dataCompression==IYUV_COMPRESSION) ? + I420_COMPRESSION : biCompression; + dataTopDown = biHeight>0; + isPlanarFormat = true; + allowedBitCount = 12; + break; + case JPEG_COMPRESSION: + case JPEG_COMPRESSION2: + case JPEG_COMPRESSION3: + case MJPG_COMPRESSION: + dataCompression = JPEG_COMPRESSION; + variableLength = true; + break; + case PNG_COMPRESSION: + case PNG_COMPRESSION2: + case PNG_COMPRESSION3: + variableLength = true; + dataCompression = PNG_COMPRESSION; + break; + default: + throw new Exception("Unsupported compression: "+Integer.toHexString(biCompression)+ + (biCompression>=0x20202020 ? " '" + fourccString(biCompression)+"'" : "")); + } + + int bitCountTest = (biBitCount==24) ? BITMASK24 : biBitCount; //convert "24" to a flag + if (allowedBitCount!=0 && (bitCountTest & allowedBitCount)==0) + throw new Exception("Unsupported: "+biBitCount+" bits/pixel for compression '"+ + fourccString(biCompression)+"'"); + + if (biHeight < 0) //negative height was for top-down data in RGB mode + biHeight = -biHeight; + + if (isPlanarFormat && ((biWidth&1)!=0 || (biHeight&1)!=0)) + throw new Exception("Odd size ("+biWidth+"x"+biHeight+") unsupported with "+fourccString(biCompression)+" compression"); + // raw & interleaved YUV: scan line is padded with zeroes to be a multiple of four bytes + scanLineSize = isPlanarFormat ? + (biWidth * biBitCount) / 8 : ((biWidth * biBitCount + 31) / 32) * 4; + + // a value of biClrUsed=0 means we determine this based on the bits per pixel, if there is a palette + long spaceForPalette = endPosition-raFile.getFilePointer(); + if (readPalette && biClrUsed==0 && spaceForPalette!=0) + biClrUsed = 1 << biBitCount; + + if (verbose) { + IJ.log(" > data compression=0x" + Integer.toHexString(dataCompression)+" '"+fourccString(dataCompression)+"'"); + IJ.log(" > palette colors=" + biClrUsed); + IJ.log(" > scan line size=" + scanLineSize); + IJ.log(" > data top down=" + dataTopDown); + } + + //read color palette + if (readPalette && biClrUsed > 0) { + if (verbose) + IJ.log(" Reading "+biClrUsed+" Palette colors: " + posSizeString(spaceForPalette)); + if (spaceForPalette < biClrUsed*4) + throw new Exception("Not enough data ("+spaceForPalette+") for palette of size "+(biClrUsed*4)); + byte[] pr = new byte[biClrUsed]; + byte[] pg = new byte[biClrUsed]; + byte[] pb = new byte[biClrUsed]; + for (int i = 0; i < biClrUsed; i++) { + pb[i] = raFile.readByte(); + pg[i] = raFile.readByte(); + pr[i] = raFile.readByte(); + raFile.readByte(); + } + cm = new IndexColorModel(biBitCount, biClrUsed, pr, pg, pb); + } + } + + /**Read from the 'movi' chunk. Skips audio ('..wb', etc.), 'LIST' 'rec' etc, only reads '..db' or '..dc'*/ + void readMovieData(long endPosition) throws Exception, IOException { + if (verbose) + IJ.log("MOVIE DATA "+posSizeString(endPosition-raFile.getFilePointer())+timeString()+ + "\nSearching for stream "+streamNumber+": '"+ + fourccString(type0xdb)+"' or '"+fourccString(type0xdc)+"' chunks"); + if (isVirtual) { + if (frameInfos==null) // we might have it already from reading the first chunk + frameInfos = new Vector(lastFrameToRead); // holds frame positions in file (for non-constant frame sizes, should hold long[] with pos and size) + } else if (stack==null) + stack = new ImageStack(dwWidth, biHeight); + while (true) { //loop over all chunks + int type = readType(endPosition); + if (type==0) break; //endPosition of 'movi' reached? + long size = readInt() & SIZE_MASK; + long pos = raFile.getFilePointer(); + long nextPos = pos + size; + if (nextPos > endPosition && nextPos < fileSize-8 && fileSize > FOUR_GB) { + endPosition = fileSize; //looks like old ImageJ AVI 1.0 >4GB: wrong endPosition + } + if ((type==type0xdb || type==type0xdc) && size>0) { + IJ.showProgress((double)frameNumber /lastFrameToRead); + if (verbose) + IJ.log(frameNumber+" movie data '"+fourccString(type)+"' "+posSizeString(size)+timeString()); + if (frameNumber >= firstFrame) { + if (isVirtual) + frameInfos.add(new long[]{pos, size, frameNumber*dwMicroSecPerFrame}); + else { //read the frame + Object pixels = readFrame(raFile, pos, (int)size); + String label = frameLabel(frameNumber*dwMicroSecPerFrame); + stack.addSlice(label, pixels); + } + } + frameNumber++; + if (frameNumber>lastFrameToRead) break; + } else if (verbose) + IJ.log("skipped '"+fourccString(type)+"' "+posSizeString(size)); + if (nextPos > endPosition) break; + raFile.seek(nextPos); + } + } + + /** Reads a frame at a given position in the file, returns pixels array */ + private Object readFrame (RandomAccessFile rFile, long filePos, int size) + throws Exception, IOException { + rFile.seek(filePos); + //if (verbose) + //IJ.log("virtual AVI: readFrame @"+posSizeString(filePos, size)+" varlength="+variableLength); + if (variableLength) //JPEG or PNG-compressed frames + return readCompressedFrame(rFile, size); + else + return readFixedLengthFrame(rFile, size); + } + + /** Reads a JPEG or PNG-compressed frame from a RandomAccessFile and + * returns the pixels array of the resulting image and sets the + * ColorModel cm (if appropriate) */ + private Object readCompressedFrame (RandomAccessFile rFile, int size) + throws Exception, IOException { + InputStream inputStream = new raInputStream(rFile, size, biCompression==MJPG_COMPRESSION); + BufferedImage bi = ImageIO.read(inputStream); + if (bi==null) throw new Exception("can't read frame, ImageIO returns null"); + int type = bi.getType(); + ImageProcessor ip = null; + if (type==BufferedImage.TYPE_BYTE_GRAY) { + ip = new ByteProcessor(bi); + } else if (type==bi.TYPE_BYTE_INDEXED) { + cm = bi.getColorModel(); + ip = new ByteProcessor((Image)bi); + } else + ip = new ColorProcessor(bi); + if (convertToGray) + ip = ip.convertToByte(false); + if (flipVertical) + ip.flipVertical(); + if (ip.getWidth()!=dwWidth || ip.getHeight()!=biHeight) + ip = ip.resize(dwWidth, biHeight); + return ip.getPixels(); + } + + /** Read a fixed-length frame (RandomAccessFile rFile, long filePos, int size) + * return the pixels array of the resulting image + */ + private Object readFixedLengthFrame (RandomAccessFile rFile, int size) throws Exception, IOException { + if (size < scanLineSize*biHeight) + size = scanLineSize*biHeight; // bugfix for RGB odd-width files + byte[] rawData = new byte[size]; + int n = rFile.read(rawData, 0, size); + if (n < rawData.length) + throw new Exception("Frame ended prematurely after " + n + " bytes"); + + boolean topDown = flipVertical ? !dataTopDown : dataTopDown; + Object pixels = null; + byte[] bPixels = null; + int[] cPixels = null; + short[] sPixels = null; + if (biBitCount <=8 || convertToGray) { + bPixels = new byte[dwWidth * biHeight]; + pixels = bPixels; + } else if (biBitCount == 16 && dataCompression == NO_COMPRESSION) { + sPixels = new short[dwWidth * biHeight]; + pixels = sPixels; + } else { + cPixels = new int[dwWidth * biHeight]; + pixels = cPixels; + } + if (isPlanarFormat && !convertToGray) + unpackPlanarImage(rawData, cPixels, topDown); + else { + int offset = topDown ? 0 : (biHeight-1)*dwWidth; + int rawOffset = 0; + for (int i = biHeight - 1; i >= 0; i--) { //for all lines + if (biBitCount <=8 || isPlanarFormat) + unpack8bit(rawData, rawOffset, bPixels, offset, dwWidth); + else if (convertToGray) + unpackGray(rawData, rawOffset, bPixels, offset, dwWidth); + else if (biBitCount==16 && dataCompression == NO_COMPRESSION) + unpackShort(rawData, rawOffset, sPixels, offset, dwWidth); + else + unpack(rawData, rawOffset, cPixels, offset, dwWidth); + rawOffset += isPlanarFormat ? dwWidth : scanLineSize; + offset += topDown ? dwWidth : -dwWidth; + } + } + return pixels; + } + + /** For one line: copy byte data into the byte array for creating a ByteProcessor */ + void unpack8bit(byte[] rawData, int rawOffset, byte[] pixels, int byteOffset, int w) { + for (int i = 0; i < w; i++) + pixels[byteOffset + i] = rawData[rawOffset + i]; + } + + /** For one line: Unpack and convert YUV or RGB video data to grayscale (byte array for ByteProcessor) */ + void unpackGray(byte[] rawData, int rawOffset, byte[] pixels, int byteOffset, int w) { + int j = byteOffset; + int k = rawOffset; + if (dataCompression == 0) { + for (int i = 0; i < w; i++) { + int b0 = (((int) (rawData[k++])) & 0xff); + int b1 = (((int) (rawData[k++])) & 0xff); + int b2 = (((int) (rawData[k++])) & 0xff); + if (biBitCount==32) k++; // ignore 4th byte (alpha value) + pixels[j++] = (byte)((b0*934 + b1*4809 + b2*2449 + 4096)>>13); //0.299*R+0.587*G+0.114*B + } + } else { + if (dataCompression==UYVY_COMPRESSION || dataCompression==AYUV_COMPRESSION) + k++; //skip first byte in these formats (chroma) + int step = dataCompression==AYUV_COMPRESSION ? 4 : 2; + for (int i = 0; i < w; i++) { + pixels[j++] = rawData[k]; //Non-standard: no scaling from 16-235 to 0-255 here + k+=step; + } + } + } + + /** For one line: Unpack 16bit grayscale data and convert to short array for ShortProcessor */ + void unpackShort(byte[] rawData, int rawOffset, short[] pixels, int shortOffset, int w) { + int j = shortOffset; + int k = rawOffset; + for (int i = 0; i < w; i++) { + pixels[j++] = (short) ((int)(rawData[k++] & 0xFF)| (((int)(rawData[k++] & 0xFF))<<8)); + } + } + + /** For one line: Read YUV, RGB or RGB+alpha data and writes RGB int array for ColorProcessor */ + void unpack(byte[] rawData, int rawOffset, int[] pixels, int intOffset, int w) { + int j = intOffset; + int k = rawOffset; + switch (dataCompression) { + case NO_COMPRESSION: + for (int i = 0; i < w; i++) { + int b0 = (((int) (rawData[k++])) & 0xff); + int b1 = (((int) (rawData[k++])) & 0xff) << 8; + int b2 = (((int) (rawData[k++])) & 0xff) << 16; + if (biBitCount==32) k++; // ignore 4th byte (alpha value) + pixels[j++] = 0xff000000 | b0 | b1 | b2; + } + break; + case YUY2_COMPRESSION: + for (int i = 0; i < w/2; i++) { + int y0 = rawData[k++] & 0xff; + int u = rawData[k++] ^ 0xffffff80; //converts byte range 0...ff to -128 ... 127 + int y1 = rawData[k++] & 0xff; + int v = rawData[k++] ^ 0xffffff80; + writeRGBfromYUV(y0, u, v, pixels, j++); + writeRGBfromYUV(y1, u, v, pixels, j++); + } + break; + case UYVY_COMPRESSION: + for (int i = 0; i < w/2; i++) { + int u = rawData[k++] ^ 0xffffff80; + int y0 = rawData[k++] & 0xff; + int v = rawData[k++] ^ 0xffffff80; + int y1 = rawData[k++] & 0xff; + writeRGBfromYUV(y0, u, v, pixels, j++); + writeRGBfromYUV(y1, u, v, pixels, j++); + } + break; + case YVYU_COMPRESSION: + for (int i = 0; i < w/2; i++) { + int y0 = rawData[k++] & 0xff; + int v = rawData[k++] ^ 0xffffff80; + int y1 = rawData[k++] & 0xff; + int u = rawData[k++] ^ 0xffffff80; + writeRGBfromYUV(y0, u, v, pixels, j++); + writeRGBfromYUV(y1, u, v, pixels, j++); + } + break; + case AYUV_COMPRESSION: + for (int i = 0; i < w; i++) { + k++; //ignore alpha channel + int y = rawData[k++] & 0xff; + int v = rawData[k++] ^ 0xffffff80; + int u = rawData[k++] ^ 0xffffff80; + writeRGBfromYUV(y, u, v, pixels, j++); + } + break; + + } + } + + /** Unpack planar YV12 or I420 format (full frame). */ + void unpackPlanarImage(byte[] rawData, int[] cPixels, boolean topDown) { + int w = dwWidth, h = dwHeight; + int uP = w*h, vP = w*h; // pointers in U, V array + int uvInc = (dataCompression==NV12_COMPRESSION || dataCompression==NV21_COMPRESSION) ? + 2 : 1; // NV12, NV21 have interleaved u,v + if (dataCompression == YV12_COMPRESSION) // separate planes for U and V, 2-fold subsampling in x&y + uP += w*h/4; // first V, then U + else if (dataCompression == I420_COMPRESSION) + vP += w*h/4; // first U, then V + else if (dataCompression == NV12_COMPRESSION) + vP++; //interleaved U, then V + else //NV21_COMPRESSION + uP++; + int lineOutInc = topDown ? w : -w; + for (int line=0; line> 13; + int g = (9535*y - 6660*v - 3203*u -148464) >> 13; + int b = (9535*y + 16531*u -148464) >> 13; + if (r>255) r=255; if (r<0) r=0; + if (g>255) g=255; if (g<0) g=0; + if (b>255) b=255; if (b<0) b=0; + pixels[intArrayIndex] = 0xff000000 | (r<<16) | (g<<8) | b; + } + + /** Read 8-byte int with Intel (little-endian) byte order + * (note: RandomAccessFile.readLong has other byte order than AVI) */ + + final long readLong() throws IOException { + long low = readInt() & 0x00000000FFFFFFFFL; + long high = readInt() & 0x00000000FFFFFFFFL; + long result = high <<32 | low; + return (long) result; //(high << 32 | low); + } + /** Read 4-byte int with Intel (little-endian) byte order + * (note: RandomAccessFile.readInt has other byte order than AVI) */ + + final int readInt() throws IOException { + int result = 0; + for (int shiftBy = 0; shiftBy < 32; shiftBy += 8) + result |= (raFile.readByte() & 0xff) << shiftBy; + return result; + } + + /** Read 2-byte short with Intel (little-endian) byte order + * (note: RandomAccessFile.readShort has other byte order than AVI) */ + final short readShort() throws IOException { + int low = raFile.readByte() & 0xff; + int high = raFile.readByte() & 0xff; + return (short) (high << 8 | low); + } + + /** Read type of next chunk that is not JUNK. + * Returns type (or 0 if no non-JUNK chunk until endPosition) */ + private int readType(long endPosition) throws IOException { + while (true) { + long pos = raFile.getFilePointer(); + if (pos%paddingGranularity!=0) { + pos = (pos/paddingGranularity+1)*paddingGranularity; + raFile.seek(pos); //pad to even address + } + if (pos >= endPosition) return 0; + int type = readInt(); + if (type != FOURCC_JUNK) + return type; + long size = readInt()&SIZE_MASK; + if (verbose) + IJ.log("Skip JUNK: "+posSizeString(size)); + raFile.seek(raFile.getFilePointer()+size); //skip junk + } + } + + private void setFramesPerSecond (ImagePlus imp) { + if (dwMicroSecPerFrame<1000 && dwStreamRate>0) //if no reasonable frame time, get it from rate + dwMicroSecPerFrame = (int)(dwStreamScale*1e6/dwStreamRate); + if (dwMicroSecPerFrame>=1000) + imp.getCalibration().fps = 1e6 / dwMicroSecPerFrame; + } + + private String frameLabel(long timeMicroSec) { + return IJ.d2s(timeMicroSec/1.e6)+" s"; + } + + private String posSizeString(long size) throws IOException { + return posSizeString(raFile.getFilePointer(), size); + } + + private String posSizeString(long pos, long size) { + return "0x"+Long.toHexString(pos)+"-0x"+Long.toHexString(pos+size-1)+" ("+size+" Bytes)"; + } + + private String timeString() { + return " (t="+(System.currentTimeMillis()-startTime)+" ms)"; + } + + /** returns a string of a four-cc code corresponding to an int (Intel byte order) */ + private String fourccString(int fourcc) { + String s = ""; + for (int i=0; i<4; i++) { + int c = fourcc&0xff; + s += Character.toString((char)c); + fourcc >>= 8; + } + return s; + } + + /** tries to close the given file (if not null) */ + private void closeFile(RandomAccessFile rFile) { + if (rFile != null) try { + rFile.close(); + } catch (Exception e) {} + } + + private void error(String msg) { + aborting = true; + IJ.error("AVI Reader", msg); + } + + private String exceptionMessage (Exception e) { + String msg; + if (e.getClass() == Exception.class) //for "home-built" exceptions: message only + msg = e.getMessage(); + else + msg = e + "\n" + e.getStackTrace()[0]+"\n"+e.getStackTrace()[1]; + return "An error occurred reading the AVI file.\n \n" + msg; + } + + /** An input stream reading from a RandomAccessFile (starting at the current position). + * This class also adds 'Define Huffman Table' (DHT) segments to convert MJPG to JPEG. + */ + final private static int BUFFERSIZE = 4096; //should be large enough to hold the full JFIF header + // up to beginning of the image data and the Huffman tables + final private static byte[] HUFFMAN_TABLES = new byte[] { //the 'DHT' segment + (byte)0xFF,(byte)0xC4,0x01,(byte)0xA2, //these 4 bytes are tag & length; data follow + 0x00,0x00,0x01,0x05,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x01,0x00,0x03,0x01,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, + 0x08,0x09,0x0A,0x0B,0x10,0x00,0x02,0x01,0x03,0x03,0x02,0x04,0x03,0x05,0x05,0x04,0x04,0x00, + 0x00,0x01,0x7D,0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61, + 0x07,0x22,0x71,0x14,0x32,(byte)0x81,(byte)0x91,(byte)0xA1,0x08,0x23,0x42, + (byte)0xB1,(byte)0xC1,0x15,0x52,(byte)0xD1,(byte)0xF0,0x24, + 0x33,0x62,0x72,(byte)0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,0x26,0x27,0x28,0x29,0x2A,0x34, + 0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56, + 0x57,0x58,0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7A,(byte)0x83,(byte)0x84,(byte)0x85,(byte)0x86,(byte)0x87,(byte)0x88,(byte)0x89, + (byte)0x8A,(byte)0x92,(byte)0x93,(byte)0x94,(byte)0x95,(byte)0x96,(byte)0x97,(byte)0x98,(byte)0x99, + (byte)0x9A,(byte)0xA2,(byte)0xA3,(byte)0xA4,(byte)0xA5,(byte)0xA6,(byte)0xA7,(byte)0xA8,(byte)0xA9, + (byte)0xAA,(byte)0xB2,(byte)0xB3,(byte)0xB4,(byte)0xB5,(byte)0xB6,(byte)0xB7,(byte)0xB8,(byte)0xB9, + (byte)0xBA,(byte)0xC2,(byte)0xC3,(byte)0xC4,(byte)0xC5,(byte)0xC6,(byte)0xC7,(byte)0xC8,(byte)0xC9, + (byte)0xCA,(byte)0xD2,(byte)0xD3,(byte)0xD4,(byte)0xD5,(byte)0xD6,(byte)0xD7,(byte)0xD8,(byte)0xD9, + (byte)0xDA,(byte)0xE1,(byte)0xE2,(byte)0xE3,(byte)0xE4,(byte)0xE5,(byte)0xE6,(byte)0xE7,(byte)0xE8, + (byte)0xE9,(byte)0xEA,(byte)0xF1,(byte)0xF2,(byte)0xF3,(byte)0xF4,(byte)0xF5,(byte)0xF6,(byte)0xF7, + (byte)0xF8,(byte)0xF9,(byte)0xFA,0x11,0x00,0x02,0x01,0x02,0x04,0x04,0x03,0x04,0x07,0x05,0x04,0x04,0x00,0x01, + 0x02,0x77,0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,(byte)0x81,0x08,0x14,0x42,(byte)0x91,(byte)0xA1,(byte)0xB1,(byte)0xC1,0x09,0x23,0x33, + 0x52,(byte)0xF0,0x15,0x62, + 0x72,(byte)0xD1,0x0A,0x16,0x24,0x34,(byte)0xE1,0x25,(byte)0xF1,0x17,0x18,0x19,0x1A,0x26,0x27,0x28,0x29,0x2A, + 0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56, + 0x57,0x58,0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7A,(byte)0x82,(byte)0x83,(byte)0x84,(byte)0x85,(byte)0x86,(byte)0x87,(byte)0x88,(byte)0x89, + (byte)0x8A,(byte)0x92,(byte)0x93,(byte)0x94,(byte)0x95,(byte)0x96,(byte)0x97,(byte)0x98, + (byte)0x99,(byte)0x9A,(byte)0xA2,(byte)0xA3,(byte)0xA4,(byte)0xA5,(byte)0xA6,(byte)0xA7,(byte)0xA8, + (byte)0xA9,(byte)0xAA,(byte)0xB2,(byte)0xB3,(byte)0xB4,(byte)0xB5,(byte)0xB6,(byte)0xB7,(byte)0xB8, + (byte)0xB9,(byte)0xBA,(byte)0xC2,(byte)0xC3,(byte)0xC4,(byte)0xC5,(byte)0xC6,(byte)0xC7,(byte)0xC8, + (byte)0xC9,(byte)0xCA,(byte)0xD2,(byte)0xD3,(byte)0xD4,(byte)0xD5,(byte)0xD6,(byte)0xD7,(byte)0xD8, + (byte)0xD9,(byte)0xDA,(byte)0xE2,(byte)0xE3,(byte)0xE4,(byte)0xE5,(byte)0xE6,(byte)0xE7,(byte)0xE8, + (byte)0xE9,(byte)0xEA,(byte)0xF2,(byte)0xF3,(byte)0xF4,(byte)0xF5,(byte)0xF6,(byte)0xF7,(byte)0xF8, + (byte)0xF9,(byte)0xFA }; + final private static int HUFFMAN_LENGTH = 420; + + class raInputStream extends InputStream { + RandomAccessFile rFile; //where to read the data from + int readableSize; //number of bytes that one should expect to be readable + boolean fixMJPG; //whether to use an ugly hack to convert MJPG frames to JPEG + byte[] buffer; //holds beginning of data for fixing Huffman tables + int bufferPointer; //next position in buffer to read + int bufferLength; //bytes allocated in buffer + + /** Constructor */ + raInputStream (RandomAccessFile rFile, int readableSize, boolean fixMJPG) throws IOException { + this.rFile = rFile; + this.readableSize = readableSize; + this.fixMJPG = fixMJPG; + if (fixMJPG) { + buffer = new byte[BUFFERSIZE]; + bufferLength = Math.min(BUFFERSIZE-HUFFMAN_LENGTH, readableSize); + bufferLength = rFile.read(buffer, 0, bufferLength); + addHuffmanTables(); + } + } + + public int available () { + return readableSize; + } + + // Read methods: + // There is no check against reading beyond the allowed range, which is + // start position + readableSize + // (i.e., reading beyond the frame in the avi file would be possible). + /** Read a single byte */ + public int read () throws IOException { + readableSize--; + if (fixMJPG) { + int result = buffer[bufferPointer] & 0xff; + bufferPointer++; + if (bufferPointer >= bufferLength) fixMJPG = false; //buffer exhausted, no more attempt to fix it + return result; + } else + return rFile.read(); + } + + /** Read bytes into an array */ + public int read (byte[] b, int off, int len) throws IOException { + //IJ.log("read "+len+" bytes, fix="+fixMJPG); + int nBytes; + if (fixMJPG) { + nBytes = Math.min(len, bufferLength-bufferPointer); + System.arraycopy(buffer, bufferPointer, b, off, nBytes); + bufferPointer += nBytes; + if (bufferPointer >= bufferLength) { + fixMJPG = false; + if (len-nBytes > 0) + nBytes += rFile.read(b, off+nBytes, len-nBytes); + } + } else + nBytes = rFile.read(b, off, len); + readableSize -= nBytes; + return nBytes; + } + // Add Huffman table if not present yet + private void addHuffmanTables() { + if (readShort(0)!=0xffd8 || bufferLength<6) return; //not a start of JPEG-like data + int offset = 2; + int segmentLength = 0; + do { + int code = readShort(offset); //read segment type + //IJ.log("code=0x"+Long.toHexString(code)); + if (code==0xffc4) //Huffman table found, nothing to do + return; + else if (code==0xffda || code==0xffd9) { //start of image data or end of image? + insertHuffmanTables(offset); + return; //finished + } + offset += 2; + segmentLength = readShort(offset); //read length of this segment + offset += segmentLength; //and skip the segment contents + } while (offset=0); + } + + // read a short from the buffer + private int readShort(int offset) { + return ((buffer[offset]&0xff)<<8) | (buffer[offset+1]&0xff); + } + + // insert Huffman tables at the given position + private void insertHuffmanTables(int position) { + //IJ.log("inserting Huffman tables"); + System.arraycopy(buffer, position, buffer, position+HUFFMAN_LENGTH, bufferLength-position); + System.arraycopy(HUFFMAN_TABLES, 0, buffer, position, HUFFMAN_LENGTH); + bufferLength += HUFFMAN_LENGTH; + readableSize += HUFFMAN_LENGTH; + } + } + + public void displayDialog(boolean displayDialog) { + this.displayDialog = displayDialog; + } + + /** Open as virtual stack? */ + public void setVirtual(boolean virtual) { + isVirtual = virtual; + } + +} diff --git a/src/ij/plugin/AboutBox.java b/src/ij/plugin/AboutBox.java new file mode 100644 index 0000000..ca22d6d --- /dev/null +++ b/src/ij/plugin/AboutBox.java @@ -0,0 +1,76 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.io.*; +import java.net.URL; +import java.awt.image.*; + + /** This plugin implements the Help/About ImageJ command by opening + * about.jpg in ij.jar, scaling it 600% and adding text to an overlay. + */ + public class AboutBox implements PlugIn { + private static final int SMALL_FONT=20, LARGE_FONT=45; + private static final Color TEXT_COLOR = new Color(255,255,80); + + public void run(String arg) { + System.gc(); + int lines = 7; + String[] text = new String[lines]; + text[0] = "ImageJ "+ImageJ.VERSION+ImageJ.BUILD; + text[1] = "Wayne Rasband and contributors"; + text[2] = "National Institutes of Health, USA"; + text[3] = IJ.URL; + text[4] = "Java "+System.getProperty("java.version")+(IJ.is64Bit()?" (64-bit)":" (32-bit)"); + text[5] = IJ.freeMemory(); + text[6] = "ImageJ is in the public domain"; + ImageProcessor ip = null; + ImageJ ij = IJ.getInstance(); + URL url = ij .getClass() .getResource("/about.jpg"); + if (url!=null) { + Image img = null; + try {img = ij.createImage((ImageProducer)url.getContent());} + catch(Exception e) {} + if (img!=null) { + ImagePlus sImp = new ImagePlus("", img); + ip = sImp.getProcessor(); + } + } + if (ip==null) + ip = new ColorProcessor(55,45); + ip = ip.resize(ip.getWidth()*6, ip.getHeight()*6); + ImagePlus imp = new ImagePlus("About ImageJ", ip); + int width = imp.getWidth(); + Overlay overlay = new Overlay(); + Font font = new Font("SansSerif", Font.PLAIN, LARGE_FONT); + int y = 60; + add(text[0], width-20, y, font, TextRoi.RIGHT, overlay); + int xcenter = 410; + font = new Font("SansSerif", Font.PLAIN, SMALL_FONT); + y += 45; + add(text[1], xcenter, y, font, TextRoi.CENTER, overlay); + y += 27; + add(text[2], xcenter, y, font, TextRoi.CENTER, overlay); + y += 27; + add(text[3], xcenter, y, font, TextRoi.CENTER, overlay); + y += 27; + add(text[4], xcenter, y, font, TextRoi.CENTER, overlay); + if (IJ.maxMemory()>0L) { + y += 27; + add(text[5], xcenter, y, font, TextRoi.CENTER, overlay); + } + add(text[6], width-10, ip.getHeight()-10, font, TextRoi.RIGHT, overlay); + imp.setOverlay(overlay); + ImageWindow.centerNextImage(); + imp.show(); + } + + private void add(String text, int x, int y, Font font, int justification, Overlay overlay) { + TextRoi roi = new TextRoi(text, x, y, font); + roi.setStrokeColor(TEXT_COLOR); + roi.setJustification(justification); + overlay.add(roi); + } + +} diff --git a/src/ij/plugin/Animator.java b/src/ij/plugin/Animator.java new file mode 100644 index 0000000..631fc6a --- /dev/null +++ b/src/ij/plugin/Animator.java @@ -0,0 +1,398 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.Calibration; +import ij.plugin.frame.Recorder; +import java.awt.Point; + +/** This plugin animates stacks. */ +public class Animator implements PlugIn { + + private static double animationRate = Prefs.getDouble(Prefs.FPS, 7.0); + private static int firstFrame=0, lastFrame=0; + private ImagePlus imp; + private StackWindow swin; + private int slice; + private int nSlices; + /** Set 'arg' to "set" to display a dialog that allows the user to specify the + animation speed. Set it to "start" to start animating the current stack. + Set it to "stop" to stop animation. Set it to "next" or "previous" + to stop any animation and display the next or previous frame. + */ + public void run(String arg) { + imp = IJ.getImage(); + nSlices = imp.getStackSize(); + if (nSlices<2) + {IJ.error("Stack required."); return;} + if (imp.isLocked()) + {IJ.beep(); IJ.showStatus("Image is locked: \""+imp.getTitle()+"\""); return;} + ImageWindow win = imp.getWindow(); + if ((win==null || !(win instanceof StackWindow)) && !arg.equals("options")) { + if (arg.equals("next")) + imp.setSlice(imp.getCurrentSlice()+1); + else if (arg.equals("previous")) + imp.setSlice(imp.getCurrentSlice()-1); + if (win!=null) imp.updateStatusbarValue(); + return; + } + swin = (StackWindow)win; + ImageStack stack = imp.getStack(); + slice = imp.getCurrentSlice(); + IJ.register(Animator.class); + + if (arg.equals("options")) { + doOptions(); + return; + } + + if (arg.equals("start")) { + startAnimation(); + return; + } + + //if (swin.getAnimate()) // "stop", "next" and "previous" all stop animation + // stopAnimation(); + + if (arg.equals("stop")) { + stopAnimation(); + return; + } + + if (arg.equals("next")) { + if (Prefs.reverseNextPreviousOrder) + changeSlice(1); + else + nextSlice(); + return; + } + + if (arg.equals("previous")) { + if (Prefs.reverseNextPreviousOrder) + changeSlice(-1); + else + previousSlice(); + return; + } + + if (arg.equals("set")) { + setSlice(); + return; + } + } + + void stopAnimation() { + swin.setAnimate(false); + IJ.wait(500+(int)(1000.0/animationRate)); + } + + void startAnimation() { + int first=firstFrame, last=lastFrame; + if (first<1 || first>nSlices || last<1 || last>nSlices) + {first=1; last=nSlices;} + if (swin.getAnimate()) + {stopAnimation(); return;} + swin.setAnimate(true); + long time, nextTime=System.currentTimeMillis(); + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + int sliceIncrement = 1; + Calibration cal = imp.getCalibration(); + if (cal.fps!=0.0) + animationRate = cal.fps; + if (animationRate<0.1) { + animationRate = 1.0; + cal.fps = animationRate; + } + int frames = imp.getNFrames(); + int slices = imp.getNSlices(); + + if (imp.isDisplayedHyperStack() && frames>1) { + int frame = imp.getFrame(); + first = 1; + last = frames; + while (swin.getAnimate()) { + time = System.currentTimeMillis(); + if (timelast) { + if (cal.loop) { + frame = last-1; + sliceIncrement = -1; + } else { + frame = first; + sliceIncrement = 1; + } + } + if (imp.isLocked()) return; + imp.setPosition(imp.getChannel(), imp.getSlice(), frame); + imp.updateStatusbarValue(); + } + return; + } + + if (imp.isDisplayedHyperStack() && slices>1) { + slice = imp.getSlice(); + first = 1; + last = slices; + while (swin.getAnimate()) { + time = System.currentTimeMillis(); + if (timelast) { + if (cal.loop) { + slice = last-1; + sliceIncrement = -1; + } else { + slice = first; + sliceIncrement = 1; + } + } + if (imp.isLocked()) return; + imp.setPosition(imp.getChannel(), slice, imp.getFrame()); + imp.updateStatusbarValue(); + } + return; + } + + long startTime=System.currentTimeMillis(); + int count = 0; + double fps = 0.0; + while (swin.getAnimate()) { + time = System.currentTimeMillis(); + count++; + if (time>startTime+1000L) { + startTime=System.currentTimeMillis(); + fps=count; + count=0; + } + ImageCanvas ic = imp.getCanvas(); + boolean showFrameRate = !(ic!=null?ic.cursorOverImage():false); + if (showFrameRate) + IJ.showStatus((int)(fps+0.5) + " fps"); + if (timelast) { + if (cal.loop) { + slice = last-1; + sliceIncrement = -1; + } else { + slice = first; + sliceIncrement = 1; + } + } + if (imp.isLocked()) return; + swin.showSlice(slice); + if (!showFrameRate) + imp.updateStatusbarValue(); + } + } + + void doOptions() { + if (firstFrame<1 || firstFrame>nSlices || lastFrame<1 || lastFrame>nSlices) + {firstFrame=1; lastFrame=nSlices;} + if (imp.isDisplayedHyperStack()) { + int frames = imp.getNFrames(); + int slices = imp.getNSlices(); + firstFrame = 1; + if (frames>1) + lastFrame = frames; + else if (slices>1) + lastFrame=slices; + } + boolean start = swin!=null && !swin.getAnimate(); + Calibration cal = imp.getCalibration(); + if (cal.fps!=0.0) + animationRate = cal.fps; + else if (cal.frameInterval!=0.0 && cal.getTimeUnit().equals("sec")) + animationRate = 1.0/cal.frameInterval; + int decimalPlaces = (int)animationRate==animationRate?0:3; + GenericDialog gd = new GenericDialog("Animation Options"); + gd.addNumericField("Speed (0.1-1000 fps):", animationRate, decimalPlaces); + if (!imp.isDisplayedHyperStack()) { + gd.addNumericField("First Frame:", firstFrame, 0); + gd.addNumericField("Last Frame:", lastFrame, 0); + } + gd.addCheckbox("Loop Back and Forth", cal.loop); + gd.addCheckbox("Start Animation", start); + gd.showDialog(); + if (gd.wasCanceled()) { + if (firstFrame==1 && lastFrame==nSlices) + {firstFrame=0; lastFrame=0;} + return; + } + double speed = gd.getNextNumber(); + if (!imp.isDisplayedHyperStack()) { + firstFrame = (int)gd.getNextNumber(); + lastFrame = (int)gd.getNextNumber(); + } + if (firstFrame==1 && lastFrame==nSlices) + {firstFrame=0; lastFrame=0;} + cal.loop = gd.getNextBoolean(); + Calibration.setLoopBackAndForth(cal.loop); + start = gd.getNextBoolean(); + if (speed>1000.0) speed = 1000.0; + //if (speed<0.1) speed = 0.1; + animationRate = speed; + if (animationRate!=0.0) + cal.fps = animationRate; + if (start && swin!=null && !swin.getAnimate()) + startAnimation(); + } + + void nextSlice() { + boolean hyperstack = imp.isDisplayedHyperStack(); + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + if (hyperstack && channels>1 && !((slices>1||frames>1)&&(IJ.controlKeyDown()||IJ.spaceBarDown()||IJ.altKeyDown()))) { + int c = imp.getChannel() + 1; + if (c>channels) c = channels; + swin.setPosition(c, imp.getSlice(), imp.getFrame()); + } else if (hyperstack && slices>1 && !(frames>1&&IJ.altKeyDown())) { + int z = imp.getSlice() + 1; + if (z>slices) z = slices; + swin.setPosition(imp.getChannel(), z, imp.getFrame()); + } else if (hyperstack && frames>1) { + int t = imp.getFrame() + 1; + if (t>frames) t = frames; + swin.setPosition(imp.getChannel(), imp.getSlice(), t); + } else { + if (IJ.altKeyDown()) + slice += 10; + else + slice++; + if (slice>nSlices) + slice = nSlices; + swin.showSlice(slice); + } + imp.updateStatusbarValue(); + } + + void previousSlice() { + boolean hyperstack = imp.isDisplayedHyperStack(); + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + if (hyperstack && channels>1 && !((slices>1||frames>1)&&(IJ.controlKeyDown()||IJ.spaceBarDown()||IJ.altKeyDown()))) { + int c = imp.getChannel() - 1; + if (c<1) c = 1; + swin.setPosition(c, imp.getSlice(), imp.getFrame()); + } else if (hyperstack && slices>1 && !(frames>1&&IJ.altKeyDown())) { + int z = imp.getSlice() - 1; + if (z<1) z = 1; + swin.setPosition(imp.getChannel(), z, imp.getFrame()); + } else if (hyperstack && frames>1) { + int t = imp.getFrame() - 1; + if (t<1) t = 1; + swin.setPosition(imp.getChannel(), imp.getSlice(), t); + } else { + if (IJ.altKeyDown()) + slice -= 10; + else + slice--; + if (slice<1) + slice = 1; + swin.showSlice(slice); + } + imp.updateStatusbarValue(); + } + + void changeSlice(int pn) { + boolean hyperstack = imp.isDisplayedHyperStack(); + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + if (swin.getAnimate() && (channels*slices*frames==Math.max(channels,Math.max(slices,frames))) ) + {stopAnimation(); return;} //if only one dimension, stop animating + if(hyperstack){ + int c=imp.getChannel(); int z=imp.getSlice(); int t=imp.getFrame(); + if (frames>1 && !((slices>1||channels>1)&&(IJ.controlKeyDown()||IJ.spaceBarDown()||IJ.altKeyDown()) || swin.getAnimate())){ + t += pn; + if (t>frames) t = frames; + if (t<1) t = 1; + } else if (slices>1 && !(channels>1&& (IJ.altKeyDown() || IJ.spaceBarDown()) || ((swin.getAnimate()|| IJ.controlKeyDown()) && frames==1)) ) { + z += pn; + if (z>slices) z = slices; + if (z<1) z = 1; + } else if (channels>1) { + c += pn; + if (c>channels) c = channels; + if (c<1) c = 1; + } + swin.setPosition(c, z, t); + } else { + if (IJ.altKeyDown()) + slice+=(pn*10); + else + slice+=pn; + if (slice>nSlices) + slice = nSlices; + if (slice<1) + slice = 1; + swin.showSlice(slice); + } + imp.updateStatusbarValue(); + } + + void setSlice() { + if (imp.isDisplayedHyperStack()) { + GenericDialog gd = new GenericDialog("Set Position"); + int c = imp.getChannel(); + int z = imp.getSlice(); + int t = imp.getFrame(); + gd.addNumericField("Channel:", c); + gd.addNumericField("Slice:", z); + gd.addNumericField("Frame:", t); + gd.showDialog(); + if (!gd.wasCanceled()) { + c = (int) gd.getNextNumber(); + z = (int) gd.getNextNumber(); + t = (int) gd.getNextNumber(); + imp.setPosition(c, z, t); + } + if (Recorder.record) { + String method = Recorder.scriptMode()?"imp":"Stack"; + Recorder.recordString(method+".setPosition("+c+","+z+","+t+");\n"); + Recorder.disableCommandRecording(); + } + } else { + GenericDialog gd = new GenericDialog("Set Slice"); + gd.addNumericField("Slice (1-"+nSlices+"):", slice, 0); + gd.showDialog(); + if (!gd.wasCanceled()) { + int slice = (int)gd.getNextNumber(); + imp.setSlice(slice); + } + } + } + + /** Returns the current animation speed in frames per second. */ + public static double getFrameRate() { + return animationRate; + } + +} diff --git a/src/ij/plugin/AppearanceOptions.java b/src/ij/plugin/AppearanceOptions.java new file mode 100644 index 0000000..bd96d6e --- /dev/null +++ b/src/ij/plugin/AppearanceOptions.java @@ -0,0 +1,189 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.io.*; +import ij.plugin.filter.*; +import ij.plugin.frame.*; +import ij.measure.Calibration; +import java.awt.*; + +/** This plugin implements the Edit/Options/Appearance command. */ +public class AppearanceOptions implements PlugIn, DialogListener { + private boolean interpolate = Prefs.interpolateScaledImages; + private boolean open100 = Prefs.open100Percent; + private boolean black = Prefs.blackCanvas; + private boolean noBorder = Prefs.noBorder; + private boolean inverting = Prefs.useInvertingLut; + private int rangeIndex = ContrastAdjuster.get16bitRangeIndex(); + private LUT[] luts = getLuts(); + private int menuFontSize = Menus.getFontSize(); + private double saveScale = Prefs.getGuiScale(); + private boolean redrawn, repainted; + + public void run(String arg) { + showDialog(); + } + + void showDialog() { + String[] ranges = ContrastAdjuster.getSixteenBitRanges(); + GenericDialog gd = new GenericDialog("Appearance"); + gd.addCheckbox("Interpolate zoomed images", Prefs.interpolateScaledImages); + gd.addCheckbox("Open images at 100%", Prefs.open100Percent); + gd.addCheckbox("Black canvas", Prefs.blackCanvas); + gd.addCheckbox("No image border", Prefs.noBorder); + gd.addCheckbox("Use inverting lookup table", Prefs.useInvertingLut); + gd.addCheckbox("Auto contrast stacks", Prefs.autoContrast); + gd.addCheckbox("IJ window always on top", Prefs.alwaysOnTop); + if (IJ.isLinux()) + gd.addCheckbox("Cancel button on right", Prefs.dialogCancelButtonOnRight); + gd.addChoice("16-bit range:", ranges, ranges[rangeIndex]); + Font font = new Font("SansSerif", Font.PLAIN, 9); + if (!IJ.isMacOSX()) { + gd.setInsets(0, 0, 0); + gd.addNumericField("Menu font size:", Menus.getFontSize(), 0, 4, "points"); + if (IJ.isWindows()) { + gd.setInsets(2,30,5); + gd.addMessage("Setting size>17 may not work on Windows", font); + } + } + gd.setInsets(0, 0, 0); + gd.addNumericField("GUI scale (0.5-3.0):", Prefs.getGuiScale(), 1, 4, ""); + gd.setInsets(2,20,0); + gd.addMessage("Set to 1.5 to double size of tool icons, or 2.5 to triple", font); + gd.addHelp(IJ.URL+"/docs/menus/edit.html#appearance"); + gd.addDialogListener(this); + gd.showDialog(); + if (gd.wasCanceled()) { + Prefs.interpolateScaledImages = interpolate; + Prefs.open100Percent = open100; + Prefs.blackCanvas = black; + Prefs.noBorder = noBorder; + Prefs.useInvertingLut = inverting; + Prefs.setGuiScale(saveScale); + if (redrawn) draw(); + if (repainted) repaintWindow(); + Prefs.open100Percent = open100; + if (rangeIndex!=ContrastAdjuster.get16bitRangeIndex()) { + ContrastAdjuster.set16bitRange(rangeIndex); + ImagePlus imp = WindowManager.getCurrentImage(); + Calibration cal = imp!=null?imp.getCalibration():null; + if (imp!=null && imp.getType()==ImagePlus.GRAY16 && !cal.isSigned16Bit()) { + imp.resetDisplayRange(); + if (rangeIndex==0 && imp.isComposite() && luts!=null) + ((CompositeImage)imp).setLuts(luts); + imp.updateAndDraw(); + } + } + return; + } + boolean messageShown = false; + double scale = Prefs.getGuiScale(); + if (scale!=saveScale) { + if (!IJ.isMacOSX()) { + IJ.showMessage("Appearance", "Restart ImageJ to resize \"ImageJ\" window"); + messageShown = true; + } else { + ImageJ ij = IJ.getInstance(); + if (ij!=null) + ij.resize(); + } + } + boolean fontSizeChanged = menuFontSize!=Menus.getFontSize(); + if (fontSizeChanged) + Menus.setFontSize(menuFontSize); + if (!messageShown && fontSizeChanged && !IJ.isMacOSX()) + IJ.showMessage("Appearance", "Restart ImageJ to use the new font size"); + if (Prefs.useInvertingLut) { + IJ.showMessage("Appearance", + "The \"Use inverting lookup table\" option is set. Newly opened\n"+ + "8-bit images will use an inverting LUT (white=0, black=255)."); + } + int range = ImagePlus.getDefault16bitRange(); + if (range>0 && Recorder.record) { + if (Recorder.scriptMode()) + Recorder.recordCall("ImagePlus.setDefault16bitRange("+range+");"); + else + Recorder.recordString("call(\"ij.ImagePlus.setDefault16bitRange\", "+range+");\n"); + } + + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (IJ.isMacOSX()) IJ.wait(100); + boolean interpolate = gd.getNextBoolean(); + Prefs.open100Percent = gd.getNextBoolean(); + boolean blackCanvas = gd.getNextBoolean(); + boolean noBorder = gd.getNextBoolean(); + Prefs.useInvertingLut = gd.getNextBoolean(); + boolean alwaysOnTop = Prefs.alwaysOnTop; + Prefs.autoContrast = gd.getNextBoolean(); + Prefs.alwaysOnTop = gd.getNextBoolean(); + if (IJ.isLinux()) + Prefs.dialogCancelButtonOnRight = gd.getNextBoolean(); + if (!IJ.isMacOSX()) + menuFontSize = (int)gd.getNextNumber(); + Prefs.setGuiScale(gd.getNextNumber()); + if (interpolate!=Prefs.interpolateScaledImages) { + Prefs.interpolateScaledImages = interpolate; + draw(); + } + if (blackCanvas!=Prefs.blackCanvas) { + Prefs.blackCanvas = blackCanvas; + repaintWindow(); + } + if (noBorder!=Prefs.noBorder) { + Prefs.noBorder = noBorder; + repaintWindow(); + } + if (alwaysOnTop!=Prefs.alwaysOnTop) { + ImageJ ij = IJ.getInstance(); + if (ij!=null) ij.setAlwaysOnTop(Prefs.alwaysOnTop); + } + int rangeIndex2 = gd.getNextChoiceIndex(); + int range1 = ImagePlus.getDefault16bitRange(); + int range2 = ContrastAdjuster.set16bitRange(rangeIndex2); + ImagePlus imp = WindowManager.getCurrentImage(); + Calibration cal = imp!=null?imp.getCalibration():null; + if (range1!=range2 && imp!=null && imp.getType()==ImagePlus.GRAY16 && !cal.isSigned16Bit()) { + imp.resetDisplayRange(); + if (rangeIndex2==0 && imp.isComposite() && luts!=null) + ((CompositeImage)imp).setLuts(luts); + imp.updateAndDraw(); + } + return true; + } + + private LUT[] getLuts() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null || imp.getBitDepth()!=16 || !imp.isComposite()) + return null; + return ((CompositeImage)imp).getLuts(); + } + + void draw() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) + imp.draw(); + redrawn = true; + } + + void repaintWindow() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + ImageWindow win = imp.getWindow(); + if (win!=null) { + if (Prefs.blackCanvas) { + win.setForeground(Color.white); + win.setBackground(Color.black); + } else { + win.setForeground(Color.black); + win.setBackground(Color.white); + } + imp.repaintWindow(); + } + } + repainted = true; + } + +} \ No newline at end of file diff --git a/src/ij/plugin/ArrowToolOptions.java b/src/ij/plugin/ArrowToolOptions.java new file mode 100644 index 0000000..49a7d2a --- /dev/null +++ b/src/ij/plugin/ArrowToolOptions.java @@ -0,0 +1,84 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import java.awt.*; + +/** This plugin implements the Edit/Options/Arrow Tool command. */ +public class ArrowToolOptions implements PlugIn, DialogListener { + private String colorName; + private static GenericDialog gd; + private static final String LOC_KEY = "arrows.loc"; + + public void run(String arg) { + if (gd!=null && gd.isVisible()) + gd.toFront(); + else + arrowToolOptions(); + } + + void arrowToolOptions() { + if (!Toolbar.getToolName().equals("arrow")) + IJ.setTool("arrow"); + double width = Arrow.getDefaultWidth(); + double headSize = Arrow.getDefaultHeadSize(); + Color color = Toolbar.getForegroundColor(); + colorName = Colors.colorToString2(color); + int style = Arrow.getDefaultStyle(); + gd = GUI.newNonBlockingDialog("Arrow Tool"); + gd.addSlider("Width:", 1, 50, (int)width); + gd.addSlider("Size:", 0, 50, headSize); + gd.addChoice("Color:", Colors.getColors(colorName), colorName); + gd.addChoice("Style:", Arrow.styles, Arrow.styles[style]); + gd.addCheckbox("Outline", Arrow.getDefaultOutline()); + gd.addCheckbox("Double head", Arrow.getDefaultDoubleHeaded()); + gd.addCheckbox("Keep after adding to overlay", Prefs.keepArrowSelections); + gd.addDialogListener(this); + Point loc = Prefs.getLocation(LOC_KEY); + if (loc!=null) { + gd.centerDialog(false); + gd.setLocation (loc); + } + gd.showDialog(); + Prefs.saveLocation(LOC_KEY, gd.getLocation()); + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + double width2 = gd.getNextNumber(); + double headSize2 = gd.getNextNumber(); + String colorName2 = gd.getNextChoice(); + int style2 = gd.getNextChoiceIndex(); + boolean outline2 = gd.getNextBoolean(); + boolean doubleHeaded2 = gd.getNextBoolean(); + Prefs.keepArrowSelections = gd.getNextBoolean(); + if (colorName!=null && !colorName2.equals(colorName)) { + Color color = Colors.decode(colorName2, null); + Toolbar.setForegroundColor(color); + } + colorName = colorName2; + Arrow.setDefaultWidth(width2); + Arrow.setDefaultHeadSize(headSize2); + Arrow.setDefaultStyle(style2); + Arrow.setDefaultOutline(outline2); + Arrow.setDefaultDoubleHeaded(doubleHeaded2); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) return true; + Roi roi = imp.getRoi(); + if (roi==null) return true; + if (roi instanceof Arrow) { + Arrow arrow = (Arrow)roi; + roi.setStrokeWidth((float)width2); + arrow.setHeadSize(headSize2); + arrow.setStyle(style2); + arrow.setOutline(outline2); + arrow.setDoubleHeaded(doubleHeaded2); + imp.draw(); + } + Prefs.set(Arrow.STYLE_KEY, style2); + Prefs.set(Arrow.WIDTH_KEY, width2); + Prefs.set(Arrow.SIZE_KEY, headSize2); + Prefs.set(Arrow.OUTLINE_KEY, outline2); + Prefs.set(Arrow.DOUBLE_HEADED_KEY, doubleHeaded2); + return true; + } + +} diff --git a/src/ij/plugin/BMP_Reader.java b/src/ij/plugin/BMP_Reader.java new file mode 100644 index 0000000..9fd3108 --- /dev/null +++ b/src/ij/plugin/BMP_Reader.java @@ -0,0 +1,328 @@ +package ij.plugin; + +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import ij.*; +import ij.io.*; + + +/** This plugin reads BMP files. If 'arg' is empty, it + displays a file open dialog and opens and displays the + selected file. If 'arg' is a path, it opens the + specified file and the calling routine can display it using + "((ImagePlus)IJ.runPlugIn("ij.plugin.BMP_Reader", path)).show()". + */ +public class BMP_Reader extends ImagePlus implements PlugIn { + + public void run(String arg) { + OpenDialog od = new OpenDialog("Open BMP...", arg); + String directory = od.getDirectory(); + String name = od.getFileName(); + if (name==null) + return; + String path = directory + name; + + //IJ.showStatus("Opening: " + path); + BMPDecoder bmp = new BMPDecoder(); + FileInputStream is = null; + try { + is = new FileInputStream(path); + bmp.read(is); + } catch (Exception e) { + String msg = e.getMessage(); + if (msg==null || msg.equals("")) + msg = ""+e; + IJ.error("BMP Reader", msg); + return; + } finally { + if (is!=null) { + try { + is.close(); + } catch (IOException e) {} + } + } + + MemoryImageSource mis = bmp.makeImageSource(); + if (mis==null) IJ.log("BMP_Reader: mis=null"); + Image img = Toolkit.getDefaultToolkit().createImage(mis); + FileInfo fi = new FileInfo(); + fi.fileFormat = FileInfo.BMP; + fi.fileName = name; + fi.directory = directory; + setImage(img); + setTitle(name); + setFileInfo(fi); + if (bmp.topDown) + getProcessor().flipVertical(); + if (arg.equals("")) + show(); + } + +} + + +/** A decoder for Windows bitmap (.BMP) files. */ +class BMPDecoder { + InputStream is; + int curPos = 0; + + int bitmapOffset; // starting position of image data + + int width; // image width in pixels + int height; // image height in pixels + short bitsPerPixel; // 1, 4, 8, or 24 (no color map) + int compression; // 0 (none), 1 (8-bit RLE), or 2 (4-bit RLE) + int actualSizeOfBitmap; + int scanLineSize; + int actualColorsUsed; + + byte r[], g[], b[]; // color palette + int noOfEntries; + + byte[] byteData; // Unpacked data + int[] intData; // Unpacked data + boolean topDown; + + + private int readInt() throws IOException { + int b1 = is.read(); + int b2 = is.read(); + int b3 = is.read(); + int b4 = is.read(); + curPos += 4; + return ((b4 << 24) + (b3 << 16) + (b2 << 8) + (b1 << 0)); + } + + + private short readShort() throws IOException { + int b1 = is.read(); + int b2 = is.read(); + curPos += 2; + return (short)((b2 << 8) + b1); + } + + + void getFileHeader() throws IOException, Exception { + // Actual contents (14 bytes): + short fileType = 0x4d42;// always "BM" + int fileSize; // size of file in bytes + short reserved1 = 0; // always 0 + short reserved2 = 0; // always 0 + + fileType = readShort(); + if (fileType != 0x4d42) + throw new Exception("Not a BMP file"); // wrong file type + fileSize = readInt(); + reserved1 = readShort(); + reserved2 = readShort(); + bitmapOffset = readInt(); + } + + void getBitmapHeader() throws IOException { + + // Actual contents (40 bytes): + int size; // size of this header in bytes + short planes; // no. of color planes: always 1 + int sizeOfBitmap; // size of bitmap in bytes (may be 0: if so, calculate) + int horzResolution; // horizontal resolution, pixels/meter (may be 0) + int vertResolution; // vertical resolution, pixels/meter (may be 0) + int colorsUsed; // no. of colors in palette (if 0, calculate) + int colorsImportant; // no. of important colors (appear first in palette) (0 means all are important) + int noOfPixels; + + size = readInt(); + width = readInt(); + height = readInt(); + planes = readShort(); + bitsPerPixel = readShort(); + compression = readInt(); + sizeOfBitmap = readInt(); + horzResolution = readInt(); + vertResolution = readInt(); + colorsUsed = readInt(); + colorsImportant = readInt(); + if (bitsPerPixel==24) + colorsUsed = colorsImportant = 0; + + topDown = (height < 0); + if (topDown) height = -height; + noOfPixels = width * height; + + // Scan line is padded with zeroes to be a multiple of four bytes + scanLineSize = ((width * bitsPerPixel + 31) / 32) * 4; + + actualSizeOfBitmap = scanLineSize * height; + + if (colorsUsed != 0) + actualColorsUsed = colorsUsed; + else + // a value of 0 means we determine this based on the bits per pixel + if (bitsPerPixel < 16) + actualColorsUsed = 1 << bitsPerPixel; + else + actualColorsUsed = 0; // no palette + /* + if (IJ.debugMode) { + IJ.log("BMP_Reader"); + IJ.log(" width: "+width); + IJ.log(" height: "+height); + IJ.log(" compression: "+compression); + IJ.log(" scanLineSize: "+scanLineSize); + IJ.log(" planes: "+planes); + IJ.log(" bitsPerPixel: "+bitsPerPixel); + IJ.log(" sizeOfBitmap: "+sizeOfBitmap); + IJ.log(" horzResolution: "+horzResolution); + IJ.log(" vertResolution: "+vertResolution); + IJ.log(" colorsUsed: "+colorsUsed); + IJ.log(" colorsImportant: "+colorsImportant); + } + */ + } + + void getPalette() throws IOException { + noOfEntries = actualColorsUsed; + if (noOfEntries>0) { + r = new byte[noOfEntries]; + g = new byte[noOfEntries]; + b = new byte[noOfEntries]; + + int reserved; + for (int i = 0; i < noOfEntries; i++) { + b[i] = (byte)is.read(); + g[i] = (byte)is.read(); + r[i] = (byte)is.read(); + reserved = is.read(); + curPos += 4; + } + } + } + + void unpack(byte[] rawData, int rawOffset, int bpp, byte[] byteData, int byteOffset, int w) throws Exception { + int j = byteOffset; + int k = rawOffset; + byte mask; + int pixPerByte; + + switch (bpp) { + case 1: mask = (byte)0x01; pixPerByte = 8; break; + case 4: mask = (byte)0x0f; pixPerByte = 2; break; + case 8: mask = (byte)0xff; pixPerByte = 1; break; + default: + throw new Exception("Unsupported bits-per-pixel value: " + bpp); + } + + for (int i = 0;;) { + int shift = 8 - bpp; + for (int ii = 0; ii < pixPerByte; ii++) { + byte br = rawData[k]; + br >>= shift; + byteData[j] = (byte)(br & mask); + //System.out.println("Setting byteData[" + j + "]=" + Test.byteToHex(byteData[j])); + j++; + i++; + if (i == w) return; + shift -= bpp; + } + k++; + } + } + + void unpack24(byte[] rawData, int rawOffset, int[] intData, int intOffset, int w) { + int j = intOffset; + int k = rawOffset; + int mask = 0xff; + for (int i = 0; i < w; i++) { + int b0 = (((int)(rawData[k++])) & mask); + int b1 = (((int)(rawData[k++])) & mask) << 8; + int b2 = (((int)(rawData[k++])) & mask) << 16; + intData[j] = 0xff000000 | b0 | b1 | b2; + j++; + } + } + + void unpack32(byte[] rawData, int rawOffset, int[] intData, int intOffset, int w) { + int j = intOffset; + int k = rawOffset; + int mask = 0xff; + for (int i = 0; i < w; i++) { + int b0 = (((int)(rawData[k++])) & mask); + int b1 = (((int)(rawData[k++])) & mask) << 8; + int b2 = (((int)(rawData[k++])) & mask) << 16; + int b3 = (((int)(rawData[k++])) & mask) << 24; // this gets ignored! + intData[j] = 0xff000000 | b0 | b1 | b2; + j++; + } + } + + void getPixelData() throws IOException, Exception { + byte[] rawData; // the raw unpacked data + + // Skip to the start of the bitmap data (if we are not already there) + long skip = bitmapOffset - curPos; + if (skip > 0) { + is.skip(skip); + curPos += skip; + } + + int len = scanLineSize; + if (bitsPerPixel > 8) + intData = new int[width * height]; + else + byteData = new byte[width * height]; + rawData = new byte[actualSizeOfBitmap]; + int rawOffset = 0; + int offset = (height - 1) * width; + for (int i = height - 1; i >= 0; i--) { + int n = is.read(rawData, rawOffset, len); + if (n < len) throw new Exception("Scan line ended prematurely after " + n + " bytes"); + if (bitsPerPixel==24) + unpack24(rawData, rawOffset, intData, offset, width); + else if (bitsPerPixel==32) + unpack32( rawData, rawOffset, intData, offset, width); + else // 8-bits or less + unpack(rawData, rawOffset, bitsPerPixel, byteData, offset, width); + rawOffset += len; + offset -= width; + } + } + + + public void read(InputStream is) throws IOException, Exception { + this.is = is; + getFileHeader(); + getBitmapHeader(); + if (compression!=0) + throw new Exception("Compression not supported"); + getPalette(); + getPixelData(); + } + + + public MemoryImageSource makeImageSource() { + ColorModel cm; + MemoryImageSource mis; + + if (noOfEntries>0 && bitsPerPixel!=24) { + // There is a color palette; create an IndexColorModel + cm = new IndexColorModel(bitsPerPixel, noOfEntries, r, g, b); + } else { + // There is no palette; use the default RGB color model + cm = ColorModel.getRGBdefault(); + } + + // Create MemoryImageSource + + if (bitsPerPixel > 8) { + // use one int per pixel + mis = new MemoryImageSource(width, + height, cm, intData, 0, width); + } else { + // use one byte per pixel + mis = new MemoryImageSource(width, + height, cm, byteData, 0, width); + } + + return mis; // this can be used by Component.createImage() + } +} diff --git a/src/ij/plugin/BMP_Writer.java b/src/ij/plugin/BMP_Writer.java new file mode 100644 index 0000000..a1e8006 --- /dev/null +++ b/src/ij/plugin/BMP_Writer.java @@ -0,0 +1,247 @@ +package ij.plugin; +import ij.*; +import ij.io.*; +import ij.process.*; +import java.awt.*; +import java.io.*; +import java.awt.image.*; + +/** Implements the File/Save As/BMP command. Based on BMPFile class from + http://www.javaworld.com/javaworld/javatips/jw-javatip60-p2.html */ + +public class BMP_Writer implements PlugIn { + //--- Private constants + private final static int BITMAPFILEHEADER_SIZE = 14; + private final static int BITMAPINFOHEADER_SIZE = 40; + //--- Private variable declaration + //--- Bitmap file header + private byte bitmapFileHeader [] = new byte [14]; + private byte bfType [] = {(byte)'B', (byte)'M'}; + private int bfSize = 0; + private int bfReserved1 = 0; + private int bfReserved2 = 0; + private int bfOffBits = BITMAPFILEHEADER_SIZE + BITMAPINFOHEADER_SIZE; + //--- Bitmap info header + private byte bitmapInfoHeader [] = new byte [40]; + private int biSize = BITMAPINFOHEADER_SIZE; + private int biWidth = 0; + private int padWidth = 0; + private int biHeight = 0; + private int biPlanes = 1; + private int biBitCount = 24; + private int biCompression = 0; + private int biSizeImage = 0; + private int biXPelsPerMeter = 0x0; + private int biYPelsPerMeter = 0x0; + private int biClrUsed = 0; + private int biClrImportant = 0; + //--- Bitmap raw data + private int intBitmap []; + private byte byteBitmap []; + //--- File section + private FileOutputStream fo; + private BufferedOutputStream bfo; + ImagePlus imp; + + public void run(String path) { + IJ.showProgress(0); + imp = WindowManager.getCurrentImage(); + if (imp==null) + {IJ.noImage(); return;} + try { + writeImage(imp, path); + } catch (Exception e) { + String msg = e.getMessage(); + if (msg==null || msg.equals("")) + msg = ""+e; + IJ.error("BMP Writer", "An error occured writing the file.\n \n" + msg); + } + IJ.showProgress(1); + IJ.showStatus(""); + } + + void writeImage(ImagePlus imp, String path) throws Exception { + if(imp.getBitDepth()==24) + biBitCount = 24; + else { + biBitCount = 8; + LookUpTable lut = imp.createLut(); + biClrUsed=lut.getMapSize(); // 8 bit color image may use less + bfOffBits+=biClrUsed*4; + } + if (path==null || path.equals("")) { + String prompt = "Save as " + biBitCount + " bit BMP"; + SaveDialog sd = new SaveDialog(prompt, imp.getTitle(), ".bmp"); + if(sd.getFileName()==null) + return; + path = sd.getDirectory()+sd.getFileName(); + } + imp.startTiming(); + saveBitmap (path, imp.getImage(), imp.getWidth(), imp.getHeight() ); + } + + + public void saveBitmap (String parFilename, Image parImage, int parWidth, int parHeight) throws Exception { + fo = new FileOutputStream (parFilename); + bfo = new BufferedOutputStream(fo); + save (parImage, parWidth, parHeight); + bfo.close(); + fo.close (); + } + + /* + * The saveMethod is the main method of the process. This method + * will call the convertImage method to convert the memory image to + * a byte array; method writeBitmapFileHeader creates and writes + * the bitmap file header; writeBitmapInfoHeader creates the + * information header; and writeBitmap writes the image. + * + */ + private void save (Image parImage, int parWidth, int parHeight) throws Exception { + convertImage (parImage, parWidth, parHeight); + writeBitmapFileHeader (); + writeBitmapInfoHeader (); + if(biBitCount == 8) + writeBitmapPalette (); + writeBitmap (); + } + + private void writeBitmapPalette() throws Exception { + LookUpTable lut = imp.createLut(); + byte[] g = lut.getGreens(); + byte[] r = lut.getReds(); + byte[] b = lut.getBlues(); + for(int i = 0;i0; row--) { + if (row%20==0) + IJ.showProgress((double)(biHeight-row)/biHeight); + for(int col = 0; col> 8) & 0xFF); + rgb [2] = (byte) ((value >> 16) & 0xFF); + bfo.write(rgb); + } else + bfo.write(byteBitmap [(row-1)*biWidth + col ]); + ++counter; + } + for (i = 1; i <= pad; i++) + bfo.write (0x00); + counter += pad; + } + } + + + /* + * writeBitmapFileHeader writes the bitmap file header to the file. + * + */ + private void writeBitmapFileHeader() throws Exception { + fo.write (bfType); + // calculate bfSize + bfSize = bfOffBits+padWidth*biHeight; + fo.write (intToDWord (bfSize)); + fo.write (intToWord (bfReserved1)); + fo.write (intToWord (bfReserved2)); + fo.write (intToDWord (bfOffBits)); + } + + /* + * + * writeBitmapInfoHeader writes the bitmap information header + * to the file. + * + */ + private void writeBitmapInfoHeader () throws Exception { + fo.write (intToDWord (biSize)); + fo.write (intToDWord (biWidth)); + fo.write (intToDWord (biHeight)); + fo.write (intToWord (biPlanes)); + fo.write (intToWord (biBitCount)); + fo.write (intToDWord (biCompression)); + fo.write (intToDWord (biSizeImage)); + fo.write (intToDWord (biXPelsPerMeter)); + fo.write (intToDWord (biYPelsPerMeter)); + fo.write (intToDWord (biClrUsed)); + fo.write (intToDWord (biClrImportant)); + } + + /* + * + * intToWord converts an int to a word, where the return + * value is stored in a 2-byte array. + * + */ + private byte [] intToWord (int parValue) { + byte retValue [] = new byte [2]; + retValue [0] = (byte) (parValue & 0x00FF); + retValue [1] = (byte) ((parValue >> 8) & 0x00FF); + return (retValue); + } + + /* + * + * intToDWord converts an int to a double word, where the return + * value is stored in a 4-byte array. + * + */ + private byte [] intToDWord (int parValue) { + byte retValue [] = new byte [4]; + retValue [0] = (byte) (parValue & 0x00FF); + retValue [1] = (byte) ((parValue >> 8) & 0x000000FF); + retValue [2] = (byte) ((parValue >> 16) & 0x000000FF); + retValue [3] = (byte) ((parValue >> 24) & 0x000000FF); + return (retValue); + } +} diff --git a/src/ij/plugin/BatchConverter.java b/src/ij/plugin/BatchConverter.java new file mode 100644 index 0000000..03ec94b --- /dev/null +++ b/src/ij/plugin/BatchConverter.java @@ -0,0 +1,154 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.util.Tools; +import ij.io.*; +import java.awt.*; +import java.awt.event.*; +import java.io.*; + +/** This plugin implements the File/ /Convert command, + which converts the images in a folder to a specified format. */ + public class BatchConverter implements PlugIn, ActionListener { + private static final String[] formats = {"TIFF", "8-bit TIFF", "JPEG", "GIF", "PNG", "PGM", "BMP", "FITS", "Text Image", "ZIP", "Raw"}; + private static String format = formats[0]; + private static double scale = 1.0; + private static boolean useBioFormats; + private static int interpolationMethod = ImageProcessor.BILINEAR; + private static boolean averageWhenDownSizing; + private String[] methods = ImageProcessor.getInterpolationMethods(); + private Button input, output; + private TextField inputDir, outputDir; + private GenericDialog gd; + + public void run(String arg) { + if (!showDialog()) + return; + String inputPath = inputDir.getText(); + if (inputPath.equals("")) { + IJ.error("Batch Converter", "Please choose an input folder"); + return; + } + String outputPath = outputDir.getText(); + if (outputPath.equals("")) { + IJ.error("Batch Converter", "Please choose an output folder"); + return; + } + File f1 = new File(inputPath); + if (!f1.exists() || !f1.isDirectory()) { + IJ.error("Batch Converter", "Input does not exist or is not a folder\n \n"+inputPath); + return; + } + File f2 = new File(outputPath); + if (!outputPath.equals("") && (!f2.exists() || !f2.isDirectory())) { + IJ.error("Batch Converter", "Output does not exist or is not a folder\n \n"+outputPath); + return; + } + String[] list = (new File(inputPath)).list(); + IJ.resetEscape(); + Opener opener = new Opener(); + opener.setSilentMode(true); + long t0 = System.currentTimeMillis(); + for (int i=0; iProcess>Batch>Virtual Stack" + +"" + +"This command runs macro code on each image in a virtual stack.
" + +"The processed images are saved in the Output folder,
" + +"in the specified Format, allowing them to be opened as a
" + +"virtual stack. Make sure the Output folder is empty
" + +"before clicking on Process.
" + +"
" + +"In the macro code, the 'i' (slice index) and 'n' (stack size) variables
" + +"are predefined. Call setOption('SaveBatchOutput',false) to
" + +"prevent the image currently being processed from being saved,
" + +"effectively removing it from the output virtual stack.

" + +"
"; + + private String macro = ""; + private int testImage; + private Button input, output, open, save, test; + private TextField inputDir, outputDir; + private GenericDialog gd; + private Thread thread; + private ImagePlus virtualStack; + private ImagePlus outputImage; + private boolean errorDisplayed; + private String filter; + private static boolean saveOutput = true; + + public void run(String arg) { + if (arg.equals("stack")) { + virtualStack = IJ.getImage(); + if (virtualStack.getStackSize()==1) { + error("This command requires a stack or virtual stack."); + return; + } + } + String macroPath = IJ.getDirectory("macros")+MACRO_FILE_NAME; + macro = IJ.openAsString(macroPath); + if (macro==null || macro.startsWith("Error: ")) { + IJ.showStatus(macro.substring(7) + ": "+macroPath); + macro = ""; + } + if (!showDialog()) return; + String inputPath = null; + if (virtualStack==null) { + inputPath = inputDir.getText(); + if (inputPath.equals("")) { + error("Please choose an input folder"); + return; + } + inputPath = addSeparator(inputPath); + File f1 = new File(inputPath); + if (!f1.exists() || !f1.isDirectory()) { + error("Input does not exist or is not a folder\n \n"+inputPath); + return; + } + } + String outputPath = outputDir.getText(); + outputPath = addSeparator(outputPath); + File f2 = new File(outputPath); + if (!outputPath.equals("") && (!f2.exists() || !f2.isDirectory())) { + error("Output does not exist or is not a folder\n \n"+outputPath); + return; + } + if (macro.equals("")) { + error("There is no macro code in the text area"); + return; + } + ImageJ ij = IJ.getInstance(); + if (ij!=null) ij.getProgressBar().setBatchMode(true); + IJ.resetEscape(); + if (virtualStack!=null) + processVirtualStack(outputPath); + else + processFolder(inputPath, outputPath); + IJ.showProgress(1,1); + if (virtualStack==null) + Prefs.set("batch.input", inputDir.getText()); + Prefs.set("batch.output", outputDir.getText()); + Prefs.set("batch.format", format); + macro = gd.getTextArea1().getText(); + if (!macro.equals("")) + IJ.saveString(macro, IJ.getDirectory("macros")+MACRO_FILE_NAME); + } + + boolean showDialog() { + validateFormat(); + gd = GUI.newNonBlockingDialog("Batch Process"); + addPanels(gd); + gd.setInsets(15, 0, 5); + gd.addChoice("Output_format:", formats, format); + gd.setInsets(0, 0, 5); + gd.addChoice("Add macro code:", code, code[0]); + if (virtualStack==null) + gd.addStringField("File name contains:", "", 10); + gd.setInsets(15, 10, 0); + Dimension screen = IJ.getScreenSize(); + gd.addTextAreas(macro, null, screen.width<=600?10:15, 60); + addButtons(gd); + gd.setOKLabel("Process"); + Vector choices = gd.getChoices(); + Choice choice = (Choice)choices.elementAt(1); + if (virtualStack!=null) + gd.addHelp(help); + choice.addItemListener(this); + gd.showDialog(); + format = gd.getNextChoice(); + if (virtualStack==null) + filter = gd.getNextString(); + macro = gd.getNextText(); + return !gd.wasCanceled(); + } + + void processVirtualStack(String outputPath) { + ImageStack stack = virtualStack.getStack(); + int n = stack.size(); + int index = 0; + for (int i=1; i<=n; i++) { + if (IJ.escapePressed()) break; + IJ.showProgress(i, n); + ImageProcessor ip = stack.getProcessor(i); + if (ip==null) return; + ImagePlus imp = new ImagePlus(i+"/"+stack.size(), ip); + if (!macro.equals("")) { + if (!runMacro("i="+(index++)+";"+"n="+stack.size()+";"+macro, imp)) + break; + } + if (saveOutput && !outputPath.equals("")) { + if (format.equals("8-bit TIFF") || format.equals("GIF")) { + if (imp.getBitDepth()==24) + IJ.run(imp, "8-bit Color", "number=256"); + else + IJ.run(imp, "8-bit", ""); + } + IJ.saveAs(imp, format, outputPath+pad(i)); + } + saveOutput = true; + imp.close(); + } + if (outputPath!=null && !outputPath.equals("")) + IJ.run("Image Sequence...", "open=[" + outputPath + "]"+" use"); + } + + String pad(int n) { + String str = ""+n; + while (str.length()<5) + str = "0" + str; + return str; + } + + + void processFolder(String inputPath, String outputPath) { + String[] list = (new File(inputPath)).list(); + list = FolderOpener.getFilteredList(list, filter, "Batch Processor"); + if (list==null) + return; + int index = 0; + int startingCount = WindowManager.getImageCount(); + for (int i=0; istartingCount) + imp = WindowManager.getCurrentImage(); + if (imp==null) + imp = Opener.openUsingBioFormats(path); + if (imp==null) { + IJ.log("openImage() and openUsingBioFormats() returned null: "+path); + continue; + } + if (!macro.equals("")) { + outputImage = null; + if (!runMacro("i="+(index++)+";"+macro, imp)) + break; + } + if (saveOutput && !outputPath.equals("")) { + if (format.equals("8-bit TIFF") || format.equals("GIF")) { + if (imp.getBitDepth()==24) + IJ.run(imp, "8-bit Color", "number=256"); + else + IJ.run(imp, "8-bit", ""); + } + if (outputImage!=null && outputImage!=imp) + IJ.saveAs(outputImage, format, outputPath+list[i]); + else + IJ.saveAs(imp, format, outputPath+list[i]); + } + saveOutput = true; + imp.close(); + } + } + + private boolean runMacro(String macro, ImagePlus imp) { + WindowManager.setTempCurrentImage(imp); + Interpreter interp = new Interpreter(); + try { + outputImage = interp.runBatchMacro(macro, imp); + } catch(Throwable e) { + interp.abortMacro(); + String msg = e.getMessage(); + if (!(e instanceof RuntimeException && msg!=null && e.getMessage().equals(Macro.MACRO_CANCELED))) + IJ.handleException(e); + return false; + } finally { + WindowManager.setTempCurrentImage(null); + } + return true; + } + + String addSeparator(String path) { + if (path.equals("")) return path; + if (!(path.endsWith("/")||path.endsWith("\\"))) + path = path + File.separator; + return path; + } + + void validateFormat() { + boolean validFormat = false; + for (int i=0; i 0) + sb.append(b,0, n); + macro = sb.toString(); + } + catch (IOException e) { + return null; + } + return macro; + } + + public void actionPerformed(ActionEvent e) { + Object source = e.getSource(); + if (source==input) { + String path = IJ.getDirectory("Input Folder"); + if (path==null) return; + inputDir.setText(path); + } else if (source==output) { + String path = IJ.getDirectory("Output Folder"); + if (path==null) return; + outputDir.setText(path); + } else if (source==test) { + thread = new Thread(this, "BatchTest"); + thread.setPriority(Math.max(thread.getPriority()-2, Thread.MIN_PRIORITY)); + thread.start(); + } else if (source==open) + open(); + else if (source==save) + save(); + } + + void open() { + String text = IJ.openAsString(""); + if (text==null) return; + if (text.startsWith("Error: ")) + error(text.substring(7)); + else { + if (text.length()>30000) + error("File is too large"); + else + gd.getTextArea1().setText(text); + } + } + + void save() { + macro = gd.getTextArea1().getText(); + if (!macro.equals("")) + IJ.saveString(macro, ""); + } + + void error(String msg) { + IJ.error("Batch Processor", msg); + } + + public void run() { + TextArea ta = gd.getTextArea1(); + //ta.selectAll(); + String macro = ta.getText(); + if (macro.equals("")) { + error("There is no macro code in the text area"); + return; + } + ImagePlus imp = null; + IJ.redirectErrorMessages(true); + if (virtualStack!=null) + imp = getVirtualStackImage(); + else + imp = getFolderImage(); + IJ.redirectErrorMessages(false); + if (imp==null) { + if (!errorDisplayed) + IJ.log("IJ.openImage() returned null"); + return; + } + runMacro("i=0;"+macro, imp); + Point loc = new Point(10, 30); + if (testImage!=0) { + ImagePlus imp2 = WindowManager.getImage(testImage); + if (imp2!=null) { + ImageWindow win = imp2.getWindow(); + if (win!=null) loc = win.getLocation(); + imp2.changes=false; + imp2.close(); + } + } + imp.show(); + ImageWindow iw = imp.getWindow(); + if (iw!=null) iw.setLocation(loc); + testImage = imp.getID(); + } + + ImagePlus getVirtualStackImage() { + ImagePlus imp = virtualStack.createImagePlus(); + imp.setProcessor("", virtualStack.getProcessor().duplicate()); + return imp; + } + + ImagePlus getFolderImage() { + String inputPath = inputDir.getText(); + inputPath = addSeparator(inputPath); + File f1 = new File(inputPath); + if (!f1.exists() || !f1.isDirectory()) { + error("Input does not exist or is not a folder\n \n"+inputPath); + errorDisplayed = true; + return null; + } + String[] list = (new File(inputPath)).list(); + String name = list[0]; + if (name.startsWith(".")&&list.length>1) name = list[1]; + String path = inputPath + name; + setDirAndName(path); + return IJ.openImage(path); + } + + void setDirAndName(String path) { + File f = new File(path); + OpenDialog.setLastDirectory(f.getParent()+File.separator); + OpenDialog.setLastName(f.getName()); + } + + public static void saveOutput(boolean b) { + saveOutput = b; + } + +} diff --git a/src/ij/plugin/Benchmark.java b/src/ij/plugin/Benchmark.java new file mode 100644 index 0000000..9ee4d40 --- /dev/null +++ b/src/ij/plugin/Benchmark.java @@ -0,0 +1,127 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.ResultsTable; +import ij.util.Tools; + +/** Implements the Plugins/Utilities/Run Benchmark command. + * Suppresses subordinate status bar messages by using + * IJ.showStatus("!"+"rest of messager") and displays + * subordinate progress bars as dots by using + * IJ.showProgress(-currentIndex,finalIndex). +*/ +public class Benchmark implements PlugIn { + private String[] results = { + "10.9|MacBook Air (M1, 2020, Native)", + "17.2|iMac Pro (2017)", + "18.1|MacBook Air (M1, 2020, Rosetta)", + "22.8|Dell T7920 (Dual Xeon, 282GB RAM, 2018)", + "24.7|27\" iMac (Early 2015)", + "29.7|13\" MacBook Pro (Late 2015)", + "29.7|15\" MacBook Pro (Early 2013)", + "62.3|Acer Aspire laptop (Core i5, 2014)" + }; + private int size = 5000; + private int ops = 62; + private int counter; + + public void run(String arg) { + ImagePlus cImp = WindowManager.getCurrentImage(); + if (cImp!=null && cImp.getWidth()==512 && cImp.getHeight()==512 && cImp.getBitDepth()==24) { + IJ.runPlugIn(cImp, "ij.plugin.filter.Benchmark", ""); + return; + } + IJ.showStatus("Creating "+size+"x"+size+" 16-bit image"); + long t0 = System.currentTimeMillis(); + ImageProcessor.setRandomSeed(12345); + ImagePlus imp = IJ.createImage("Untitled", "16-bit noise", size, size, 1); + ImageProcessor.setRandomSeed(Double.NaN); + imp.copy(); + for (int i=0; i<3; i++) + analyzeParticles(imp); + for (int i=0; i<3; i++) { + IJ.run(imp, "Median...", "radius=2"); + showProgress("Median"); + } + for (int i=0; i<12; i++) { + IJ.run(imp, "Unsharp Mask...", "radius=1 mask=0.60"); + showProgress("Unsharp Mask"); + } + ImageProcessor ip = imp.getProcessor(); + ip.snapshot(); + for (int i=0; i<12; i++) { + ip.blurGaussian(40); + showProgress("Gaussian blur"); + } + ip.reset(); + for (int i=0; i<360; i+=20) { + ip.reset(); + ip.rotate(i); + showProgress("Rotate"); + } + double scale = 1.2; + for (int i=0; i<14; i++) { + ip.reset(); + ip.scale(scale, scale); + showProgress("Scale"); + scale = scale*1.2; + } + double time = (System.currentTimeMillis()-t0)/1000.0; + ResultsTable rt = new ResultsTable(); + rt.showRowNumbers(true); + for (int i=0; i1?"S":"")+")>>"; + rt.addValue("Computer", "<1?" ("+msg+")":""; + IJ.showStatus("!"+counter + "/"+ops+msg); + IJ.showProgress(-counter, ops); + } + + void error(String msg) { + IJ.log("Benchmark: "+msg+" error"); + } +} + + diff --git a/src/ij/plugin/Binner.java b/src/ij/plugin/Binner.java new file mode 100644 index 0000000..2549a66 --- /dev/null +++ b/src/ij/plugin/Binner.java @@ -0,0 +1,286 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.Calibration; +import java.awt.*; +import java.awt.image.*; + +/** This plugin implements the Image/Transform/Bin command. + * It reduces the size of an image or stack by binning groups of + * pixels of user-specified sizes. The resulting pixel can be + * calculated as average, median, maximum or minimum. + * + * @author Nico Stuurman + * @author Wayne Rasband + */ +public class Binner implements PlugIn { + public static int AVERAGE=0, MEDIAN=1, MIN=2, MAX=3, SUM=4; + private static String[] methods = {"Average", "Median", "Min", "Max", "Sum"}; + private int xshrink=2, yshrink=2, zshrink=1; + private int method = AVERAGE; + private float maxValue; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + if (!showDialog(imp)) + return; + if (imp.getStackSize()==1) + Undo.setup(Undo.TYPE_CONVERSION, imp); + imp.startTiming(); + ImagePlus imp2 = shrink(imp, xshrink, yshrink, zshrink, method); + IJ.showTime(imp, imp.getStartTime(), "", imp.getStackSize()); + imp.setStack(imp2.getStack()); + imp.setCalibration(imp2.getCalibration()); + if (zshrink>1) + imp.setSlice(1); + } + + public ImagePlus shrink(ImagePlus imp, int xshrink, int yshrink, int zshrink, int method) { + this.xshrink = xshrink; + this.yshrink = yshrink; + int w = imp.getWidth()/xshrink; + int h = imp.getHeight()/yshrink; + ColorModel cm=imp.createLut().getColorModel(); + ImageStack stack=imp.getStack(); + ImageStack stack2 = new ImageStack (w, h, cm); + int d = stack.size(); + if (method==SUM) { + int bitDepth = imp.getBitDepth(); + if (bitDepth==8) + maxValue = 255; + else if (bitDepth==16) + maxValue = 65535; + else + maxValue = 0; + } + for (int z=1; z<=d; z++) { + IJ.showProgress(z, d); + ImageProcessor ip = stack.getProcessor(z); + if (ip.isInvertedLut()) + ip.invert(); + ImageProcessor ip2 = shrink(ip, method); + if (ip.isInvertedLut()) ip2.invert(); + stack2.addSlice(stack.getSliceLabel(z), ip2); + } + if (zshrink>1 && !imp.isHyperStack()) + stack2 = shrinkZ(stack2, zshrink); + ImagePlus imp2 = imp.createImagePlus(); + imp2.setStack("Reduced "+imp.getShortTitle(), stack2); + Calibration cal2 = imp2.getCalibration(); + if (cal2.scaled()) { + cal2.pixelWidth *= xshrink; + cal2.pixelHeight *= yshrink; + cal2.pixelDepth *= zshrink; + } + //if (zshrink>1 && imp.isHyperStack()) + // imp2 = shrinkHyperstackZ(imp2, zshrink); + imp2.setOpenAsHyperStack(imp.isHyperStack()); + if (method==SUM && imp2.getBitDepth()>8) { + ImageProcessor ip = imp2.getProcessor(); + ip.setMinAndMax(ip.getMin(), ip.getMax()*xshrink*yshrink*zshrink); + } + return imp2; + } + + private ImageStack shrinkZ(ImageStack stack, int zshrink) { + int w = stack.getWidth(); + int h = stack.getHeight(); + int d = stack.size(); + int d2 = d/zshrink; + ImageStack stack2 = new ImageStack (w, h, stack.getColorModel()); + for (int z=1; z<=d2; z++) + stack2.addSlice(stack.getProcessor(z).duplicate()); + boolean rgb = stack.getBitDepth()==24; + ImageProcessor ip = rgb?new ColorProcessor(d, h):new FloatProcessor(d, h); + for (int x=0; xmethods.length) + method = AVERAGE; + int w = ip.getWidth()/xshrink; + int h = ip.getHeight()/yshrink; + ImageProcessor ip2 = ip.createProcessor(w, h); + if (ip instanceof ColorProcessor) + return shrinkRGB((ColorProcessor)ip, (ColorProcessor)ip2, method); + for (int y=0; ymax) { + max = pixels[j]; + mj = j; + } + } + pixels[mj] = 0; + } + float max = -Float.MAX_VALUE; + for (int j=0; jmax) + max = pixels[j]; + } + return max; + } + + private float getMin(ImageProcessor ip, int x, int y) { + float min = Float.MAX_VALUE; + float pixel; + for (int y2=0; y2max) + max = pixel; + } + } + return max; + } + + private float getSum(ImageProcessor ip, int x, int y) { + float sum = 0; + for (int y2=0; y20f && sum>maxValue) + sum = maxValue; + return sum; + } + + private boolean showDialog(ImagePlus imp) { + boolean stack = imp.getStackSize()>1; + if (imp.isComposite() && imp.getNChannels()==imp.getStackSize()) + stack = false; + GenericDialog gd = new GenericDialog("Image Shrink"); + gd.addNumericField("X shrink factor:", xshrink, 0); + gd.addNumericField("Y shrink factor:", yshrink, 0); + if (stack) + gd.addNumericField("Z shrink factor:", zshrink, 0); + if (method>methods.length) + method = 0; + gd.addChoice ("Bin Method: ", methods, methods[method]); + if (imp.getStackSize()==1) { + gd.setInsets(5, 0, 0); + gd.addMessage("This command supports Undo", null, Color.darkGray); + } + gd.showDialog(); + if (gd.wasCanceled()) + return false; + xshrink = (int) gd.getNextNumber(); + yshrink = (int) gd.getNextNumber(); + if (stack) + zshrink = (int) gd.getNextNumber(); + method = gd.getNextChoiceIndex(); + return true; + } + +} diff --git a/src/ij/plugin/BrowserLauncher.java b/src/ij/plugin/BrowserLauncher.java new file mode 100644 index 0000000..a47c93d --- /dev/null +++ b/src/ij/plugin/BrowserLauncher.java @@ -0,0 +1,128 @@ +package ij.plugin; +import ij.IJ; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * This plugin implements the File/Import/URL command and the commands in the Help menu that + * open web pages. It is based on Eric Albert's cross-platform (Windows, Mac OS X and Unix) + * BrowserLauncher class. + *

+ * BrowserLauncher is a class that provides one static method, openURL, which opens the default + * web browser for the current user of the system to the given URL. It may support other + * protocols depending on the system -- mailto, ftp, etc. -- but that has not been rigorously + * tested and is not guaranteed to work. + *

+ * Yes, this is platform-specific code, and yes, it may rely on classes on certain platforms + * that are not part of the standard JDK. What we're trying to do, though, is to take something + * that's frequently desirable but inherently platform-specific -- opening a default browser -- + * and allow programmers (you, for example) to do so without worrying about dropping into native + * code or doing anything else similarly evil. + *

+ * Anyway, this code is completely in Java and will run on all JDK 1.1-compliant systems without + * modification or a need for additional libraries. All classes that are required on certain + * platforms to allow this to run are dynamically loaded at runtime via reflection and, if not + * found, will not cause this to do anything other than returning an error when opening the + * browser. + *

+ * This code is Copyright 1999-2001 by Eric Albert (ejalbert@cs.stanford.edu) and may be + * redistributed or modified in any form without restrictions as long as the portion of this + * comment from this paragraph through the end of the comment is not removed. The author + * requests that he be notified of any application, applet, or other binary that makes use of + * this code, but that's more out of curiosity than anything and is not required. This software + * includes no warranty. The author is not repsonsible for any loss of data or functionality + * or any adverse or unexpected effects of using this software. + *

+ * Credits: + *
Steven Spencer, JavaWorld magazine (
Java Tip 66) + *
Thanks also to Ron B. Yeh, Eric Shapiro, Ben Engber, Paul Teitlebaum, Andrea Cantatore, + * Larry Barowski, Trevor Bedzek, Frank Miedrich, and Ron Rabakukk + * + * @author Eric Albert (ejalbert@cs.stanford.edu) + * @version 1.4b1 (Released June 20, 2001) + */ +public class BrowserLauncher implements PlugIn { + /** The com.apple.mrj.MRJFileUtils class */ + private static Class mrjFileUtilsClass; + /** The openURL method of com.apple.mrj.MRJFileUtils */ + private static Method openURL; + private static boolean error; + + + /** Opens the specified URL (default is the ImageJ home page). */ + public void run(String theURL) { + if (error) return; + if (theURL==null || theURL.equals("")) + theURL = IJ.URL; + try {openURL(theURL);} + catch (IOException e) {} + } + + /** Opens the specified URL in the default browser. + * Returns a message if there is an error. + * Call from a macro using:
+ * call("ij.plugin.BrowserLauncher.open",url); + */ + public static String open(String url) { + try { + openURL(url); + } catch (IOException e) { + return e.getMessage(); + } + return ""; + } + + /** + * Attempts to open the default web browser to the given URL. + * @param url The URL to open + * @throws IOException If the web browser could not be located or does not run + */ + public static void openURL(String url) throws IOException { + String errorMessage = ""; + if (IJ.isMacOSX()) + IJ.runMacro("exec('open', getArgument())",url); + else if (IJ.isWindows()) { + String cmd = "rundll32 url.dll,FileProtocolHandler " + url; + if (System.getProperty("os.name").startsWith("Windows 2000")) + cmd = "rundll32 shell32.dll,ShellExec_RunDLL " + url; + Process process = Runtime.getRuntime().exec(cmd); + // This avoids a memory leak on some versions of Java on Windows. + // That's hinted at in . + try { + process.waitFor(); + process.exitValue(); + } catch (InterruptedException ie) { + throw new IOException("InterruptedException while launching browser: " + ie.getMessage()); + } + } else { + // Assume Linux or Unix + // Based on BareBonesBrowserLaunch (http://www.centerkey.com/java/browser/) + // The utility 'xdg-open' launches the URL in the user's preferred browser, + // therefore we try to use it first, before trying to discover other browsers. + String[] browsers = {"xdg-open", "netscape", "firefox", "konqueror", "mozilla", "opera", "epiphany", "lynx" }; + String browserName = null; + try { + for (int count=0; count=0; jj--) {//isolate CB components + Roi roi = overlaySep.get(jj); + if(roi.getName() == null || !roi.getName().equals(CALIBRATION_BAR)) + overlaySep.remove(roi); + } + Rectangle r = overlaySep.get(0).getBounds(); + overlaySep.translate(-r.x, -r.y); + ImagePlus impSep = IJ.createImage("CBar", "RGB", r.width, r.height, 1); + impSep.setOverlay(overlaySep); + impSep = impSep.flatten();//ignore the 'overlay' checkbox + impSep.setTitle("CBar"); + impSep.show(); + } + overlay.remove(CALIBRATION_BAR); + imp.draw(); + } + if(imp2 != null) + imp2.show(); + } + } + + private void updateColorBar() { + Roi roi = imp.getRoi(); + if (roi!=null && location.equals(locations[AT_SELECTION])) { + Rectangle r = roi.getBounds(); + drawBarAsOverlay(imp, r.x, r.y); + } else if ( location.equals(locations[UPPER_LEFT])) + drawBarAsOverlay(imp, insetPad, insetPad); + else if (location.equals(locations[UPPER_RIGHT])) { + calculateWidth(); + drawBarAsOverlay(imp, imp.getWidth()-insetPad-win_width, insetPad); + } else if (location.equals(locations[LOWER_LEFT]) ) + drawBarAsOverlay(imp, insetPad,imp.getHeight() - (int)(WIN_HEIGHT*zoom + 2*(int)(YMARGIN*zoom)) - (int)(insetPad*zoom)); + else if(location.equals(locations[LOWER_RIGHT])) { + calculateWidth(); + drawBarAsOverlay(imp, imp.getWidth()-win_width-insetPad, + imp.getHeight() - (int)(WIN_HEIGHT*zoom + 2*(int)(YMARGIN*zoom)) - insetPad); + } + else if ( location.equals(locations[SEPARATE_IMAGE])){ + drawBarAsOverlay(imp, insetPad, insetPad); + } + this.imp.updateAndDraw(); + } + + private boolean showDialog() { + gd = new LiveDialog("Calibration Bar"); + gd.addChoice("Location:", locations, location); + gd.addChoice("Fill color: ", colors, fillColor); + gd.addChoice("Label color: ", colors, textColor); + gd.addNumericField("Number of labels:", numLabels, 0); + gd.addNumericField("Decimal places:", decimalPlaces, 0); + gd.addNumericField("Font size:", fontSize, 0); + gd.addNumericField("Zoom factor:", zoom, 1); + String[] labels = {"Bold text", "Overlay", "Show unit"}; + boolean[] states = {boldText, !flatten, showUnit}; + gd.setInsets(10, 30, 0); + gd.addCheckboxGroup(2, 2, labels, states); + Checkbox overlayBox = (Checkbox)(gd.getCheckboxes().elementAt(1)); + if (location.equals(locations[SEPARATE_IMAGE])) + overlayBox.setEnabled(false); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + location = gd.getNextChoice(); + fillColor = gd.getNextChoice(); + textColor = gd.getNextChoice(); + numLabels = (int)gd.getNextNumber(); + decimalPlaces = (int)gd.getNextNumber(); + fontSize = (int)gd.getNextNumber(); + zoom = (double)gd.getNextNumber(); + boldText = gd.getNextBoolean(); + flatten = !gd.getNextBoolean(); + showUnit = gd.getNextBoolean(); + if (!IJ.isMacro()) { + sFlatten = flatten; + sFillColor = fillColor; + sTextColor = textColor; + sLocation = location; + sZoom = zoom; + sNumLabels = numLabels; + sFontSize = fontSize; + sDecimalPlaces = decimalPlaces; + sBoldText = boldText; + } + return true; + } + + private void drawBarAsOverlay(ImagePlus imp, int x, int y) { + Roi roi = imp.getRoi(); + if (roi!=null) + imp.deleteRoi(); + stats = imp.getStatistics(Measurements.MIN_MAX, nBins); + if (roi!=null) + imp.setRoi(roi); + histogram = stats.histogram; + cal = imp.getCalibration(); + Overlay overlay = imp.getOverlay(); + if (overlay==null) + overlay = new Overlay(); + else + overlay.remove(CALIBRATION_BAR); + int maxTextWidth = addText(null, 0, 0); + win_width = (int)(XMARGIN*zoom) + 5 + (int)(BAR_THICKNESS*zoom) + maxTextWidth + (int)((XMARGIN/2)*zoom); + if (x==-1 && y==-1) + return; // return if calculating width + Color c = getColor(fillColor); + if (c!=null) { + Roi r = new Roi(x, y, win_width, (int)(WIN_HEIGHT*zoom + 2*(int)(YMARGIN*zoom))); + r.setFillColor(c); + overlay.add(r, CALIBRATION_BAR); + } + int xOffset = x; + int yOffset = y; + if (decimalPlaces == -1) + decimalPlaces = Analyzer.getPrecision(); + x = (int)(XMARGIN*zoom) + xOffset; + y = (int)(YMARGIN*zoom) + yOffset; + addVerticalColorBar(overlay, x, y, (int)(BAR_THICKNESS*zoom), (int)(BAR_LENGTH*zoom) ); + addText(overlay, x + (int)(BAR_THICKNESS*zoom), y); + c = getColor(boxOutlineColor); + overlay.setIsCalibrationBar(true); + if (imp.getCompositeMode()>0) { + for (int i=0; i255) max = 255; + colors = max-min+1; + start = min; + } + for (int i = 0; i<(int)(BAR_LENGTH*zoom); i++) { + int iMap = start + (int)Math.round((i*colors)/(BAR_LENGTH*zoom)); + if (iMap>=mapSize) + iMap =mapSize - 1; + int j = (int)(BAR_LENGTH*zoom) - i - 1; + Line line = new Line(x, j+y, thickness+x, j+y); + line.setStrokeColor(new Color(rLUT[iMap]&0xff, gLUT[iMap]&0xff, bLUT[iMap]&0xff)); + line.setStrokeWidth(STROKE_WIDTH); + overlay.add(line, CALIBRATION_BAR); + } + + Color c = getColor(barOutlineColor); + if (c!=null) { + Roi r = new Roi(x, y, width, height); + r.setStrokeColor(c); + r.setStrokeWidth(1.0); + overlay.add(r, CALIBRATION_BAR); + } + } + + private int addText(Overlay overlay, int x, int y) { + + Color c = getColor(textColor); + if (c == null) + return 0; + double hmin = cal.getCValue(stats.histMin); + double hmax = cal.getCValue(stats.histMax); + double barStep = (double)(BAR_LENGTH*zoom) ; + if (numLabels > 2) + barStep /= (numLabels - 1); + + int fontType = boldText?Font.BOLD:Font.PLAIN; + Font font = null; + if (fontSize<9) + font = new Font("SansSerif", fontType, 9); + else + font = new Font("SansSerif", fontType, (int)( fontSize*zoom)); + int maxLength = 0; + + FontMetrics metrics = getFontMetrics(font); + fontHeight = metrics.getHeight(); + + for (int i = 0; i < numLabels; i++) { + double yLabelD = (int)(YMARGIN*zoom + BAR_LENGTH*zoom - i*barStep - 1); + int yLabel = (int)(Math.round( y + BAR_LENGTH*zoom - i*barStep - 1)); + Calibration cal = imp.getCalibration(); + String s = ""; + if (showUnit) + s = cal.getValueUnit(); + ImageProcessor ipOrig = imp.getProcessor(); + double min = ipOrig.getMin(); + double max = ipOrig.getMax(); + if (ipOrig instanceof ByteProcessor) { + if (min<0) min = 0; + if (max>255) max = 255; + } + double grayLabel = min + (max-min)/(numLabels-1) * i; + if (cal.calibrated()) { + grayLabel = cal.getCValue(grayLabel); + double cmin = cal.getCValue(min); + double cmax = cal.getCValue(max); + if (!decimalPlacesChanged && decimalPlaces==0 && ((int)cmax!=cmax||(int)cmin!=cmin)) + decimalPlaces = 2; + } + String todisplay = d2s(grayLabel)+" "+s; + if (overlay!=null) { + TextRoi label = new TextRoi(todisplay, x + 5, yLabel + fontHeight/2, font); + label.setStrokeColor(c); + overlay.add(label, CALIBRATION_BAR); + } + int iLength = metrics.stringWidth(todisplay); + if (iLength > maxLength) + maxLength = iLength+5; + } + return maxLength; + } + + String d2s(double d) { + return IJ.d2s(d,decimalPlaces); + } + + int getFontHeight() { + int fontType = boldText?Font.BOLD:Font.PLAIN; + Font font = new Font("SansSerif", fontType, (int) (fontSize*zoom) ); + FontMetrics metrics = getFontMetrics(font); + return metrics.getHeight(); + } + + Color getColor(String color) { + Color c = Color.white; + if (color.equals(colors[1])) + c = Color.lightGray; + else if (color.equals(colors[2])) + c = Color.darkGray; + else if (color.equals(colors[3])) + c = Color.black; + else if (color.equals(colors[4])) + c = Color.red; + else if (color.equals(colors[5])) + c = Color.green; + else if (color.equals(colors[6])) + c = Color.blue; + else if (color.equals(colors[7])) + c = Color.yellow; + else if (color.equals(colors[8])) + c = null; + return c; + } + + void calculateWidth() { + drawBarAsOverlay(imp, -1, -1); + } + + private FontMetrics getFontMetrics(Font font) { + BufferedImage bi =new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + Graphics g = (Graphics2D)bi.getGraphics(); + g.setFont(font); + return g.getFontMetrics(font); + } + + class LiveDialog extends GenericDialog { + + LiveDialog(String title) { + super(title); + } + + public void textValueChanged(TextEvent e) { + + if (fieldNames == null) { + fieldNames = new String[4]; + for(int i=0;i<4;i++) + fieldNames[i] = ((TextField)numberField.elementAt(i)).getName(); + } + + TextField tf = (TextField)e.getSource(); + String name = tf.getName(); + String value = tf.getText(); + + if (value.equals("")) + return; + + int i=0; + boolean needsRefresh = false; + + if (name.equals(fieldNames[0])) { + + i = getValue( value ).intValue() ; + if(i<1) + return; + else { + needsRefresh = true; + numLabels = i; + } + } else if (name.equals(fieldNames[1])) { + i = getValue( value ).intValue() ; + if (i<0) + return; + else { + needsRefresh = true; + decimalPlaces = i; + decimalPlacesChanged = true; + } + + } else if (name.equals(fieldNames[2])) { + i = getValue( value ).intValue() ; + if(i<1) + return; + else { + needsRefresh = true; + fontSize = i; + + } + + } else if (name.equals(fieldNames[3])) { + double d = 0; + d = getValue( "0" + value ).doubleValue() ; + if(d<=0) + return; + else { + needsRefresh = true; + zoom = d; + } + } + + if (needsRefresh) + updateColorBar(); + return; + } + + public void itemStateChanged(ItemEvent e) { + location = ( (Choice)(choice.elementAt(0)) ).getSelectedItem(); + fillColor = ( (Choice)(choice.elementAt(1)) ).getSelectedItem(); + textColor = ( (Choice)(choice.elementAt(2)) ).getSelectedItem(); + boldText = ( (Checkbox)(checkbox.elementAt(0)) ).getState(); + flatten = !( (Checkbox)(checkbox.elementAt(1)) ).getState(); + showUnit = ( (Checkbox)(checkbox.elementAt(2)) ).getState(); + Checkbox overlayBox = (Checkbox)(checkbox.elementAt(1) ); + if (location.equals(locations[SEPARATE_IMAGE])) + overlayBox.setEnabled(false); + else + overlayBox.setEnabled(true); + updateColorBar(); + } + + } //LiveDialog inner class + +} diff --git a/src/ij/plugin/CanvasResizer.java b/src/ij/plugin/CanvasResizer.java new file mode 100644 index 0000000..38afc8f --- /dev/null +++ b/src/ij/plugin/CanvasResizer.java @@ -0,0 +1,131 @@ +package ij.plugin; +import ij.*; +import ij.plugin.filter.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; + +/** This plugin implements the Image/Adjust/Canvas Size command. + It changes the canvas size of an image or stack without resizing the image. + The border is filled with the current background color. + @author Jeffrey Kuhn (jkuhn at ccwf.cc.utexas.edu) +*/ +public class CanvasResizer implements PlugIn { + boolean zeroFill = Prefs.get("resizer.zero", false); + + public void run(String arg) { + int wOld, hOld, wNew, hNew; + boolean fIsStack = false; + + ImagePlus imp = IJ.getImage(); + wOld = imp.getWidth(); + hOld = imp.getHeight(); + if (!imp.okToDeleteRoi()) return; + + ImageStack stackOld = imp.getStack(); + if ((stackOld != null) && (stackOld.getSize() > 1)) + fIsStack = true; + + String[] sPositions = { + "Top-Left", "Top-Center", "Top-Right", + "Center-Left", "Center", "Center-Right", + "Bottom-Left", "Bottom-Center", "Bottom-Right" + }; + + String strTitle = fIsStack ? "Resize Stack Canvas" : "Resize Image Canvas"; + GenericDialog gd = new GenericDialog(strTitle); + gd.addNumericField("Width:", wOld, 0, 5, "pixels"); + gd.addNumericField("Height:", hOld, 0, 5, "pixels"); + gd.addChoice("Position:", sPositions, sPositions[4]); + gd.addCheckbox("Zero Fill", zeroFill); + gd.showDialog(); + if (gd.wasCanceled()) + return; + + wNew = (int)gd.getNextNumber(); + hNew = (int)gd.getNextNumber(); + int iPos = gd.getNextChoiceIndex(); + zeroFill = gd.getNextBoolean(); + Prefs.set("resizer.zero", zeroFill); + + int xOff, yOff; + int xC = (wNew - wOld)/2; // offset for centered + int xR = (wNew - wOld); // offset for right + int yC = (hNew - hOld)/2; // offset for centered + int yB = (hNew - hOld); // offset for bottom + + switch(iPos) { + case 0: // TL + xOff=0; yOff=0; break; + case 1: // TC + xOff=xC; yOff=0; break; + case 2: // TR + xOff=xR; yOff=0; break; + case 3: // CL + xOff=0; yOff=yC; break; + case 4: // C + xOff=xC; yOff=yC; break; + case 5: // CR + xOff=xR; yOff=yC; break; + case 6: // BL + xOff=0; yOff=yB; break; + case 7: // BC + xOff=xC; yOff=yB; break; + case 8: // BR + xOff=xR; yOff=yB; break; + default: // center + xOff=xC; yOff=yC; break; + } + + if (fIsStack) { + ImageStack stackNew = expandStack(stackOld, wNew, hNew, xOff, yOff); + imp.setStack(null, stackNew); + } else { + if (!IJ.macroRunning()) + Undo.setup(Undo.COMPOUND_FILTER, imp); + ImageWindow win = imp.getWindow(); + if (win!=null && (win instanceof PlotWindow)) + ((PlotWindow)win).getPlot().setFrozen(true); + ImageProcessor newIP = expandImage(imp.getProcessor(), wNew, hNew, xOff, yOff); + imp.setProcessor(null, newIP); + if (!IJ.macroRunning()) + Undo.setup(Undo.COMPOUND_FILTER_DONE, imp); + } + Overlay overlay = imp.getOverlay(); + if (overlay!=null) + overlay.translate(xOff, yOff); + } + + public ImageStack expandStack(ImageStack stackOld, int wNew, int hNew, int xOff, int yOff) { + int nFrames = stackOld.getSize(); + ImageProcessor ipOld = stackOld.getProcessor(1); + ImageStack stackNew = new ImageStack(wNew, hNew, stackOld.getColorModel()); + ImageProcessor ipNew; + + for (int i=1; i<=nFrames; i++) { + IJ.showProgress((double)i/nFrames); + ipNew = ipOld.createProcessor(wNew, hNew); + if (zeroFill) + ipNew.setValue(0.0); + else + ipNew.setGlobalBackgroundColor(); + ipNew.fill(); + ipNew.insert(stackOld.getProcessor(i), xOff, yOff); + stackNew.addSlice(stackOld.getSliceLabel(i), ipNew); + } + return stackNew; + } + + public ImageProcessor expandImage(ImageProcessor ipOld, int wNew, int hNew, int xOff, int yOff) { + ImageProcessor ipNew = ipOld.createProcessor(wNew, hNew); + if (zeroFill) + ipNew.setValue(0.0); + else + ipNew.setGlobalBackgroundColor(); + ipNew.fill(); + ipNew.insert(ipOld, xOff, yOff); + return ipNew; + } + +} + diff --git a/src/ij/plugin/ChannelArranger.java b/src/ij/plugin/ChannelArranger.java new file mode 100644 index 0000000..c07cd57 --- /dev/null +++ b/src/ij/plugin/ChannelArranger.java @@ -0,0 +1,352 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.plugin.ChannelSplitter; +import java.awt.image.BufferedImage; +import java.awt.event.*; +import java.util.Vector; + +/** + * This plugin implements the Image/Colors/Arrange Channels command, + * which allows the user to change the order of channels. + * + * @author Norbert Vischer 23-sep-2012 + */ +public class ChannelArranger implements PlugIn, TextListener { + private ThumbnailsCanvas thumbNails; + private String patternString; + private String allowedDigits; + private TextField orderField; + private int nChannels; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + nChannels = imp.getNChannels(); + if (nChannels==1) { + IJ.error("Channel Arranger", "Image must have more than one channel"); + return; + } + if (nChannels>9) { + IJ.error("Channel Arranger", "This command does not work with more than 9 channels."); + return; + } + patternString = "1234567890".substring(0, nChannels); + allowedDigits = patternString; + GenericDialog gd = new GenericDialog("Arrange Channels"); + thumbNails = new ThumbnailsCanvas(imp); + Panel panel = new Panel(); + panel.add(thumbNails); + gd.addPanel(panel); + //gd.setInsets(20, 20, 5); + gd.addStringField("New channel order:", allowedDigits); + Vector v = gd.getStringFields(); + orderField = (TextField)v.elementAt(0); + orderField.addTextListener(this); + gd.addHelp(IJ.URL+"/docs/menus/image.html#arrange"); + gd.showDialog(); + if (gd.wasCanceled()) + return; + String newOrder = gd.getNextString(); + int nChannels2 = newOrder.length(); + if (nChannels2==0) + return; + for (int i=0; i + The following example opens the FluorescentCells sample + image and reverses the order of the channels. +

+		ImagePlus img = IJ.openImage("http://imagej.nih.gov/ij/images/FluorescentCells.zip");
+		int[] order = {3,2,1};
+		ImagePlus img2 = ChannelArranger.run(img, order);
+		img2.setDisplayMode(IJ.COLOR);
+		img2.show();
+		
+ */ + public static ImagePlus run(ImagePlus img, int[] newOrder) { + int channel = img.getChannel(); + int slice = img.getSlice(); + int frame = img.getFrame(); + ImagePlus[] channels = ChannelSplitter.split(img); + int nChannels2 = newOrder.length; + if (nChannels2>channels.length) + nChannels2 = channels.length; + ImagePlus[] channels2 = new ImagePlus[nChannels2]; + for (int i=0; i=channels.length) + throw new IllegalArgumentException("value out of range:"+newOrder[i]); + channels2[i] = channels[index]; + } + ImagePlus img2 = null; + if (nChannels2==1) + img2 = channels2[0]; + else + img2 = RGBStackMerge.mergeChannels(channels2, false); + int mode2 = IJ.COLOR; + if (img.isComposite()) + mode2 = ((CompositeImage)img).getMode(); + if (img2.isComposite()) + ((CompositeImage)img2).setMode(mode2); + if (channel<=nChannels2) { + int channel2 = newOrder[channel-1]; + img2.setPosition(channel2, slice, frame); + } + Overlay overlay = img.getOverlay(); + if (overlay!=null) { + for (int i=0; i=1 && c<=nChannels2) + roi.setPosition(newOrder[c-1], z, t); + } + img2.setOverlay(overlay); + } + img.changes = false; + img.close(); + return img2; + } + + public void textValueChanged(TextEvent e) { + TextField tf = (TextField) e.getSource(); + String typed = tf.getText(); + if (typed.length()>nChannels) { + orderField.setText(patternString); + return; + } + for (int jj=0; jj hh) { + iconHeight = iconWidth * hh / ww; + dy = (iconWidth - iconHeight) / 2; + } + if (ww < hh) { + iconWidth = iconHeight * ww / hh; + dx = (iconHeight - iconWidth) / 2; + } + nChannels = cImp.getNChannels(); + seq = seq.substring(0, nChannels); + setSize((nChannels + 1) * iconSize, 2 * iconSize + 30); + } + + public void update(Graphics g) { + paint(g); + } + + public void setSequence(String seq) { + this.seq = seq; + } + + public int[] getStackPos() { + return new int[]{currentChannel, currentSlice, currentFrame}; + } + + public void paint(Graphics g) { + if (g == null) + return; + int savedMode = cImp.getMode(); + if (savedMode==IJ.COMPOSITE) + cImp.setMode(IJ.COLOR); + BufferedImage bImg; + ImageProcessor ipSmall; + os = createImage((nChannels + 1) * iconSize, 2 * iconSize + 30); + osg = os.getGraphics(); + osg.setFont(IJ.font12); + int y1; + for (int chn = 1; chn <= nChannels; chn++) { + cImp.setPositionWithoutUpdate(chn, currentSlice, currentFrame); + cImp.updateImage(); + ipSmall = cImp.getProcessor().resize(iconWidth, iconHeight, true); + bImg = ipSmall.getBufferedImage(); + int index = chn - 1; + y1 = marginY; + for (int row = 0; row < 2; row++) { + if (index >= 0) { + int xx = index * iconSize + marginX; + osg.drawImage(bImg, xx + dx, y1 + dy, null); + osg.setColor(Color.LIGHT_GRAY); + osg.drawRect(xx, y1, iconSize, iconSize); + osg.fillRoundRect(xx + iconSize / 2 - 4, y1 + iconSize - 22, 18, 18, 6, 6); + osg.setColor(Color.BLACK); + osg.drawRoundRect(xx + iconSize / 2 - 4, y1 + iconSize - 22, 18, 18, 6, 6); + osg.drawString("" + chn, xx + 52, y1 + iconSize - 7); + index = seq.indexOf("" + chn, 0); + if (seq.indexOf("" + chn, index) == -1) {//char must not occur twice + index = -1; + } + } + y1 += (iconSize + separatorY); + } + } + y1 = marginY + iconSize - 7; + osg.drawString("Old:", 6, y1); + y1 += (iconSize + separatorY); + osg.drawString("New:", 6, y1); + osg.dispose(); + if (os == null) + return; + g.drawImage(os, 0, 0, this); + if (savedMode==IJ.COMPOSITE) + cImp.setMode(savedMode); + cImp.setPosition(currentChannel, currentSlice, currentFrame); + cImp.updateImage(); + } + + protected void handlePopupMenu(MouseEvent e) { + int x = e.getX(); + int y = e.getY(); + PopupMenu popup = new PopupMenu(); + String[] colors = "Grays,-,Red,Green,Blue,Yellow,Magenta,Cyan,-,Fire,Ice,Spectrum,3-3-2 RGB,Red/Green".split(","); + for (int jj = 0; jj < colors.length; jj++) { + if (colors[jj].equals("-")) + popup.addSeparator(); + else { + MenuItem mi = new MenuItem(colors[jj]); + popup.add(mi); + mi.addActionListener(this); + } + } + add(popup); + if (IJ.isMacOSX()) + IJ.wait(10); + popup.show(this, x, y); + setCursor(defaultCursor); + } + + public void actionPerformed(ActionEvent e) { + String cmd = e.getActionCommand(); + cImp.setPosition(currentChannel, currentSlice, currentFrame); + CompositeImage cImp = (CompositeImage) this.cImp; + IJ.run(cmd); + repaint(); + setCursor(defaultCursor); + } + + public void mouseMoved(MouseEvent e) { + int x = e.getX() - marginX; + int y = e.getY() - marginY; + if (x < 0 || x > nChannels * iconSize || y < 0 || y > iconSize * 2 + separatorY) { + setCursor(defaultCursor); + channelUnderCursor = 0; + } else { + int chn = x / iconSize + 1; + if (y > iconSize) { + if (chn <= seq.length()) { + String digit = seq.substring(chn - 1, chn); + chn = "1234567890".indexOf(digit) + 1; + } else { + chn = 0; + } + } + if (y > 2 * iconSize + separatorY) { + chn = 0; + } + channelUnderCursor = chn; + } + if (channelUnderCursor > 0) + setCursor(handCursor); + else + setCursor(defaultCursor); + } + + public void mouseEntered(MouseEvent e) { + } + + public void mousePressed(MouseEvent e) { + if (channelUnderCursor > 0) { + currentChannel = channelUnderCursor; + handlePopupMenu(e); + repaint(); + } + } + + public void mouseReleased(MouseEvent e) { + mouseMoved(e); + } + + public void mouseExited(MouseEvent e) { + } + + public void mouseDragged(MouseEvent e) { + } + + public void mouseClicked(MouseEvent e) { + } +} diff --git a/src/ij/plugin/ChannelSplitter.java b/src/ij/plugin/ChannelSplitter.java new file mode 100644 index 0000000..c6c16ee --- /dev/null +++ b/src/ij/plugin/ChannelSplitter.java @@ -0,0 +1,152 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.Calibration; +import ij.plugin.HyperStackReducer; +import java.awt.*; +import java.util.Vector; + +/** This plugin implements the Image/Color/Split Channels command. */ +public class ChannelSplitter implements PlugIn { + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + if (imp.isComposite()) { + int z = imp.getSlice(); + int t = imp.getFrame(); + ImagePlus[] channels = split(imp); + imp.changes = false; + imp.setIgnoreFlush(true); + imp.close(); + for (int i=0; i1 || t>1) + channels[i].setPosition(1, z, t); + } + } else if (imp.getType()==ImagePlus.COLOR_RGB) + splitRGB(imp); + else + IJ.error("Split Channels", "Multichannel image required"); + } + + private void splitRGB(ImagePlus imp) { + boolean keepSource = IJ.altKeyDown(); + String title = imp.getTitle(); + Calibration cal = imp.getCalibration(); + int pos = imp.getCurrentSlice(); + ImageStack[] channels = splitRGB(imp.getStack(), keepSource); + if (!keepSource) + {imp.unlock(); imp.changes=false; imp.close();} + ImagePlus rImp = new ImagePlus(title+" (red)", channels[0]); + rImp.setCalibration(cal); + rImp.setIJMenuBar(false); + rImp.show(); + rImp.setSlice(pos); + if (IJ.isMacOSX()) IJ.wait(500); + ImagePlus gImp = new ImagePlus(title+" (green)", channels[1]); + gImp.setCalibration(cal); + gImp.setIJMenuBar(false); + gImp.show(); + gImp.setSlice(pos); + if (IJ.isMacOSX()) IJ.wait(500); + ImagePlus bImp = new ImagePlus(title+" (blue)", channels[2]); + bImp.setCalibration(cal); + bImp.show(); + bImp.setSlice(pos); + } + + /** Splits the specified image into separate channels. */ + public static ImagePlus[] split(ImagePlus imp) { + if (imp.getType()==ImagePlus.COLOR_RGB) { + ImageStack[] stacks = splitRGB(imp.getStack(), true); + ImagePlus[] images = new ImagePlus[3]; + images[0] = new ImagePlus("red", stacks[0]); + images[1] = new ImagePlus("green", stacks[1]); + images[2] = new ImagePlus("blue", stacks[2]); + return images; + } + int width = imp.getWidth(); + int height = imp.getHeight(); + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + int bitDepth = imp.getBitDepth(); + int size = slices*frames; + Vector images = new Vector(); + HyperStackReducer reducer = new HyperStackReducer(imp); + for (int c=1; c<=channels; c++) { + ImageStack stack2 = new ImageStack(width, height, size); // create empty stack + stack2.setPixels(imp.getProcessor().getPixels(), 1); // can't create ImagePlus will null 1st image + ImagePlus imp2 = new ImagePlus("C"+c+"-"+imp.getTitle(), stack2); + stack2.setPixels(null, 1); + imp.setPosition(c, 1, 1); + imp2.setDimensions(1, slices, frames); + imp2.setCalibration(imp.getCalibration()); + reducer.reduce(imp2); + if (imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.GRAYSCALE) + IJ.run(imp2, "Grays", ""); + if (imp2.getNDimensions()>3) + imp2.setOpenAsHyperStack(true); + images.add(imp2); + } + ImagePlus[] array = new ImagePlus[images.size()]; + return (ImagePlus[])images.toArray(array); + } + + /** Returns, as an ImageStack, the specified channel, where 'c' must be greater + than zero and less than or equal to the number of channels in the image. */ + public static ImageStack getChannel(ImagePlus imp, int c) { + if (imp.getBitDepth()==24) { // RGB? + if (c<1 || c>3) + throw new IllegalArgumentException("Channel must be 1,2 or 3"); + ImageStack[] channels = splitRGB(imp.getStack(), true); + return channels[c-1]; + } + if (c<1 || c>imp.getNChannels()) + throw new IllegalArgumentException("Channel less than 1 or greater than "+imp.getNChannels()); + ImageStack stack1 = imp.getStack(); + ImageStack stack2 = new ImageStack(imp.getWidth(), imp.getHeight()); + for (int t=1; t<=imp.getNFrames(); t++) { + for (int z=1; z<=imp.getNSlices(); z++) { + int n = imp.getStackIndex(c, z, t); + stack2.addSlice(stack1.getProcessor(n)); + } + } + return stack2; + } + + /** Splits the specified RGB stack into three 8-bit grayscale stacks. + Deletes the source stack if keepSource is false. */ + public static ImageStack[] splitRGB(ImageStack rgb, boolean keepSource) { + int w = rgb.getWidth(); + int h = rgb.getHeight(); + ImageStack[] channels = new ImageStack[3]; + for (int i=0; i<3; i++) + channels[i] = new ImageStack(w,h); + byte[] r,g,b; + ColorProcessor cp; + int slice = 1; + int inc = keepSource?1:0; + int n = rgb.getSize(); + for (int i=1; i<=n; i++) { + IJ.showStatus(i+"/"+n); + r = new byte[w*h]; + g = new byte[w*h]; + b = new byte[w*h]; + cp = (ColorProcessor)rgb.getProcessor(slice); + slice += inc; + cp.getRGB(r,g,b); + if (!keepSource) + rgb.deleteSlice(1); + channels[0].addSlice(null,r); + channels[1].addSlice(null,g); + channels[2].addSlice(null,b); + IJ.showProgress((double)i/n); + } + return channels; + } + +} + diff --git a/src/ij/plugin/CircularRoiMaker.java b/src/ij/plugin/CircularRoiMaker.java new file mode 100644 index 0000000..ceff4cb --- /dev/null +++ b/src/ij/plugin/CircularRoiMaker.java @@ -0,0 +1,83 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.measure.Calibration; +import java.awt.*; + +/** This class implements the Process/FFT/Make Circular Selection command. */ +public class CircularRoiMaker implements PlugIn, DialogListener { + private static double saveRadius; + private double xcenter, ycenter, radius; + private boolean bAbort; + private ImagePlus imp; + private Calibration cal; + + public void run(String arg) { + imp = IJ.getImage(); + cal = imp.getCalibration(); + int width = imp.getWidth(); + int height = imp.getHeight(); + xcenter = width/2; + ycenter = height/2; + boolean macro = Macro.getOptions()!=null; + radius = !macro&&saveRadius!=0.0?saveRadius:width/4; + if (radius>width/2) + radius = width/2; + if (radius>height/2) + radius = height/2; + showDialog(); + if (!macro) + saveRadius = radius; + + } + + private void showDialog() { + Roi roi = imp.getRoi(); + drawRoi(); + GenericDialog gd = new GenericDialog("Circular ROI"); + gd.addSlider("Radius:", 0, imp.getWidth()/2, radius); + gd.addDialogListener(this); + gd.showDialog(); + if (gd.wasCanceled()) { + if (roi==null) + imp.deleteRoi(); + else // restore initial ROI when cancelled + imp.setRoi(roi); + } + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + radius = gd.getNextNumber(); + if (gd.invalidNumber()) + return false; + drawRoi(); + return true; + } + + private void drawRoi() { + double x = xcenter - radius; + double y = ycenter - radius; + Roi roi = new OvalRoi(x, y, radius*2.0, radius*2.0); + imp.setRoi(roi); + showRadius(); + } + + private void showRadius() { + String units = cal.getUnits(); + String s = " radius = "; + if (imp.getProperty("FHT")!=null) { + int width = imp.getWidth(); + if (radius<1.0) + s += "Infinity/c"; + else if (cal.scaled()) + s += IJ.d2s((width/radius)*cal.pixelWidth,2) + " " + units + "/c"; + else + s += IJ.d2s(width/radius,2) + " p/c"; + } else { + int digits = cal.pixelWidth==1.0?0:2; + s += IJ.d2s(radius*cal.pixelWidth,digits)+" "+units; + } + IJ.showStatus(s); + } + +} diff --git a/src/ij/plugin/ClassChecker.java b/src/ij/plugin/ClassChecker.java new file mode 100644 index 0000000..9ce3781 --- /dev/null +++ b/src/ij/plugin/ClassChecker.java @@ -0,0 +1,107 @@ +package ij.plugin; +import ij.*; +import ij.util.*; +import java.io.*; +import java.util.*; + +/** Checks for duplicate class and JAR files in the plugins folders and deletes older duplicates. */ +public class ClassChecker implements PlugIn { + String[] paths; + String[] names; + + public void run(String arg) { + //long start = System.currentTimeMillis(); + deleteDuplicates(); + //IJ.log("Time: "+(System.currentTimeMillis()-start)); + } + + void deleteDuplicates() { + getPathsAndNames(); + if (paths==null || paths.length<2) return; + String[] sortedNames = new String[names.length]; + for (int i=0; i1) { + imp.deleteRoi(); + int slice = imp.getCurrentSlice(); + imp = new Duplicator().run(imp, slice, slice); + } + imp = imp.flatten(); + imp.setRoi(roi); + imp2.setRoi(roi); + } + return imp; + } + + void paste() { + if (ImagePlus.getClipboard()==null) + showSystemClipboard(); + else { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + imp.paste(); + if (Recorder.scriptMode()) + Recorder.recordCall("imp.paste();"); + } else + showInternalClipboard (); + } + } + + void setup() { + if (clipboard==null) + clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + } + + void copyToSystem() { + this.gImp = WindowManager.getCurrentImage(); + setup(); + try { + clipboard.setContents(this, null); + } catch (Throwable t) {} + if (Recorder.scriptMode()) + Recorder.recordCall("imp.copyToSystem();"); + } + + void showSystemClipboard() { + setup(); + IJ.showStatus("Opening system clipboard..."); + try { + Transferable transferable = clipboard.getContents(null); + boolean imageSupported = transferable.isDataFlavorSupported(DataFlavor.imageFlavor); + boolean textSupported = transferable.isDataFlavorSupported(DataFlavor.stringFlavor); + if (imageSupported) { + Image img = (Image)transferable.getTransferData(DataFlavor.imageFlavor); + if (img==null) { + IJ.error("Unable to convert image on system clipboard"); + IJ.showStatus(""); + return; + } + int width = img.getWidth(null); + int height = img.getHeight(null); + BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics g = bi.createGraphics(); + g.drawImage(img, 0, 0, null); + g.dispose(); + WindowManager.checkForDuplicateName = true; + new ImagePlus("Clipboard", bi).show(); + } else if (textSupported) { + String text = (String)transferable.getTransferData(DataFlavor.stringFlavor); + if (IJ.isMacintosh()) + text = Tools.fixNewLines(text); + Editor ed = new Editor(); + ed.setSize(600, 300); + ed.create("Clipboard", text); + IJ.showStatus(""); + } else + IJ.error("Unable to find an image on the system clipboard"); + } catch (Throwable e) { + IJ.handleException(e); + } + } + + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[] { DataFlavor.imageFlavor }; + } + + public boolean isDataFlavorSupported(DataFlavor flavor) { + return DataFlavor.imageFlavor.equals(flavor); + } + + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { + if (!isDataFlavorSupported(flavor)) + throw new UnsupportedFlavorException(flavor); + ImagePlus imp = gImp!=null?gImp:WindowManager.getCurrentImage(); + if (imp==null) + return null; + Roi roi = imp.getRoi(); + if (roi!=null && !roi.isLine()) { + Rectangle bounds = roi.getBounds(); + if (!(bounds.x==0&&bounds.y==0&&bounds.width==imp.getWidth()&&bounds.height==imp.getHeight())) + imp = imp.crop(); + } + boolean overlay = imp.getOverlay()!=null && !imp.getHideOverlay(); + if (overlay && !imp.tempOverlay()) + imp = imp.flatten(); + return imp.getImage(); + } + + void showInternalClipboard() { + ImagePlus clipboard = ImagePlus.getClipboard(); + if (clipboard!=null) { + ImageProcessor ip = clipboard.getProcessor(); + ImagePlus imp2 = new ImagePlus("Clipboard", ip.duplicate()); + Roi roi = clipboard.getRoi(); + imp2.deleteRoi(); + if (roi!=null && roi.isArea() && roi.getType()!=Roi.RECTANGLE) { + roi = (Roi)roi.clone(); + roi.setLocation(0, 0); + imp2.setRoi(roi); + IJ.run(imp2, "Clear Outside", null); + imp2.deleteRoi(); + } + WindowManager.checkForDuplicateName = true; + imp2.show(); + } else + IJ.error("The internal clipboard is empty."); + } + +} + + + diff --git a/src/ij/plugin/Colors.java b/src/ij/plugin/Colors.java new file mode 100644 index 0000000..3605817 --- /dev/null +++ b/src/ij/plugin/Colors.java @@ -0,0 +1,276 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.io.*; +import ij.plugin.filter.*; +import ij.util.Tools; +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +/** This plugin implements most of the Edit/Options/Colors command. */ +public class Colors implements PlugIn, ItemListener { + public static final String[] colors = {"red","green","blue","magenta","cyan","yellow","orange","black","white","gray","lightgray","darkgray","pink"}; + private static final String[] colors2 = {"Red","Green","Blue","Magenta","Cyan","Yellow","Orange","Black","White","Gray","lightGray","darkGray","Pink"}; + private Choice fchoice, bchoice, schoice; + private Color fc2, bc2, sc2; + + public void run(String arg) { + showDialog(); + } + + /** The Edit>Options>Colors dialog */ + void showDialog() { + Color fc =Toolbar.getForegroundColor(); + String fname = getColorName(fc, "black"); + Color bc =Toolbar.getBackgroundColor(); + String bname = getColorName(bc, "white"); + Color sc =Roi.getColor(); + String sname = getColorName(sc, "yellow"); + GenericDialog gd = new GenericDialog("Colors"); + gd.addChoice("Foreground:", colors, fname); + gd.addChoice("Background:", colors, bname); + gd.addChoice("Selection:", colors, sname); + Vector choices = gd.getChoices(); + if (choices!=null) { + fchoice = (Choice)choices.elementAt(0); + bchoice = (Choice)choices.elementAt(1); + schoice = (Choice)choices.elementAt(2); + fchoice.addItemListener(this); + bchoice.addItemListener(this); + schoice.addItemListener(this); + } + + gd.showDialog(); + if (gd.wasCanceled()) { + if (fc2!=fc) Toolbar.setForegroundColor(fc); + if (bc2!=bc) Toolbar.setBackgroundColor(bc); + if (sc2!=sc) { + Roi.setColor(sc); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null && imp.getRoi()!=null) imp.draw(); + } + return; + } + fname = gd.getNextChoice(); + bname = gd.getNextChoice(); + sname = gd.getNextChoice(); + fc2 = getColor(fname, Color.black); + bc2 = getColor(bname, Color.white); + sc2 = getColor(sname, Color.yellow); + if (fc2!=fc) Toolbar.setForegroundColor(fc2); + if (bc2!=bc) Toolbar.setBackgroundColor(bc2); + if (sc2!=sc) { + Roi.setColor(sc2); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) imp.draw(); + Toolbar tb = Toolbar.getInstance(); + if (tb!=null) tb.repaint(); + } + } + + /** For named colors, returns the name, or 'defaultName' if not a named color. + * If 'defaultName' is non-null and starts with an uppercase character, + * the returned name is capitalized (first character uppercase). + * Use colorToString or colorToString2 to get a String representation (hexadecimal) + * also for unnamed colors.*/ + public static String getColorName(Color c, String defaultName) { + if (c==null) return defaultName; + boolean useCapitalizedName = defaultName!=null && defaultName.length()>0 && Character.isUpperCase(defaultName.charAt(0)); + return getColorName(c, defaultName, useCapitalizedName); + } + + /** For named colors, returns the name, or 'defaultName' if not a named color. + * 'color' must not be null. */ + private static String getColorName(Color c, String defaultName, boolean useCapitalizedName) { + String[] colorNames = useCapitalizedName ? colors2 : colors; + if (c.equals(Color.red)) return colorNames[0]; + else if (c.equals(Color.green)) return colorNames[1]; + else if (c.equals(Color.blue)) return colorNames[2]; + else if (c.equals(Color.magenta)) return colorNames[3]; + else if (c.equals(Color.cyan)) return colorNames[4]; + else if (c.equals(Color.yellow)) return colorNames[5]; + else if (c.equals(Color.orange)) return colorNames[6]; + else if (c.equals(Color.black)) return colorNames[7]; + else if (c.equals(Color.white)) return colorNames[8]; + else if (c.equals(Color.gray)) return colorNames[9]; + else if (c.equals(Color.lightGray)) return colorNames[10]; + else if (c.equals(Color.darkGray)) return colorNames[11]; + else if (c.equals(Color.pink)) return colorNames[12]; + return defaultName; + } + + /** For named colors, converts the name String to the corresponding color. + * Returns 'defaultColor' if the color has no name. + * Use 'decode' to also decode hex color names like "#ffff00" */ + public static Color getColor(String name, Color defaultColor) { + if (name==null || name.length()<2) + return defaultColor; + name = name.toLowerCase(Locale.US); + Color c = defaultColor; + if (name.contains(colors[7])) c = Color.black; + else if (name.contains(colors[8])) c = Color.white; + else if (name.contains(colors[0])) c = Color.red; + else if (name.contains(colors[2])) c = Color.blue; + else if (name.contains(colors[5])) c = Color.yellow; + else if (name.contains(colors[1])) c = Color.green; + else if (name.contains(colors[3])) c = Color.magenta; + else if (name.contains(colors[4])) c = Color.cyan; + else if (name.contains(colors[6])) c = Color.orange; + else if (name.contains(colors[12])) c = Color.pink; + else if (name.contains(colors[9]) || name.contains("grey")) { //gray or grey + if (name.contains("light")) c = Color.lightGray; + else if (name.contains("dark")) c = Color.darkGray; + else c = Color.gray; + } + return c; + } + + /** Converts a String with the color name or the hexadecimal representation + * of a color with 6 or 8 hex digits to a Color. + * With 8 hex digits, the first two digits are the alpha. + * With 6 hex digits, the color is opaque (alpha = hex ff). + * A hex String may be preceded by '#' such as "#80ff00". + * When the string does not include a valid color name or hex code, + * returns Color.GRAY. */ + public static Color decode(String hexColor) { + return decode(hexColor, Color.gray); + } + + /** Converts a String with the color name or the hexadecimal representation + * of a color with 6 or 8 hex digits to a Color. + * With 8 hex digits, the first two digits are the alpha. + * With 6 hex digits, the color is opaque (alpha = hex ff). + * A hex String may be preceded by "#" such as "#80ff00" or "0x". + * When the string does not include a valid color name or hex code, + * returns 'defaultColor'. */ + public static Color decode(String hexColor, Color defaultColor) { + if (hexColor==null || hexColor.length()<2) + return defaultColor; + Color color = getColor(hexColor, null); //for named colors + if (color==null) { + if (hexColor.startsWith("#")) + hexColor = hexColor.substring(1); + else if (hexColor.startsWith("0x")) + hexColor = hexColor.substring(2); + int len = hexColor.length(); + if (!(len==6 || len==8)) + return defaultColor; + boolean hasAlpha = len==8; + try { + int rgba = (int)Long.parseLong(hexColor, 16); + color = new Color(rgba, hasAlpha); + } catch (NumberFormatException e) { + return defaultColor; + } + } + return color; + } + + public static int getRed(String hexColor) { + return decode(hexColor, Color.black).getRed(); + } + + public static int getGreen(String hexColor) { + return decode(hexColor, Color.black).getGreen(); + } + + public static int getBlue(String hexColor) { + return decode(hexColor, Color.black).getBlue(); + } + + /** Converts a hex color (e.g., "ffff00") into "red", "green", "yellow", etc. + * Returns null if the hex color does not have a name. + * Unused in ImageJ, for compatibility only. */ + public static String hexToColor(String hex) { + if (hex==null) return null; + Color color = decode(hex, null); + if (color==null) return null; + return getColorName(color, null, false); + } + + /** Converts a hex color (e.g., "ffff00" or "#ffff00") into a color name + * "Red", "Green", "Yellow", etc. + * Returns null if the hex color does not have a name. + * Unused in ImageJ, for compatibility only. */ + public static String hexToColor2(String hex) { + if (hex==null) return null; + Color color = decode(hex, null); + if (color==null) return null; + return getColorName(color, null, true); + } + + /** Converts a Color into a lowercase string ("red", "green", "#aa55ff", etc.). + * If color is null, returns the String "none". */ + public static String colorToString(Color color) { + if (color == null) return "none"; + String str = getColorName(color, null, false); + if (str == null) + str = "#"+getHexString(color); + return str; + } + + /** Converts a Color into a string ("Red", "Green", #aa55ff, etc.). + * If color is null, returns the String "None". */ + public static String colorToString2(Color color) { + if (color == null) return "None"; + String str = getColorName(color, null, true); + if (str == null) + str = "#"+getHexString(color); + return str; + } + + /** Returns the 6-digit hex string such as "aa55ff" for opaque colors or + * or 8-digit like "80aa55ff" for other colors (the first two hex digits are alpha). + * 'color' must not be null. */ + private static String getHexString(Color color) { + int rgb = color.getRGB(); + boolean isOpaque = (rgb & 0xff000000) == 0xff000000; + if (isOpaque) + rgb &= 0x00ffffff; //don't show alpha for opaque colors + String format = isOpaque? "%06x" : "%08x"; + return String.format(format, rgb); + } + + /** Returns an opaque color with the specified red, green, and blue values. + * Values is outside the 0-255 range are replaced by the nearest + * valid number (0 or 255) */ + public static Color toColor(int red, int green, int blue) { + if (red<0) red=0; if (green<0) green=0; if (blue<0) blue=0; + if (red>255) red=255; if (green>255) green=255; if (blue>255) blue=255; + return new Color(red, green, blue); + } + + /** Callback listener for Choice modifications in the dialog */ + public void itemStateChanged(ItemEvent e) { + Choice choice = (Choice)e.getSource(); + String item = choice.getSelectedItem(); + Color color = getColor(item, Color.black); + if (choice==fchoice) + Toolbar.setForegroundColor(color); + else if (choice==bchoice) + Toolbar.setBackgroundColor(color); + else if (choice==schoice) { + Roi.setColor(color); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null && imp.getRoi()!=null) imp.draw(); + Toolbar.getInstance().repaint(); + } + } + + /** Returns an array of the color Strings in the argument(s) and the 13 + * predefined color names "Red", "Green", ... "Pink". + * The Strings arguments must be either "None" or hex codes starting with "#". + * Any null arguments are ignored. */ + public static String[] getColors(String... moreColors) { + ArrayList names = new ArrayList(); + for (String arg: moreColors) { + if (arg!=null && arg.length()>0 && (!Character.isLetter(arg.charAt(0))||arg.equals("None"))) + names.add(arg); + } + for (String arg: colors2) + names.add(arg); + return (String[])names.toArray(new String[names.size()]); + } +} diff --git a/src/ij/plugin/CommandFinder.java b/src/ij/plugin/CommandFinder.java new file mode 100644 index 0000000..26fb513 --- /dev/null +++ b/src/ij/plugin/CommandFinder.java @@ -0,0 +1,705 @@ +/** This plugin implements the Plugins/Utilities/Find Commands + command. It provides an easy user interface to finding commands + you might know the name of without having to go through + all the menus. If you type a part of a command name, the box + below will only show commands that match that substring (case + insensitively). If only a single command matches then that + command can be run by hitting Enter. If multiple commands match, + they can be selected by selecting with the mouse and clicking + "Run"; alternatively hitting the up or down arrows will move the + keyboard focus to the list and the selected command can be run + with Enter. When the list has focus, it is also possible to use + keyboard "scrolling": E.g., pressing "H" will select the first + command starting with the char "H". Pressing "H" again will select + the next row starting with the char "H", etc., looping between all + "H" starting commands. Double-clicking on a command in the list + should also run the appropriate command. + + @author Mark Longair + @author Johannes Schindelin + @author Curtis Rueden + @author Tiago Ferreira + + */ + +package ij.plugin; + +import ij.*; +import ij.text.*; +import ij.plugin.frame.Editor; +import ij.process.ImageProcessor; +import ij.gui.GUI; +import ij.gui.HTMLDialog; +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import java.io.File; +import javax.swing.*; +import javax.swing.table.*; +import javax.swing.event.*; +import javax.swing.event.DocumentEvent; + +public class CommandFinder implements PlugIn, ActionListener, WindowListener, KeyListener, ItemListener, MouseListener { + + private static final int TABLE_WIDTH = 640; + private static final int TABLE_ROWS = 18; + private int multiClickInterval; + private long lastClickTime; + private static JFrame frame; + private JTextField prompt; + private JScrollPane scrollPane; + private JButton runButton, sourceButton, closeButton, commandsButton, helpButton; + private JCheckBox closeCheckBox; + private JCheckBox lutCheckBox; + private Hashtable commandsHash; + private String[] commands; + private static boolean closeWhenRunning = Prefs.get("command-finder.close", false); + private static boolean applyLUT; + private JTable table; + private TableModel tableModel; + private int lastClickedRow; + + public CommandFinder() { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Integer interval = (Integer) toolkit.getDesktopProperty("awt.multiClickInterval"); + if (interval == null) + // Hopefully 300ms is a sensible default when the property + // is not available. + multiClickInterval = 300; + else + multiClickInterval = interval.intValue(); + } + + class CommandAction { + CommandAction(String classCommand, MenuItem menuItem, String menuLocation) { + this.classCommand = classCommand; + this.menuItem = menuItem; + this.menuLocation = menuLocation; + } + + String classCommand; + MenuItem menuItem; + String menuLocation; + + public String toString() { + return "classCommand: " + classCommand + ", menuItem: " + menuItem + ", menuLocation: " + menuLocation; + } + } + + protected String[] makeRow(String command, CommandAction ca) { + String[] result = new String[tableModel.getColumnCount()]; + result[0] = command; + if (ca.menuLocation != null) + result[1] = ca.menuLocation; + if (ca.classCommand != null) + result[2] = ca.classCommand; + String jarFile = Menus.getJarFileForMenuEntry(command); + if (jarFile != null) + result[3] = jarFile; + return result; + } + + protected void populateList(String matchingSubstring) { + String substring = matchingSubstring.toLowerCase(); + ArrayList list = new ArrayList(); + int count = 0; + for (int i = 0; i < commands.length; ++i) { + String commandName = commands[i]; + String command = commandName.toLowerCase(); + CommandAction ca = (CommandAction) commandsHash.get(commandName); + String menuPath = ca.menuLocation; + if (menuPath == null) + menuPath = ""; + menuPath = menuPath.toLowerCase(); + if (command.indexOf(substring) >= 0 || menuPath.indexOf(substring) >= 0) { + String[] row = makeRow(commandName, ca); + list.add(row); + } + } + tableModel.setData(list); + prompt.requestFocus(); + } + + public void actionPerformed(ActionEvent ae) { + Object source = ae.getSource(); + if (source == runButton) { + int row = table.getSelectedRow(); + if (row < 0) { + error("Please select a command to run"); + return; + } + runCommand(tableModel.getCommand(row)); + } else if (source == sourceButton) { + int row = table.getSelectedRow(); + if (row < 0) { + error("Please select a command"); + return; + } + showSource(tableModel.getCommand(row)); + } else if (source == closeButton) { + closeWindow(); + } else if (source == commandsButton) { + IJ.doCommand("Commands..."); + } else if (source == helpButton) { + String text = "Shortcuts:
" + " ↑ ↓  Select items
" + + " ↵  Open item
" + " A-Z  Alphabetic scroll
" + + " ⌫ Activate search field"; + new HTMLDialog("", text); + } + } + + public void itemStateChanged(ItemEvent ie) { + populateList(prompt.getText()); + applyLUT = lutCheckBox.isSelected(); + if (applyLUT) + prompt.setText("Lookup Tables"); + } + + public void mouseClicked(MouseEvent e) { + long now = System.currentTimeMillis(); + int row = table.getSelectedRow(); + // Is this fast enough to be a double-click? + long thisClickInterval = now - lastClickTime; + if (thisClickInterval < multiClickInterval) { + if (row >= 0 && lastClickedRow >= 0 && row == lastClickedRow) + runCommand(tableModel.getCommand(row)); + } + lastClickTime = now; + lastClickedRow = row; + if (lutCheckBox.isSelected()) + previewLUT(); + + } + + public void mousePressed(MouseEvent e) { + } + + public void mouseReleased(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + void showSource(String cmd) { + if (showMacro(cmd)) + return; + Hashtable table = Menus.getCommands(); + String className = (String) table.get(cmd); + if (IJ.debugMode) + IJ.log("showSource: " + cmd + " " + className); + if (className == null) { + error("No source associated with this command:\n " + cmd); + return; + } + int mstart = className.indexOf("ij.plugin.Macro_Runner(\""); + if (mstart >= 0) { // macro or script + int mend = className.indexOf("\")"); + if (mend == -1) + return; + String macro = className.substring(mstart + 24, mend); + IJ.open(IJ.getDirectory("plugins") + macro); + return; + } + if (className.endsWith("\")")) { + int openParen = className.lastIndexOf("(\""); + if (openParen > 0) + className = className.substring(0, openParen); + } + if (className.startsWith("ij.")) { + className = className.replaceAll("\\.", "/"); + IJ.runPlugIn("ij.plugin.BrowserLauncher", IJ.URL + "/source/" + className + ".java"); + return; + } + className = IJ.getDirectory("plugins") + className.replaceAll("\\.", "/"); + String path = className + ".java"; + File f = new File(path); + if (f.exists()) { + IJ.open(path); + return; + } + error("Unable to display source for this plugin:\n " + className); + } + + private boolean showMacro(String cmd) { + String name = null; + if (cmd.equals("Display LUTs")) + name = "ShowAllLuts.txt"; + else if (cmd.equals("Search...")) + name = "Search.txt"; + if (name == null) + return false; + String code = BatchProcessor.openMacroFromJar(name); + if (code != null) { + Editor ed = new Editor(); + ed.setSize(700, 600); + ed.create(name, code); + return true; + } + return false; + } + + private void error(String msg) { + IJ.error("Command Finder", msg); + } + + protected void runCommand(String command) { + IJ.showStatus("Running command " + command); + IJ.doCommand(command); + closeWhenRunning = closeCheckBox.isSelected(); + if (closeWhenRunning) + closeWindow(); + } + + public void keyPressed(KeyEvent ke) { + int key = ke.getKeyCode(); + int flags = ke.getModifiers(); + int items = tableModel.getRowCount(); + Object source = ke.getSource(); + boolean meta = ((flags & KeyEvent.META_MASK) != 0) || ((flags & KeyEvent.CTRL_MASK) != 0); + if (key == KeyEvent.VK_ESCAPE || (key == KeyEvent.VK_W && meta)) { + closeWindow(); + } else if (source == prompt) { + /* + * If you hit enter in the text field, and there's only one command + * that matches, run that: + */ + if (key == KeyEvent.VK_ENTER) { + if (1 == items) + runCommand(tableModel.getCommand(0)); + } + /* + * If you hit the up or down arrows in the text field, move the + * focus to the table and select the row at the bottom or top. + */ + int index = -1; + if (key == KeyEvent.VK_UP) { + index = table.getSelectedRow() - 1; + if (index < 0) + index = items - 1; + } else if (key == KeyEvent.VK_DOWN) { + index = table.getSelectedRow() + 1; + if (index >= items) + index = Math.min(items - 1, 0); + } + if (index >= 0) { + table.requestFocus(); + // completions.ensureIndexIsVisible(index); + table.setRowSelectionInterval(index, index); + } + } else if (key == KeyEvent.VK_BACK_SPACE || key == KeyEvent.VK_DELETE) { + /* + * If someone presses backspace or delete they probably want to + * remove the last letter from the search string, so switch the + * focus back to the prompt: + */ + prompt.requestFocus(); + } else if (source == table) { + /* + * If you hit enter with the focus in the table, run the selected + * command + */ + if (key == KeyEvent.VK_ENTER) { + ke.consume(); + int row = table.getSelectedRow(); + if (row >= 0) + runCommand(tableModel.getCommand(row)); + /* Loop through the list using the arrow keys */ + } else if (key == KeyEvent.VK_UP) { + if (table.getSelectedRow() == 0) + table.setRowSelectionInterval(tableModel.getRowCount() - 1, tableModel.getRowCount() - 1); + } else if (key == KeyEvent.VK_DOWN) { + if (table.getSelectedRow() == tableModel.getRowCount() - 1) + table.setRowSelectionInterval(0, 0); + } + } + } + + public void keyReleased(KeyEvent ke) { + if (lutCheckBox.isSelected()) + previewLUT(); + } + + public void previewLUT() { + int row = table.getSelectedRow(); + if (row >= 0) { + String cmd = tableModel.getCommand(row); + String mPath = (String) tableModel.getValueAt(row, 1); + String cName = (String) tableModel.getValueAt(row, 2); + if ((mPath.indexOf("Lookup Table") > 0) && ((null == cName) || (cName.indexOf("LutLoader") > 0))) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (null == imp) { + imp = IJ.createImage("LUT Preview", "8-bit ramp", 256, 32, 1); + imp.show(); + } + if (imp.getBitDepth() != 24) { + if (imp.isComposite()) + ((CompositeImage)imp).setChannelColorModel(LutLoader.getLut(cmd)); + else { + ImageProcessor ip = imp.getProcessor(); + ip.setColorModel(LutLoader.getLut(cmd)); + IJ.showStatus(cmd); + } + imp.updateAndDraw(); + } + } + } + } + + public void keyTyped(KeyEvent ke) { + } + + class PromptDocumentListener implements DocumentListener { + public void insertUpdate(DocumentEvent e) { + populateList(prompt.getText()); + } + + public void removeUpdate(DocumentEvent e) { + populateList(prompt.getText()); + } + + public void changedUpdate(DocumentEvent e) { + populateList(prompt.getText()); + } + } + + /* + * This function recurses down through a menu, adding to commandsHash the + * location and MenuItem of any items it finds that aren't submenus. + */ + + public void parseMenu(String path, Menu menu) { + int n = menu.getItemCount(); + for (int i = 0; i < n; ++i) { + MenuItem m = menu.getItem(i); + String label = m.getActionCommand(); + if (m instanceof Menu) { + Menu subMenu = (Menu) m; + parseMenu(path + ">" + label, subMenu); + } else { + String trimmedLabel = label.trim(); + if (trimmedLabel.length() == 0 || trimmedLabel.equals("-")) + continue; + CommandAction ca = (CommandAction) commandsHash.get(label); + if (ca == null) + commandsHash.put(label, new CommandAction(null, m, path)); + else { + ca.menuItem = m; + ca.menuLocation = path; + } + CommandAction caAfter = (CommandAction) commandsHash.get(label); + } + } + } + + /* + * Finds all the top level menus from the menu bar and recurses down through + * each. + */ + + public void findAllMenuItems() { + MenuBar menuBar = Menus.getMenuBar(); + int topLevelMenus = menuBar.getMenuCount(); + for (int i = 0; i < topLevelMenus; ++i) { + Menu topLevelMenu = menuBar.getMenu(i); + parseMenu(topLevelMenu.getLabel(), topLevelMenu); + } + } + + /** + * Displays the Command Finder dialog. If a Command Finder window is already + * being displayed and initialSearch contains a valid query, it + * will be closed and a new one displaying the new search will be rebuilt at + * the same screen location. + * + * @param initialSearch + * The search string that populates Command Finder's search + * field. It is ignored if contains an invalid query (ie, if it + * is either null or empty). + */ + public void run(String initialSearch) { + if (frame != null) { + if (initialSearch != null && !initialSearch.isEmpty()) { + frame.dispose(); // Rebuild dialog with new search string + } else { + WindowManager.toFront(frame); + return; + } + } + commandsHash = new Hashtable(); + + /* + * Find the "normal" commands; those which are registered plugins: + */ + Hashtable realCommandsHash = (Hashtable) (ij.Menus.getCommands().clone()); + Set realCommandSet = realCommandsHash.keySet(); + for (Iterator i = realCommandSet.iterator(); i.hasNext();) { + String command = (String) i.next(); + // Some of these are whitespace only or separators - ignore them: + String trimmedCommand = command.trim(); + if (trimmedCommand.length() > 0 && !trimmedCommand.equals("-")) { + commandsHash.put(command, new CommandAction((String) realCommandsHash.get(command), null, null)); + } + } + + /* + * There are some menu items that don't correspond to plugins, such as + * those added by RefreshScripts, so look through all the menus as well: + */ + findAllMenuItems(); + + /* + * Sort the commands, generate list labels for each and put them into a + * hash: + */ + commands = (String[]) commandsHash.keySet().toArray(new String[0]); + Arrays.sort(commands); + + /* The code below just constructs the dialog: */ + ImageJ imageJ = IJ.getInstance(); + frame = new JFrame("Command Finder") { + public void setVisible(boolean visible) { + if (visible) + WindowManager.addWindow(this); + super.setVisible(visible); + } + + public void dispose() { + WindowManager.removeWindow(this); + Prefs.set("command-finder.close", closeWhenRunning); + frame = null; + super.dispose(); + } + }; + Container contentPane = frame.getContentPane(); + contentPane.setLayout(new BorderLayout()); + frame.addWindowListener(this); + if (imageJ != null && !IJ.isMacOSX()) { + Image img = imageJ.getIconImage(); + if (img != null) + try { + frame.setIconImage(img); + } catch (Exception e) { + } + } + + closeCheckBox = new JCheckBox("Close window after running command", closeWhenRunning); + GUI.scale(closeCheckBox); + closeCheckBox.addItemListener(this); + + lutCheckBox = new JCheckBox("Apply LUTs", applyLUT); + GUI.scale(lutCheckBox); + lutCheckBox.addItemListener(this); + + JPanel northPanel = new JPanel(new BorderLayout()); + JLabel searchLabel = new JLabel(" Search:"); + GUI.scale(searchLabel); + northPanel.add(searchLabel, BorderLayout.WEST); + prompt = new JTextField("", 20); + GUI.scale(prompt); + + prompt.getDocument().addDocumentListener(new PromptDocumentListener()); + prompt.addKeyListener(this); + northPanel.add(prompt); + contentPane.add(northPanel, BorderLayout.NORTH); + + tableModel = new TableModel(); + table = new JTable(tableModel); + // table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + table.setRowSelectionAllowed(true); + table.setColumnSelectionAllowed(false); + // table.setAutoCreateRowSorter(true); + tableModel.setColumnWidths(table.getColumnModel()); + GUI.scale(table); + + Dimension dim = new Dimension(TABLE_WIDTH, table.getRowHeight() * TABLE_ROWS); + table.setPreferredScrollableViewportSize(dim); + table.addKeyListener(this); + table.addMouseListener(this); + + // Auto-scroll table using keystrokes + table.addKeyListener(new KeyAdapter() { + public void keyTyped(final KeyEvent evt) { + if (evt.isControlDown() || evt.isMetaDown()) + return; + final int nRows = tableModel.getRowCount(); + final char ch = Character.toLowerCase(evt.getKeyChar()); + if (!Character.isLetterOrDigit(ch)) { + return; // Ignore searches for non alpha-numeric characters + } + final int sRow = table.getSelectedRow(); + for (int row = (sRow + 1) % nRows; row != sRow; row = (row + 1) % nRows) { + final String rowData = tableModel.getValueAt(row, 0).toString(); + final char rowCh = Character.toLowerCase(rowData.charAt(0)); + if (ch == rowCh) { + table.setRowSelectionInterval(row, row); + table.scrollRectToVisible(table.getCellRect(row, 0, true)); + break; + } + } + } + }); + + scrollPane = new JScrollPane(table); + if (initialSearch == null) + initialSearch = ""; + prompt.setText(initialSearch); + populateList(initialSearch); + contentPane.add(scrollPane, BorderLayout.CENTER); + + runButton = new JButton("Run"); + GUI.scale(runButton); + sourceButton = new JButton("Source"); + GUI.scale(sourceButton); + closeButton = new JButton("Close"); + GUI.scale(closeButton); + commandsButton = new JButton("Commands"); + GUI.scale(commandsButton); + helpButton = new JButton("Help"); + GUI.scale(helpButton); + runButton.addActionListener(this); + sourceButton.addActionListener(this); + closeButton.addActionListener(this); + commandsButton.addActionListener(this); + helpButton.addActionListener(this); + runButton.addKeyListener(this); + sourceButton.addKeyListener(this); + closeButton.addKeyListener(this); + commandsButton.addKeyListener(this); + helpButton.addKeyListener(this); + + JPanel southPanel = new JPanel(); + southPanel.setLayout(new BorderLayout()); + + JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); + optionsPanel.add(closeCheckBox); + optionsPanel.add(lutCheckBox); + + JPanel buttonsPanel = new JPanel(); + buttonsPanel.add(runButton); + buttonsPanel.add(sourceButton); + buttonsPanel.add(closeButton); + buttonsPanel.add(commandsButton); + buttonsPanel.add(helpButton); + + southPanel.add(optionsPanel, BorderLayout.CENTER); + southPanel.add(buttonsPanel, BorderLayout.SOUTH); + + contentPane.add(southPanel, BorderLayout.SOUTH); + + Rectangle screen = GUI.getMaxWindowBounds(IJ.getInstance()); + + frame.pack(); + + int dialogWidth = frame.getWidth(); + int dialogHeight = frame.getHeight(); + + Point pos = imageJ.getLocationOnScreen(); + Dimension size = imageJ.getSize(); + + /* + * Generally try to position the dialog slightly offset from the main + * ImageJ window, but if that would push the dialog off to the screen to + * any side, adjust it so that it's on the screen. + */ + int initialX = pos.x + 10; + int initialY = pos.y + 10 + size.height; + + initialX = Math.max(screen.x, Math.min(initialX, screen.x + screen.width - dialogWidth)); + initialY = Math.max(screen.y, Math.min(initialY, screen.y + screen.height - dialogHeight)); + + frame.setLocation(initialX, initialY); + frame.setVisible(true); + frame.toFront(); + } + + /* Make sure that clicks on the close icon close the window: */ + public void windowClosing(WindowEvent e) { + closeWindow(); + } + + private void closeWindow() { + if (frame != null) + frame.dispose(); + } + + public void windowActivated(WindowEvent e) { + if (IJ.isMacOSX() && frame != null) + frame.setMenuBar(Menus.getMenuBar()); + } + + public void windowDeactivated(WindowEvent e) { + } + + public void windowClosed(WindowEvent e) { + } + + public void windowOpened(WindowEvent e) { + } + + public void windowIconified(WindowEvent e) { + } + + public void windowDeiconified(WindowEvent e) { + } + + private class TableModel extends AbstractTableModel { + protected ArrayList list; + public final static int COLUMNS = 4; + + public TableModel() { + list = new ArrayList(); + } + + public void setData(ArrayList list) { + this.list = list; + fireTableDataChanged(); + } + + public int getColumnCount() { + return COLUMNS; + } + + public String getColumnName(int column) { + switch (column) { + case 0: + return "Command"; + case 1: + return "Menu Path"; + case 2: + return "Class"; + case 3: + return "File"; + } + return null; + } + + public int getRowCount() { + return list.size(); + } + + public Object getValueAt(int row, int column) { + if (row >= list.size() || column >= COLUMNS) + return null; + String[] strings = (String[]) list.get(row); + return strings[column]; + } + + public String getCommand(int row) { + if (row < 0 || row >= list.size()) + return ""; + else + return (String) getValueAt(row, 0); + } + + public void setColumnWidths(TableColumnModel columnModel) { + int[] widths = { 170, 150, 170, 30 }; + for (int i = 0; i < widths.length; i++) + columnModel.getColumn(i).setPreferredWidth(widths[i]); + } + + } + +} \ No newline at end of file diff --git a/src/ij/plugin/CommandLister.java b/src/ij/plugin/CommandLister.java new file mode 100644 index 0000000..fb994ce --- /dev/null +++ b/src/ij/plugin/CommandLister.java @@ -0,0 +1,85 @@ +package ij.plugin; +import ij.*; +import ij.text.*; +import ij.util.*; +import java.util.*; +import java.awt.*; +import java.awt.event.*; + +/** This class is used by the Plugins/Shortcuts/List Shortcuts + command to display a list keyboard shortcuts. */ +public class CommandLister implements PlugIn { + + public void run(String arg) { + if (arg.equals("shortcuts")) + listShortcuts(); + else + listCommands(); + } + + public void listCommands() { + Hashtable commands = Menus.getCommands(); + Vector v = new Vector(); + int index = 1; + for (Enumeration en=commands.keys(); en.hasMoreElements();) { + String command = (String)en.nextElement(); + v.addElement(index+"\t"+command+"\t"+(String)commands.get(command)); + index++; + } + String[] list = new String[v.size()]; + v.copyInto((String[])list); + showList("Commands", " \tCommand\tPlugin", list); + } + + public void listShortcuts() { + String[] shortcuts = getShortcuts(); + for (int i=0; i=200+65 && keyCode<=200+90) { + upperCase = true; + keyCode -= 200; + } + String shortcut = KeyEvent.getKeyText(keyCode); + if (!upperCase && shortcut.length()==1) { + char c = shortcut.charAt(0); + if (c>=65 && c<=90) + c += 32; + char[] chars = new char[1]; + chars[0] = c; + shortcut = new String(chars); + } + if (shortcut.length()>1) + shortcut = " " + shortcut; + v.addElement(shortcut+"\t"+(String)shortcuts.get(key)); + } + } + + void showList(String title, String headings, String[] list) { + Arrays.sort(list, String.CASE_INSENSITIVE_ORDER); + ArrayList list2 = new ArrayList(); + for (int i=0; i1) { + imp.setIgnoreFlush(true); + new FileSaver(imp).save(); + imp.setIgnoreFlush(false); + } else + new FileSaver(imp).save(); + } else + IJ.noImage(); + } + + void undo() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) + Undo.undo(); + else + IJ.noImage(); + } + + void close() { + ImagePlus imp = WindowManager.getCurrentImage(); + Window win = WindowManager.getActiveWindow(); + if (win==null || (Interpreter.isBatchMode() && win instanceof ImageWindow)) + closeImage(imp); + else if (win instanceof PlugInFrame && !"Commands".equals(((PlugInFrame)win).getTitle())) + ((PlugInFrame)win).close(); + else if (win instanceof PlugInDialog) + ((PlugInDialog)win).close(); + else if (win instanceof TextWindow) + ((TextWindow)win).close(); + else + closeImage(imp); + } + + /** Closes all image windows, or returns 'false' if the user cancels the unsaved changes dialog box. */ + public static boolean closeAll() { + int[] list = WindowManager.getIDList(); + if (list!=null) { + int imagesWithChanges = 0; + for (int i=0; i0 && !IJ.macroRunning()) { + GenericDialog gd = new GenericDialog("Close All"); + String msg = null; + String pronoun = null; + if (imagesWithChanges==1) { + msg = "There is one image"; + pronoun = "It"; + } else { + msg = "There are "+imagesWithChanges+" images"; + pronoun = "They"; + } + gd.addMessage(msg+" with unsaved changes. "+pronoun + +" will\nbe closed without being saved if you click \"OK\"."); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + } + Prefs.closingAll = true; + for (int i=0; iMacros>Open Startup Macros command + void openStartupMacros() { + Applet applet = IJ.getApplet(); + if (applet!=null) + IJ.run("URL...", "url="+IJ.URL+"/applet/StartupMacros.txt"); + else { + String path = IJ.getDirectory("macros")+"StartupMacros.txt"; + File f = new File(path); + if (!f.exists()) { + path = IJ.getDirectory("macros")+"StartupMacros.ijm"; + f = new File(path); + } + if (!f.exists()) { + path = IJ.getDirectory("macros")+"StartupMacros.fiji.ijm"; + f = new File(path); + } + if (!f.exists()) + IJ.error("\"StartupMacros.txt\" not found in ImageJ/macros/"); + else + IJ.open(path); + } + } + +} + + + diff --git a/src/ij/plugin/Compiler.java b/src/ij/plugin/Compiler.java new file mode 100644 index 0000000..a9ec95f --- /dev/null +++ b/src/ij/plugin/Compiler.java @@ -0,0 +1,451 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.io.*; +import ij.plugin.frame.Editor; +import ij.plugin.Macro_Runner; +import ij.plugin.filter.PlugInFilter; +import ij.plugin.filter.PlugInFilterRunner; +import java.awt.Font; +import java.lang.reflect.Method; +import java.io.*; +import java.util.*; +import javax.tools.*; + +/** Compiles and runs plugins using the javac compiler. */ +public class Compiler implements PlugIn, FilenameFilter { + + private static final String info = + "Library JAR files (e.g., imagescience.jar) should\n" + +"be located in either plugins/jars or plugins/lib.\n \n" + +"The javac command line will be displayed in\n" + +"the Log window if ImageJ is in debug mode."; + private static final int TARGET14=0, TARGET15=1, TARGET16=2, TARGET17=3, TARGET18=4, TARGET19=5; + private static final String[] targets = {"1.4", "1.5", "1.6", "1.7", "1.8", "1.9"}; + private static final String TARGET_KEY = "javac.target"; + private static CompilerTool compilerTool; + private static String dir, name; + private static Editor errors; + private static boolean generateDebuggingInfo; + private static int target = (int)Prefs.get(TARGET_KEY, TARGET18); + private static boolean checkForUpdateDone; + + public void run(String arg) { + if (arg.equals("edit")) + edit(); + else if (arg.equals("options")) + showDialog(); + else { + if (arg!=null && arg.length()>0 && !arg.endsWith(".java")) + IJ.error("Compiler", "File name must end with \".java\""); + else + compileAndRun(arg); + } + } + + void edit() { + if (open("", "Open macro or plugin")) { + Editor ed = (Editor)IJ.runPlugIn("ij.plugin.frame.Editor", ""); + if (ed!=null) ed.open(dir, name); + } + } + + void compileAndRun(String path) { + if (!open(path, "Compile and Run Plugin...")) + return; + if (name.endsWith(".class")) { + runPlugin(name.substring(0, name.length()-1)); + return; + } + if (!isJavac()) { + if (IJ.debugMode) IJ.log("Compiler: javac not found"); + if (!checkForUpdateDone) { + checkForUpdate("/plugins/compiler/Compiler.jar", "1.48c"); + checkForUpdateDone = true; + } + Object compiler = IJ.runPlugIn("Compiler", dir+name); + if (compiler==null) { + boolean ok = Macro_Runner.downloadJar("/plugins/compiler/Compiler.jar"); + if (ok) + IJ.runPlugIn("Compiler", dir+name); + } + return; + } + if (compile(dir+name)) + runPlugin(name); + } + + private void checkForUpdate(String plugin, String currentVersion) { + int slashIndex = plugin.lastIndexOf("/"); + if (slashIndex==-1 || !plugin.endsWith(".jar")) + return; + String className = plugin.substring(slashIndex+1, plugin.length()-4); + File f = new File(Prefs.getImageJDir()+"plugins"+File.separator+"jars"+File.separator+className+".jar"); + if (!f.exists() || !f.canWrite()) { + if (IJ.debugMode) IJ.log("checkForUpdate: jar not found ("+plugin+")"); + return; + } + String version = null; + try { + Class c = IJ.getClassLoader().loadClass("Compiler"); + version = "0.00a"; + Method m = c.getDeclaredMethod("getVersion", new Class[0]); + version = (String)m.invoke(null, new Object[0]); + } + catch (Exception e) {} + if (version==null) { + if (IJ.debugMode) IJ.log("checkForUpdate: class not found ("+className+")"); + return; + } + if (version.compareTo(currentVersion)>=0) { + if (IJ.debugMode) IJ.log("checkForUpdate: up to date ("+className+" "+version+")"); + return; + } + boolean ok = Macro_Runner.downloadJar(plugin); + if (IJ.debugMode) IJ.log("checkForUpdate: "+className+" "+version+" "+ok); + } + + boolean isJavac() { + if (compilerTool==null) + compilerTool=CompilerTool.getDefault(); + return compilerTool!=null; + } + + boolean compile(String path) { + IJ.showStatus("compiling "+path); + String classpath = getClassPath(path); + Vector options = new Vector(); + if (generateDebuggingInfo) + options.addElement("-g"); + validateTarget(); + options.addElement("-source"); + options.addElement(targets[target]); + options.addElement("-target"); + options.addElement(targets[target]); + options.addElement("-Xlint:unchecked"); + options.addElement("-deprecation"); + options.addElement("-classpath"); + options.addElement(classpath); + + Vector sources = new Vector(); + sources.add(path); + + if (IJ.debugMode) { + StringBuilder builder = new StringBuilder(); + builder.append("javac"); + for (int i=0; i< options.size(); i++){ + builder.append(" "); + builder.append(options.get(i)); + } + for (int i=0; i< sources.size(); i++){ + builder.append(" "); + builder.append(sources.get(i)); + } + IJ.log(builder.toString()); + } + + boolean errors = true; + String s = "not compiled"; + if (compilerTool != null) { + final StringWriter outputWriter = new StringWriter(); + errors = !compilerTool.compile(sources, options, outputWriter); + s = outputWriter.toString(); + } else { + errors = true; + } + + if (errors) + showErrors(s); + else + IJ.showStatus("done"); + return !errors; + } + + // Returns a string containing the Java classpath, + // the path to the directory containing the plugin, + // and paths to any .jar files in the plugins folder. + String getClassPath(String path) { + long start = System.currentTimeMillis(); + StringBuffer sb = new StringBuffer(); + sb.append(System.getProperty("java.class.path")); + File f = new File(path); + if (f!=null) // add directory containing file to classpath + sb.append(File.pathSeparator + f.getParent()); + String pluginsDir = Menus.getPlugInsPath(); + if (pluginsDir!=null) + addJars(pluginsDir, sb); + return sb.toString(); + } + + // Adds .jar files in plugins folder, and subfolders, to the classpath + void addJars(String path, StringBuffer sb) { + String[] list = null; + File f = new File(path); + if (f.exists() && f.isDirectory()) + list = f.list(); + if (list==null) + return; + boolean isJarsFolder = path.endsWith("jars")|| path.endsWith("lib"); + path = IJ.addSeparator(path); + for (int i=0; i0) { + directory = path.substring(0, i+1); + fileName = path.substring(i+1); + } else { + directory = ""; + fileName = path; + } + okay = true; + } + if (okay) { + name = fileName; + dir = directory; + Editor.setDefaultDirectory(dir); + } + return okay; + } + + // only show files with names ending in ".java" + // doesn't work with Windows + public boolean accept(File dir, String name) { + return name.endsWith(".java")||name.endsWith(".macro")||name.endsWith(".txt"); + } + + // run the plugin using a new class loader + void runPlugin(String name) { + name = name.substring(0,name.length()-5); // remove ".java" or ".clas" + new PlugInExecuter(name); + } + + public void showDialog() { + validateTarget(); + GenericDialog gd = new GenericDialog("Compile and Run"); + gd.addChoice("Target: ", targets, targets[target]); + gd.setInsets(15,5,0); + gd.addCheckbox("Generate debugging info (javac -g)", generateDebuggingInfo); + gd.addHelp(IJ.URL+"/docs/menus/edit.html#compiler"); + Font font = IJ.font10; + gd.addMessage(info, font); + gd.showDialog(); + if (gd.wasCanceled()) return; + target = gd.getNextChoiceIndex(); + generateDebuggingInfo = gd.getNextBoolean(); + validateTarget(); + } + + void validateTarget() { + if (target>TARGET19) + target = TARGET19; + if (targetTARGET16 && IJ.javaVersion()<7) + target = TARGET16; + if (target>TARGET17 && IJ.javaVersion()<8) + target = TARGET17; + if (target>TARGET18 && IJ.javaVersion()<9) + target = TARGET18; + Prefs.set(TARGET_KEY, target); + } + +} + +class PlugInExecuter implements Runnable { + private String plugin; + private Thread thread; + + /** Create a new object that runs the specified plugin + in a separate thread. */ + PlugInExecuter(String plugin) { + this.plugin = plugin; + thread = new Thread(this, plugin); + thread.setPriority(Math.max(thread.getPriority()-2, Thread.MIN_PRIORITY)); + thread.start(); + } + + public void run() { + IJ.resetEscape(); + IJ.runPlugIn("ij.plugin.ClassChecker", ""); + runCompiledPlugin(plugin); + } + + void runCompiledPlugin(String className) { + if (IJ.debugMode) IJ.log("Compiler: running \""+className+"\""); + IJ.resetClassLoader(); + ClassLoader loader = IJ.getClassLoader(); + Object thePlugIn = null; + try { + thePlugIn = (loader.loadClass(className)).newInstance(); + if (thePlugIn instanceof PlugIn) + ((PlugIn)thePlugIn).run(""); + else if (thePlugIn instanceof PlugInFilter) + new PlugInFilterRunner(thePlugIn, className, ""); + } + catch (ClassNotFoundException e) { + if (className.indexOf('_')!=-1) + IJ.error("Plugin or class not found: \"" + className + "\"\n(" + e+")"); + } + catch (NoClassDefFoundError e) { + String err = e.getMessage(); + if (IJ.debugMode) IJ.log("NoClassDefFoundError: "+err); + int index = err!=null?err.indexOf("wrong name: "):-1; + if (index>-1 && !className.contains(".")) { + String className2 = err.substring(index+12, err.length()-1); + className2 = className2.replace("/", "."); + if (className2.equals(className)) { // Java 9 error format different + int spaceIndex = err.indexOf(" "); + if (spaceIndex>-1) { + className2 = err.substring(0, spaceIndex); + className2 = className2.replace("/", "."); + } + } + if (className2.equals(className)) + IJ.error("Plugin not found: "+className2); + else + runCompiledPlugin(className2); + return; + } + if (className.indexOf('_')!=-1) + IJ.error("Plugin or class not found: \"" + className + "\"\n(" + e+")"); + } + catch (Exception e) { + //IJ.error(""+e); + IJ.handleException(e); //Marcel Boeglin 2013.09.01 + //Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, e); //IDE output + } + } + +} + +abstract class CompilerTool { + + public static class JavaxCompilerTool extends CompilerTool { + + public boolean compile(List sources, List options, StringWriter log) { + if (IJ.debugMode) IJ.log("Compiler: using javax.tool.JavaCompiler"); + try { + JavaCompiler javac = getJavac(); + DiagnosticCollector diagnostics = new DiagnosticCollector(); + StandardJavaFileManager fileManager = javac.getStandardFileManager(diagnostics, null, null); + Iterable compilationUnits = fileManager.getJavaFileObjectsFromStrings(sources); + JavaCompiler.CompilationTask task =javac.getTask(log, fileManager, null, options, null, compilationUnits); + fileManager.close(); + return task.call(); + } catch (Exception e) { + PrintWriter printer = new PrintWriter(log); + e.printStackTrace(printer); + printer.flush(); + } + return false; + } + + protected JavaCompiler getJavac() throws Exception { + JavaCompiler javac = ToolProvider.getSystemJavaCompiler(); + return javac; + } + } + + public static class LegacyCompilerTool extends CompilerTool { + protected static Class javacC; + + public boolean compile(List sources, List options, StringWriter log) { + if (IJ.debugMode) IJ.log("Compiler: using com.sun.tools.javac"); + try { + final String[] args = new String[sources.size() + options.size()]; + int argsIndex = 0; + for (int optionsIndex = 0; optionsIndex < options.size(); optionsIndex++) + args[argsIndex++] = (String) options.get(optionsIndex); + for (int sourcesIndex = 0; sourcesIndex < sources.size(); sourcesIndex++) + args[argsIndex++] = (String) sources.get(sourcesIndex); + PrintWriter printer = new PrintWriter(log); + Object javac = getJavac(); + Class[] compileTypes = new Class[] { String[].class, PrintWriter.class }; + Method compile = javacC.getMethod("compile", compileTypes); + Object result = compile.invoke(javac, new Object[] { args, printer }); + printer.flush(); + return Integer.valueOf(0).equals(result); + } catch (Exception e) { + e.printStackTrace(new PrintWriter(log)); + } + return false; + } + + protected Object getJavac() throws Exception { + if (javacC==null) + javacC = Class.forName("com.sun.tools.javac.Main"); + return javacC.newInstance(); + } + } + + public static CompilerTool getDefault() { + CompilerTool javax = new JavaxCompilerTool(); + if (javax.isSupported()) + return javax; + CompilerTool legacy = new LegacyCompilerTool(); + if (legacy.isSupported()) + return legacy; + return null; + } + + public abstract boolean compile(List sources, List options, StringWriter log); + + protected abstract Object getJavac() throws Exception; + + public boolean isSupported() { + try { + return null != getJavac(); + } catch (Exception e) { + return false; + } + } +} + diff --git a/src/ij/plugin/CompositeConverter.java b/src/ij/plugin/CompositeConverter.java new file mode 100644 index 0000000..0a81dd9 --- /dev/null +++ b/src/ij/plugin/CompositeConverter.java @@ -0,0 +1,111 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import java.awt.image.*; +import ij.plugin.frame.ContrastAdjuster; +import ij.macro.Interpreter; +import ij.plugin.frame.Recorder; + +/** This plugin implements the Image/Color/Make Composite command. */ +public class CompositeConverter implements PlugIn { + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + if (imp.isComposite()) { + CompositeImage ci = (CompositeImage)imp; + if (ci.getMode()!=IJ.COMPOSITE) { + ci.setMode(IJ.COMPOSITE); + ci.updateAndDraw(); + } + return; + } + int c = imp.getNChannels(); + int z = imp.getNSlices(); + int t = imp.getNFrames(); + if (imp.getBitDepth()==24) { + ImageWindow win = imp.getWindow(); + Point loc = win!=null?win.getLocation():null; + int slice = imp.getCurrentSlice(); + ImagePlus imp2 = makeComposite(imp); + if (loc!=null) ImageWindow.setNextLocation(loc); + imp2.show(); + imp.changes = false; + if (z*t==1) { + imp.hide(); + WindowManager.setCurrentWindow(imp2.getWindow()); + } else { + if (arg!=null && arg.equals("color")) + ((CompositeImage)imp2).setMode(IJ.COLOR); + imp2.setSlice(slice); + imp.close(); + } + if (IJ.isMacro() && !Interpreter.isBatchMode()) + IJ.wait(500); + } else if (c>=2 || (IJ.macroRunning()&&c>=1)) { + String[] modes = {"Composite", "Color", "Grayscale"}; + String mode = modes[0]; + if (c==1 && z*t>7) + mode = modes[2]; + GenericDialog gd = new GenericDialog("Make Composite"); + gd.addChoice("Display Mode:", modes, mode); + gd.showDialog(); + if (gd.wasCanceled()) return; + int index = gd.getNextChoiceIndex(); + CompositeImage ci = new CompositeImage(imp, index+1); + if (imp.getBitDepth()!=8) { + ci.reset(); + ci.resetDisplayRanges(); + } + ImageWindow win = imp.getWindow(); + Point location = win!=null?win.getLocation():null; + imp.hide(); + if (location!=null) + ImageWindow.setNextLocation(location); + if (IJ.isMacro()) + IJ.wait(250); + ci.show(); + } else + IJ.error("To create a composite, the current image must be\n a stack with at least 2 channels or be in RGB format."); + } + + public static ImagePlus makeComposite(ImagePlus imp) { + if (imp.getBitDepth()==24) { + if (Recorder.scriptMode()) + Recorder.recordCall("ImagePlus", "imp2 = CompositeConverter.makeComposite(imp);"); + return convertRGBToComposite(imp); + } else + return null; + } + + private static ImagePlus convertRGBToComposite(ImagePlus imp) { + if (imp.getBitDepth()!=24) + throw new IllegalArgumentException("RGB image or stack required"); + if (imp.getStackSize()==1) + return new CompositeImage(imp, IJ.COMPOSITE); + int width = imp.getWidth(); + int height = imp.getHeight(); + ImageStack stack1 = imp.getStack(); + int z = imp.getNSlices(); + int t = imp.getNFrames(); + int n = z*t; + ImageStack stack2 = new ImageStack(width, height); + for (int i=0; i1) + newImp = concatenateHyperstacks(images, newtitle, keep); + else + newImp = createHypervol(); + if (Recorder.scriptMode()) { + String args = "imp1"; + for (int i=1; i1 && frames==1; + boolean keepCalibration = true; + Calibration cal = images[0].getCalibration(); + maxWidth = width; + maxHeight = height; + + for (int i=1; i1) + concatSlices = false; + if (images[i].getBitDepth()!=bitDepth + || images[i].getNChannels()!=channels + || (!concatSlices && images[i].getNSlices()!=slices)) { + IJ.error(pluginName, "Images do not all have the same dimensions or type"); + return null; + } + Calibration cal2 = images[i].getCalibration(); + if (cal2.pixelWidth!=cal.pixelWidth + || cal2.pixelHeight!=cal.pixelHeight + || cal2.pixelDepth != cal.pixelDepth) + keepCalibration = false; + if (images[i].getWidth()>maxWidth) + maxWidth = images[i].getWidth(); + if (images[i].getHeight()>maxHeight) + maxHeight = images[i].getHeight(); + } + ImageStack stack2 = new ImageStack(maxWidth, maxHeight); + int slices2=0, frames2=0; + for (int i=0;i1) { + int mode = 0; + if (images[0].isComposite()) + mode = ((CompositeImage)images[0]).getMode(); + imp2 = new CompositeImage(imp2, mode); + ((CompositeImage)imp2).copyLuts(images[0]); + } + if (channels>1 && frames2>1) + imp2.setOpenAsHyperStack(true); + if (keepCalibration) + imp2.setCalibration(cal); + if (!keep) { + for (int i=0; i= nImages) break; // reached the 'none' string or handled all images (in case of all_windows) + if (! titles[index].equals("")) { + tmpStrArr[count] = titles[index]; + tmpImpArr[count] = WindowManager.getImage(wList[index]); + count++; + } + } + if (count<2) { + IJ.error(pluginName, "Please select at least 2 images"); + return false; + } + + imageTitles = new String[count]; + images = new ImagePlus[count]; + System.arraycopy(tmpStrArr, 0, imageTitles, 0, count); + System.arraycopy(tmpImpArr, 0, images, 0, count); + return true; + } + + // test if this imageplus appears again in the list + boolean isDuplicated(ImagePlus imp, int index) { + int length = images.length; + if (index >= length - 1) return false; + for (int i = index + 1; i < length; i++) { + if (imp == images[i]) return true; + } + return false; + } + + public void itemStateChanged(ItemEvent ie) { + Choice c; + if (ie.getSource() == allWindows) { // User selected / unselected 'all windows' button + int count = 0; + if (allWindows.getState()) { + for (Enumeration e = choices.elements() ; e.hasMoreElements() ;) { + c = (Choice)e.nextElement(); + c.select(count++); + c.setEnabled(false); + } + } else { + for (Enumeration e = choices.elements() ; e.hasMoreElements() ;) { + c = (Choice)e.nextElement(); + c.setEnabled(true); + } + } + } else { // User image selection triggered event + boolean foundNone = false; + // All image choices after an occurance of 'none' are reset to 'none' + for (Enumeration e = choices.elements() ; e.hasMoreElements() ;) { + c = (Choice)e.nextElement(); + if (! foundNone) { + c.setEnabled(true); + if (c.getSelectedItem().equals(none)) foundNone = true; + } else { // a previous choice was 'none' + c.select(none); + c.setEnabled(false); + } + } + } + } + + public void setIm5D(boolean bool) { + im4D_option = bool; + im4D = bool; + } + + private void findMaxDimensions(ImagePlus[] images) { + boolean first = true; + ImagePlus currentImp = null; + int dataType = 0; + maxWidth = maxHeight = 0; + for (int i = 0; i < images.length; i++) { + if (images[i] != null) { + currentImp = images[i]; + if (first) { + dataType = currentImp.getType(); + first = false; + } + if (currentImp.getType() != dataType) + continue; + if (currentImp.getWidth()>maxWidth) + maxWidth = currentImp.getWidth(); + if (currentImp.getHeight()>maxHeight) + maxHeight = currentImp.getHeight(); + } + } + } + +} diff --git a/src/ij/plugin/ContrastEnhancer.java b/src/ij/plugin/ContrastEnhancer.java new file mode 100644 index 0000000..baab9e9 --- /dev/null +++ b/src/ij/plugin/ContrastEnhancer.java @@ -0,0 +1,369 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.*; +import java.awt.*; + +/** Implements ImageJ's Process/Enhance Contrast command. */ +public class ContrastEnhancer implements PlugIn, Measurements { + static final double defaultSaturated = 0.35; + static double gSaturated = defaultSaturated; + static boolean gEqualize; + double saturated = defaultSaturated; + int max, range; + boolean classicEqualization; + int stackSize; + boolean updateSelectionOnly; + boolean equalize, normalize, processStack, useStackHistogram, entireImage; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + stackSize = imp.getStackSize(); + imp.trimProcessor(); + if (!showDialog(imp)) + return; + Roi roi = imp.getRoi(); + if (roi!=null) roi.endPaste(); + if (stackSize==1) + Undo.setup(Undo.TRANSFORM, imp); + else + Undo.reset(); + if (equalize) + equalize(imp); + else + stretchHistogram(imp, saturated); + if (normalize) { + ImageProcessor ip = imp.getProcessor(); + ip.setMinAndMax(0,ip.getBitDepth()==32?1.0:ip.maxValue()); + } + imp.updateAndDraw(); + } + + boolean showDialog(ImagePlus imp) { + String options = IJ.isMacro()?Macro.getOptions():null; + if (options!=null && options.contains("normalize_all")) + Macro.setOptions(options.replaceAll("normalize_all", "process_all")); + boolean isMacro = options!=null; + if (!isMacro) { + equalize = gEqualize; + saturated = gSaturated; + } + int bitDepth = imp.getBitDepth(); + boolean composite = imp.isComposite(); + if (composite) stackSize = 1; + Roi roi = imp.getRoi(); + boolean areaRoi = roi!=null && roi.isArea() && !composite; + GenericDialog gd = new GenericDialog("Enhance Contrast"); + gd.addNumericField("Saturated pixels:", saturated, 1, 4, "%"); + if (bitDepth!=24 && !composite) + gd.addCheckbox("Normalize", normalize); + if (areaRoi) { + String label = bitDepth==24?"Update entire image":"Update all when normalizing"; + gd.addCheckbox(label, entireImage); + } + gd.addCheckbox("Equalize histogram", equalize); + if (stackSize>1) { + if (!composite) + gd.addCheckbox("Process_all "+stackSize+" slices", processStack); + gd.addCheckbox("Use stack histogram", useStackHistogram); + } + gd.addHelp(IJ.URL+"/docs/menus/process.html#enhance"); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + saturated = gd.getNextNumber(); + if (bitDepth!=24 && !composite) + normalize = gd.getNextBoolean(); + else + normalize = false; + if (areaRoi) { + entireImage = gd.getNextBoolean(); + updateSelectionOnly = !entireImage; + if (!normalize && bitDepth!=24) + updateSelectionOnly = false; + } + equalize = gd.getNextBoolean(); + processStack = stackSize>1?gd.getNextBoolean():false; + useStackHistogram = stackSize>1?gd.getNextBoolean():false; + if (saturated<0.0) saturated = 0.0; + if (saturated>100.0) saturated = 100; + if (processStack && !equalize) + normalize = true; + if (!isMacro) { + gEqualize = equalize; + gSaturated = saturated; + } + return true; + } + + public void stretchHistogram(ImagePlus imp, double saturated) { + ImageStatistics stats = null; + if (useStackHistogram) + stats = new StackStatistics(imp); + if (processStack) { + ImageStack stack = imp.getStack(); + int size = this.stackSize==0?stack.size():this.stackSize; + for (int i=1; i<=size; i++) { + IJ.showProgress(i, size); + ImageProcessor ip = stack.getProcessor(i); + ip.setRoi(imp.getRoi()); + if (!useStackHistogram) + stats = ImageStatistics.getStatistics(ip, MIN_MAX, null); + stretchHistogram(ip, saturated, stats); + } + } else { + ImageProcessor ip = imp.getProcessor(); + ip.setRoi(imp.getRoi()); + if (stats==null) + stats = ImageStatistics.getStatistics(ip, MIN_MAX, null); + if (imp.isComposite()) + stretchCompositeImageHistogram((CompositeImage)imp, saturated, stats); + else + stretchHistogram(ip, saturated, stats); + } + } + + public void stretchHistogram(ImageProcessor ip, double saturated) { + useStackHistogram = false; + stretchHistogram(new ImagePlus("", ip), saturated); + } + + public void stretchHistogram(ImageProcessor ip, double saturated, ImageStatistics stats) { + int[] a = getMinAndMax(ip, saturated, stats); + int hmin=a[0], hmax=a[1]; + if (hmax>hmin) { + double min = stats.histMin+hmin*stats.binSize; + double max = stats.histMin+hmax*stats.binSize; + if (stats.histogram16!=null && ip instanceof ShortProcessor) { + min = hmin; + max = hmax; + } + if (!updateSelectionOnly) + ip.resetRoi(); + if (normalize) + normalize(ip, min, max); + else { + if (updateSelectionOnly) { + ImageProcessor mask = ip.getMask(); + if (mask!=null) ip.snapshot(); + ip.setMinAndMax(min, max); + if (mask!=null) ip.reset(mask); + } else + ip.setMinAndMax(min, max); + } + } + } + + void stretchCompositeImageHistogram(CompositeImage imp, double saturated, ImageStatistics stats) { + ImageProcessor ip = imp.getProcessor(); + int[] a = getMinAndMax(ip, saturated, stats); + int hmin=a[0], hmax=a[1]; + if (hmax>hmin) { + double min = stats.histMin+hmin*stats.binSize; + double max = stats.histMin+hmax*stats.binSize; + if (stats.histogram16!=null && imp.getBitDepth()==16) { + min = hmin; + max = hmax; + } + imp.setDisplayRange(min, max); + } + /* + int channels = imp.getNChannels();b + int channel = imp.getChannel(); + int slice = imp.getSlice(); + int frame = imp.getFrame(); + for (int c=1; c<=channels; c++) { + imp.setPosition(c, slice, frame); + ImageProcessor ip = imp.getProcessor(); + int[] a = getMinAndMax(ip, saturated, stats); + int hmin=a[0], hmax=a[1]; + if (hmax>hmin) { + double min = stats.histMin+hmin*stats.binSize; + double max = stats.histMin+hmax*stats.binSize; + imp.setDisplayRange(min, max); + } + } + imp.setPosition(channel, slice, frame); + */ + } + + int[] getMinAndMax(ImageProcessor ip, double saturated, ImageStatistics stats) { + int hmin, hmax; + int threshold; + int[] histogram = stats.histogram; + if (stats.histogram16!=null && ip instanceof ShortProcessor) + histogram = stats.histogram16; + int hsize = histogram.length; + if (saturated>0.0) + threshold = (int)(stats.pixelCount*saturated/200.0); + else + threshold = 0; + int i = -1; + boolean found = false; + int count = 0; + int maxindex = hsize-1; + do { + i++; + count += histogram[i]; + found = count>threshold; + } while (!found && ithreshold; + } while (!found && i>0); + hmax = i; + int[] a = new int[2]; + a[0]=hmin; a[1]=hmax; + return a; + } + + void normalize(ImageProcessor ip, double min, double max) { + int min2 = 0; + int max2 = 255; + int range = 256; + if (ip instanceof ShortProcessor) + {max2 = 65535; range=65536;} + else if (ip instanceof FloatProcessor) + normalizeFloat(ip, min, max); + int[] lut = new int[range]; + for (int i=0; i=max) + lut[i] = max2; + else + lut[i] = (int)(((double)(i-min)/(max-min))*max2); + } + applyTable(ip, lut); + } + + void applyTable(ImageProcessor ip, int[] lut) { + if (updateSelectionOnly) { + ImageProcessor mask = ip.getMask(); + if (mask!=null) ip.snapshot(); + ip.applyTable(lut); + if (mask!=null) ip.reset(mask); + } else + ip.applyTable(lut); + } + + void normalizeFloat(ImageProcessor ip, double min, double max) { + double scale = max>min?1.0/(max-min):1.0; + int size = ip.getWidth()*ip.getHeight(); + float[] pixels = (float[])ip.getPixels(); + double v; + for (int i=0; i1.0) v = 1.0; + pixels[i] = (float)v; + } + } + + public void equalize(ImagePlus imp) { + if (imp.getBitDepth()==32) { + IJ.showMessage("Contrast Enhancer", "Equalization of 32-bit images not supported."); + return; + } + classicEqualization = IJ.altKeyDown(); + int[] histogram = null; + if (useStackHistogram) { + ImageStatistics stats = new StackStatistics(imp); + histogram = stats.histogram; + if (stats.histogram16!=null && imp.getBitDepth()==16) + histogram = stats.histogram16; + } + if (processStack) { + ImageStack stack = imp.getStack(); + int size = this.stackSize==0?stack.size():this.stackSize; + for (int i=1; i<=size; i++) { + IJ.showProgress(i, size); + ImageProcessor ip = stack.getProcessor(i); + if (histogram==null) + histogram = ip.getHistogram(); + equalize(ip, histogram); + } + } else { + ImageProcessor ip = imp.getProcessor(); + if (histogram==null) + histogram = ip.getHistogram(); + equalize(ip, histogram); + } + if (imp.getBitDepth()==16 && processStack && imp.getStackSize()>1) { + ImageStack stack = imp.getStack(); + ImageProcessor ip = stack.getProcessor(stack.size()/2); + ImageStatistics stats = ip.getStats(); + imp.getProcessor().setMinAndMax(stats.min, stats.max); + } else + imp.getProcessor().resetMinAndMax(); + } + + /** + Changes the tone curves of images. + It should bring up the detail in the flat regions of your image. + Histogram Equalization can enhance meaningless detail and hide + important but small high-contrast features. This method uses a + similar algorithm, but uses the square root of the histogram + values, so its effects are less extreme. Hold the alt key down + to use the standard histogram equalization algorithm. + This code was contributed by Richard Kirk (rak@cre.canon.co.uk). + */ + public void equalize(ImageProcessor ip) { + equalize(ip, ip.getHistogram()); + } + + private void equalize(ImageProcessor ip, int[] histogram) { + ip.resetRoi(); + if (ip instanceof ShortProcessor) { // Short + max = 65535; + range = 65535; + } else { //bytes + max = 255; + range = 255; + } + double sum; + sum = getWeightedValue(histogram, 0); + for (int i=1; i + */ +public class ControlPanel implements PlugIn { + + /** The platform-specific file separator string.*/ + private static final String fileSeparator=System.getProperty("file.separator"); + + /** The platform-specific file separator character. */ + private static final char sep=fileSeparator.charAt(0); + + private Hashtable panels = new Hashtable(); + private Vector visiblePanels = new Vector(); + private Vector expandedNodes = new Vector(); + private String defaultArg = ""; + + private boolean savePropsUponClose=true; + private boolean propertiesChanged=true; + private boolean closeChildPanelOnExpand = true; + private boolean requireDoubleClick; + private boolean quitting = true; + + Vector menus = new Vector(); + Vector allMenus = new Vector(); + Hashtable commands=new Hashtable(); + Hashtable menuCommands=new Hashtable(); + String[] pluginsArray; + Hashtable treeCommands = new Hashtable(); + int argLength; + + private String path=null; + private DefaultMutableTreeNode root; + + MenuItem reloadMI = null; + + public ControlPanel() { + //requireDoubleClick = !(IJ.isWindows() || IJ.isMacintosh()); + Java2.setSystemLookAndFeel(); + } + + + /** Creates a panel with the hierarchical tree structure of ImageJ's commands. */ + public void run(String arg) { + load(); + } + + + /* *********************************************************************** */ + /* Tree logic */ + /* *********************************************************************** */ + + synchronized void load() { + commands = Menus.getCommands(); + pluginsArray = Menus.getPlugins(); + root=doRootFromMenus(); + if (root==null || root.getChildCount()==0 ) return; // do nothing if there's no tree or a root w/o children + loadProperties(); + restoreVisiblePanels(); + if (panels.isEmpty()) + newPanel(root); + } + + /** Builds up a root tree from ImageJ's menu bar. + * The root tree replicates ImageJ's menu bar with its menus and their submenus. + * Delegates to the {@link recursesubMenu(Menu, DefaultMutableTreeNode} method to gather the root children. + * + */ + private synchronized DefaultMutableTreeNode doRootFromMenus() { + DefaultMutableTreeNode node = new DefaultMutableTreeNode("ImageJ Menus"); + if(argLength==0) node.setUserObject("Control Panel"); + MenuBar menuBar = Menus.getMenuBar(); + for (int i=0; iij.Menus.getCommands() + * except for the "Reload Plugins" menu item, for which a local action command string is assigned + * to avoid clashes with the action fired from ImageJ Plugins->Utilties->Reload Plugins + * menu item.
+ * Note: this method bypasses the tree buildup based on the + * {@link populateNode(Hashtable,DefaultMutableTreeNode)} method. + * @param menu The Menu instance to be searched recursively for menu items + * @param node The DefaultMutableTreeNode corresponding to the Menu menu argument. + */ + private void recurseSubMenu(Menu menu, DefaultMutableTreeNode node) { + int items = menu.getItemCount(); + if(items==0) return; + for (int i=0; inode argument with items retrieved from the collection. + * Actually, the method delegates to {@link populateNode{String[], String[], DefaultMutableTreeNode)}. + * @param collection Hashtable with `keys' (java.lang.String) representing a tree path which + * is to be added to this node, but excludes it; the path extends to the last child (leaf node). + * The `values' are (java.lang.String) as labels and user objects for the last child of the path. + * Actually, null elements for labels are allowed, in which case + * the last child will be constructed on the last token in the `key'. + * @param node The TreeNode to be populated. + */ + private void populateNode(Hashtable collection, DefaultMutableTreeNode node) { + Vector labelVector = new Vector(); + for (Enumeration e=collection.keys(); e.hasMoreElements();) { + String key = (String)e.nextElement(); + labelVector.addElement(key); + } + String[] labels = new String[labelVector.size()]; + String[] items = new String[labelVector.size()]; + labelVector.copyInto((String[])labels); // keys into labels[] + StringSorter.sort(labels); + for(int i=0; inode argument with items retrieved from the two String[] arguments. + * Delegates indirectly to {@link buildTreePath(String.String,String,DefaultMutableTreeNode)} to do the job. + * If either arguments are empty (i.e., length=0) or have different sizes, the method does nothing. + * @param items String array where each element is the source of a tree path (see {@see buildTreePath(String, String, DefaultMutableTreeNode)} + * and {@see buildTreePath(String,String,String,DefaultMutableTreeNode)}. + * @param labels String array where each element is the label of the root of the tree path + * @param node The TreeNode to be populated + */ + private void populateNode(String[] items, String[] labels, DefaultMutableTreeNode node) { + if (items.length==0 || items.length!=labels.length) return; + String label=null; + for (int i=0; ibuildTreePath method. + * Calls buildTreePath(source,label,null,topNode). + * @see buildTreePath(String,String,String,DefaultMutableTreeNode). + * + */ + private void buildTreePath(String source, String label, DefaultMutableTreeNode topNode) { + buildTreePath(source, label, null, topNode); + } + + /**Builds up a tree path structure. + * Populates the node argument with a tree path constructed as described below: + * @param source String to be parsed in a tree path; must be composed of tokens delimited by "/" + * @param label The label (String) of the leaf node for this path. If null + * then the leafe nod will be constructed from the last token. + * @param command The command string of the action event fired upon clicking the leaf node. + * If null, then the last token will be taken as action command. + * @param topNode The DefaulMutableTreeNode to which this path will be added + */ + private void buildTreePath(String source, String label, String command, DefaultMutableTreeNode topNode) { + String local=source; // will contain the string to be parsed into the tree path + String argument=""; // will store any argument for the plugin + String delimiter = fileSeparator; // meaning `/' + + // 1. store plugin arguments (the string between parentheses) in `argument' + // then store the rest into `local' + // if there aren't any plugin arguments, then local remains the same as `source' + int leftParen=source.indexOf('('); + int rightParen = source.indexOf(')'); + if (leftParen>-1 && rightParen>leftParen) { + argument = source.substring(leftParen+1, rightParen); + local = source.substring(0,leftParen); + } + // 2. maybe `local' was passed in from some plugin class finder, and is prefixed by + // full path of the plugins directory; if so, then remove this prefix + String pluginsPath=Menus.getPlugInsPath(); + if (local.startsWith(pluginsPath)) + local = local.substring(pluginsPath.length(),local.length()); + // 3. convert package/class separators into file separators, + // to allow parsing into tree path later + local=local.replace('.',delimiter.charAt(0)); + // 4. append the plugin arguments, but with file separator so that to the same + // plugin with different arguments will show up as children of the same tree branch + if (argument.length()>0) + local=local.concat(fileSeparator).concat(argument); + + DefaultMutableTreeNode node=null; + + // 5. parse the tree path: this code is entirely different from the logic in TreePanel, + // so don't hold your breath: + // split the string into tokens delimited by file separator + // and iterate through the tokens adding an intermediate subnode for each token + // + // use the name of the token for intermediate nodes, and the `label' argument for + // the leaf node if not null; else use the last token for the leaf node + // + // for leaf node replace `_' with ` ' and trim away the `.class' extension + StringTokenizer pathParser = new StringTokenizer(local,delimiter); + int tokens = pathParser.countTokens(); + while(pathParser.hasMoreTokens()) { + String token = pathParser.nextToken(); + tokens--; + if (topNode.isLeaf()&&topNode.getAllowsChildren()) { + if(token.indexOf("ControlPanel")==-1) {// avoid showing this up in the tree + // when at leaf level the user object for the node is the `label' if not null, + // else is the `token' + if(tokens==0) { + if(label!=null) token=label; // if label is not null use it instead of token + token=token.replace('_',' '); + if(token.endsWith(".class")) + token = token.substring(0,token.length()-6);//... + } + + node = new DefaultMutableTreeNode(token); // finally, construct the node + + // when at leaf level, store the `command' (or the `token' if `command' is null) + // into our internal table + if (tokens==0) { + String cmd = (command==null) ? token : command; + if(treeCommands==null) treeCommands = new Hashtable(); + if(!treeCommands.containsKey(token)) treeCommands.put(token,cmd); + } + // add this node to the top node, then make it top node and continue iteration + topNode.add(node); + topNode=node; + } + continue; + } else { + // this node may have been visited before, so we avoid duplicate nodes + // by recursing through the tree until we find a token that has not been + // "made" into a node + boolean hasTokenAsNode=false; + Enumeration nodes = topNode.children(); + while(nodes.hasMoreElements()) { + node = (DefaultMutableTreeNode)nodes.nextElement(); + if(((String)node.getUserObject()).equals(token)) { + hasTokenAsNode = true; + topNode = node; + break; + } + } + if (!hasTokenAsNode) { + if (token.indexOf("ControlPanel")==-1) { + if (tokens==0) {// we're at leaf level + if(label!=null) token = label; + token=token.replace('_',' '); + if (token.endsWith(".class")) + token=token.substring(0,token.length()-6); // ... + } + node = new DefaultMutableTreeNode(token); + topNode.add(node); + topNode=node; + } + } + } + } + } + + /* *********************************************************************** */ + /* Factories */ + /* *********************************************************************** */ + + /**Constructs a TreePanel rooted at the node argument. + * + */ + TreePanel newPanel(DefaultMutableTreeNode node) { + boolean main = node.getUserObject().equals(root.getUserObject()); + TreePanel panel = new TreePanel(node, this, main); + return panel; + } + + TreePanel newPanel(DefaultMutableTreeNode node, Point location) { + boolean main = node.getUserObject().equals(root.getUserObject()); + TreePanel panel = new TreePanel(node, this, main, location); + return panel; + } + /**Constructs a TreePanel rooted at the path. + * + * @param s A string with the structure "[item1,item2,...,itemn]", as returned by + * a call to the toString() method in the javax.swing.tree.TreePath class. + * + */ + TreePanel newPanel(String path) { + if (path.equals("Control_Panel.@Main")) path = "Control_Panel"; + path = key2pStr(path); + TreePanel pnl = null; + for(Enumeration e = root.breadthFirstEnumeration(); e.hasMoreElements();) { + DefaultMutableTreeNode n = (DefaultMutableTreeNode)e.nextElement(); + TreePath p = new TreePath(n.getPath()); + if (p.toString().equals(path)) + pnl=newPanel(n); + } + return pnl; + } + + /* *************************************************************************** */ + /* Various Accessors */ + /* *************************************************************************** */ + + boolean requiresDoubleClick() {return requireDoubleClick;} + + void setDoubleClick(boolean dc) {requireDoubleClick = dc;} + + boolean hasPanelForNode(DefaultMutableTreeNode node) { + TreePath path = new TreePath(node.getPath()); + return panels.containsKey(pStr2Key(path.toString())); + } + + TreePanel getPanelForNode(DefaultMutableTreeNode node) { + TreePath path = new TreePath(node.getPath()); + String pathString = path.toString(); + if (panels.containsKey(pStr2Key(pathString))) + return (TreePanel)panels.get(pStr2Key(pathString)); + //else return newPanel(node); + else return null; + } + + public DefaultMutableTreeNode getRoot() {return root;} + + Hashtable getPanels() {return panels;} + + Hashtable getTreeCommands() { + return treeCommands; + } + + boolean hasVisiblePanels() { + return visiblePanels.size()>0; + } + + int getVisiblePanelsCount() { return visiblePanels.size(); } + + + + /* ************************************************************************** */ + /* Properties and panels management */ + /* ************************************************************************** */ + + void registerPanel(TreePanel panel) { + String key = pStr2Key(panel.getRootPath().toString()); + panels.put(key,panel); + setPanelShowingProperty(panel.getRootPath().toString()); + propertiesChanged=true; + } + + /** All properties related to the ControlPanel have keywords starting with "Control_Panel". + * This is to facilitate the integration of these properties with ImageJ's properties database. + * The keywords are dot-separated lists of tokens; in each token, spaces are relaced by + * underscores. Each token represents a node, and hence the keyword represents a tree path. + * The values can be either: + *
    + *
  1. a space-separated list of integers, + * indicating panel frame geometry in the following fixed order: + * x coordinate, y coordinate , width, height
  2. + *
  3. or the word "expand" which indicates that the path represented by the keyword is + * an expanded branch + *
  4. + *
+ * + */ + void loadProperties() { + if (IJ.debugMode) IJ.log("CP.loadProperties"); + visiblePanels.removeAllElements(); + expandedNodes.removeAllElements(); + panels.clear(); + Properties properties = Prefs.getControlPanelProperties(); + for (Enumeration e=properties.keys(); e.hasMoreElements();) { + String key = (String)e.nextElement(); + if (key.startsWith(".Control_Panel.")) { + key = key.substring(1, key.length()); + String val = Prefs.get(key, null); + if (IJ.debugMode) IJ.log(" "+key+": "+val); + if (Character.isDigit(val.charAt(0))) // value starts with digit + visiblePanels.addElement(key); + else if (val.equals("expand")) + expandedNodes.addElement(key); + } + } + } + + void saveProperties() { + if (IJ.debugMode) IJ.log("CP.saveProperties: "+propertiesChanged); + if (propertiesChanged) { + clearProperties(); + for (Enumeration e=visiblePanels.elements(); e.hasMoreElements();) { + String s = (String)e.nextElement(); + TreePanel p = (TreePanel)panels.get(s); + if (p!=null) recordGeometry(p); + } + for(Enumeration e=expandedNodes.elements(); e.hasMoreElements();) + Prefs.set((String)e.nextElement(),"expand"); + } + propertiesChanged=false; + } + + void clearProperties() { + Properties properties = Prefs.getControlPanelProperties(); + for (Enumeration e=properties.keys(); e.hasMoreElements();) { + String key = (String)e.nextElement(); + if (key.startsWith(".Control_Panel.")) + properties.remove(key); + } + } + + void setExpandedStateProperty(String item) { + String s = pStr2Key(item); + expandedNodes.addElement(s); + propertiesChanged=true; + } + + boolean hasExpandedStateProperty(String item) { + String s = pStr2Key(item); + return expandedNodes.contains(s); + } + + void unsetExpandedStateProperty(String item) { + String s = pStr2Key(item); + expandedNodes.remove(s); + propertiesChanged=true; + } + + void setPanelShowingProperty(String item) { + String s = pStr2Key(item); + if (!(visiblePanels.contains(s))) + visiblePanels.addElement(s); + propertiesChanged=true; + } + + void unsetPanelShowingProperty(String item) { + String s = pStr2Key(item); + visiblePanels.remove(s); + } + + boolean hasPanelShowingProperty(String item) { + String s = pStr2Key(item); + return visiblePanels.contains(s); + } + + void restoreVisiblePanels() { + String[] visPanls = new String[visiblePanels.size()]; + visiblePanels.toArray(visPanls); + Arrays.sort(visPanls); + for (int i=0; itoString() method in the javax.swing.tree.TreePath class. +// * @return A string with the structure "item1,item2,...,itemn". +// * @see javax.swing.tree.TreePath +// */ +// String trimPathString(String s) +// { +// int leftBracket = s.indexOf("["); +// int rightBracket = s.indexOf("]"); +// return s.substring(leftBracket+1,rightBracket); +// } + + void showHelp() { + IJ.showMessage("About Control Panel...", + "This plugin displays a panel with ImageJ commands in a hierarchical tree structure.\n"+" \n"+ + "Usage:\n"+" \n"+ + " Click on a leaf node to launch the corresponding ImageJ command (or plugin)\n"+ + " (double-click on X Window Systems)\n"+" \n"+ + " Double-click on a tree branch node (folder) to expand or collapse it\n"+" \n"+ + " Click and drag on a tree branch node (folder) to display its descendants,\n"+ + " in a separate (child) panel (\"tear-off\" mock-up)\n"+" \n"+ + " In a child panel, use the \"Show Parent\" menu item to re-open the parent panel\n"+ + " if it was accidentally closed\n"+" \n"+ + "Author: Cezar M. Tigaret (c.tigaret@ucl.ac.uk)\n"+ + "This code is in the public domain." + ); + } + + + // 1. trim away the eclosing brackets + // 2. replace comma-space with dots + // 3. replace spaces with underscores + String pStr2Key(String pathString) { + String keyword = pathString; + if(keyword.startsWith("[")) + keyword = keyword.substring(keyword.indexOf("[")+1,keyword.length()); + if(keyword.endsWith("]")) + keyword = keyword.substring(0,keyword.lastIndexOf("]")); + StringTokenizer st = new StringTokenizer(keyword,","); + String result = ""; + while(st.hasMoreTokens()) { + String token = st.nextToken(); + if(token.startsWith(" ")) token = token.substring(1,token.length()); // remove leading space + result+=token+"."; + } + result = result.substring(0,result.length()-1);//remove trailing dot + result = result.replace(' ','_'); + return result; + } + + String key2pStr(String keyword) { + //keyword = keyword.replace('_',' '); // restore the spaces from underscores + StringTokenizer st = new StringTokenizer(keyword,"."); + String result = ""; + while(st.hasMoreTokens()) { + String token = st.nextToken(); + result += token +", "; + } + result = result.substring(0,result.length()-2); // trim away the ending comma-space + result = "["+result+"]"; + result = result.replace('_', ' '); + return result; + } + + + // Thank you, Wayne! + /** Breaks the specified string into an array + of ints. Returns null if there is an error.*/ + public int[] s2ints(String s) { + StringTokenizer st = new StringTokenizer(s, ", \t"); + int nInts = st.countTokens(); + if (nInts==0) return null; + int[] ints = new int[nInts]; + for(int i=0; i +* This class lays out the ImageJ user plugins in a vertical, hierarchical tree.. Plugins are launched by double-clicking on their names, in the vertical tree. +* +* Advantages: uses less screen estate, and provides a realistic graphical presentation of the plugins installed in the file system.
+* +* Created: Thu Nov 23 02:12:12 2000 +* @see ControlPanel +* @author Cezar M. Tigaret +* @version 1.0f +*/ + + +class TreePanel implements + ActionListener, WindowListener, TreeExpansionListener, TreeWillExpandListener { + + ControlPanel pcp; + //Vector childrenPanels = new Vector(); + boolean isMainPanel; + String title; + boolean isDragging=false; + //boolean requireDoubleClick=false; + Point defaultLocation; + + private JTree pTree; + private JMenuBar pMenuBar; + private DefaultMutableTreeNode root; + private DefaultMutableTreeNode draggingNode=null; + private DefaultTreeModel pTreeModel; + private ActionListener listener; + private JFrame pFrame; + private JCheckBoxMenuItem pMenu_saveOnClose, pMenu_noClutter; + private TreePath rootPath; + + // the "up" arrow + private static final int _uparrow1_data[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x0d,0x0e,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x0d,0x01,0x01,0x0d,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x01,0x0e,0x02,0x01,0x03,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x01,0x0e,0x04,0x05,0x06,0x01,0x07,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x08,0x04,0x09,0x0e,0x02,0x06,0x01, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x08,0x04,0x09,0x0e,0x0e,0x0e, + 0x02,0x06,0x01,0x00,0x00,0x00,0x00,0x00,0x08,0x08,0x04,0x09,0x0e,0x0e, + 0x0e,0x0e,0x0e,0x02,0x06,0x02,0x00,0x00,0x00,0x08,0x0a,0x0e,0x08,0x0a, + 0x0b,0x0b,0x0c,0x0c,0x0c,0x0c,0x0c,0x06,0x02,0x00,0x0e,0x01,0x01,0x01, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x0e,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00 + }; + + private static final int _uparrow1_ctable[] = { + 0x21,0xff000000,0xff303030,0xffaaaaaa,0xffffffff,0xff3c3c3c,0xff252525,0xffb6b6b6,0xff585858,0xffc3c3c3,0xff222222,0xff2b2b2b,0xff2e2e2e,0xffa0a0a0, + 0xff808080 + }; + + private static IndexColorModel iconCM = new IndexColorModel(8,_uparrow1_ctable.length,_uparrow1_ctable,0,true,255,DataBuffer.TYPE_BYTE); + private static final ImageIcon upIcon = new ImageIcon( Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(16,16,iconCM,_uparrow1_data,0,16))); + + public TreePanel (DefaultMutableTreeNode root, ControlPanel pcp, boolean isMainPanel) { + new TreePanel(root,pcp,isMainPanel,null); + } + + public TreePanel(DefaultMutableTreeNode root, ControlPanel pcp, boolean isMainPanel, Point location) { + this.root=root; + this.pcp=pcp; + this.isMainPanel = isMainPanel; + defaultLocation = location; + rootPath=new TreePath(root.getPath()); + title = (String)root.getUserObject(); + buildTreePanel(); + pcp.registerPanel(this); + } + + /* ************************************************************************** */ + /* GUI factories */ + /* ************************************************************************** */ + + public void buildTreePanel() { + pFrame=new JFrame(title); + pFrame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + pTreeModel = new DefaultTreeModel(root); + pTree=new JTree(pTreeModel); + pTree.setEditable(false); + pTree.putClientProperty("JTree.lineStyle","Angled"); + pTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + pTree.setRootVisible(false); + pTree.setShowsRootHandles(true); + //pTree.setDragEnabled(true); + JScrollPane ptView=new JScrollPane(pTree); + addMenu(); + pFrame.getContentPane().add(ptView, BorderLayout.CENTER); + addListeners(); + pFrame.pack(); + if (defaultLocation!=null) { + if (IJ.debugMode) IJ.log("CP.buildTreePanel: "+defaultLocation); + pFrame.setLocation(defaultLocation.x, defaultLocation.y); + } else + pcp.restoreGeometry(this); + //restoreExpandedNodes(); + if (pFrame.getLocation().x==0) + GUI.center(pFrame); + setVisible(); + ImageJ ij = IJ.getInstance(); + ij.addWindowListener(this); + pFrame.addKeyListener(ij); + pTree.addKeyListener(ij); + } + + void addMenu() { + pMenuBar=new JMenuBar(); + Insets ins = new Insets(0,0,0,10); + pMenuBar.setMargin(ins); + if (isMainPanel) { + JMenuItem helpMI = new JMenuItem("Help"); + helpMI.addActionListener(this); + helpMI.setActionCommand("Help"); + pMenuBar.add(helpMI); +/* if(pcp.reloadMI!=null) + { + pcp.reloadMI.addActionListener(this); + pMenuBar.add(pcp.reloadMI); + }*/ + } + else { + JMenuItem spMI = new JMenuItem("Show Parent",upIcon); + spMI.addActionListener(this); + spMI.setActionCommand("Show Parent"); + pMenuBar.add(spMI); + } + pFrame.setJMenuBar(pMenuBar); + } + + void addListeners() { + addActionListener(this); + pFrame.addWindowListener(this); + pFrame.addComponentListener(new ComponentAdapter() { + public void componentMoved(ComponentEvent e) { + Rectangle r = e.getComponent().getBounds(); + if (IJ.debugMode) IJ.log("CP.componentMoved: "+r); + if (r.x>0) { + defaultLocation = new Point(r.x, r.y); + recordGeometry(); + } + } + }); + pTree.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + isDragging = false; + if (e.getClickCount()!=2) + return; + int selRow=pTree.getRowForLocation(e.getX(),e.getY()); + if (selRow!=-1) toAction(); + } + + public void mouseReleased(MouseEvent e) { + if (isDragging) { + Point pnt = new Point(e.getX(), e.getY()); + SwingUtilities.convertPointToScreen(pnt,pTree); + tearOff(null,pnt); + } + isDragging = false; + } + }); + pTree.addMouseMotionListener(new MouseMotionAdapter() + { + public void mouseDragged(MouseEvent e) + { + int selRow = pTree.getRowForLocation(e.getX(), e.getY()); + if(selRow!=-1) + { + if(((DefaultMutableTreeNode)pTree.getLastSelectedPathComponent()).isLeaf()) return; + pFrame.setCursor(new Cursor(Cursor.MOVE_CURSOR)); + isDragging = true; + } + } + }); + pTree.addTreeExpansionListener(this); + pTree.addTreeWillExpandListener(this); + } + + /* ************************************************************************** */ + /* Accessors -- see also Properties management section */ + /* ************************************************************************** */ + + public String getTitle() {return title;} + + public TreePath getRootPath() {return rootPath;} + + public boolean isTheMainPanel() {return isMainPanel;} + + public JFrame getFrame() {return pFrame;} + + public JTree getTree() {return pTree;} + + public DefaultMutableTreeNode getRootNode() {return root;} + + public Point getDefaultLocation() {return defaultLocation;} + + /* ************************************************************************** */ + /* Properties managmenent */ + /* ************************************************************************** */ + + boolean isVisible() { + return pFrame.isVisible(); + } + + void setBounds(int x, int y, int w, int h) { + pFrame.setBounds(new Rectangle(x,y,w,h)); + defaultLocation = new Point(x,y); + } + + void setAutoSaveProps(boolean autoSave) { + if(isTheMainPanel()) pMenu_saveOnClose.setSelected(autoSave); + } + + boolean getAutoSaveProps() {return pMenu_saveOnClose.isSelected();} + + void restoreExpandedNodes() { + if (pTree==null || root==null) + return; + pTree.removeTreeExpansionListener(this); + TreeNode[] rootPath = root.getPath(); + for(Enumeration e = root.breadthFirstEnumeration(); e.hasMoreElements();) + //for(Enumeration e = root.children(); e.hasMoreElements();) + { + DefaultMutableTreeNode node = (DefaultMutableTreeNode)e.nextElement(); + if(!node.isLeaf() && node != root) + { + TreeNode[] nodePath = node.getPath(); + TreePath nTreePath = new TreePath(nodePath); + String npS = nTreePath.toString(); + DefaultMutableTreeNode[] localPath = new DefaultMutableTreeNode[nodePath.length-rootPath.length+1]; + for(int i=0; i0) return; + String aCmd=nde.toString(); + String cmd= aCmd; + if(pcp.treeCommands.containsKey(aCmd)) + cmd = (String)pcp.treeCommands.get(aCmd); + processEvent(new ActionEvent(this,ActionEvent.ACTION_PERFORMED,cmd)); + } + + void setVisible() { + if (pFrame!=null && !pFrame.isVisible()) { + restoreExpandedNodes(); + if (defaultLocation!=null) pFrame.setLocation(defaultLocation); + pFrame.setVisible(true); + // close expanded path to this panel in its parent panel (if visible and if the path is expanded) + DefaultMutableTreeNode parent = (DefaultMutableTreeNode)root.getParent(); + if (parent!=null) { + TreePanel pnl = pcp.getPanelForNode(parent); + if (pnl!=null && pnl.isVisible()) { + TreeNode[] rPath = root.getPath(); + TreeNode[] pPath = pnl.getRootNode().getPath(); + DefaultMutableTreeNode[] tPath = new DefaultMutableTreeNode[rPath.length-pPath.length+1]; + for(int i=0; i>"); + } else + IJ.noImage(); + } + + /** Converts the ImagePlus to the specified image type. The string + argument corresponds to one of the labels in the Image/Type submenu + ("8-bit", "16-bit", "32-bit", "8-bit Color", "RGB Color", "RGB Stack", "HSB Stack", "Lab Stack" or "HSB (32-bit)"). */ + public void convert(String item) { + int type = imp.getType(); + ImageStack stack = null; + if (imp.getStackSize()>1) + stack = imp.getStack(); + String msg = "Converting to " + item; + IJ.showStatus(msg + "..."); + long start = System.currentTimeMillis(); + Roi roi = imp.getRoi(); + imp.deleteRoi(); + if (imp.isThreshold()) + imp.getProcessor().resetThreshold(); + boolean saveChanges = imp.changes; + imp.changes = IJ.getApplet()==null; //if not applet, set 'changes' flag + ImageWindow win = imp.getWindow(); + try { + if (stack!=null) { + boolean wasVirtual = stack.isVirtual(); + // do stack conversions + if (stack.isRGB() && item.equals("RGB Color")) { + new ImageConverter(imp).convertRGBStackToRGB(); + if (win!=null) new ImageWindow(imp, imp.getCanvas()); // replace StackWindow with ImageWindow + } else if (stack.isHSB() && item.equals("RGB Color")) { + new ImageConverter(imp).convertHSBToRGB(); + if (win!=null) new ImageWindow(imp, imp.getCanvas()); + } else if (stack.isHSB32() && item.equals("RGB Color")) { + new ImageConverter(imp).convertHSB32ToRGB(); + if (win!=null) new ImageWindow(imp, imp.getCanvas()); + } else if (stack.isLab() && item.equals("RGB Color")) { + new ImageConverter(imp).convertLabToRGB(); + if (win!=null) new ImageWindow(imp, imp.getCanvas()); + } else if (item.equals("8-bit")) + new StackConverter(imp).convertToGray8(); + else if (item.equals("16-bit")) + new StackConverter(imp).convertToGray16(); + else if (item.equals("32-bit")) + new ImageConverter(imp).convertToGray32(); + else if (item.equals("RGB Color")) + new StackConverter(imp).convertToRGB(); + else if (item.equals("RGB Stack")) + new StackConverter(imp).convertToRGBHyperstack(); + else if (item.equals("HSB Stack")) + new StackConverter(imp).convertToHSBHyperstack(); + else if (item.equals("HSB (32-bit)")) + new StackConverter(imp).convertToHSB32Hyperstack(); + else if (item.equals("Lab Stack")) + new StackConverter(imp).convertToLabHyperstack(); + else if (item.equals("8-bit Color")) { + int nColors = getNumber(); + if (nColors!=0) + new StackConverter(imp).convertToIndexedColor(nColors); + } else throw new IllegalArgumentException(); + if (wasVirtual) imp.setTitle(imp.getTitle()); + } else { + // do single image conversions + Undo.setup(Undo.TYPE_CONVERSION, imp); + ImageConverter ic = new ImageConverter(imp); + if (item.equals("8-bit")) + ic.convertToGray8(); + else if (item.equals("16-bit")) + ic.convertToGray16(); + else if (item.equals("32-bit")) + ic.convertToGray32(); + else if (item.equals("RGB Stack")) { + Undo.reset(); // Reversible; no need for Undo + ic.convertToRGBStack(); + } else if (item.equals("HSB Stack")) { + Undo.reset(); + ic.convertToHSB(); + } else if (item.equals("HSB (32-bit)")) { + Undo.reset(); + ic.convertToHSB32(); + } else if (item.equals("Lab Stack")) { + Undo.reset(); + ic.convertToLab(); + } else if (item.equals("RGB Color")) { + ic.convertToRGB(); + } else if (item.equals("8-bit Color")) { + int nColors = getNumber(); + start = System.currentTimeMillis(); + if (nColors!=0) + ic.convertRGBtoIndexedColor(nColors); + } else { + imp.changes = saveChanges; + return; + } + IJ.showProgress(1.0); + } + if ("RGB Color".equals(item)) + imp.setProp(LUT.nameKey,null); + } + catch (IllegalArgumentException e) { + unsupportedConversion(imp); + IJ.showStatus(""); + Undo.reset(); + imp.changes = saveChanges; + Menus.updateMenus(); + Macro.abort(); + return; + } + if (roi!=null) + imp.setRoi(roi); + IJ.showTime(imp, start, ""); + imp.repaintWindow(); + Menus.updateMenus(); + } + + void unsupportedConversion(ImagePlus imp) { + IJ.error("Converter", + "Supported Conversions:\n" + + " \n" + + "8-bit -> 16-bit*\n" + + "8-bit -> 32-bit*\n" + + "8-bit -> RGB Color*\n" + + "16-bit -> 8-bit*\n" + + "16-bit -> 32-bit*\n" + + "16-bit -> RGB Color*\n" + + "32-bit -> 8-bit*\n" + + "32-bit -> 16-bit\n" + + "32-bit -> RGB Color*\n" + + "8-bit Color -> 8-bit (grayscale)*\n" + + "8-bit Color -> RGB Color\n" + + "RGB -> 8-bit (grayscale)*\n" + + "RGB -> 8-bit Color*\n" + + "RGB -> RGB Stack*\n" + + "RGB -> HSB Stack*\n" + + "RGB -> Lab Stack\n" + + "RGB Stack -> RGB Color\n" + + "HSB Stack -> RGB Color\n" + + " \n" + + "* works with stacks\n" + ); + } + + int getNumber() { + if (imp.getType()!=ImagePlus.COLOR_RGB) + return 256; + GenericDialog gd = new GenericDialog("MedianCut"); + gd.addNumericField("Number of Colors (2-256):", 256, 0); + gd.showDialog(); + if (gd.wasCanceled()) + return 0; + int n = (int)gd.getNextNumber(); + if (n<2) n = 2; + if (n>256) n = 256; + return n; + } + +} diff --git a/src/ij/plugin/Coordinates.java b/src/ij/plugin/Coordinates.java new file mode 100644 index 0000000..0a6f728 --- /dev/null +++ b/src/ij/plugin/Coordinates.java @@ -0,0 +1,187 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.measure.Calibration; +import ij.plugin.frame.Recorder; +import java.awt.AWTEvent; +import java.awt.geom.Rectangle2D; + +/** + * The plugin implements the Image/Adjust/Coordinates command. It allows + * the user to set the corner coordinates of the selection bounds or the full image. + * This modifies the image scale (pixelWidth, pixelHeight) and the xOrigin, yOrigin. + * If a single point is selected, the coordinates of the point can be specified, which only + * sets the xOrigin and yOrigin. + * Units for x and y can be also selected (2016-08-30, Michael Schmid). + * Z unit of stacks can be selected (2019-09-30, Stein Rorvik). + */ + +public class Coordinates implements PlugIn, DialogListener { + + private static final String help = "" + +"

Image>Adjust>Coordinates

" + +"" + +"This command allows the user to set the corner coordinates of
the selection bounds " + +"or the full image. This modifies the image
scale (pixelWidth, pixelHeight) and xOrigin and yOrigin. " + +"If a
single point is selected, the coordinates of the point can be
specified, which only " + +"sets xOrigin and yOrigin. The units for X
and Y can be also selected.
" + +"
"; + + private final static String SAME_AS_X = ""; + private final static int IMAGE = 0, ROI_BOUNDS = 1, POINT = 2; //mode: coordinates of what to specify + private int mode = IMAGE; + private boolean isStack; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + int imageHeight = imp.getHeight(); + Calibration cal = imp.getCalibration(); + Roi roi = imp.getRoi(); + Rectangle2D.Double bounds = null; + int numSlices = imp.getNSlices(); + int currSlice = imp.getCurrentSlice(); + isStack = numSlices>1; + if (roi != null) { + bounds = roi.getFloatBounds(); + if (bounds.width==0 && bounds.height==0) + mode = POINT; + else + mode = ROI_BOUNDS; + } else { //no Roi, use image bounds + bounds = new Rectangle2D.Double(0, 0, imp.getWidth(), imp.getHeight()); + } + String title = (mode==IMAGE ? "Image" : "Selection") +" Coordinates"; + if (mode == POINT) + title = "Point Coordinates"; + GenericDialog gd = new GenericDialog(title); + if (mode == POINT) { + gd.addNumericField("X:", cal.getX(bounds.x), 2, 8, ""); + gd.addNumericField("Y:", cal.getY(bounds.y, imageHeight), 2, 8, ""); + if (isStack) + gd.addNumericField("Z:", cal.getZ(currSlice-1), 2, 8, ""); + } else { + gd.addNumericField("Left:", cal.getX(bounds.x), 2, 8, ""); + gd.addNumericField("Right:", cal.getX(bounds.x+bounds.width), 2, 8, ""); + gd.addNumericField("Top:", cal.getY(bounds.y, imageHeight), 2, 8, ""); + gd.addNumericField("Bottom:", cal.getY(bounds.y+bounds.height, imageHeight), 2, 8, ""); + if (isStack) { + gd.addNumericField("Front:", cal.getZ(0), 2, 8, ""); + gd.addNumericField("Back:", cal.getZ(numSlices), 2, 8, ""); + } + } + String xUnit = cal.getUnit(); + String yUnit = cal.getYUnit(); + String zUnit = cal.getZUnit(); + boolean xUnitChanged=false,yUnitChanged=false,zUnitChanged=false; + gd.addStringField("X_unit:", xUnit, 18); + gd.addStringField("Y_unit:", yUnit.equals(xUnit) ? SAME_AS_X : yUnit, 18); + if (isStack) + gd.addStringField("Z_unit:", zUnit.equals(xUnit) ? SAME_AS_X : zUnit, 18); + gd.addHelp(help); + gd.addDialogListener(this); + gd.showDialog(); + if (gd.wasCanceled()) + return; + if (mode == POINT) { + double x = gd.getNextNumber(); + double y = gd.getNextNumber(); + if (gd.invalidNumber()) { + IJ.error("Invalid number"); + return; + } + cal.xOrigin = coordinate2offset(x, bounds.x, cal.pixelWidth); + cal.yOrigin = coordinate2offset(y, bounds.y, cal.getInvertY() ? -cal.pixelHeight : cal.pixelHeight); + if (isStack) { + double z = gd.getNextNumber(); + if (gd.invalidNumber()) { + IJ.error("Invalid number"); + return; + } + cal.zOrigin = coordinate2offset(z, currSlice-1, cal.pixelDepth); + } + } else { + double xl = gd.getNextNumber(); + double xr = gd.getNextNumber(); + double yt = gd.getNextNumber(); + double yb = gd.getNextNumber(); + if (gd.invalidNumber()) { + IJ.error("Invalid number"); + return; + } + cal.pixelWidth = (xr-xl)/bounds.width; + cal.pixelHeight = (yb-yt)/bounds.height; + cal.xOrigin = coordinate2offset(xl, bounds.x, cal.pixelWidth); + cal.yOrigin = coordinate2offset(yt, bounds.y, cal.pixelHeight); + cal.setInvertY(cal.pixelHeight < 0); + if (isStack) { + double zf = gd.getNextNumber(); + double zl = gd.getNextNumber(); + cal.pixelDepth = (zl-zf)/numSlices; + cal.zOrigin = coordinate2offset(zf, 0, cal.pixelDepth); + } + if (cal.pixelHeight < 0) + cal.pixelHeight = -cal.pixelHeight; + } + String xUnit2 = gd.getNextString(); + xUnitChanged = !xUnit2.equals(xUnit); + if (xUnitChanged) + cal.setXUnit(xUnit2); + String yUnit2 = gd.getNextString(); + yUnitChanged = !yUnit2.equals(yUnit) && !yUnit2.equals(SAME_AS_X); + if (yUnitChanged) + cal.setYUnit(yUnit2); + String zUnit2 = null; + if (isStack) { + zUnit2 = gd.getNextString(); + zUnitChanged = !zUnit2.equals(zUnit) && !zUnit2.equals(SAME_AS_X); + if (zUnitChanged) + cal.setZUnit(zUnit2); + } + ImageWindow win = imp.getWindow(); + imp.repaintWindow(); + if (Recorder.record) { + if (Recorder.scriptMode()) { + if (xUnitChanged) + Recorder.recordCall("imp.getCalibration().setXUnit(\""+xUnit2+"\");", true); + if (yUnitChanged) + Recorder.recordCall("imp.getCalibration().setYUnit(\""+yUnit2+"\");", true); + if (zUnitChanged) + Recorder.recordCall("imp.getCalibration().setZUnit(\""+zUnit2+"\");", true); + } else { + if (xUnitChanged) + Recorder.record("Stack.setXUnit", xUnit2); + if (yUnitChanged) + Recorder.record("Stack.setYUnit", yUnit2); + if (zUnitChanged) + Recorder.record("Stack.setZUnit", zUnit2); + } + } + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (mode == POINT) { + gd.getNextNumber(); + gd.getNextNumber(); + if (isStack) + gd.getNextNumber(); + return (!gd.invalidNumber()); + } else { + double xl = gd.getNextNumber(); + double xr = gd.getNextNumber(); + double yt = gd.getNextNumber(); + double yb = gd.getNextNumber(); + if (isStack) { + double zf = gd.getNextNumber(); + double zl = gd.getNextNumber(); + return (!gd.invalidNumber() && (xr>xl) && (yt!=yb) && (zl>zf)); + } else + return (!gd.invalidNumber() && xr>xl && yt!=yb); + } + } + + // Calculates pixel offset from scaled coordinates of a point with given pixel position + private double coordinate2offset(double coordinate, double pixelPos, double pixelSize) { + return pixelPos - coordinate/pixelSize; + } + +} \ No newline at end of file diff --git a/src/ij/plugin/DICOM.java b/src/ij/plugin/DICOM.java new file mode 100644 index 0000000..04effb5 --- /dev/null +++ b/src/ij/plugin/DICOM.java @@ -0,0 +1,1727 @@ +package ij.plugin; +import java.io.*; +import java.util.*; +import java.net.URL; +import ij.*; +import ij.io.*; +import ij.process.*; +import ij.util.Tools; +import ij.measure.Calibration; + +/** This plugin decodes DICOM files. If 'arg' is empty, it + displays a file open dialog and opens and displays the + image selected by the user. If 'arg' is a path, it opens the + specified image and the calling routine can display it using + "((ImagePlus)IJ.runPlugIn("ij.plugin.DICOM", path)).show()". + */ + +/* RAK (Richard Kirk, rak@cre.canon.co.uk) changes 14/7/99 + + InputStream.skip() looped to check the actual number of + bytes is read. + + Big/little-endian options on element length. + + Explicit check for each known VR to make mistaken identifications + of explicit VR's less likely. + + Variables b1..b4 renamed as b0..b3. + + Increment of 4 to offset on (7FE0,0010) tag removed. + + Queries on some other unrecognized tags. + Anyone want to claim them? + + RAK changes 15/7/99 + + Bug fix on magic values for explicit VRs with 32-bit lengths. + + Various bits of tidying up, including... + 'location' incremented on read using getByte() or getString(). + simpler debug mode message generation (values no longer reported). + + Added z pixel aspect ratio support for multi-slice DICOM volumes. + Michael Abramoff, 31-10-2000 + + Added DICOM tags to the dictionary (now contains about 2700 tags). + implemented getDouble() for VR = FD (Floating Double) and getFloat() + for VR = FL (Floating Single). + Extended case statement in getHeaderInfo to retrieve FD and FL values. + Johannes Hermen, Christian Moll, 25-04-2008 + + */ + +public class DICOM extends ImagePlus implements PlugIn { + private boolean showErrors = true; + private boolean gettingInfo; + private BufferedInputStream inputStream; + private String info; + + /** Default constructor. */ + public DICOM() { + } + + /** Constructs a DICOM reader that using an InputStream. Here + is an example that shows how to open and display a DICOM: +
+ */ + void makeInterpolationArrays(int[] smallIndices, float[] weights, int length, int smallLength, int shrinkFactor) { + for (int i=0; i= smallLength-1) smallIndex = smallLength - 2; + smallIndices[i] = smallIndex; + float distance = (i + 0.5f)/shrinkFactor - (smallIndex + 0.5f); //distance of pixel centers (in smallImage pixels) + weights[i] = 1f - distance; + } + } + + // C O M M O N S E C T I O N F O R B O T H A L G O R I T H M S + + /** Replace the pixels by the mean or maximum in a 3x3 neighborhood. + * No snapshot is required (less memory needed than e.g., fp.smooth()). + * When used as maximum filter, it returns the average change of the + * pixel value by this operation + */ + double filter3x3(FloatProcessor fp, int type) { + int width = fp.getWidth(); + int height = fp.getHeight(); + double shiftBy = 0; + float[] pixels = (float[])fp.getPixels(); + for (int y=0; y v3 ? v1 : v3; + if (v2 > max) max = v2; + shiftBy += max - v2; + pixels[p] = max; + } else + pixels[p] = (v1+v2+v3)*0.33333333f; + } + return shiftBy; + } + + + public void setNPasses(int nPasses) { + if (isRGB && separateColors) + nPasses *= 3; + if (useParaboloid) + nPasses*= (doPresmooth) ? DIRECTION_PASSES+2 : DIRECTION_PASSES; + this.nPasses = nPasses; + pass = 0; + } + + private void showProgress(double percent) { + if (nPasses <= 0) return; + percent = (double)pass/nPasses + percent/nPasses; + IJ.showProgress(percent); + } +} + +// C L A S S R O L L I N G B A L L + +/** A rolling ball (or actually a square part thereof) + * Here it is also determined whether to shrink the image + */ +class RollingBall { + + float[] data; + int width; + int shrinkFactor; + + RollingBall(double radius) { + int arcTrimPer; + if (radius<=10) { + shrinkFactor = 1; + arcTrimPer = 24; // trim 24% in x and y + } else if (radius<=30) { + shrinkFactor = 2; + arcTrimPer = 24; // trim 24% in x and y + } else if (radius<=100) { + shrinkFactor = 4; + arcTrimPer = 32; // trim 32% in x and y + } else { + shrinkFactor = 8; + arcTrimPer = 40; // trim 40% in x and y + } + buildRollingBall(radius, arcTrimPer); + } + + /** Computes the location of each point on the rolling ball patch relative to the + center of the sphere containing it. The patch is located in the top half + of this sphere. The vertical axis of the sphere passes through the center of + the patch. The projection of the patch in the xy-plane below is a square. + */ + void buildRollingBall(double ballradius, int arcTrimPer) { + double rsquare; // rolling ball radius squared + int xtrim; // # of pixels trimmed off each end of ball to make patch + int xval, yval; // x,y-values on patch relative to center of rolling ball + double smallballradius; // radius of rolling ball (downscaled in x,y and z when image is shrunk) + int halfWidth; // distance in x or y from center of patch to any edge (patch "radius") + + this.shrinkFactor = shrinkFactor; + smallballradius = ballradius/shrinkFactor; + if (smallballradius<1) + smallballradius = 1; + rsquare = smallballradius*smallballradius; + xtrim = (int)(arcTrimPer*smallballradius)/100; // only use a patch of the rolling ball + halfWidth = (int)Math.round(smallballradius - xtrim); + width = 2*halfWidth+1; + data = new float[width*width]; + + for (int y=0, p=0; y0. ? (float)(Math.sqrt(temp)) : 0f; + //-Float.MAX_VALUE might be better than 0f, but gives different results than earlier versions + } + } + +} diff --git a/src/ij/plugin/filter/Benchmark.java b/src/ij/plugin/filter/Benchmark.java new file mode 100644 index 0000000..56d706d --- /dev/null +++ b/src/ij/plugin/filter/Benchmark.java @@ -0,0 +1,105 @@ +package ij.plugin.filter; +import java.awt.*; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.text.*; +/** Implements the Plugins/Utilities/Run Benchmark command when + the current image is 512x512 and RGB. Results and additional + benchmarks are available at
+ "http://imagej.nih.gov/ij/plugins/benchmarks.html". */ +public class Benchmark implements PlugInFilter{ + + String arg; + ImagePlus imp; + boolean showUpdates= true; + int counter; + + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + return DOES_ALL+NO_CHANGES+SNAPSHOT; + } + + public void run(ImageProcessor ip) { + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + ip.setInterpolate(false); + for (int i=0; i <4; i++) { + ip.invert(); + updateScreen(imp); + } + for (int i=0; i <4; i++) { + ip.flipVertical(); + updateScreen(imp); + } + ip.flipHorizontal(); updateScreen(imp); + ip.flipHorizontal(); updateScreen(imp); + for (int i=0; i <6; i++) { + ip.smooth(); + updateScreen(imp); + } + ip.reset(); + for (int i=0; i <6; i++) { + ip.sharpen(); + updateScreen(imp); + } + ip.reset(); + ip.smooth(); updateScreen(imp); + ip.findEdges(); updateScreen(imp); + ip.invert(); updateScreen(imp); + ip.autoThreshold(); updateScreen(imp); + ip.reset(); + ip.medianFilter(); updateScreen(imp); + for (int i=0; i <360; i +=15) { + ip.reset(); + ip.rotate(i); + updateScreen(imp); + } + double scale = 1.5; + for (int i=0; i <8; i++) { + ip.reset(); + ip.scale(scale, scale); + updateScreen(imp); + scale = scale*1.5; + } + for (int i=0; i <12; i++) { + ip.reset(); + scale = scale/1.5; + ip.scale(scale, scale); + updateScreen(imp); + } + ip.reset(); + updateScreen(imp); + } + + void updateScreen(ImagePlus imp) { + if (showUpdates) + imp.updateAndDraw(); + IJ.showStatus((counter++) + "/"+72); + } + + /* + void showBenchmarkResults() { + TextWindow tw = new TextWindow("ImageJ Benchmark", "", 450, 450); + tw.setFont(new Font("Monospaced", Font.PLAIN, 12)); + tw.append("Time in seconds needed to perform 62 image processing"); + tw.append("operations on the 512x512 \"Mandrill\" image"); + tw.append("---------------------------------------------------------"); + tw.append(" 1.6 Pentium 4/3.0, WinXP Java 1.3.1"); + tw.append(" 2.4 PPC G5/2.0x2, MacOSX Java 1.3.1"); + tw.append(" 3.3 Pentium 4/1.4, Win2K IE 5.0"); + tw.append(" 5.3 Pentium 3/750, Win98 IE 5.0"); + tw.append(" 5.6 Pentium 4/1.4, Win2K JDK 1.3"); + tw.append(" 6.0 Pentium 3/750, Win98 Netscape 4.7"); + tw.append(" 8.6 PPC G4/400, MacOS MRJ 2.2"); + tw.append(" 11 Pentium 2/400, Win95 JRE 1.1.8"); + tw.append(" 14 PPC G3/300, MacOS MRJ 2.1"); + tw.append(" 38 PPC 604/132, MacOS MRJ 2.1"); + tw.append(" 89 Pentium/100, Win95 JRE 1.1.6"); + tw.append(" 96 Pentium/400, Linux Sun JDK 1.2.2 (17 with JIT)"); + tw.append(""); + } + */ + +} + + diff --git a/src/ij/plugin/filter/Binary.java b/src/ij/plugin/filter/Binary.java new file mode 100644 index 0000000..27163e6 --- /dev/null +++ b/src/ij/plugin/filter/Binary.java @@ -0,0 +1,191 @@ +package ij.plugin.filter; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.plugin.frame.ThresholdAdjuster; +import java.awt.*; + +/** Implements the Erode, Dilate, Open, Close, Outline, Skeletonize + and Fill Holes commands in the Process/Binary submenu. + Gabriel Landini contributed the clever binary fill algorithm + that fills holes in objects by filling the background. + Version 2009-06-23 preview added, interations can be aborted by escape (Michael Schmid) +*/ +public class Binary implements ExtendedPlugInFilter, DialogListener { + static final int MAX_ITERATIONS = 100; + static final String NO_OPERATION = "Nothing"; + static final String[] outputTypes = {"Overwrite", "8-bit", "16-bit", "32-bit"}; + static final String[] operations = {NO_OPERATION, "Erode", "Dilate", "Open", "Close", "Outline", "Fill Holes", "Skeletonize"}; + + //parameters / options + static int iterations = 1; //iterations for erode, dilate, open, close + static int count = 1; //nearest neighbor count for erode, dilate, open, close + String operation = NO_OPERATION; //for dialog; will be copied to 'arg' for actual previewing + + String arg; + ImagePlus imp; //null if only setting options with no preview possibility + PlugInFilterRunner pfr; + boolean doOptions; //whether options dialog is required + boolean previewing; + boolean escapePressed; + int foreground, background; + int flags = DOES_8G | DOES_8C | SUPPORTS_MASKING | PARALLELIZE_STACKS | KEEP_PREVIEW | KEEP_THRESHOLD; + int nPasses; + + public int setup(String arg, ImagePlus imp) { + this.arg = arg; + IJ.register(Binary.class); + doOptions = arg.equals("options"); + if (doOptions) { + if (imp == null) return NO_IMAGE_REQUIRED; //options dialog does not need a (suitable) image + ImageProcessor ip = imp.getProcessor(); + if (!(ip instanceof ByteProcessor)) return NO_IMAGE_REQUIRED; + if (!((ByteProcessor)ip).isBinary()) return NO_IMAGE_REQUIRED; + } + return flags; + } + + public int showDialog (ImagePlus imp, String command, PlugInFilterRunner pfr) { + if (doOptions) { + this.imp = imp; + this.pfr = pfr; + GenericDialog gd = new GenericDialog("Binary Options"); + gd.addNumericField("Iterations (1-"+MAX_ITERATIONS+"):", iterations, 0, 3, ""); + gd.addNumericField("Count (1-8):", count, 0, 3, ""); + gd.addCheckbox("Black background", Prefs.blackBackground); + gd.addCheckbox("Pad edges when eroding", Prefs.padEdges); + gd.addChoice("EDM output:", outputTypes, outputTypes[EDM.getOutputType()]); + if (imp != null) { + gd.addChoice("Do:", operations, operation); + gd.addPreviewCheckbox(pfr); + gd.addDialogListener(this); + previewing = true; + } + gd.addHelp(IJ.URL+"/docs/menus/process.html#options"); + gd.showDialog(); + previewing = false; + if (gd.wasCanceled()) return DONE; + if (imp==null) { //options dialog only, no do/preview + dialogItemChanged(gd, null); //read dialog result + return DONE; + } + return operation.equals(NO_OPERATION) ? DONE : IJ.setupDialog(imp, flags); + } else { //no dialog, 'arg' is operation type + if (!((ByteProcessor)imp.getProcessor()).isBinary()) { + IJ.error("8-bit binary (black and white only) image required."); + return DONE; + } + return IJ.setupDialog(imp, flags); + } + } + + public boolean dialogItemChanged (GenericDialog gd, AWTEvent e) { + iterations = (int)gd.getNextNumber(); + count = (int)gd.getNextNumber(); + boolean bb = Prefs.blackBackground; + Prefs.blackBackground = gd.getNextBoolean(); + if (Prefs.blackBackground!=bb) + ThresholdAdjuster.update(); + Prefs.padEdges = gd.getNextBoolean(); + gd.setSmartRecording(EDM.getOutputType()==0); + EDM.setOutputType(gd.getNextChoiceIndex()); + gd.setSmartRecording(false); + boolean isInvalid = gd.invalidNumber(); + if (iterations<1) {iterations = 1; isInvalid = true;} + if (iterations>MAX_ITERATIONS) {iterations = MAX_ITERATIONS; isInvalid = true;} + if (count < 1) {count = 1; isInvalid = true;} + if (count > 8) {count = 8; isInvalid = true;} + if (isInvalid) return false; + if (imp != null) { + operation = gd.getNextChoice(); + arg = operation.toLowerCase(); + } + return true; + } + + public void setNPasses (int nPasses) { + this.nPasses = nPasses; + } + + public void run (ImageProcessor ip) { + int fg = Prefs.blackBackground ? 255 : 0; + foreground = ip.isInvertedLut() ? 255-fg : fg; + background = 255 - foreground; + ip.setSnapshotCopyMode(true); + if (arg.equals("outline")) + outline(ip); + else if (arg.startsWith("fill")) + fill(ip, foreground, background); + else if (arg.startsWith("skel")) { + ip.resetRoi(); + skeletonize(ip); + } else if (arg.equals("erode") || arg.equals("dilate")) + doIterations((ByteProcessor)ip, arg); + else if (arg.equals("open")) { + doIterations(ip, "erode"); + doIterations(ip, "dilate"); + } else if (arg.equals("close")) { + doIterations(ip, "dilate"); + doIterations(ip, "erode"); + } + ip.setSnapshotCopyMode(false); + ip.setBinaryThreshold(); + } + + void doIterations (ImageProcessor ip, String mode) { + if (escapePressed) return; + if (!previewing && iterations>1) + IJ.showStatus(arg+"... press ESC to cancel"); + for (int i=0; i0)) { + ImageStatistics stats = imp.getProcessor().getStats(); + if (imp.getDisplayRangeMin()stats.max) { + imp.resetDisplayRange(); + imp.updateAndDraw(); + } + } + if (global2 || global2!=global1) + WindowManager.repaintImageWindows(); + else + imp.repaintWindow(); + if (global2 && global2!=global1) + FileOpener.setShowConflictMessage(true); + if (function!=Calibration.NONE && showPlotFlag) { + if (curveFitter!=null) + Fitter.plot(curveFitter, bitDepth==8); + else + showPlot(x, y, cal, fitGoodness); + } + } + + private boolean validateXValues(ImagePlus imp, double[] x) { + int bitDepth = imp.getBitDepth(); + if (bitDepth==32 || x==null) + return true; + int max = 255; + if (bitDepth==16) + max = 65535; + for (int i=0; imax) { + String title = (bitDepth==8?"8-bit":"16-bit") + " Calibration"; + String msg = "Measured (uncalibrated) values in the left\ncolumn must be in the range 0-"; + IJ.error(title, msg+max+"."); + return false; + } + } + return true; + } + + double[] doCurveFitting(double[] x, double[] y, int fitType) { + if (x.length!=y.length || y.length==0) { + IJ.error("Calibrate", + "To create a calibration curve, the left column must\n" + +"contain a list of measured mean pixel values and the\n" + +"right column must contain the same number of calibration\n" + +"standard values. Use the Measure command to add mean\n" + +"pixel value measurements to the left column.\n" + +" \n" + +" Left column: "+x.length+" values\n" + +" Right column: "+y.length+" values\n" + ); + return null; + } + int n = x.length; + double xmin=0.0,xmax; + if (imp.getType()==ImagePlus.GRAY16) + xmax=65535.0; + else + xmax=255.0; + double[] a = Tools.getMinMax(y); + double ymin=a[0], ymax=a[1]; + CurveFitter cf = new CurveFitter(x, y); + cf.doFit(fitType, showSettings); + if (cf.getStatus() == Minimizer.INITIALIZATION_FAILURE) { + curveFitError = cf.getStatusString(); + return null; + } + if (IJ.debugMode) IJ.log(cf.getResultString()); + int np = cf.getNumParams(); + double[] p = cf.getParams(); + fitGoodness = IJ.d2s(cf.getRSquared(),6); + curveFitter = cf; + double[] parameters = new double[np]; + for (int i=0; i0&&y.length>0) + plot.addPoints(x, y, PlotWindow.CIRCLE); + double[] p = cal.getCoefficients(); + if (fit<=Calibration.LOG2) { + drawLabel(plot, CurveFitter.fList[fit]); + ly += 0.04; + } + if (p!=null) { + int np = p.length; + drawLabel(plot, "a="+IJ.d2s(p[0],6,10)); + drawLabel(plot, "b="+IJ.d2s(p[1],6,10)); + if (np>=3) + drawLabel(plot, "c="+IJ.d2s(p[2],6,10)); + if (np>=4) + drawLabel(plot, "d="+IJ.d2s(p[3],6,10)); + if (np>=5) + drawLabel(plot, "e="+IJ.d2s(p[4],6,10)); + ly += 0.04; + } + if (rSquared!=null) + {drawLabel(plot, "R^2="+rSquared); rSquared=null;} + plot.show(); + } + + void drawLabel(Plot plot, String label) { + plot.addLabel(lx, ly, label); + ly += 0.08; + } + + + double sqr(double x) {return x*x;} + + String[] getFunctionList(boolean custom) { + int n = nFits+4; + if (custom) n++; + String[] list = new String[n]; + list[0] = NONE; + for (int i=0; iMAX_STANDARDS) + count = MAX_STANDARDS; + String s = ""; + for (int i=0; i='0'&&c<='9') || c=='-' || c=='.' || c==',' || c=='\n' || c=='\r' || c==' ') + sb.append(c); + } + xData = sb.toString(); + + StringTokenizer st = new StringTokenizer(xData); + int nTokens = st.countTokens(); + if (nTokens<1) + return new double[0]; + int n = nTokens; + double data[] = new double[n]; + for (int i=0; i1)) { + IJ.error("Calibrate", "This appears to not be a one or two column text file"); + return; + } + StringBuffer sb = new StringBuffer(); + for (int i=0; i=3 && (kw&1)==1) { + StringBuffer sb = new StringBuffer(); + int i = 0; + for (int y=0; yip with a kernel of width kw and + height kh. Returns false if the user cancels the operation. */ + public boolean convolve(ImageProcessor ip, float[] kernel, int kw, int kh) { + if (canceled || kernel==null || kw*kh!=kernel.length) + return false; + if ((kw&1)!=1 || (kh&1)!=1) + throw new IllegalArgumentException("Kernel width or height not odd ("+kw+"x"+kh+")"); + boolean notFloat = !(ip instanceof FloatProcessor); + ImageProcessor ip2 = ip; + if (notFloat) { + if (ip2 instanceof ColorProcessor) + throw new IllegalArgumentException("RGB images not supported"); + ip2 = ip2.convertToFloat(); + } + if (kw==1 || kh==1) + convolveFloat1D((FloatProcessor)ip2, kernel, kw, kh, normalize?getScale(kernel):1.0); + else + convolveFloat(ip2, kernel, kw, kh); + if (notFloat) { + if (ip instanceof ByteProcessor) + ip2 = ip2.convertToByte(false); + else + ip2 = ip2.convertToShort(false); + ip.setPixels(ip2.getPixels()); + } + return !canceled; + } + + /** If 'normalize' is true (the default), the convolve(), convolveFloat() and + convolveFloat1D() (4 argument version) methods divide each kernel + coefficient by the sum of the coefficients, preserving image brightness. */ + public void setNormalize(boolean normalizeKernel) { + normalize = normalizeKernel; + } + + /** Convolves the float image ip with a kernel of width + kw and height kh. Returns false if + the user cancels the operation by pressing 'Esc'. */ + public boolean convolveFloat(ImageProcessor ip, float[] kernel, int kw, int kh) { + if (!(ip instanceof FloatProcessor)) + throw new IllegalArgumentException("FloatProcessor required"); + if (canceled) return false; + int width = ip.getWidth(); + int height = ip.getHeight(); + Rectangle r = ip.getRoi(); + int x1 = r.x; + int y1 = r.y; + int x2 = x1 + r.width; + int y2 = y1 + r.height; + int uc = kw/2; + int vc = kh/2; + float[] pixels = (float[])ip.getPixels(); + float[] pixels2 = (float[])ip.getSnapshotPixels(); + if (pixels2==null) + pixels2 = (float[])ip.getPixelsCopy(); + double scale = normalize?getScale(kernel):1.0; + Thread thread = Thread.currentThread(); + boolean isMainThread = thread==mainThread || thread.getName().indexOf("Preview")!=-1; + if (isMainThread) pass++; + double sum; + int offset, i; + boolean edgePixel; + int xedge = width-uc; + int yedge = height-vc; + long lastTime = System.currentTimeMillis(); + for (int y=y1; y100) { + lastTime = time; + if (thread.isInterrupted()) return false; + if (isMainThread) { + if (IJ.escapePressed()) { + canceled = true; + ip.reset(); + ImageProcessor originalIp = imp.getProcessor(); + if (originalIp.getNChannels() > 1) + originalIp.reset(); + return false; + } + showProgress((y-y1)/(double)(y2-y1)); + } + } + for (int x=x1; x=yedge || x=xedge; + for (int v=-vc; v <= vc; v++) { + offset = x+(y+v)*width; + for(int u = -uc; u <= uc; u++) { + if (edgePixel) { + if (i>=kernel.length) // work around for JIT compiler bug on Linux + IJ.log("kernel index error: "+i); + sum += getPixel(x+u, y+v, pixels2, width, height)*kernel[i++]; + } else + sum += pixels2[offset+u]*kernel[i++]; + } + } + pixels[x+y*width] = (float)(sum*scale); + } + } + return true; + } + + /** Convolves the image ip with a kernel of width + kw and height kh. */ + public void convolveFloat1D(FloatProcessor ip, float[] kernel, int kw, int kh) { + convolveFloat1D(ip, kernel, kw, kh, normalize?getScale(kernel):1.0); + } + + /** Convolves the image ip with a kernel of width + kw and height kh. */ + public void convolveFloat1D(FloatProcessor ip, float[] kernel, int kw, int kh, double scale) { + int width = ip.getWidth(); + int height = ip.getHeight(); + Rectangle r = ip.getRoi(); + int x1 = r.x; + int y1 = r.y; + int x2 = x1 + r.width; + int y2 = y1 + r.height; + int uc = kw/2; + int vc = kh/2; + float[] pixels = (float[])ip.getPixels(); + float[] pixels2 = (float[])ip.getSnapshotPixels(); + if (pixels2==null) + pixels2 = (float[])ip.getPixelsCopy(); + boolean vertical = kw==1; + + double sum; + int offset, i; + boolean edgePixel; + int xedge = width-uc; + int yedge = height-vc; + for(int y=y1; y=yedge; + offset = x+(y-vc)*width; + for(int v=-vc; v<=vc; v++) { + if (edgePixel) + sum += getPixel(x+uc, y+v, pixels2, width, height)*kernel[i++]; + else + sum += pixels2[offset+uc]*kernel[i++]; + offset += width; + } + } else { + edgePixel = x=xedge; + offset = x+(y-vc)*width; + for(int u = -uc; u<=uc; u++) { + if (edgePixel) + sum += getPixel(x+u, y+vc, pixels2, width, height)*kernel[i++]; + else + sum += pixels2[offset+u]*kernel[i++]; + } + } + pixels[x+y*width] = (float)(sum*scale); + } + } + } + + public static double getScale(float[] kernel) { + double scale = 1.0; + double sum = 0.0; + for (int i=0; i=width) x = width-1; + if (y<=0) y = 0; + if (y>=height) y = height-1; + return pixels[x+y*width]; + } + + void save() { + TextArea ta1 = gd.getTextArea1(); + ta1.selectAll(); + String text = ta1.getText(); + ta1.select(0, 0); + if (text==null || text.length()==0) + return; + text += "\n"; + SaveDialog sd = new SaveDialog("Save as Text...", "kernel", ".txt"); + String name = sd.getFileName(); + if (name == null) + return; + String directory = sd.getDirectory(); + PrintWriter pw = null; + try { + FileOutputStream fos = new FileOutputStream(directory+name); + BufferedOutputStream bos = new BufferedOutputStream(fos); + pw = new PrintWriter(bos); + } + catch (IOException e) { + IJ.error("" + e); + return; + } + IJ.wait(250); // give system time to redraw ImageJ window + pw.print(text); + pw.close(); + } + + void open() { + OpenDialog od = new OpenDialog("Open Kernel...", ""); + String directory = od.getDirectory(); + String name = od.getFileName(); + if (name==null) + return; + String path = directory + name; + TextReader tr = new TextReader(); + ImageProcessor ip = tr.open(path); + if (ip==null) + return; + int width = ip.getWidth(); + int height = ip.getHeight(); + if ((width&1)!=1 || (height&1)!=1) { + IJ.error("Convolver", "Kernel must be have odd width and height"); + return; + } + StringBuffer sb = new StringBuffer(); + boolean integers = true; + for (int y=0; y=13, deviations from the true result can occur, but are very rare: typically + * the fraction of pixels deviating from the exact result is in the 10^-5 range, with + * most deviations between -0.03 and -0.04. + * + * Limitations: + * Maximum image diagonal for EDM: 46340 pixels (sqrt(2^31)); if the particles are + * dense enough it also works for width, height <=65534. + * + * Version 30-Apr-2008 Michael Schmid: more accurate EDM algorithm, + * 16-bit and float output possible, + * parallel processing for stacks + * Voronoi output added + */ +public class EDM implements ExtendedPlugInFilter { + /** Output type: overwrite current 8-bit image */ + public static final int BYTE_OVERWRITE = 0; + /** Output type: new 8-bit image */ + public static final int BYTE = 1; + /** Output type: new 16-bit image */ + public static final int SHORT = 2; + /** Output type: new 32-bit image */ + public static final int FLOAT = 3; + /** Unit in old make16bitEDM: this pixel value corresponds to a distance of one pixel. + * For compatibility only. */ + public static final int ONE = 41; + /** In old make16bitEDM this pixel value corresponds to a pixel distance of sqrt(2) */ + public static final int SQRT2 = 58; // ~ 41 * sqrt(2) + /** In old make16bitEDM this pixel value corresponds to a pixel distance of sqrt(5) */ + public static final int SQRT5 = 92; // ~ 41 * sqrt(5) + + private ImagePlus imp; //input + private ImagePlus outImp; //output if a new window is desired + private PlugInFilterRunner pfr; //needed to extract the stack slice if needed + private String command; //for showing status + private int outImageType; //output type; BYTE_OVERWRITE, BYTE, SHORT or FLOAT + private ImageStack outStack; //in case output should be a new stack + private int processType; //can be EDM, WATERSHED, UEP, VORONOI + private MaximumFinder maxFinder = new MaximumFinder(); //we use only one MaximumFinder (nice progress bar) + private double progressDone; //for progress bar, fraction of work done so far + private int nPasses; //for progress bar, how many images to process (sequentially or parallel threads) + private boolean interrupted; //whether watershed segmentation has been interrrupted by the user + + private boolean background255; //whether background for EDM is 255, not zero + private int flags = DOES_8G | PARALLELIZE_STACKS | FINAL_PROCESSING; + //processType can be: + private static final int EDM = 0, WATERSHED = 1, UEP = 2, VORONOI = 3; + //whether MaximumFinder is needed for processType: + private static final boolean[] USES_MAX_FINDER = new boolean[] { + false, true, true, true }; + //whether watershed segmentation is needed for processType: + private static final boolean[] USES_WATERSHED = new boolean[] { + false, true, false, true }; + //prefixes for titles of separate output images; for each processType: + private static final String[] TITLE_PREFIX = new String[] { + "EDM of ", null, "UEPs of ", "Voronoi of "}; + private static final int NO_POINT = -1; //no nearest point in array of nearest points + private static final double MAXFINDER_TOLERANCE = 0.5; //reasonable values are 0.3 ... 0.8; + //segmentation is more aggressive with smaller values + /** Output type (BYTE_OVERWRITE, BYTE, SHORT or FLOAT) */ + private static int outputType = BYTE_OVERWRITE; + + /** Prepare for processing; also called at the very end with argument 'final' + * to show any newly created output image. + */ + public int setup (String arg, ImagePlus imp) { + if (arg.equals("final")) { + showOutput(); + return DONE; + } + this.imp = imp; + //'arg' is processing type; default is 'EDM' (0) + if (arg.equals("watershed")) { + processType = WATERSHED; + flags += KEEP_THRESHOLD; + } else if (arg.equals("points")) + processType = UEP; + else if (arg.equals("voronoi")) + processType = VORONOI; + + //output type + if (processType != WATERSHED) //Watershed always has output BYTE_OVERWRITE=0 + outImageType = outputType; //otherwise use the static variable from setOutputType + if (outImageType != BYTE_OVERWRITE) + flags |= NO_CHANGES; + + //check image and prepare + if (imp != null) { + ImageProcessor ip = imp.getProcessor(); + if (!ip.isBinary()) { + IJ.error("8-bit binary image (0 and 255) required."); + return DONE; + } + ip.resetRoi(); + //processing routines assume background=0; image may be otherwise + boolean invertedLut = imp.isInvertedLut(); + background255 = (invertedLut && Prefs.blackBackground) || (!invertedLut && !Prefs.blackBackground); + } + return flags; + } //public int setup + + /** Called by the PlugInFilterRunner after setup. + * Asks the user in case of a stack and prepares a separate ouptut stack if required + */ + + public int showDialog (ImagePlus imp, String command, PlugInFilterRunner pfr) { + this.pfr = pfr; + int width = imp.getWidth(); + int height= imp.getHeight(); + //ask whether to process all slices of stack & prepare stack + //(if required) for writing into it in parallel threads + flags = IJ.setupDialog(imp, flags); + if ((flags&DOES_STACKS)!=0 && outImageType!=BYTE_OVERWRITE) { + outStack = new ImageStack(width, height, imp.getStackSize()); + maxFinder.setNPasses(imp.getStackSize()); + } + return flags; + } //public int showDialog + + /** Called by the PlugInFilterRunner to process the image or one frame of a stack */ + public void run (ImageProcessor ip) { + if (interrupted) return; + int width = ip.getWidth(); + int height = ip.getHeight(); + + int backgroundValue = (processType==VORONOI) ? + (background255 ? 0 : (byte)255) : //Voronoi needs EDM of the background + (background255 ? (byte)255 : 0); //all others do EDM of the foreground + if (USES_WATERSHED[processType]) nPasses = 0; //watershed has its own progress bar + FloatProcessor floatEdm = makeFloatEDM(ip, backgroundValue, false); + + ByteProcessor maxIp = null; + if (USES_MAX_FINDER[processType]) { + if (processType == VORONOI) floatEdm.multiply(-1); //Voronoi starts from minima of EDM + int maxOutputType = USES_WATERSHED[processType] ? MaximumFinder.SEGMENTED : MaximumFinder.SINGLE_POINTS; + boolean isEDM = processType!=VORONOI; + maxIp = maxFinder.findMaxima(floatEdm, MAXFINDER_TOLERANCE, + ImageProcessor.NO_THRESHOLD, maxOutputType, false, isEDM); + if (maxIp == null) { //segmentation cancelled by user? + interrupted = true; + return; + } else if (processType != WATERSHED) { + if (processType == VORONOI) floatEdm.multiply(-1); + resetMasked(floatEdm, maxIp, processType == VORONOI ? -1 : 0); + } + } + + ImageProcessor outIp = null; + if (processType==WATERSHED) { + if (background255) maxIp.invert(); + ip.copyBits(maxIp, 0, 0, Blitter.COPY); + ip.setBinaryThreshold(); + } else switch (outImageType) { //for all these, output contains the values of the EDM + case FLOAT: + outIp = floatEdm; + break; + case SHORT: + floatEdm.setMinAndMax(0., 65535.); + outIp = floatEdm.convertToShort(true); + break; + case BYTE: + floatEdm.setMinAndMax(0., 255.); + outIp = floatEdm.convertToByte(true); + break; + case BYTE_OVERWRITE: + ip.setPixels(0, floatEdm); + if (floatEdm.getMax() > 255.) + ip.resetMinAndMax(); //otherwise we have max of floatEdm + } + + if (outImageType != BYTE_OVERWRITE) { //new output image + if (outStack==null) { + outImp = new ImagePlus(TITLE_PREFIX[processType]+imp.getShortTitle(), outIp); + } else + outStack.setPixels(outIp.getPixels(), pfr.getSliceNumber()); + } + } //public void run + + /** Prepare the progress bar. + * Without calling it or if nPasses=0, no progress bar will be shown. + * @param nPasses Number of images that this EDM will process. + */ + public void setNPasses (int nPasses) { + this.nPasses = nPasses; + progressDone = 0; + if (USES_MAX_FINDER[processType]) maxFinder.setNPasses(nPasses); + } + + /** Converts a binary image into a 8-bit grayscale Euclidean Distance Map + * (EDM). Each foreground (nonzero) pixel in the binary image is + * assigned a value equal to its distance from the nearest + * background (zero) pixel. + */ + public void toEDM (ImageProcessor ip) { + ip.setPixels(0, makeFloatEDM(ip, 0, false)); + ip.resetMinAndMax(); + } + + /** Do watershed segmentation based on the EDM of the + * foreground objects (nonzero pixels) in an 8-bit image. + * Particles are segmented by their shape; segmentation + * lines added are background pixels (value = 0); + */ + public void toWatershed (ImageProcessor ip) { + FloatProcessor floatEdm = makeFloatEDM(ip, 0, false); + ByteProcessor maxIp = maxFinder.findMaxima(floatEdm, MAXFINDER_TOLERANCE, + ImageProcessor.NO_THRESHOLD, MaximumFinder.SEGMENTED, false, true); + if (maxIp != null) ip.copyBits(maxIp, 0, 0, Blitter.AND); + } + + /** Calculates a 16-bit grayscale Euclidean Distance Map for a binary 8-bit image. + * Each foreground (nonzero) pixel in the binary image is assigned a value equal to + * its distance from the nearest background (zero) pixel, multiplied by EDM.ONE. + * For compatibility with previous versions of ImageJ only. + */ + public ShortProcessor make16bitEDM (ImageProcessor ip) { + FloatProcessor floatEdm = makeFloatEDM(ip, 0, false); + floatEdm.setMinAndMax(0, 65535./ONE); + return (ShortProcessor)floatEdm.convertToShort(true); + } + + /** + * Creates the Euclidian Distance Map of a (binary) byte image. + * @param ip The input image, not modified; must be a ByteProcessor. + * @param backgroundValue Pixels in the input with this value are interpreted as background. + * Note: for pixel value 255, write either -1 or (byte)255. + * @param edgesAreBackground Whether out-of-image pixels are considered background + * @return The EDM, containing the distances to the nearest background pixel. + * Returns null if the thread is interrupted. + */ + public FloatProcessor makeFloatEDM (ImageProcessor ip, int backgroundValue, boolean edgesAreBackground) { + int width = ip.getWidth(); + int height = ip.getHeight(); + FloatProcessor fp = new FloatProcessor(width, height); + byte[] bPixels = (byte[])ip.getPixels(); + float[] fPixels = (float[])fp.getPixels(); + final int progressInterval = 100; + int nProgressUpdates = height/progressInterval; //how often the progress bar is updated when passing once through y range + double progressAddendum = (nProgressUpdates>0) ? 0.5/nProgressUpdates : 0; + + for (int i=0; i=0; y--) { + if (edgesAreBackground) yDist = height-y; + edmLine(bPixels, fPixels, pointBufs, width, y*width, y, backgroundValue, yDist); + if (y%progressInterval == 0) { + if (Thread.currentThread().isInterrupted()) return null; + addProgress(progressAddendum); + } + } + + fp.sqrt(); + return fp; + } //public FloatProcessor makeFloatEDM + + // Handle a line; two passes: left-to-right and right-to-left + private void edmLine(byte[] bPixels, float[] fPixels, int[][] pointBufs, int width, + int offset, int y, int backgroundValue, int yDist) { + int[] points = pointBufs[0]; // the buffer for the left-to-right pass + int pPrev = NO_POINT; + int pDiag = NO_POINT; // point at (-/+1, -/+1) to current one (-1,-1 in the first pass) + int pNextDiag; + boolean edgesAreBackground = yDist != Integer.MAX_VALUE; + int distSqr = Integer.MAX_VALUE; // this value is used only if edges are not background + for (int x=0; x dist2) fPixels[offset] = dist2; + } + pPrev = points[x]; + pDiag = pNextDiag; + } + offset--; //now points to the last pixel in the line + points = pointBufs[1]; // the buffer for the right-to-left pass. Low short contains x, high short y + pPrev = NO_POINT; + pDiag = NO_POINT; + for (int x=width-1; x>=0; x--, offset--) { + pNextDiag = points[x]; + if (bPixels[offset] == backgroundValue) { + points[x] = x | y<<16; // remember coordinates as a candidate for nearest background point + } else { // foreground pixel: + if (edgesAreBackground) + distSqr = (width-x < yDist) ? (width-x)*(width-x) : yDist*yDist; + float dist2 = minDist2(points, pPrev, pDiag, x, y, distSqr); + if (fPixels[offset] > dist2) fPixels[offset] = dist2; + } + pPrev = points[x]; + pDiag = pNextDiag; + } + } //private void edmLine + + // Calculates minimum distance^2 of x,y from the following three points: + // - points[x] (nearest point found for previous line, same x) + // - pPrev (nearest point found for same line, previous x), and + // - pDiag (nearest point found for diagonal, i.e., previous line, previous x) + // Sets array element points[x] to the coordinates of the point having the minimum distance to x,y + // If the distSqr parameter is lower than the distance^2, then distSqr is used + // Returns to the minimum distance^2 obtained + private float minDist2 (int[] points, int pPrev, int pDiag, int x, int y, int distSqr) { + int p0 = points[x]; // the nearest background point for the same x in the previous line + int nearestPoint = p0; + if (p0 != NO_POINT) { + int x0 = p0& 0xffff; int y0 = (p0>>16)&0xffff; + int dist1Sqr = (x-x0)*(x-x0)+(y-y0)*(y-y0); + if (dist1Sqr < distSqr) + distSqr = dist1Sqr; + } + if (pDiag!=p0 && pDiag!=NO_POINT) { + int x1 = pDiag&0xffff; int y1 = (pDiag>>16)&0xffff; + int dist1Sqr = (x-x1)*(x-x1)+(y-y1)*(y-y1); + if (dist1Sqr < distSqr) { + nearestPoint = pDiag; + distSqr = dist1Sqr; + } + } + if (pPrev!=pDiag && pPrev!=NO_POINT) { + int x1 = pPrev& 0xffff; int y1 = (pPrev>>16)&0xffff; + int dist1Sqr = (x-x1)*(x-x1)+(y-y1)*(y-y1); + if (dist1Sqr < distSqr) { + nearestPoint = pPrev; + distSqr = dist1Sqr; + } + } + points[x] = nearestPoint; + return (float)distSqr; + } //private float minDist2 + + // overwrite ip with floatEdm converted to bytes + private void byteFromFloat(ImageProcessor ip, FloatProcessor floatEdm) { + int width = ip.getWidth(); + int height = ip.getHeight(); + byte[] bPixels = (byte[])ip.getPixels(); + float[] fPixels = (float[])floatEdm.getPixels(); + for (int i=0; iFLOAT) + throw new IllegalArgumentException("Invalid type: "+type); + outputType = type; + } + + /** Returns the current output type (BYTE_OVERWRITE, BYTE, SHORT or FLOAT) */ + public static int getOutputType() { + return outputType; + } + +} diff --git a/src/ij/plugin/filter/ExtendedPlugInFilter.java b/src/ij/plugin/filter/ExtendedPlugInFilter.java new file mode 100644 index 0000000..923ca43 --- /dev/null +++ b/src/ij/plugin/filter/ExtendedPlugInFilter.java @@ -0,0 +1,73 @@ +package ij.plugin.filter; +import ij.*; +import ij.process.*; + +/** ImageJ plugins that process an image may implement this interface. + * In addition to the features of PlugInFilter, it is better suited + * for filters that have a dialog asking for the options or filter + * parameters. It also offers support for preview, for a smooth + * progress bar when processing stacks and for calling back + * the PlugInFilterRunner (needed, e.g., to get the slice number + * when processing a stack in parallel threads). + *

+ * The sequence of calls to an ExtendedPlugInFilter is the following: + *

+ * - setup(arg, imp): The filter should return its flags.

+ * - showDialog(imp, command, pfr): The filter should display + * the dialog asking for parameters (if any) and do all operations + * needed to prepare for processing the individual image(s) (E.g., + * slices of a stack). For preview, a separate thread may call + * setNPasses(nPasses) and run(ip) while + * the dialog is displayed. The filter should return its flags.

+ * - setNPasses(nPasses): Informs the filter of the number + * of calls of run(ip) that will follow.

+ * - run(ip): Processing of the image(s). With the + * CONVERT_TO_FLOAT flag, this method will be called for + * each color channel of an RGB image. With DOES_STACKS, + * it will be called for each slice of a stack.

+ * - setup("final", imp): called only if flag + * FINAL_PROCESSING has been specified. + *

+ * Flag DONE stops this sequence of calls. + */ +public interface ExtendedPlugInFilter extends PlugInFilter { + + /** + * This method is called after setup(arg, imp) unless the + * DONE flag has been set. + * @param imp The active image already passed in the + * setup(arg, imp) call. It will be null, however, if + * the NO_IMAGE_REQUIRED flag has been set. + * @param command The command that has led to the invocation of + * the plugin-filter. Useful as a title for the dialog. + * @param pfr The PlugInFilterRunner calling this plugin-filter. + * It can be passed to a GenericDialog by addPreviewCheckbox + * to enable preview by calling the run(ip) method of this + * plugin-filter. pfr can be also used later for calling back + * the PlugInFilterRunner, e.g., to obtain the slice number + * currently processed by run(ip). + * @return The method should return a combination (bitwise OR) + * of the flags specified in interfaces PlugInFilter and + * ExtendedPlugInFilter. + */ + public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr); + + /** + * This method is called by ImageJ to inform the plugin-filter + * about the passes to its run method. During preview, the number of + * passes is one (or 3 for RGB images, if CONVERT_TO_FLOAT + * has been specified). When processing a stack, it is the number + * of slices to be processed (minus one, if one slice has been + * processed for preview before), and again, 3 times that number + * for RGB images processed with CONVERT_TO_FLOAT. + */ + public void setNPasses(int nPasses); + + /** Set this flag if the last preview image may be kept as a result. + For stacks, this flag can lead to out-of-sequence processing of the + slices, irrespective of the PARALLELIZE_STACKS flag. + */ + public final int KEEP_PREVIEW = 0x1000000; + + +} diff --git a/src/ij/plugin/filter/FFTCustomFilter.java b/src/ij/plugin/filter/FFTCustomFilter.java new file mode 100644 index 0000000..c2c3eb6 --- /dev/null +++ b/src/ij/plugin/filter/FFTCustomFilter.java @@ -0,0 +1,182 @@ +package ij.plugin.filter; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.*; +import ij.plugin.ContrastEnhancer; +import ij.plugin.frame.Recorder; +import java.awt.*; +import java.util.*; + + +/** This class implements the Process/FFT/Custom Filter command. */ +public class FFTCustomFilter implements PlugInFilter, Measurements { + + private ImagePlus imp; + private static int filterIndex = 1; + private int slice; + private int stackSize; + private ImageProcessor filter; + private static boolean processStack; + private boolean padded; + private int originalWidth; + private int originalHeight; + private Rectangle rect = new Rectangle(); + + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + if (imp==null) { + IJ.noImage(); + return DONE; + } + this.stackSize = imp.getStackSize(); + filter = getFilter(); + if (filter==null) + return DONE; + if (imp.getProperty("FHT")!=null) { + IJ.error("FFT Custom Filter", "Spatial domain (non-FFT) image required"); + return DONE; + } else + return processStack?DOES_ALL+DOES_STACKS:DOES_ALL; + } + + public void run(ImageProcessor ip) { + slice++; + FHT fht = newFHT(ip); + if (slice==1) + filter = resizeFilter(filter, fht.getWidth()); + ((FHT)fht).transform(); + customFilter(fht); + doInverseTransform(fht, ip); + if (slice==1) + ip.resetMinAndMax(); + if (slice==stackSize) { + new ContrastEnhancer().stretchHistogram(imp, 0.0); + imp.updateAndDraw(); + } + IJ.showProgress(1.0); + if (Recorder.record && slice==1) + Recorder.recordCall("FFT.filter(imp,filter); //see Help/Examples/JavaScript/FFT Filter"); + } + + void doInverseTransform(FHT fht, ImageProcessor ip) { + showStatus("Inverse transform"); + fht.inverseTransform(); + //if (fht.quadrantSwapNeeded) + // fht.swapQuadrants(); + fht.resetMinAndMax(); + ImageProcessor ip2 = fht; + fht.setRoi(rect.x, rect.y, rect.width, rect.height); + ip2 = fht.crop(); + int bitDepth = fht.originalBitDepth>0?fht.originalBitDepth:imp.getBitDepth(); + switch (bitDepth) { + case 8: ip2 = ip2.convertToByte(true); break; + case 16: ip2 = ip2.convertToShort(true); break; + case 24: + showStatus("Setting brightness"); + fht.rgb.setBrightness((FloatProcessor)ip2); + ip2 = fht.rgb; + fht.rgb = null; + break; + case 32: break; + } + ip.insert(ip2, 0, 0); + } + + FHT newFHT(ImageProcessor ip) { + FHT fht; + int width = ip.getWidth(); + int height = ip.getHeight(); + int maxN = Math.max(width, height); + int size = 2; + while (size<1.5*maxN) size *= 2; + rect.x = (int)Math.round((size-width)/2.0); + rect.y = (int)Math.round((size-height)/2.0); + rect.width = width; + rect.height = height; + FFTFilter fftFilter = new FFTFilter(); + if (ip instanceof ColorProcessor) { + showStatus("Extracting brightness"); + ImageProcessor ip2 = ((ColorProcessor)ip).getBrightness(); + fht = new FHT(fftFilter.tileMirror(ip2, size, size, rect.x, rect.y)); + fht.rgb = (ColorProcessor)ip.duplicate(); // save so we can later update the brightness + } else + fht = new FHT(fftFilter.tileMirror(ip, size, size, rect.x, rect.y)); + fht.originalWidth = originalWidth; + fht.originalHeight = originalHeight; + fht.originalBitDepth = imp.getBitDepth(); + return fht; + } + + void showStatus(String msg) { + if (stackSize>1) + IJ.showStatus("FFT: " + slice+"/"+stackSize); + else + IJ.showStatus(msg); + } + + void customFilter(FHT fht) { + int size = fht.getWidth(); + showStatus("Filtering"); + fht.swapQuadrants(filter); + float[] fhtPixels = (float[])fht.getPixels(); + boolean isFloat = filter.getBitDepth()==32; + for (int i=0; i=titles.length) + filterIndex = 1; + GenericDialog gd = new GenericDialog("FFT Filter"); + gd.addChoice("Filter:", titles, titles[filterIndex]); + if (stackSize>1) + gd.addCheckbox("Process entire stack", processStack); + gd.addHelp(IJ.URL+"/docs/menus/process.html#fft-filter"); + gd.showDialog(); + if (gd.wasCanceled()) + return null; + filterIndex = gd.getNextChoiceIndex(); + if (stackSize>1) + processStack = gd.getNextBoolean(); + ImagePlus filterImp = WindowManager.getImage(wList[filterIndex]); + if (filterImp==imp) { + IJ.error("FFT", "The filter cannot be the same as the image being filtered."); + return null; + } + if (filterImp.getStackSize()>1) { + IJ.error("FFT", "The filter cannot be a stack."); + return null; + } + ImageProcessor filter = filterImp.getProcessor(); + if (filter!=null && filter.getBitDepth()!=32) + filter = filter.convertToByte(true); + return filter; + } + + ImageProcessor resizeFilter(ImageProcessor ip, int maxN) { + int width = ip.getWidth(); + int height = ip.getHeight(); + if (width==maxN && height==maxN) + return ip; + showStatus("Scaling filter to "+ maxN + "x" + maxN); + return ip.resize(maxN, maxN); + } + +} + diff --git a/src/ij/plugin/filter/FFTFilter.java b/src/ij/plugin/filter/FFTFilter.java new file mode 100644 index 0000000..2922de1 --- /dev/null +++ b/src/ij/plugin/filter/FFTFilter.java @@ -0,0 +1,442 @@ +package ij.plugin.filter; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.*; +import ij.plugin.ContrastEnhancer; +import java.awt.*; +import java.util.*; + +/** +This class implements the Process/FFT/Bandpass Filter command. It is based on +Joachim Walter's FFT Filter plugin at "http://imagej.nih.gov/ij/plugins/fft-filter.html". +2001/10/29: First Version (JW) +2003/02/06: 1st bugfix (works in macros/plugins, works on stacks, overwrites image(=>filter)) (JW) +2003/07/03: integrated into ImageJ, added "Display Filter" option (WSR) +2007/03/26: 2nd bugfix (Fixed incorrect calculation of filter from structure sizes, which caused + the real structure sizes to be wrong by a factor of 0.75 to 1.5 depending on the image size.) +*/ +public class FFTFilter implements PlugInFilter, Measurements { + + private ImagePlus imp; + private String arg; + private static int filterIndex = 1; + private FHT fht; + private int slice; + private int stackSize = 1; + + private static double filterLargeDia = 40.0; + private static double filterSmallDia = 3.0; + private static int choiceIndex = 0; + private static String[] choices = {"None","Horizontal","Vertical"}; + private static String choiceDia = choices[0]; + private static double toleranceDia = 5.0; + private static boolean doScalingDia = true; + private static boolean saturateDia = true; + private static boolean displayFilter; + private static boolean processStack; + + public int setup(String arg, ImagePlus imp) { + this.arg = arg; + this.imp = imp; + if (imp==null) + {IJ.noImage(); return DONE;} + stackSize = imp.getStackSize(); + fht = (FHT)imp.getProperty("FHT"); + if (fht!=null) { + IJ.error("FFT Filter", "Spatial domain image required"); + return DONE; + } + if (!showBandpassDialog(imp)) + return DONE; + else + return processStack?DOES_ALL+DOES_STACKS+PARALLELIZE_STACKS:DOES_ALL; + } + + public void run(ImageProcessor ip) { + slice++; + filter(ip); + } + + void filter(ImageProcessor ip) { + ImageProcessor ip2 = ip; + if (ip2 instanceof ColorProcessor) { + showStatus("Extracting brightness"); + ip2 = ((ColorProcessor)ip2).getBrightness(); + } + Rectangle roiRect = ip2.getRoi(); + int maxN = Math.max(roiRect.width, roiRect.height); + double sharpness = (100.0 - toleranceDia) / 100.0; + boolean doScaling = doScalingDia; + boolean saturate = saturateDia; + + IJ.showProgress(1,20); + + /* tile mirrored image to power of 2 size + first determine smallest power 2 >= 1.5 * image width/height + factor of 1.5 to avoid wrap-around effects of Fourier Trafo */ + + int i=2; + while(i<1.5 * maxN) i *= 2; + + // Calculate the inverse of the 1/e frequencies for large and small structures. + double filterLarge = 2.0*filterLargeDia / (double)i; + double filterSmall = 2.0*filterSmallDia / (double)i; + + // fit image into power of 2 size + Rectangle fitRect = new Rectangle(); + fitRect.x = (int) Math.round( (i - roiRect.width) / 2.0 ); + fitRect.y = (int) Math.round( (i - roiRect.height) / 2.0 ); + fitRect.width = roiRect.width; + fitRect.height = roiRect.height; + + // put image (ROI) into power 2 size image + // mirroring to avoid wrap around effects + showStatus("Pad to "+i+"x"+i); + ip2 = tileMirror(ip2, i, i, fitRect.x, fitRect.y); + IJ.showProgress(2,20); + + // transform forward + showStatus(i+"x"+i+" forward transform"); + FHT fht = new FHT(ip2); + fht.setShowProgress(false); + fht.transform(); + IJ.showProgress(9,20); + //new ImagePlus("after fht",ip2.crop()).show(); + + // filter out large and small structures + showStatus("Filter in frequency domain"); + filterLargeSmall(fht, filterLarge, filterSmall, choiceIndex, sharpness); + //new ImagePlus("filter",ip2.crop()).show(); + IJ.showProgress(11,20); + + // transform backward + showStatus("Inverse transform"); + fht.inverseTransform(); + IJ.showProgress(19,20); + //new ImagePlus("after inverse",ip2).show(); + + // crop to original size and do scaling if selected + showStatus("Crop and convert to original type"); + fht.setRoi(fitRect); + ip2 = fht.crop(); + if (doScaling) { + ImagePlus imp2 = new ImagePlus(imp.getTitle()+"-filtered", ip2); + new ContrastEnhancer().stretchHistogram(imp2, saturate?1.0:0.0); + ip2 = imp2.getProcessor(); + } + + // convert back to original data type + int bitDepth = imp.getBitDepth(); + switch (bitDepth) { + case 8: ip2 = ip2.convertToByte(doScaling); break; + case 16: ip2 = ip2.convertToShort(doScaling); break; + case 24: + ip.snapshot(); + showStatus("Setting brightness"); + ((ColorProcessor)ip).setBrightness((FloatProcessor)ip2); + break; + case 32: break; + } + + // copy filtered image back into original image + if (bitDepth!=24) { + ip.snapshot(); + ip.copyBits(ip2, roiRect.x, roiRect.y, Blitter.COPY); + } + ip.resetMinAndMax(); + IJ.showProgress(20,20); + } + + void showStatus(String msg) { + if (stackSize>1 && processStack) + IJ.showStatus("FFT Filter: "+slice+"/"+stackSize); + else + IJ.showStatus(msg); + } + + /** Puts ImageProcessor (ROI) into a new ImageProcessor of size width x height y at position (x,y). + The image is mirrored around its edges to avoid wrap around effects of the FFT. */ + public ImageProcessor tileMirror(ImageProcessor ip, int width, int height, int x, int y) { + if (IJ.debugMode) IJ.log("FFT.tileMirror: "+width+"x"+height+" "+ip); + if (x < 0 || x > (width -1) || y < 0 || y > (height -1)) { + IJ.error("Image to be tiled is out of bounds."); + return null; + } + + ImageProcessor ipout = ip.createProcessor(width, height); + + ImageProcessor ip2 = ip.crop(); + int w2 = ip2.getWidth(); + int h2 = ip2.getHeight(); + + //how many times does ip2 fit into ipout? + int i1 = (int) Math.ceil(x / (double) w2); + int i2 = (int) Math.ceil( (width - x) / (double) w2); + int j1 = (int) Math.ceil(y / (double) h2); + int j2 = (int) Math.ceil( (height - y) / (double) h2); + + //tile + if ( (i1%2) > 0.5) + ip2.flipHorizontal(); + if ( (j1%2) > 0.5) + ip2.flipVertical(); + + for (int i=-i1; i1) + gd.addCheckbox("Process entire stack", processStack); + gd.addHelp(IJ.URL+"/docs/menus/process.html#fft-bandpass"); + gd.showDialog(); + if(gd.wasCanceled()) + return false; + if(gd.invalidNumber()) { + IJ.error("Error", "Invalid input number"); + return false; + } + filterLargeDia = gd.getNextNumber(); + filterSmallDia = gd.getNextNumber(); + choiceIndex = gd.getNextChoiceIndex(); + choiceDia = choices[choiceIndex]; + toleranceDia = gd.getNextNumber(); + doScalingDia = gd.getNextBoolean(); + saturateDia = gd.getNextBoolean(); + displayFilter = gd.getNextBoolean(); + if (stackSize>1) + processStack = gd.getNextBoolean(); + return true; + } + +} + diff --git a/src/ij/plugin/filter/Filler.java b/src/ij/plugin/filter/Filler.java new file mode 100644 index 0000000..88027a1 --- /dev/null +++ b/src/ij/plugin/filter/Filler.java @@ -0,0 +1,257 @@ +package ij.plugin.filter; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import java.awt.*; + +/** This plugin implements ImageJ's Fill, Clear, Clear Outside and Draw commands. */ +public class Filler implements PlugInFilter, Measurements { + + String arg; + Roi roi; + ImagePlus imp; + int sliceCount; + ImageProcessor mask; + boolean isTextRoi; + + public int setup(String arg, ImagePlus imp) { + this.arg = arg; + this.imp = imp; + if (imp!=null) + roi = imp.getRoi(); + isTextRoi = roi!=null && (roi instanceof TextRoi); + if (isTextRoi && (arg.equals("draw") || arg.equals("fill")) && ((TextRoi)roi).getAngle()!=0.0) { + String s = IJ.isMacOSX()?"command+b":"ctrl+b"; + IJ.error("Draw rotated text by pressing "+s+" (Image>Overlay>Add Selection)."); + return DONE; + } + IJ.register(Filler.class); + int baseCapabilities = DOES_ALL+ROI_REQUIRED; + if (arg.equals("clear")) { + if (roi!=null && roi.getType()==Roi.POINT) { + IJ.error("Clear", "Area selection required"); + return DONE; + } + if (isTextRoi || isLineSelection()) + return baseCapabilities; + else + return IJ.setupDialog(imp,baseCapabilities+SUPPORTS_MASKING); + } else if (arg.equals("draw")) + return IJ.setupDialog(imp,baseCapabilities); + else if (arg.equals("label")) { + if (Analyzer.firstParticle1) + ip.fillPolygon(roi.getPolygon()); + else + roi.drawPixels(); + } else if (roi!=null && roi instanceof TextRoi) + ((TextRoi)roi).clear(ip); + else + ip.fill(); // fill with background color + ip.setGlobalForegroundColor(); + } + + /** + * @deprecated + * replaced by ImageProcessor.fill(Roi) + */ + public void fill(ImageProcessor ip) { + if (!IJ.isMacro() || !ip.fillValueSet()) + ip.setGlobalForegroundColor(); + if (isLineSelection()) { + if (isStraightLine() && roi.getStrokeWidth()>1 && !(roi instanceof Arrow)) { + Roi roi2=Roi.convertLineToArea(roi); + ip.setRoi(roi2); + ip.fill(roi2.getMask()); + ip.setRoi(roi); + } else + roi.drawPixels(ip); + } else + ip.fill(); // fill with foreground color + } + + /** + * @deprecated + * replaced by ImageProcessor.draw(Roi) + */ + public void draw(ImageProcessor ip) { + ip.setGlobalForegroundColor(); + roi.drawPixels(ip); + if (IJ.altKeyDown()) + drawLabel(ip); + } + + public void label(ImageProcessor ip) { + if (!IJ.isMacro()) { + IJ.error("Label", "To label a selection, enable \"Add to overlay\" in Analyze>\nSet Measurements and press 'm' (Analyze>Measure)."); + return; + } + if (Analyzer.getCounter()==0) { + IJ.error("Label", "Measurement counter is zero"); + return; + } + if (Analyzer.firstParticle=count || last>=count) + return; + if (!rt.columnExists(ResultsTable.X_CENTROID)) { + IJ.error("Label", "\"Centroids\" required to label particles"); + return; + } + for (int i=first; i<=last; i++) { + int x = (int)rt.getValueAsDouble(ResultsTable.X_CENTROID, i); + int y = (int)rt.getValueAsDouble(ResultsTable.Y_CENTROID, i); + drawLabel(imp, ip, i+1, new Rectangle(x,y,0,0)); + } + } + + void drawLabel(ImageProcessor ip) { + int count = Analyzer.getCounter(); + if (count>0 && roi!=null) + drawLabel(imp, ip, count, roi.getBounds()); + } + + public void drawLabel(ImagePlus imp, ImageProcessor ip, int count, Rectangle r) { + Color foreground = Toolbar.getForegroundColor(); + Color background = Toolbar.getBackgroundColor(); + if (foreground.equals(background)) { + foreground = Color.black; + background = Color.white; + } + int size = 9; + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) { + double mag = ic.getMagnification(); + if (mag<1.0) + size /= mag; + } + if (size==9 && r.width>50 && r.height>50) + size = 12; + ip.setFont(new Font("SansSerif", Font.PLAIN, size)); + String label = "" + count; + int w = ip.getStringWidth(label); + int x = r.x + r.width/2 - w/2; + int y = r.y + r.height/2 + Math.max(size/2,6); + FontMetrics metrics = ip.getFontMetrics(); + int h = metrics.getHeight(); + ip.setColor(background); + ip.setRoi(x-1, y-h+2, w+1, h-3); + ip.fill(); + ip.resetRoi(); + ip.setColor(foreground); + ip.drawString(label, x, y); + } + + /** + * @deprecated + * replaced by ImageProcessor.fillOutside(Roi) + */ + public synchronized void clearOutside(ImageProcessor ip) { + if (isLineSelection()) { + IJ.error("\"Clear Outside\" does not work with line selections."); + return; + } + sliceCount++; + Rectangle r = ip.getRoi(); + if (mask==null) + makeMask(ip, r); + ip.setGlobalBackgroundColor(); + int stackSize = imp.getStackSize(); + if (stackSize>1) + ip.snapshot(); + ip.fill(); + ip.reset(mask); + int width = ip.getWidth(); + int height = ip.getHeight(); + ip.setRoi(0, 0, r.x, height); + ip.fill(); + ip.setRoi(r.x, 0, r.width, r.y); + ip.fill(); + ip.setRoi(r.x, r.y+r.height, r.width, height-(r.y+r.height)); + ip.fill(); + ip.setRoi(r.x+r.width, 0, width-(r.x+r.width), height); + ip.fill(); + ip.setRoi(r); // restore original ROI + if (sliceCount==stackSize) { + ip.setGlobalForegroundColor(); + Roi roi = imp.getRoi(); + imp.deleteRoi(); + imp.updateAndDraw(); + imp.setRoi(roi); + } + } + + public void makeMask(ImageProcessor ip, Rectangle r) { + mask = ip.getMask(); + if (mask==null) { + mask = new ByteProcessor(r.width, r.height); + mask.invert(); + } else { + // duplicate mask (needed because getMask caches masks) + mask = mask.duplicate(); + } + mask.invert(); + } + +} diff --git a/src/ij/plugin/filter/Filters.java b/src/ij/plugin/filter/Filters.java new file mode 100644 index 0000000..89f6be5 --- /dev/null +++ b/src/ij/plugin/filter/Filters.java @@ -0,0 +1,99 @@ +package ij.plugin.filter; +import ij.*; +import ij.gui.*; +import ij.process.*; +import java.awt.*; + +/** This plugin implements the Invert, Smooth, Sharpen, Find Edges, + and Add Noise commands. */ +public class Filters implements PlugInFilter { + + private static double sd = Prefs.getDouble(Prefs.NOISE_SD, 25.0); + private String arg; + private ImagePlus imp; + private int slice; + private boolean canceled; + private boolean noRoi; + + public int setup(String arg, ImagePlus imp) { + this.arg = arg; + this.imp = imp; + if (imp!=null) { + Roi roi = imp.getRoi(); + if (imp.getType()==ImagePlus.GRAY16 && arg.equals("invert")) { + imp.resetRoi(); + roi = null; + } + if (roi!=null && !roi.isArea()) + noRoi = true; + } + int flags = IJ.setupDialog(imp, DOES_ALL-DOES_8C+SUPPORTS_MASKING); + return flags; + } + + public void run(ImageProcessor ip) { + + if (noRoi) + ip.resetRoi(); + + if (arg.equals("invert")) { + ip.invert(); + slice++; + if (imp.getBitDepth()==16 && imp.getStackSize()>1 && slice==imp.getStackSize()) + imp.resetDisplayRange(); + return; + } + + if (arg.equals("smooth")) { + ip.setSnapshotCopyMode(true); + ip.smooth(); + ip.setSnapshotCopyMode(false); + return; + } + + if (arg.equals("sharpen")) { + ip.setSnapshotCopyMode(true); + ip.sharpen(); + ip.setSnapshotCopyMode(false); + return; + } + + if (arg.equals("edge")) { + ip.setSnapshotCopyMode(true); + ip.findEdges(); + ip.setSnapshotCopyMode(false); + return; + } + + if (arg.equals("add")) { + ip.noise(25.0); + return; + } + + if (arg.equals("noise")) { + if (canceled) + return; + slice++; + if (slice==1) { + GenericDialog gd = new GenericDialog("Gaussian Noise"); + gd.addNumericField("Standard Deviation:", sd, 2); + gd.showDialog(); + if (gd.wasCanceled()) { + canceled = true; + return; + } + sd = gd.getNextNumber(); + } + ip.noise(sd); + IJ.register(Filters.class); + return; + } + + } + + /** Returns the default standard deviation used by Process/Noise/Add Specified Noise. */ + public static double getSD() { + return sd; + } + +} diff --git a/src/ij/plugin/filter/FractalBoxCounter.java b/src/ij/plugin/filter/FractalBoxCounter.java new file mode 100644 index 0000000..028cdff --- /dev/null +++ b/src/ij/plugin/filter/FractalBoxCounter.java @@ -0,0 +1,241 @@ +package ij.plugin.filter; +import java.awt.*; +import java.awt.image.*; +import java.util.*; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.*; +import ij.util.*; + +/** +Calculate the so-called "capacity" fractal dimension. The algorithm +is called, in fractal parlance, the "box counting" method. In the +simplest terms, the routine counts the number of boxes of a given size +needed to cover a one pixel wide, binary (black on white) border. +The procedure is repeated for boxes that are 2 to 64 pixels wide. +The output consists of two columns labeled "size" and "count". A plot +is generated with the log of size on the x-axis and the log of count on +the y-axis and the data is fitted with a straight line. The slope (S) +of the line is the negative of the fractal dimension, i.e. D=-S. + +A full description of the technique can be found in T. G. Smith, +Jr., G. D. Lange and W. B. Marks, Fractal Methods and Results in Cellular Morphology, +which appeared in J. Neurosci. Methods, 69:1123-126, 1996. + +--- +12/Jun/2006 G. Landini added "set is white" option, otherwise the plugin +assumes that the object is always low-dimensional (i.e. the phase with +the smallest number of pixels). Now it works fine for sets with D near to 2.0 + +*/ +public class FractalBoxCounter implements PlugInFilter { + static String sizes = "2,3,4,6,8,12,16,32,64"; + static boolean blackBackground; + int[] boxSizes; + float[] boxCountSums; + int maxBoxSize; + int[] counts; + Rectangle roi; + int foreground; + ImagePlus imp; + + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + return DOES_8G+NO_CHANGES; + } + + public void run(ImageProcessor ip) { + + GenericDialog gd = new GenericDialog("Fractal Box Counter"); + gd.addStringField("Box Sizes:", sizes, 20); + gd.addCheckbox("Black Background", blackBackground); + + gd.showDialog(); + if (gd.wasCanceled()) + return; + + String s = gd.getNextString(); + + blackBackground = gd.getNextBoolean (); + + if (s.equals("")) + return; + boxSizes = s2ints(s); + if (boxSizes==null || boxSizes.length<1) + return; + boxCountSums = new float[boxSizes.length]; + sizes = s; + for (int i=0; i=width) { + IJ.error("No non-backround pixels found."); + return false; + } + ip.setRoi(left, 0, 1, height); + histogram = ip.getHistogram(); + } while (histogram[foreground]==0); + + //Find top edge + top = -1; + do { + top++; + ip.setRoi(left, top, width-left, 1); + histogram = ip.getHistogram(); + } while (histogram[foreground]==0); + + //Find right edge + right =width+1; + do { + right--; + ip.setRoi(right-1, top, 1, height-top); + histogram = ip.getHistogram(); + } while (histogram[foreground]==0); + + //Find bottom edge + bottom =height+1; + do { + bottom--; + ip.setRoi(left, bottom-1, right-left, 1); + histogram = ip.getHistogram(); + } while (histogram[foreground]==0); + + roi = new Rectangle(left, top, right-left, bottom-top); + return true; + } + + int count(int size, ImageProcessor ip) { + int[] histogram = new int[256]; + int count; + int x = roi.x; + int y = roi.y; + int w = (size<=roi.width)?size:roi.width; + int h = (size<=roi.height)?size:roi.height; + int right = roi.x+roi.width; + int bottom = roi.y+roi.height; + int maxCount = size*size; + + for (int i=1; i<=maxCount; i++) + counts[i] = 0; + boolean done = false; + do { + ip.setRoi(x, y, w, h); + histogram = ip.getHistogram(); + counts[histogram[foreground]]++; + x+=size; + if (x+size>=right) { + w = right-x; + if (x>=right) { + w = size; + x = roi.x; + y += size; + if (y+size>=bottom) + h = bottom-y; + done = y>=bottom; + } + } + } while (!done); + int boxSum = 0; + int nBoxes; + for (int i=1; i<=maxCount; i++) { + nBoxes = counts[i]; + if (nBoxes!=0) + boxSum += nBoxes; + } + return boxSum; + } + + double plot() { + int n = boxSizes.length; + float[] sizes = new float[boxSizes.length]; + for (int i=0; i 0 || roiRect.y+roiRect.height < imp.getDimensions()[1]) + flags |= SNAPSHOT; // snapshot for pixels above and/or below roi rectangle + } + return flags; + } + + /** Ask the user for the parameters + */ + public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) { + calledAsPlugin = true;; + String options = Macro.getOptions(); + boolean oldMacro = false; + nChannels = imp.getProcessor().getNChannels(); + setNPasses(1); + if (options!=null) { + if (options.indexOf("radius=") >= 0) { // ensure compatibility with old macros + oldMacro = true; // specifying "radius=", not "sigma= + Macro.setOptions(options.replaceAll("radius=", "sigma=")); + } + } + GenericDialog gd = GUI.newNonBlockingDialog(command, imp); + sigma = Math.abs(sigma); + gd.addNumericField("Sigma (Radius):", sigma, 2); + if (imp.getCalibration()!=null && !imp.getCalibration().getUnits().equals("pixels")) { + hasScale = true; + gd.addCheckbox("Scaled Units ("+imp.getCalibration().getUnits()+")", sigmaScaled); + } else + sigmaScaled = false; + gd.addPreviewCheckbox(pfr); + gd.addDialogListener(this); + gd.showDialog(); // input by the user (or macro) happens here + if (gd.wasCanceled()) return DONE; + if (options==null) { // interactive use: remember values as default for the next invocation + sigmaS = sigma; + sigmaScaledS = sigmaScaled; + } + if (oldMacro) sigma /= 2.5; // for old macros, "radius" was 2.5 sigma + IJ.register(this.getClass()); // protect static class variables (parameters) from garbage collection + return IJ.setupDialog(imp, flags); // ask whether to process all slices of stack (if a stack) + } + + /** Listener to modifications of the input fields of the dialog */ + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + sigma = gd.getNextNumber(); + if (sigma < 0 || gd.invalidNumber()) + return false; + if (hasScale) + sigmaScaled = gd.getNextBoolean(); + return true; + } + + /** Set the number of passes of the blur1Direction method. If called by the + * PlugInFilterRunner of ImageJ, an ImagePlus is known and conversion of RGB images + * to float as well as the two filter directions are taken into account. + * Otherwise, the caller should set nPasses to the number of 1-dimensional + * filter operations required. + */ + public void setNPasses(int nPasses) { + this.nPasses = 2 * nChannels * nPasses; + pass = 0; + } + + /** This method is invoked for each slice during execution + * @param ip The image subject to filtering. It must have a valid snapshot if + * the height of the roi is less than the full image height. + */ + public void run(ImageProcessor ip) { + double sigmaX = sigmaScaled ? sigma/imp.getCalibration().pixelWidth : sigma; + double sigmaY = sigmaScaled ? sigma/imp.getCalibration().pixelHeight : sigma; + double accuracy = (ip instanceof ByteProcessor || ip instanceof ColorProcessor) ? + 0.002 : 0.0002; + Rectangle roi = ip.getRoi(); + blurGaussian(ip, sigmaX, sigmaY, accuracy); + } + + /** Gaussian Filtering of an ImageProcessor. This method is for compatibility with the + * previous code (before 1.38r) and uses a low-accuracy kernel, only slightly better + * than the previous ImageJ code. + * The 'radius' in this call is different from the one used in ImageJ 1.38r and later. + * Therefore, use blurGaussian(ip, sigma, sigma, accuracy), where 'sigma' is equivalent + * to the 'sigma (radius)' of the Menu, and accuracy should be 0.02 unless better + * accuracy is desired. + */ + @Deprecated + public boolean blur(ImageProcessor ip, double radius) { + Rectangle roi = ip.getRoi(); + if (roi.height!=ip.getHeight() && ip.getMask()==null) + ip.snapshot(); // a snapshot is needed for out-of-Rectangle pixels + blurGaussian(ip, 0.4*radius, 0.4*radius, 0.01); + return true; + } + + /** Gaussian Filtering of an ImageProcessor + * @param ip The ImageProcessor to be filtered. + * @param sigma Standard deviation of the Gaussian (pixels) + * + * @see ij.process.ImageProcessor#blurGaussian(double) + */ + public void blurGaussian(ImageProcessor ip, double sigma) { + double accuracy = (ip instanceof ByteProcessor||ip instanceof ColorProcessor)?0.002:0.0002; + blurGaussian(ip, sigma, sigma, accuracy); + } + + /** Gaussian Filtering of an ImageProcessor + * @param ip The ImageProcessor to be filtered. + * @param sigmaX Standard deviation of the Gaussian in x direction (pixels) + * @param sigmaY Standard deviation of the Gaussian in y direction (pixels) + * @param accuracy Accuracy of kernel, should not be above 0.02. Better (lower) + * accuracy needs slightly more computing time. + */ + public void blurGaussian(ImageProcessor ip, double sigmaX, double sigmaY, double accuracy) { + boolean hasRoi = ip.getRoi().height!=ip.getHeight() && sigmaX>0 && sigmaY>0; + if (hasRoi && !calledAsPlugin) + ip.snapshot(); + if (nPasses<=1) + nPasses = ip.getNChannels() * (sigmaX>0 && sigmaY>0 ? 2 : 1); + FloatProcessor fp = null; + for (int i=0; i 0) + blur1Direction(ip, sigmaX, accuracy, true, (int)Math.ceil(5*sigmaY)); + if (Thread.currentThread().isInterrupted()) return; // interruption for new parameters during preview? + if (sigmaY > 0) + blur1Direction(ip, sigmaY, accuracy, false, 0); + return; + } + + /** Blur an image in one direction (x or y) by a Gaussian, using multiple threads on multiprocessor machines + * @param ip The Image with the original data where also the result will be stored + * @param sigma Standard deviation of the Gaussian + * @param accuracy Accuracy of kernel, should not be > 0.02 + * @param xDirection True for bluring in x direction, false for y direction + * @param extraLines Number of lines (parallel to the blurring direction) + * below and above the roi bounds that should be processed. + */ + public void blur1Direction( final FloatProcessor ip, final double sigma, final double accuracy, + final boolean xDirection, final int extraLines) { + + final int UPSCALE_K_RADIUS = 2; //number of pixels to add for upscaling + final double MIN_DOWNSCALED_SIGMA = 4.; //minimum standard deviation in the downscaled image + final float[] pixels = (float[])ip.getPixels(); + final int width = ip.getWidth(); + final int height = ip.getHeight(); + final Rectangle roi = ip.getRoi(); + final int length = xDirection ? width : height; //number of points per line (line can be a row or column) + final int pointInc = xDirection ? 1 : width; //increment of the pixels array index to the next point in a line + final int lineInc = xDirection ? width : 1; //increment of the pixels array index to the next line + final int lineFromA = (xDirection ? roi.y : roi.x) - extraLines; //the first line to process + final int lineFrom; + if (lineFromA < 0) lineFrom = 0; + else lineFrom = lineFromA; + final int lineToA = (xDirection ? roi.y+roi.height : roi.x+roi.width) + extraLines; //the last line+1 to process + final int lineTo; + if (lineToA > (xDirection ? height:width)) lineTo = (xDirection ? height:width); + else lineTo = lineToA; + final int writeFrom = xDirection? roi.x : roi.y; //first point of a line that needs to be written + final int writeTo = xDirection ? roi.x+roi.width : roi.y+roi.height; + + /* large radius (sigma): scale down, then convolve, then scale up */ + final boolean doDownscaling = sigma > 2*MIN_DOWNSCALED_SIGMA + 0.5; + final int reduceBy = doDownscaling ? //downscale by this factor + Math.min((int)Math.floor(sigma/MIN_DOWNSCALED_SIGMA), length) + : 1; + /* Downscaling and upscaling blur the image a bit - we have to correct the standard + * deviation for this: + * Downscaling gives std devation sigma = 1/sqrt(3); upscale gives sigma = 1/2 (in downscaled pixels). + * All sigma^2 values add to full sigma^2, which should be the desired value */ + final double sigmaGauss = doDownscaling ? + Math.sqrt(sigma*sigma/(reduceBy*reduceBy) - 1./3. - 1./4.) + : sigma; + final int maxLength = doDownscaling ? + (length+reduceBy-1)/reduceBy + 2*(UPSCALE_K_RADIUS + 1) //downscaled line can't be longer + : length; + final float[][] gaussKernel = makeGaussianKernel(sigmaGauss, accuracy, maxLength); + final int kRadius = gaussKernel[0].length*reduceBy; //Gaussian kernel radius after upscaling + final int readFrom = (writeFrom-kRadius < 0) ? 0 : writeFrom-kRadius; //not including broadening by downscale&upscale + final int readTo = (writeTo+kRadius > length) ? length : writeTo+kRadius; + final int newLength = doDownscaling ? //line length for convolution + (readTo-readFrom+reduceBy-1)/reduceBy + 2*(UPSCALE_K_RADIUS + 1) + : length; + final int unscaled0 = readFrom - (UPSCALE_K_RADIUS + 1)*reduceBy; //input point corresponding to cache index 0 + //the following is relevant for upscaling only + //IJ.log("reduce="+reduceBy+", newLength="+newLength+", unscaled0="+unscaled0+", sigmaG="+(float)sigmaGauss+", kRadius="+gaussKernel[0].length); + final float[] downscaleKernel = doDownscaling ? makeDownscaleKernel(reduceBy) : null; + final float[] upscaleKernel = doDownscaling ? makeUpscaleKernel(reduceBy) : null; + + int numThreads1 = Math.min(Prefs.getThreads(), lineTo-lineFrom); + int numThreads2 = (int)((lineTo - lineFrom)*(long)(writeTo - writeFrom)*gaussKernel[0].length/ + (doDownscaling ? 8000 : 16000)) + 1; //use fewer threads if a small task + final int numThreads = Math.min(numThreads1, numThreads2); + final Callable[] callables = new Callable[numThreads]; + final AtomicInteger nextLine = new AtomicInteger(lineFrom); + final AtomicLong lastShowProgressTime = new AtomicLong(System.currentTimeMillis()); + + for ( int t = 0; t < numThreads; t++ ) { + final float[] cache1 = new float[newLength]; //holds data before convolution (after downscaling, if any) + final float[] cache2 = doDownscaling ? new float[newLength] : null; //holds data after convolution + + callables[t] = new Callable() { + final public Void call() { /*try{*/ + while (!Thread.currentThread().isInterrupted()) { + int line = nextLine.getAndIncrement(); + if (line >= lineTo) break; + int pixel0 = line*lineInc; + if ((line&0x1f)==0) { //every 32 lines, check whether progress bar should be updated + long time = System.currentTimeMillis(); + if (time - lastShowProgressTime.get() >110) { + lastShowProgressTime.set(time); + showProgress((double)(line-lineFrom)/(lineTo-lineFrom)); + } + } + if (doDownscaling) { + downscaleLine(pixels, cache1, downscaleKernel, reduceBy, pixel0, unscaled0, length, pointInc, newLength); + convolveLine(cache1, cache2, gaussKernel, 0, newLength, 1, newLength-1, 0, 1); + upscaleLine(cache2, pixels, upscaleKernel, reduceBy, pixel0, unscaled0, writeFrom, writeTo, pointInc); + } else { + int p = pixel0 + readFrom*pointInc; + for (int i=readFrom; i nPasses) pass = 1; + } + + /** Scale a line (row or column of a FloatProcessor or part thereof) + * down by a factor reduceBy and write the result into + * cache. + * Input line pixel # unscaled0 will correspond to output + * line pixel # 0. unscaled0 may be negative. Out-of-line + * pixels of the input are replaced by the edge pixels. + * @param pixels input array + * @param cache output array + * @param kernel downscale kernel, runs form -1.5 to +1.5 in downscaled coordinates + * @param reduceBy downscaling factor + * @param pixel0 index in pixels array corresponding to start of line or column + * @param unscaled0 index in input line corresponding to output line index 0, May be negative. + * @param length length of full input line or column + * @param pointInc spacing of values in input array (1 for lines, image width for columns) + * @param newLength length of downscaled data + */ + final static private void downscaleLine(final float[] pixels, final float[] cache, final float[] kernel, + final int reduceBy, final int pixel0, final int unscaled0, final int length, final int pointInc, final int newLength) { + int p = pixel0 + pointInc*(unscaled0-reduceBy*3/2); //pointer in pixels array + final int pLast = pixel0 + pointInc*(length-1); + for (int xout=-1; xout<=newLength; xout++) { + float sum0 = 0, sum1 = 0, sum2 = 0; + for (int x=0; xpLast ? pLast : p)]; + sum0 += v * kernel[x+2*reduceBy]; + sum1 += v * kernel[x+reduceBy]; + sum2 += v * kernel[x]; + } + if (xout>0) cache[xout-1] += sum0; + if (xout>=0 && xoutpLast ? pLast : pp)]; + v += kernel[x+reduceBy] * pixels[ppLast ? pLast : p)]; + pp = p+pointInc*reduceBy; + v += kernel[x+2*reduceBy] * pixels[pppLast ? pLast : pp)]; + } + cache[xout] = v; + } + }*/ + + /* Create a kernel for downscaling. The kernel function preserves + * norm and 1st moment (i.e., position) and has fixed 2nd moment, + * (in contrast to linear interpolation). + * In scaled space, the length of the kernel runs from -1.5 to +1.5, + * and the standard deviation is 1/2. + * Array index corresponding to the kernel center is + * unitLength*3/2 + */ + final static private float[] makeDownscaleKernel (final int unitLength) { + final int mid = unitLength*3/2; + final float[] kernel = new float[3*unitLength]; + for (int i=0; i<=unitLength/2; i++) { + final double x = i/(double)unitLength; + final float v = (float)((0.75-x*x)/unitLength); + kernel[mid-i] = v; + kernel[mid+i] = v; + } + for (int i=unitLength/2+1; i<(unitLength*3+1)/2; i++) { + final double x = i/(double)unitLength; + final float v = (float)((0.125 + 0.5*(x-1)*(x-2))/unitLength); + kernel[mid-i] = v; + kernel[mid+i] = v; + } + return kernel; + } + + /** Scale a line up by factor reduceBy and write as a row + * or column (or part thereof) to the pixels array of a FloatProcessor. + */ + final static private void upscaleLine (final float[] cache, final float[] pixels, final float[] kernel, + final int reduceBy, final int pixel0, final int unscaled0, final int writeFrom, final int writeTo, final int pointInc) { + int p = pixel0 + pointInc*writeFrom; + for (int xout = writeFrom; xout < writeTo; xout++, p+=pointInc) { + final int xin = (xout-unscaled0+reduceBy-1)/reduceBy; //the corresponding point in the cache (if exact) or the one above + final int x = reduceBy - 1 - (xout-unscaled0+reduceBy-1)%reduceBy; + pixels[p] = cache[xin-2]*kernel[x] + + cache[xin-1]*kernel[x+reduceBy] + + cache[xin]*kernel[x+2*reduceBy] + + cache[xin+1]*kernel[x+3*reduceBy]; + } + } + + /** Create a kernel for upscaling. The kernel function is a convolution + * of four unit squares, i.e., four uniform kernels with value +1 + * from -0.5 to +0.5 (in downscaled coordinates). The second derivative + * of this kernel is smooth, the third is not. Its standard deviation + * is 1/sqrt(3) in downscaled cordinates. + * The kernel runs from [-2 to +2[, corresponding to array index + * 0 ... 4*unitLength (whereby the last point is not in the array any more). + */ + final static private float[] makeUpscaleKernel (final int unitLength) { + final float[] kernel = new float[4*unitLength]; + final int mid = 2*unitLength; + kernel[0] = 0; + for (int i=0; iwriteFrom-kernel.length or 0. + * @param readTo Last array element+1 of the line that must be read. + * writeTo+kernel.length or input.length + * @param writeFrom Index of the first point in the line that should be written + * @param writeTo Index+1 of the last point in the line that should be written + * @param point0 Array index of first element of the 'line' in pixels (i.e., lineNumber * lineInc) + * @param pointInc Increment of the pixels array index to the next point (for an ImageProcessor, + * it should be 1 for a row, width for a column) + */ + final static private void convolveLine( final float[] input, final float[] pixels, final float[][] kernel, final int readFrom, + final int readTo, final int writeFrom, final int writeTo, final int point0, final int pointInc) { + final int length = input.length; + final float first = input[0]; //out-of-edge pixels are replaced by nearest edge pixels + final float last = input[length-1]; + final float[] kern = kernel[0]; //the kernel itself + final float kern0 = kern[0]; + final float[] kernSum = kernel[1]; //the running sum over the kernel + final int kRadius = kern.length; + final int firstPart = kRadius < length ? kRadius : length; + int p = point0 + writeFrom*pointInc; + int i = writeFrom; + for (; ilength) result += kernSum[length-i-1]*last; + for (int k=1; k= 0) v += input[i-k]; + if (i+k= length + float result = input[i]*kern0; + if (i=length) result += kernSum[length-i-1]*last; + for (int k=1; k= 0) v += input[i-k]; + if (i+k n, including non-calculated values in case the kernel + * size is limited by maxRadius. + */ + public float[][] makeGaussianKernel(final double sigma, final double accuracy, int maxRadius) { + int kRadius = (int)Math.ceil(sigma*Math.sqrt(-2*Math.log(accuracy)))+1; + if (maxRadius < 50) maxRadius = 50; // too small maxRadius would result in inaccurate sum. + if (kRadius > maxRadius) kRadius = maxRadius; + float[][] kernel = new float[2][kRadius]; + for (int i=0; i 3) { // edge correction + double sqrtSlope = Double.MAX_VALUE; + int r = kRadius; + while (r > kRadius/2) { + r--; + double a = Math.sqrt(kernel[0][r])/(kRadius-r); + if (a < sqrtSlope) + sqrtSlope = a; + else + break; + } + for (int r1 = r+2; r1 < kRadius; r1++) + kernel[0][r1] = (float)((kRadius-r1)*(kRadius-r1)*sqrtSlope*sqrtSlope); + } + double sum; // sum over all kernel elements for normalization + if (kRadius < maxRadius) { + sum = kernel[0][0]; + for (int i=1; i5.0) { + if (!previewing() && !canceled) { + canceled = true; + IJ.error("Gamma must be between 0.05 and 5.0"); + } + gammaValue = defaultGammaValue; + } else + ip.gamma(gammaValue); + } else if (arg.equals("set")) { + ip.set(addValue); + } else if (arg.equals("log")) { + ip.log(); + } else if (arg.equals("exp")) { + ip.exp(); + } else if (arg.equals("sqr")) { + ip.sqr(); + } else if (arg.equals("sqrt")) { + ip.sqrt(); + } else if (arg.equals("reciprocal")) { + if (!isFloat(ip)) + return; + float[] pixels = (float[])ip.getPixels(); + for (int i=0; i0 || (int)defaultValue!=defaultValue) + digits = Math.max(places, 1); + gd = GUI.newNonBlockingDialog(title, imp); + gd.addNumericField(prompt, defaultValue, digits, 8, null); + gd.addPreviewCheckbox(pfr); + gd.addDialogListener(this); + gd.showDialog(); + } + + void getBinaryValue (String title, String prompt, String defaultValue) { + gd = GUI.newNonBlockingDialog(title, imp); + gd.addStringField(prompt, defaultValue); + gd.addPreviewCheckbox(pfr); + gd.addDialogListener(this); + gd.showDialog(); + } + + void getGammaValue (double defaultValue) { + gd = GUI.newNonBlockingDialog("Gamma", imp); + if (GraphicsEnvironment.isHeadless()) + gd.addNumericField("Value:", defaultValue, 2); + else + gd.addSlider("Value:", 0.0, 5.0, defaultValue, 0.02); + gd.addPreviewCheckbox(pfr); + gd.addDialogListener(this); + gd.showDialog(); + } + + /** Set non-thresholded pixels in a float image to NaN. */ + void setBackgroundToNaN(ImageProcessor ip) { + if (lower==-1.0 && upper==-1.0) { + lower = ip.getMinThreshold(); + upper = ip.getMaxThreshold(); + if (lower==ImageProcessor.NO_THRESHOLD || !(ip instanceof FloatProcessor)) { + String title = imp!=null?"\n\""+imp.getTitle()+"\"":""; + IJ.error("NaN Backround", "Thresholded 32-bit float image required:"+title); + canceled = true; + return; + } + } + if (!(ip instanceof FloatProcessor)) + return; + float[] pixels = (float[])ip.getPixels(); + int width = ip.getWidth(); + int height = ip.getHeight(); + double v; + for (int y=0; yupper) + pixels[y*width+x] = Float.NaN; + } + } + ip.resetMinAndMax(); + return; + } + + // first default: v = v+(sin(x/(w/25))+sin(y/(h/25)))*40 + // a=round(a/10); if (a%2==0) v=0; + // cone: v=d + // translate: v=getPixel(x+10,y+10) + // flip vertically: v=getPixel(x,h-y-1) + // spiral: v=(sin(d/10+a*PI/180)+1)*128 + // spiral on image: v=v+50*sin(a*PI/180+d/5) + // spiral rotation: a+=PI+d*PI/360; v=getPixel(d*cos(a)+w/2,d*sin(a)+h/2); + // v=sin(log(d)*8 + a) * sin(a*8) + // v=(a * 40.74 + d) % 32 + // v=floor((a * 40.75 + 1) % 2) + // v=sin(x) * sin(y) + // v=cos(0.2*x) + sin(0.2*y) + + private void applyMacro(ImageProcessor ip) { + if (macro2==null) return; + macro = macro2; + ip.setSliceNumber(pfr.getSliceNumber()); + boolean showProgress = pfr.getSliceNumber()==1 && !Interpreter.isBatchMode(); + applyMacro(ip, macro, showProgress); + if (pfr.getSliceNumber()==1) + ip.resetMinAndMax(); + } + + public static void applyMacro(ImageProcessor ip, String macro, boolean showProgress) { + if (!macro.contains("=")) + macro = "v="+macro; + ImagePlus temp = WindowManager.getTempCurrentImage(); + WindowManager.setTempCurrentImage(new ImagePlus("",ip)); + int PCStart = 23; + Program pgm = (new Tokenizer()).tokenize(macro); + boolean hasX = pgm.hasWord("x"); + boolean hasA = pgm.hasWord("a"); + boolean hasD = pgm.hasWord("d"); + boolean hasGetPixel = pgm.hasWord("getPixel"); + int w = ip.getWidth(); + int h = ip.getHeight(); + int w2 = w/2; + int h2 = h/2; + String code = + "var v,x,y,z,w,h,d,a;\n"+ + "function dummy() {}\n"+ + macro+";\n"; // code starts at program counter location 'PCStart' + Interpreter interp = new Interpreter(); + interp.run(code, null); + if (interp.wasError()) { + WindowManager.setTempCurrentImage(temp); + return; + } + Prefs.set(MACRO_KEY, macro); + interp.setVariable("w", w); + interp.setVariable("h", h); + interp.setVariable("z", ip.getSliceNumber()-1); + int bitDepth = ip.getBitDepth(); + Rectangle r = ip.getRoi(); + int inc = r.height/50; + if (inc<1) inc = 1; + double v; + int index, v2; + if (bitDepth==8) { + byte[] pixels1 = (byte[])ip.getPixels(); + byte[] pixels2 = pixels1; + if (hasGetPixel) + pixels2 = new byte[w*h]; + for (int y=r.y; y<(r.y+r.height); y++) { + if (showProgress && y%inc==0) + IJ.showProgress(y-r.y, r.height); + interp.setVariable("y", y); + for (int x=r.x; x<(r.x+r.width); x++) { + index = y*w+x; + v = pixels1[index]&255; + interp.setVariable("v", v); + if (hasX) interp.setVariable("x", x); + if (hasA) interp.setVariable("a", getA((h-y-1)-h2, x-w2)); + if (hasD) interp.setVariable("d", getD(x-w2,y-h2)); + interp.run(PCStart); + v2 = (int)interp.getVariable("v"); + if (v2<0) v2 = 0; + if (v2>255) v2 = 255; + pixels2[index] = (byte)v2; + } + } + if (hasGetPixel) System.arraycopy(pixels2, 0, pixels1, 0, w*h); + } else if (bitDepth==24) { + int rgb, red, green, blue; + int[] pixels1 = (int[])ip.getPixels(); + int[] pixels2 = pixels1; + if (hasGetPixel) + pixels2 = new int[w*h]; + for (int y=r.y; y<(r.y+r.height); y++) { + if (showProgress && y%inc==0) + IJ.showProgress(y-r.y, r.height); + interp.setVariable("y", y); + for (int x=r.x; x<(r.x+r.width); x++) { + if (hasX) interp.setVariable("x", x); + if (hasA) interp.setVariable("a", getA((h-y-1)-h2, x-w2)); + if (hasD) interp.setVariable("d", getD(x-w2,y-h2)); + index = y*w+x; + rgb = pixels1[index]; + if (hasGetPixel) { + interp.setVariable("v", rgb); + interp.run(PCStart); + rgb = (int)interp.getVariable("v"); + } else { + red = (rgb&0xff0000)>>16; + green = (rgb&0xff00)>>8; + blue = rgb&0xff; + interp.setVariable("v", red); + interp.run(PCStart); + red = (int)interp.getVariable("v"); + if (red<0) red=0; if (red>255) red=255; + interp.setVariable("v", green); + interp.run(PCStart); + green= (int)interp.getVariable("v"); + if (green<0) green=0; if (green>255) green=255; + interp.setVariable("v", blue); + interp.run(PCStart); + blue = (int)interp.getVariable("v"); + if (blue<0) blue=0; if (blue>255) blue=255; + rgb = 0xff000000 | ((red&0xff)<<16) | ((green&0xff)<<8) | blue&0xff; + } + pixels2[index] = rgb; + } + } + if (hasGetPixel) System.arraycopy(pixels2, 0, pixels1, 0, w*h); + } else if (ip.isSigned16Bit()) { + for (int y=r.y; y<(r.y+r.height); y++) { + if (showProgress && y%inc==0) + IJ.showProgress(y-r.y, r.height); + interp.setVariable("y", y); + for (int x=r.x; x<(r.x+r.width); x++) { + v = ip.getPixelValue(x, y); + interp.setVariable("v", v); + if (hasX) interp.setVariable("x", x); + if (hasA) interp.setVariable("a", getA((h-y-1)-h2, x-w2)); + if (hasD) interp.setVariable("d", getD(x-w2,y-h2)); + interp.run(PCStart); + ip.putPixelValue(x, y, interp.getVariable("v")); + } + } + } else if (bitDepth==16) { + short[] pixels1 = (short[])ip.getPixels(); + short[] pixels2 = pixels1; + if (hasGetPixel) + pixels2 = new short[w*h]; + for (int y=r.y; y<(r.y+r.height); y++) { + if (showProgress && y%inc==0) + IJ.showProgress(y-r.y, r.height); + interp.setVariable("y", y); + for (int x=r.x; x<(r.x+r.width); x++) { + index = y*w+x; + v = pixels1[index]&65535; + interp.setVariable("v", v); + if (hasX) interp.setVariable("x", x); + if (hasA) interp.setVariable("a", getA((h-y-1)-h2, x-w2)); + if (hasD) interp.setVariable("d", getD(x-w2,y-h2)); + interp.run(PCStart); + v2 = (int)interp.getVariable("v"); + if (v2<0) v2 = 0; + if (v2>65535) v2 = 65535; + pixels2[index] = (short)v2; + } + } + if (hasGetPixel) System.arraycopy(pixels2, 0, pixels1, 0, w*h); + } else { //32-bit + float[] pixels1 = (float[])ip.getPixels(); + float[] pixels2 = pixels1; + if (hasGetPixel) + pixels2 = new float[w*h]; + for (int y=r.y; y<(r.y+r.height); y++) { + if (showProgress && y%inc==0) + IJ.showProgress(y-r.y, r.height); + interp.setVariable("y", y); + for (int x=r.x; x<(r.x+r.width); x++) { + index = y*w+x; + v = pixels1[index]; + interp.setVariable("v", v); + if (hasX) interp.setVariable("x", x); + if (hasA) interp.setVariable("a", getA((h-y-1)-h2, x-w2)); + if (hasD) interp.setVariable("d", getD(x-w2,y-h2)); + interp.run(PCStart); + pixels2[index] = (float)interp.getVariable("v"); + } + } + if (hasGetPixel) System.arraycopy(pixels2, 0, pixels1, 0, w*h); + } + if (showProgress) + IJ.showProgress(1.0); + WindowManager.setTempCurrentImage(temp); + } + + private static final double getD(int dx, int dy) { + return Math.sqrt(dx*dx + dy*dy); + } + + private static final double getA(int y, int x) { + double angle = Math.atan2(y, x); + if (angle<0) angle += 2*Math.PI; + return angle; + } + + void getMacro(String macro) { + String options = Macro.getOptions(); + if (options!=null && options.startsWith("v=")) + Macro.setOptions("code="+options); + gd = GUI.newNonBlockingDialog("Expression Evaluator", imp); + gd.addStringField("Code:", macro, 42); + gd.setInsets(0,40,0); + gd.addMessage("v=pixel value, x,y&z=pixel coordinates, w=image width,\nh=image height, a=angle, d=distance from center\n"); + gd.setInsets(5,40,0); + gd.addPreviewCheckbox(pfr); + gd.addDialogListener(this); + gd.addHelp(IJ.URL+"/docs/menus/process.html#math-macro"); + gd.showDialog(); + } + + public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) { + this.pfr = pfr; + boolean interactive = Macro.getOptions()==null; + if (interactive) { + addValue = lastAddValue; + mulValue = lastMulValue; + minValue = lastMinValue; + maxValue = lastMaxValue; + andValue = lastAndValue; + gammaValue = lastGammaValue; + macro = lastMacro; + } + if (arg.equals("macro")) + getMacro(macro); + else if (arg.equals("add")) + getValue("Add", "Value: ", addValue, 0); + else if (arg.equals("sub")) + getValue("Subtract", "Value: ", addValue, 0); + else if (arg.equals("mul")) + getValue("Multiply", "Value: ", mulValue, 2); + else if (arg.equals("div")) + getValue("Divide", "Value: ", mulValue, 2); + else if (arg.equals("and")) + getBinaryValue("AND", "Value (binary): ", andValue); + else if (arg.equals("or")) + getBinaryValue("OR", "Value (binary): ", andValue); + else if (arg.equals("xor")) + getBinaryValue("XOR", "Value (binary): ", andValue); + else if (arg.equals("min")) + getValue("Min", "Value: ", minValue, 0); + else if (arg.equals("max")) + getValue("Max", "Value: ", maxValue, 0); + else if (arg.equals("gamma")) + getGammaValue(gammaValue); + else if (arg.equals("set")) { + boolean rgb = imp.getBitDepth()==24; + String prompt = rgb?"Value (0-255): ":"Value: "; + getValue("Set", prompt, addValue, 0); + } + if (gd!=null && gd.wasCanceled()) + return DONE; + else { + if (interactive) { + lastAddValue = addValue; + lastMulValue = mulValue; + lastMinValue = minValue; + lastMaxValue = maxValue; + lastAndValue = andValue; + lastGammaValue = gammaValue; + lastMacro = macro2; + } + return IJ.setupDialog(imp, flags); + } + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (arg.equals("macro")) { + String str = gd.getNextString(); + if (previewing() && macro2!=null && !str.equals(macro2)) + gd.getPreviewCheckbox().setState(false); + macro2 = str; + } else if (arg.equals("add")||arg.equals("sub")||arg.equals("set")) + addValue = gd.getNextNumber(); + else if (arg.equals("mul")||arg.equals("div")) + mulValue = gd.getNextNumber(); + else if (arg.equals("and")||arg.equals("or")||arg.equals("xor")) + andValue = gd.getNextString(); + else if (arg.equals("min")) + minValue = gd.getNextNumber(); + else if (arg.equals("max")) + maxValue = gd.getNextNumber(); + else if (arg.equals("gamma")) { + gammaValue = gd.getNextNumber(); + if (gammaValue<0.05 || gammaValue>5.0) { + if (previewing()) { + IJ.showStatus("Gamma must be between 0.05 and 5.0"); + gammaValue = defaultGammaValue; + return false; + } + } + } + canceled = gd.invalidNumber(); + if (gd.wasOKed() && canceled) { + IJ.error("Value is invalid."); + return false; + } + return true; + } + + public void setNPasses(int nPasses) { + } + +} diff --git a/src/ij/plugin/filter/ImageProperties.java b/src/ij/plugin/filter/ImageProperties.java new file mode 100644 index 0000000..2703a92 --- /dev/null +++ b/src/ij/plugin/filter/ImageProperties.java @@ -0,0 +1,352 @@ +package ij.plugin.filter; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.util.Tools; +import ij.io.FileOpener; +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import ij.measure.Calibration; +import ij.plugin.frame.Recorder; + +public class ImageProperties implements PlugInFilter, TextListener { + private final String SAME = "-"; + ImagePlus imp; + static final int NANOMETER=0, MICROMETER=1, MILLIMETER=2, CENTIMETER=3, + METER=4, KILOMETER=5, INCH=6, FOOT=7, MILE=8, PIXEL=9, OTHER_UNIT=10; + int oldUnitIndex; + double oldUnitsPerCm; + Vector nfields, sfields; + boolean duplicatePixelWidth = true; + String calUnit; + double calPixelWidth, calPixelHeight, calPixelDepth; + TextField pixelWidthField, pixelHeightField, pixelDepthField; + int textChangedCount; + + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + return DOES_ALL+NO_CHANGES; + } + + public void run(ImageProcessor ip) { + showDialog(imp); + } + + void showDialog(ImagePlus imp) { + String options = Macro.getOptions(); + boolean legacyMacro = false; + if (options!=null ) { + String options2 = options.replaceAll(" depth=", " slices="); + options2 = options2.replaceAll(" interval=", " frame="); + Macro.setOptions(options2); + if (options.contains("unit=")) + legacyMacro = true; + } + Calibration cal = imp.getCalibration(); + Calibration calOrig = cal.copy(); + oldUnitIndex = getUnitIndex(cal.getUnit()); + oldUnitsPerCm = getUnitsPerCm(oldUnitIndex); + int stackSize = imp.getImageStackSize(); + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + boolean global1 = imp.getGlobalCalibration()!=null; + boolean global2; + int digits = cal.pixelWidth<1.0||cal.pixelHeight<1.0||cal.pixelDepth<1.0?7:4; + String xunit = cal.getXUnit(); + String yunit = cal.getYUnit(); + String zunit = cal.getZUnit(); + GenericDialog gd = new GenericDialog(imp.getTitle()); + gd.addNumericField("Channels (c):", channels, 0); + gd.addNumericField("Slices (z):", slices, 0); + gd.addNumericField("Frames (t):", frames, 0); + gd.setInsets(0, 5, 0); + gd.addMessage("Note: c*z*t must equal "+stackSize, null, Color.darkGray); + gd.setInsets(15, 0, 0); + if (legacyMacro) + gd.addStringField("Unit of length:", cal.getUnit()); + int fieldWidth = 9; + gd.addNumericField("Pixel_width:", cal.pixelWidth, digits, fieldWidth, null); + gd.addToSameRow(); + gd.addStringField("_", xunit, 5); + gd.addNumericField("Pixel_height:", cal.pixelHeight, digits, fieldWidth, null); + gd.addToSameRow(); + gd.addStringField("_", yunit.equals(xunit)?SAME:yunit, 5); + gd.addNumericField("Voxel_depth:", cal.pixelDepth, digits, fieldWidth, null); + gd.addToSameRow(); + gd.addStringField("_", zunit.equals(xunit)?SAME:zunit, 5); + gd.setInsets(10, 0, 5); + double interval = cal.frameInterval; + String intervalStr = IJ.d2s(interval, (int)interval==interval?0:2) + " " + cal.getTimeUnit(); + gd.addStringField("Frame interval:", intervalStr); + String xo = cal.xOrigin==(int)cal.xOrigin?IJ.d2s(cal.xOrigin,0):IJ.d2s(cal.xOrigin,2); + String yo = cal.yOrigin==(int)cal.yOrigin?IJ.d2s(cal.yOrigin,0):IJ.d2s(cal.yOrigin,2); + String zo = ""; + if (imp.getNSlices()>1) { + zo = cal.zOrigin==(int)cal.zOrigin?IJ.d2s(cal.zOrigin,0):IJ.d2s(cal.zOrigin,2); + zo = "," + zo; + } + gd.addStringField("Origin (pixels):", xo+","+yo+zo); + gd.setInsets(5, 20, 0); + gd.addCheckbox("Invert Y coordinates", cal.getInvertY()); + gd.addCheckbox("Global", global1); + nfields = gd.getNumericFields(); + if (nfields!=null) { + pixelWidthField = (TextField)nfields.elementAt(3); + pixelHeightField = (TextField)nfields.elementAt(4); + pixelDepthField = (TextField)nfields.elementAt(5); + for (int i=0; i=2?intAndUnit[1]:"sec"; + if (timeUnit.equals("sec")&&cal.frameInterval<=2.0&&cal.frameInterval>=1.0/30.0) + cal.fps = 1.0/cal.frameInterval; + if (timeUnit.equals("usec")) + timeUnit = IJ.micronSymbol + "sec"; + cal.setTimeUnit(timeUnit); + + gd.setSmartRecording(cal.xOrigin==0&&cal.yOrigin==0&&cal.zOrigin==0); + String[] origin = Tools.split(gd.getNextString(), " ,"); + gd.setSmartRecording(false); + double x = origin.length>=1?Tools.parseDouble(origin[0]):Double.NaN; + double y = origin.length>=2?Tools.parseDouble(origin[1]):Double.NaN; + double z = origin.length>=3?Tools.parseDouble(origin[2]):Double.NaN; + cal.xOrigin= Double.isNaN(x)?0.0:x; + cal.yOrigin= Double.isNaN(y)?cal.xOrigin:y; + cal.zOrigin= Double.isNaN(z)?0.0:z; + cal.setInvertY(gd.getNextBoolean()); + global2 = gd.getNextBoolean(); + if (!cal.equals(calOrig)) + imp.setCalibration(cal); + imp.setGlobalCalibration(global2?cal:null); + if (global2 || global2!=global1) + WindowManager.repaintImageWindows(); + else + imp.repaintWindow(); + if (global2 && global2!=global1) + FileOpener.setShowConflictMessage(true); + + if (Recorder.record) { + if (Recorder.scriptMode()) { + if (xUnitChanged) + Recorder.recordCall("imp.getCalibration().setXUnit(\""+xunit2+"\");", true); + if (yUnitChanged) + Recorder.recordCall("imp.getCalibration().setYUnit(\""+yunit2+"\");", true); + if (zUnitChanged) + Recorder.recordCall("imp.getCalibration().setZUnit(\""+zunit2+"\");", true); + } else { + if (xUnitChanged) + Recorder.record("Stack.setXUnit", xunit2); + if (yUnitChanged) + Recorder.record("Stack.setYUnit", yunit2); + if (zUnitChanged) + Recorder.record("Stack.setZUnit", zunit2); + } + } + + } + + String validateInterval(String interval) { + if (interval.indexOf(" ")!=-1) + return interval; + int firstLetter = -1; + for (int i=0; i0 && firstLetterAdjust>Brightness/Contrast\n" + +"or threshold levels defined using\n" + +"Image>Adjust>Threshold."); + return; + } + if (imp.getType()==ImagePlus.COLOR_RGB) { + if (imp.getStackSize()>1) + applyRGBStack(imp); + else { + ip.reset(); + Undo.setup(Undo.TRANSFORM, imp); + ip.setMinAndMax(min, max); + } + ((ColorProcessor)ip).caSnapshot(false); + resetContrastAdjuster(); + return; + } + ip.resetMinAndMax(); + int range = 256; + if (depth==16) { + range = 65536; + int defaultRange = imp.getDefault16bitRange(); + if (defaultRange>0) + range = (int)Math.pow(2,defaultRange)-1; + } + int tableSize = depth==16?65536:256; + int[] table = new int[tableSize]; + for (int i=0; i=max) + table[i] = range-1; + else + table[i] = (int)(((double)(i-min)/(max-min))*range); + } + ImageProcessor mask = imp.getMask(); + if (imp.getStackSize()>1) { + ImageStack stack = imp.getStack(); + int flags = IJ.setupDialog(imp, 0); + if (flags==PlugInFilter.DONE) { + ip.setMinAndMax(min, max); + return; + } + if (flags==PlugInFilter.DOES_STACKS) { + int current = imp.getCurrentSlice(); + for (int i=1; i<=imp.getStackSize(); i++) { + imp.setSlice(i); + ip = imp.getProcessor(); + if (mask!=null) ip.snapshot(); + ip.applyTable(table); + ip.reset(mask); + } + imp.setSlice(current); + Undo.reset(); + } else { + ip.applyTable(table); + ip.reset(mask); + } + } else { + ip.applyTable(table); + ip.reset(mask); + } + if (depth==16) + imp.setDisplayRange(0,range-1); + resetContrastAdjuster(); + } + + private void resetContrastAdjuster() { + ContrastAdjuster.update(); + } + + void applyRGBStack(ImagePlus imp) { + int current = imp.getCurrentSlice(); + int n = imp.getStackSize(); + if (!IJ.showMessageWithCancel("Update Entire Stack?", + "Apply brightness and contrast settings\n"+ + "to all "+n+" slices in the stack?\n \n"+ + "NOTE: There is no Undo for this operation.")) { + canceled = true; + return; + } + for (int i=1; i<=n; i++) { + if (i!=current) { + imp.setSlice(i); + ImageProcessor ip = imp.getProcessor(); + ip.setMinAndMax(min, max); + IJ.showProgress((double)i/n); + } + } + imp.setSlice(current); + } + +} diff --git a/src/ij/plugin/filter/LutViewer.java b/src/ij/plugin/filter/LutViewer.java new file mode 100644 index 0000000..a7a8711 --- /dev/null +++ b/src/ij/plugin/filter/LutViewer.java @@ -0,0 +1,162 @@ +package ij.plugin.filter; +import ij.*; +import ij.process.*; +import ij.gui.*; +//import ij.text.*; +import ij.measure.ResultsTable; +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; +import java.util.ArrayList; + +/** Displays the active image's look-up table. */ +public class LutViewer implements PlugInFilter { + + ImagePlus imp; + + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + return DOES_ALL+NO_UNDO+NO_CHANGES; + } + + public void run(ImageProcessor ip) { + if (ip.getNChannels()==3) { + IJ.error("RGB images do not have LUTs."); + return; + } + int xMargin = 35; + int yMargin = 20; + int width = 256; + int height = 128; + int x, y, x1, y1, x2, y2; + int imageWidth, imageHeight; + int barHeight = 12; + boolean isGray; + double scale; + + ip = imp.getChannelProcessor(); + IndexColorModel cm = (IndexColorModel)ip.getColorModel(); + LookUpTable lut = new LookUpTable(cm); + int mapSize = lut.getMapSize(); + byte[] reds = lut.getReds(); + byte[] greens = lut.getGreens(); + byte[] blues = lut.getBlues(); + isGray = lut.isGrayscale(); + + imageWidth = width + 2*xMargin; + imageHeight = height + 3*yMargin; + Image img = IJ.getInstance().createImage(imageWidth, imageHeight); + Graphics g = img.getGraphics(); + g.setColor(Color.white); + g.fillRect(0, 0, imageWidth, imageHeight); + g.setColor(Color.black); + g.drawRect(xMargin, yMargin, width, height); + + scale = 256.0/mapSize; + if (isGray) + g.setColor(Color.black); + else + g.setColor(Color.red); + x1 = xMargin; + y1 = yMargin + height - (reds[0]&0xff)/2; + for (int i = 1; i<256; i++) { + x2 = xMargin + i; + y2 = yMargin + height - (reds[(int)(i/scale)]&0xff)/2; + g.drawLine(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + } + + if (!isGray) { + g.setColor(Color.green); + x1 = xMargin; + y1 = yMargin + height - (greens[0]&0xff)/2; + for (int i = 1; i<256; i++) { + x2 = xMargin + i; + y2 = yMargin + height - (greens[(int)(i/scale)]&0xff)/2; + g.drawLine(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + } + } + + if (!isGray) { + g.setColor(Color.blue); + x1 = xMargin; + y1 = yMargin + height - (blues[0]&0xff)/2; + for (int i = 1; i<255; i++) { + x2 = xMargin + i; + y2 = yMargin + height - (blues[(int)(i/scale)]&0xff)/2; + g.drawLine(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + } + } + + x = xMargin; + y = yMargin + height + 2; + lut.drawColorBar(g, x, y, 256, barHeight); + + y += barHeight + 15; + g.setColor(Color.black); + g.drawString("0", x - 4, y); + g.drawString(""+(mapSize-1), x + width - 10, y); + g.drawString("255", 7, yMargin + 4); + g.dispose(); + + ImagePlus imp = new ImagePlus("Look-Up Table", img); + //imp.show(); + new LutWindow(imp, new ImageCanvas(imp), ip); + } + +} // LutViewer class + +class LutWindow extends ImageWindow implements ActionListener { + + private Button button; + private ImageProcessor ip; + + LutWindow(ImagePlus imp, ImageCanvas ic, ImageProcessor ip) { + super(imp, ic); + this.ip = ip; + addPanel(); + } + + void addPanel() { + Panel panel = new Panel(); + panel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + button = new Button(" List... "); + button.addActionListener(this); + panel.add(button); + add(panel); + pack(); + } + + public void actionPerformed(ActionEvent e) { + Object b = e.getSource(); + if (b==button) + list(ip); + } + + void list(ImageProcessor ip) { + IndexColorModel icm = (IndexColorModel)ip.getColorModel(); + int size = icm.getMapSize(); + byte[] r = new byte[size]; + byte[] g = new byte[size]; + byte[] b = new byte[size]; + icm.getReds(r); + icm.getGreens(g); + icm.getBlues(b); + ResultsTable rt = new ResultsTable(); + for (int i=0; i32768 pixels in width or height + * version 01-Nov-2009 Bugfix: extra lines in segmented output eliminated; watershed is also faster now + * Maximum points encoded in long array for sorting instead of separete objects that need gc + * New output type 'List' + * version 22-May-2011 Bugfix: Maximum search in EDM and float images with large dynamic range could omit maxima + * version 13-Sep-2013 added the findMaxima() and findMinima() functions for arrays (Norbert Vischer) + * version 20-Mar-2014 Watershed segmentation of EDM with tolerance>=1.0 does not kill fine particles + * version 11-Mar-2019 adds "strict" option, "noise tolerance" renamed to "prominence" + */ + +public class MaximumFinder implements ExtendedPlugInFilter, DialogListener { + //filter params + /** prominence: maximum height difference between points that are not counted as separate maxima */ + private static double tolerance = 10; + /** strict=off allows one maximum even if it is not higher than the prominence above all other pixels */ + private static boolean strict = false; + /** Output type single points */ + public final static int SINGLE_POINTS=0; + /** Output type all points around the maximum within the tolerance */ + public final static int IN_TOLERANCE=1; + /** Output type watershed-segmented image */ + public final static int SEGMENTED=2; + /** Do not create image, only mark points */ + public final static int POINT_SELECTION=3; + /** Do not create an image, just list x, y of maxima in the Results table */ + public final static int LIST=4; + /** Do not create an image, just count maxima and add count to Results table */ + public final static int COUNT=5; + /** what type of output to create (see constants above)*/ + private static int outputType; + /** what type of output to create was chosen in the dialog (see constants above)*/ + private static int dialogOutputType = POINT_SELECTION; + /** output type names */ + final static String[] outputTypeNames = new String[] + {"Single Points", "Maxima Within Tolerance", "Segmented Particles", "Point Selection", "List", "Count"}; + /** whether to exclude maxima at the edge of the image*/ + private static boolean excludeOnEdges; + /** whether to accept maxima only in the thresholded height range*/ + private static boolean useMinThreshold; + /** whether to find darkest points on light background */ + private static boolean lightBackground; + private boolean oldMacro = false; // till 1.52m, "strict" was the same as "excludeOnEdges" and "prominence" was called "noise tolerance" + private ImagePlus imp; // the ImagePlus of the setup call + private int flags = DOES_ALL|NO_CHANGES|NO_UNDO;// the flags (see interfaces PlugInFilter & ExtendedPlugInFilter) + private boolean thresholded; // whether the input image has a threshold + private boolean roiSaved; // whether the filter has changed the roi and saved the original roi + private boolean previewing; // true while dialog is displayed (processing for preview) + private Vector checkboxes; // a reference to the Checkboxes of the dialog + private boolean thresholdWarningShown = false; // whether the warning "can't find minima with thresholding" has been shown + private Label messageArea; // reference to the textmessage area for displaying the number of maxima + private double progressDone; // for progress bar, fraction of work done so far + private int nPasses = 0; // for progress bar, how many images to process (sequentially or parallel threads) + //the following are class variables for having shorter argument lists + private int width, height; // image dimensions + private int intEncodeXMask; // needed for encoding x & y in a single int (watershed): mask for x + private int intEncodeYMask; // needed for encoding x & y in a single int (watershed): mask for y + private int intEncodeShift; // needed for encoding x & y in a single int (watershed): shift of y + /** directions to 8 neighboring pixels, clockwise: 0=North (-y), 1=NE, 2=East (+x), ... 7=NW */ + private int[] dirOffset; // pixel offsets of neighbor pixels for direct addressing + private Polygon xyCoordinates; // maxima found by findMaxima() POINT_SELECTION, LIST, COUNT + final static int[] DIR_X_OFFSET = new int[] { 0, 1, 1, 1, 0, -1, -1, -1 }; + final static int[] DIR_Y_OFFSET = new int[] { -1, -1, 0, 1, 1, 1, 0, -1 }; + /** the following constants are used to set bits corresponding to pixel types */ + final static byte MAXIMUM = (byte)1; // marks local maxima (irrespective of noise tolerance) + final static byte LISTED = (byte)2; // marks points currently in the list + final static byte PROCESSED = (byte)4; // marks points processed previously + final static byte MAX_AREA = (byte)8; // marks areas near a maximum, within the tolerance + final static byte EQUAL = (byte)16; // marks contigous maximum points of equal level + final static byte MAX_POINT = (byte)32; // marks a single point standing for a maximum + final static byte ELIMINATED = (byte)64; // marks maxima that have been eliminated before watershed + /** type masks corresponding to the output types */ + final static byte[] outputTypeMasks = new byte[] {MAX_POINT, MAX_AREA, MAX_AREA}; + final static float SQRT2 = 1.4142135624f; + + + /** Method to return types supported + * @param arg Not used by this plugin-filter + * @param imp The image to be filtered + * @return Code describing supported formats etc. + * (see ij.plugin.filter.PlugInFilter & ExtendedPlugInFilter) + */ + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + return flags; + } + + public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) { + ImageProcessor ip = imp.getProcessor(); + ip.resetBinaryThreshold(); // remove any invisible threshold set by Make Binary or Convert to Mask + thresholded = ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD; + String options = Macro.getOptions(); + if (options!=null && options.indexOf("noise=") >= 0) { // ensure compatibility with old macros + oldMacro = true; // specifying "noise=", not "prominence=" + Macro.setOptions(options.replaceAll("noise=", "prominence=")); + } + GenericDialog gd = new GenericDialog(command); + String unit = (imp.getCalibration()!=null)?imp.getCalibration().getValueUnit():null; + int digits = (ip instanceof FloatProcessor || unit != null) ? 2 : 0; + if (unit.equals("Gray Value")) unit = null; + gd.addNumericField("Prominence >",tolerance, digits, 6, unit); + gd.addCheckbox("Strict", strict); + gd.addCheckbox("Exclude edge maxima", excludeOnEdges); + if (thresholded) + gd.addCheckbox("Above lower threshold", useMinThreshold); + gd.addCheckbox("Light background", lightBackground); + gd.addChoice("Output type:", outputTypeNames, outputTypeNames[dialogOutputType]); + gd.addPreviewCheckbox(pfr, "Preview point selection"); + gd.addMessage(" "); //space for number of maxima + messageArea = (Label)gd.getMessage(); + gd.addDialogListener(this); + checkboxes = gd.getCheckboxes(); + previewing = true; + gd.addHelp(IJ.URL+"/docs/menus/process.html#find-maxima"); + gd.showDialog(); //input by the user (or macro) happens here + if (gd.wasCanceled()) + return DONE; + previewing = false; + if (!dialogItemChanged(gd, null)) //read parameters + return DONE; + IJ.register(this.getClass()); //protect static class variables (parameters) from garbage collection + return flags; + } // boolean showDialog + + /** Read the parameters (during preview or after showing the dialog) */ + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + tolerance = gd.getNextNumber(); + if (tolerance<0) tolerance = 0; + outputType = previewing ? POINT_SELECTION : dialogOutputType; + strict = gd.getNextBoolean(); + excludeOnEdges = gd.getNextBoolean(); + if (thresholded) + useMinThreshold = gd.getNextBoolean(); + else + useMinThreshold = false; + lightBackground = gd.getNextBoolean(); + dialogOutputType = gd.getNextChoiceIndex(); + boolean invertedLut = imp.isInvertedLut(); + if (useMinThreshold && ((invertedLut&&!lightBackground) || (!invertedLut&&lightBackground))) { + if (!thresholdWarningShown) + if (!IJ.showMessageWithCancel( + "Find Maxima", + "\"Above Lower Threshold\" option cannot be used\n"+ + "when finding minima (image with light background\n"+ + "or image with dark background and inverting LUT).") + && !previewing) + return false; // if faulty input is not detected during preview, "cancel" quits + thresholdWarningShown = true; + useMinThreshold = false; + ((Checkbox)(checkboxes.elementAt(1))).setState(false); //reset "Above Lower Threshold" checkbox + } + if (!gd.isPreviewActive() && messageArea!=null) + messageArea.setText(""); // no "nnn Maxima" message when not previewing + return (!gd.invalidNumber()); + } // public boolean DialogItemChanged + + /** Set his to the number of images to process (for the watershed progress bar only). + * Don't call or set nPasses to zero if no progress bar is desired. */ + public void setNPasses(int nPasses) { + this.nPasses = nPasses; + } + + /** The plugin is inferred from ImageJ by this method + * @param ip The image where maxima (or minima) should be found + */ + public void run(ImageProcessor ip) { + Roi roi = imp.getRoi(); + if (outputType == POINT_SELECTION && !roiSaved) { + imp.saveRoi(); // save previous selection so user can restore it + roiSaved = true; + } + if (roi!=null && (!roi.isArea() || outputType==SEGMENTED)) { + imp.deleteRoi(); + roi = null; + } + boolean invertedLut = imp.isInvertedLut(); + double threshold = useMinThreshold?ip.getMinThreshold():ImageProcessor.NO_THRESHOLD; + if ((invertedLut&&!lightBackground) || (!invertedLut&&lightBackground)) { + threshold = ImageProcessor.NO_THRESHOLD; //don't care about threshold when finding minima + float[] cTable = ip.getCalibrationTable(); + ip = ip.duplicate(); + if (cTable==null) { //invert image for finding minima of uncalibrated images + ip.invert(); + } else { //we are using getPixelValue, so the CalibrationTable must be inverted + float[] invertedCTable = new float[cTable.length]; + for (int i=cTable.length-1; i>=0; i--) + invertedCTable[i] = -cTable[i]; + ip.setCalibrationTable(invertedCTable); + } + ip.setRoi(roi); + } + ByteProcessor outIp = null; + boolean strictMode = oldMacro ? excludeOnEdges : strict; + outIp = findMaxima(ip, tolerance, strictMode, threshold, outputType, excludeOnEdges, false); //process the image + if (outIp == null) return; //cancelled by user or previewing or no output image + if (!Prefs.blackBackground) //normally, output has an inverted LUT, "active" pixels black (255) - like a mask + outIp.invertLut(); + String resultName; + if (outputType == SEGMENTED) //Segmentation required + resultName = " Segmented"; + else + resultName = " Maxima"; + String outname = imp.getTitle(); + if (imp.getNSlices()>1) + outname += "("+imp.getCurrentSlice()+")"; + outname += resultName; + if (WindowManager.getImage(outname)!=null) + outname = WindowManager.getUniqueName(outname); + ImagePlus maxImp = new ImagePlus(outname, outIp); + Calibration cal = imp.getCalibration().copy(); + cal.disableDensityCalibration(); + maxImp.setCalibration(cal); //keep the spatial calibration + maxImp.show(); + } //public void run + + + /** Finds the image maxima and returns them as a Polygon, where + * poly.npoints is the number of maxima. There is an example at
+ * http://imagej.nih.gov/ij/macros/js/FindMaxima.js. + * @param ip The input image + * @param tolerance Height tolerance: maxima are accepted only if protruding more than this value + * from the ridge to a higher maximum + * @param excludeOnEdges Whether to exclude edge maxima. Also determines whether strict mode is on, i.e., + * whether the global maximum is accepted even if all other pixel are less than 'tolerance' + * below this level (In 1.52m and before, 'strict' and 'excludeOnEdges' were the same). + * @return A Polygon containing the coordinates of the maxima, where poly.npoints + * is the number of maxima. Note that poly.xpoints.length may be greater + * than the number of maxima. + */ + public Polygon getMaxima(ImageProcessor ip, double tolerance, boolean excludeOnEdges) { + return getMaxima(ip, tolerance, excludeOnEdges, excludeOnEdges); + } + + /** Finds the image maxima and returns them as a Polygon, where poly.npoints is + * the number of maxima. + * @param ip The input image + * @param tolerance Height tolerance: maxima are accepted only if protruding more than this value + * from the ridge to a higher maximum + * @param strict When off, the global maximum is accepted even if all other pixel are less than + * 'tolerance' below this level. With excludeOnEdges=true, 'strict' also + * means that the surounding of a maximum within 'tolerance' must not include an edge pixel + * (otherwise, it is enough that there is no edge pixel with the maximum value). + * @param excludeOnEdges Whether to exclude edge maxima. Also determines whether strict mode is on, i.e., + * whether the global maximum is accepted even if all other pixel are less than 'tolerance' + * below this level (In 1.52m and before, 'strict' and 'excludeOnEdges' were the same). + * @return A Polygon containing the coordinates of the maxima, where poly.npoints + * is the number of maxima. Note that poly.xpoints.length may be greater + * than the number of maxima. + */ + public Polygon getMaxima(ImageProcessor ip, double tolerance, boolean strict, boolean excludeOnEdges) { + findMaxima(ip, tolerance, strict, ImageProcessor.NO_THRESHOLD, + MaximumFinder.POINT_SELECTION, excludeOnEdges, false); + if (xyCoordinates==null) + return new Polygon(); + else + return xyCoordinates; + } + + /** + * Calculates peak positions of 1D array N.Vischer, 06-mar-2017 + * + * @param xx Array containing peaks. + * @param tolerance Depth of a qualified valley must exceed tolerance. + * Tolerance must be >= 0. Flat tops are marked at their centers. + * @param edgeMode 0=include, 1=exclude, 3=circular + * edgeMode = 0 (include edges) peak may be separated by one qualified valley and by a border. + * edgeMode = 1 (exclude edges) peak must be separated by two qualified valleys + * edgeMode = 2 (circular) array is regarded to be circular + * @return Positions of peaks, sorted with decreasing amplitude + */ + public static int[] findMaxima(double[] xx, double tolerance, int edgeMode ) { + final int INCLUDE_EDGE = 0; + final int CIRCULAR = 2; + int len = xx.length; + int origLen = len; + if (len<2) + return new int[0]; + if (tolerance < 0) + tolerance = 0; + if(edgeMode==CIRCULAR){ + double[] cascade3 = new double[len * 3]; + for (int jj = 0; jj min + tolerance) + leftValleyFound = true; + if (val > max && leftValleyFound) { + max = val; + maxPos = jj; + } + if (leftValleyFound) + lastMaxPos = maxPos; + if (val < max - tolerance && leftValleyFound) { + maxPositions[maxCount] = maxPos; + maxCount++; + leftValleyFound = false; + min = val; + max = val; + } + if (val < min) { + min = val; + if (!leftValleyFound) + max = val; + } + } + if (edgeMode == INCLUDE_EDGE) { + if (maxCount > 0 && maxPositions[maxCount - 1] != lastMaxPos) + maxPositions[maxCount++] = lastMaxPos; + if (maxCount == 0 && max - min >= tolerance) + maxPositions[maxCount++] = lastMaxPos; + } + int[] cropped = new int[maxCount]; + System.arraycopy(maxPositions, 0, cropped, 0, maxCount); + maxPositions = cropped; + double[] maxValues = new double[maxCount]; + for (int jj = 0; jj < maxCount; jj++) { + int pos = maxPositions[jj]; + double midPos = pos; + while (pos < len - 1 && xx[pos] == xx[pos + 1]) { + midPos += 0.5; + pos++; + } + maxPositions[jj] = (int) midPos; + maxValues[jj] = xx[maxPositions[jj]]; + } + int[] rankPositions = Tools.rank(maxValues); + int[] returnArr = new int[maxCount]; + for (int jj = 0; jj < maxCount; jj++) { + int pos = maxPositions[rankPositions[jj]]; + returnArr[maxCount - jj - 1] = pos;//use descending order + } + if(edgeMode == CIRCULAR){ + int count = 0; + for(int jj = 0; jj < returnArr.length;jj++){ + int pos = returnArr[jj] - origLen; + if(pos >= 0 && pos < origLen )//pick maxima from cascade center part + returnArr[count++] = pos; + } + int[] returrn2Arr = new int[count]; + System.arraycopy(returnArr, 0, returrn2Arr, 0, count); + returnArr = returrn2Arr; + + } + return returnArr; + } + + public static int[] findMaxima(double[] xx, double tolerance, boolean excludeOnEdges) { + int edgeBehavior = (excludeOnEdges) ? 1 : 0; + return findMaxima(xx, tolerance, edgeBehavior); + } + + /** + * Returns minimum positions of array xx, sorted with decreasing strength + */ + public static int[] findMinima(double[] xx, double tolerance, boolean excludeEdges ) { + int edgeMode = (excludeEdges) ? 1 : 0; + return findMinima(xx, tolerance, edgeMode); + } + + public static int[] findMinima(double[] xx, double tolerance, int edgeMode) { + int len = xx.length; + double[] negArr = new double[len]; + for (int jj = 0; jj < len; jj++) + negArr[jj] = -xx[jj]; + int[] minPositions = findMaxima(negArr, tolerance, edgeMode); + return minPositions; + } + + /** Find the maxima of an image. + * @param ip The input image + * @param tolerance Height tolerance: maxima are accepted only if protruding more than this value + * from the ridge to a higher maximum + * @param outputType What to mark in output image: SINGLE_POINTS, IN_TOLERANCE or SEGMENTED. + * No output image is created for output types POINT_SELECTION, LIST and COUNT. + * @param excludeOnEdges Whether to exclude edge maxima. Also determines whether strict mode is on, i.e., + * whether the global maximum is accepted even if all other pixel are less than 'tolerance' + * below this level (In 1.52m and before, 'strict' and 'excludeOnEdges' were the same). + * @return A new byteProcessor with a normal (uninverted) LUT where the marked points + * are set to 255 (Background 0). Pixels outside of the roi of the input ip are not set. + * Returns null if outputType does not require an output or if cancelled by escape + */ + public ByteProcessor findMaxima(ImageProcessor ip, double tolerance, int outputType, boolean excludeOnEdges) { + return findMaxima(ip, tolerance, ImageProcessor.NO_THRESHOLD, outputType, excludeOnEdges, false); + } + + /** Finds the maxima of an image (does not find minima). + * + * LIMITATIONS: With outputType=SEGMENTED (watershed segmentation), some segmentation lines + * may be improperly placed if local maxima are suppressed by the tolerance. + * + * @param ip The input image + * @param tolerance Height tolerance: maxima are accepted only if protruding more than this value + * from the ridge to a higher maximum + * @param threshold minimum height of a maximum (uncalibrated); for no minimum height set it to + * ImageProcessor.NO_THRESHOLD + * @param outputType What to mark in output image: SINGLE_POINTS, IN_TOLERANCE or SEGMENTED. + * No output image is created for output types POINT_SELECTION, LIST and COUNT. + * @param excludeOnEdges Whether to exclude edge maxima. Also determines whether strict mode is on, i.e., + * whether the global maximum is accepted even if all other pixel are less than 'tolerance' + * below this level (In 1.52m and before, 'strict' and 'excludeOnEdges' were the same). + * @param isEDM Whether the image is a float Euclidian Distance Map. + * @return A new byteProcessor with a normal (uninverted) LUT where the marked points + * are set to 255 (Background 0). Pixels outside of the roi of the input ip are not set. + * Returns null if outputType does not require an output or if cancelled by escape + */ + public ByteProcessor findMaxima(ImageProcessor ip, double tolerance, double threshold, + int outputType, boolean excludeOnEdges, boolean isEDM) { + return findMaxima(ip, tolerance, excludeOnEdges, threshold, outputType, excludeOnEdges, isEDM); + } + + /** Here the processing is done: Find the maxima of an image (does not find minima). + * + * LIMITATIONS: With outputType=SEGMENTED (watershed segmentation), some segmentation lines + * may be improperly placed if local maxima are suppressed by the tolerance. + * + * @param ip The input image + * @param tolerance Height tolerance: maxima are accepted only if protruding more than this value + * from the ridge to a higher maximum + * @param strict When off, the global maximum is accepted even if all other pixel are less than + * 'tolerance' below this level. With excludeOnEdges=true, 'strict' also + * means that the surounding of a maximum within 'tolerance' must not include an edge pixel + * (otherwise, it is enough that there is no edge pixel with the maximum value). + * @param threshold Minimum height of a maximum (uncalibrated); for no minimum height set it to + * ImageProcessor.NO_THRESHOLD + * @param outputType What to mark in output image: SINGLE_POINTS, IN_TOLERANCE or SEGMENTED. + * No output image is created for output types POINT_SELECTION, LIST and COUNT. + * @param excludeOnEdges Whether to exclude edge maxima + * @param isEDM Whether the image is a float Euclidian Distance Map. + * @return A new byteProcessor with a normal (uninverted) LUT where the marked points + * are set to 255 (Background 0). Pixels outside of the roi of the input ip are not set. + * Returns null if outputType does not require an output or if cancelled by escape + */ + public ByteProcessor findMaxima(ImageProcessor ip, double tolerance, boolean strict, double threshold, + int outputType, boolean excludeOnEdges, boolean isEDM) { + if (dirOffset == null) makeDirectionOffsets(ip); + Rectangle roi = ip.getRoi(); + byte[] mask = ip.getMaskArray(); + if (threshold!=ImageProcessor.NO_THRESHOLD && ip.getCalibrationTable()!=null && + threshold>0 && thresholdv) globalMin = v; + if (globalMaxglobalMin; + if (strict && globalMax - globalMin <= tolerance) + maximumPossible = false; + + if (threshold !=ImageProcessor.NO_THRESHOLD) + threshold -= (globalMax-globalMin)*1e-6;//avoid rounding errors + //for segmentation, exclusion of edge maxima cannot be done now but has to be done after segmentation: + boolean excludeEdgesNow = excludeOnEdges && outputType!=SEGMENTED; + + if (Thread.currentThread().isInterrupted()) return null; + IJ.showStatus("Getting sorted maxima..."); + long[] maxPoints = maximumPossible ? + getSortedMaxPoints(ip, typeP, excludeEdgesNow, isEDM, globalMin, globalMax, threshold) : new long[0]; + if (Thread.currentThread().isInterrupted()) return null; + IJ.showStatus("Analyzing maxima..."); + float maxSortingError = 0; + if (ip instanceof FloatProcessor) //sorted sequence may be inaccurate by this value + maxSortingError = 1.1f * (isEDM ? SQRT2/2f : (globalMax-globalMin)/2e9f); + analyzeAndMarkMaxima(ip, typeP, maxPoints, excludeEdgesNow, isEDM, globalMin, tolerance, strict, outputType, maxSortingError); + //new ImagePlus("Pixel types",typeP.duplicate()).show(); + if (outputType==POINT_SELECTION || outputType==LIST || outputType==COUNT) + return null; + + ByteProcessor outIp; + byte[] pixels; + if (outputType == SEGMENTED) { + // Segmentation required, convert to 8bit (also for 8-bit images, since the calibration + // may have a negative slope). outIp has background 0, maximum areas 255 + outIp = make8bit(ip, typeP, isEDM, globalMin, globalMax, threshold); + //if (IJ.debugMode) new ImagePlus("pixel types precleanup", typeP.duplicate()).show(); + cleanupMaxima(outIp, typeP, maxPoints); //eliminate all the small maxima (i.e. those outside MAX_AREA) + //if (IJ.debugMode) new ImagePlus("pixel types postcleanup", typeP).show(); + //if (IJ.debugMode) new ImagePlus("pre-watershed", outIp.duplicate()).show(); + if (!watershedSegment(outIp)) //do watershed segmentation + return null; //if user-cancelled, return + if (!isEDM) cleanupExtraLines(outIp); //eliminate lines due to local minima (none in EDM) + watershedPostProcess(outIp); //levels to binary image + if (excludeOnEdges) deleteEdgeParticles(outIp, typeP); + } else { //outputType other than SEGMENTED + for (int i=0; i=roi.x+roi.width || y=roi.y+roi.height) outPixels[i] = (byte)0; + else if (mask !=null && (mask[x-roi.x + roi.width*(y-roi.y)]==0)) outPixels[i] = (byte)0; + } + } + } + + return outIp; + } // public ByteProcessor findMaxima + + /** Find all local maxima (irrespective whether they finally qualify as maxima or not) + * @param ip The image to be analyzed + * @param typeP A byte image, same size as ip, where the maximum points are marked as MAXIMUM + * (do not use it as output: for rois, the points are shifted w.r.t. the input image) + * @param excludeEdgesNow Whether to exclude edge pixels + * @param isEDM Whether ip is a float Euclidian distance map + * @param globalMin The minimum value of the image or roi + * @param threshold The threshold (calibrated) below which no pixels are processed. Ignored if ImageProcessor.NO_THRESHOLD + * @return Maxima sorted by value. In each array element (long, i.e., 64-bit integer), the value + * is encoded in the upper 32 bits and the pixel offset in the lower 32 bit + * Note: Do not use the positions of the points marked as MAXIMUM in typeP, they are invalid for images with a roi. + */ + long[] getSortedMaxPoints(ImageProcessor ip, ByteProcessor typeP, boolean excludeEdgesNow, + boolean isEDM, float globalMin, float globalMax, double threshold) { + Rectangle roi = ip.getRoi(); + byte[] types = (byte[])typeP.getPixels(); + int nMax = 0; //counts local maxima + boolean checkThreshold = threshold!=ImageProcessor.NO_THRESHOLD; + Thread thread = Thread.currentThread(); + //long t0 = System.currentTimeMillis(); + for (int y=roi.y; y v && vNeighborTrue > vTrue) { + isMax = false; + break; + } + } + } + if (isMax) { + types[i] = MAXIMUM; + nMax++; + } + } // for x + } // for y + if (thread.isInterrupted()) return null; + //long t1 = System.currentTimeMillis();IJ.log("markMax:"+(t1-t0)); + + float vFactor = (float)(2e9/(globalMax-globalMin)); //for converting float values into a 32-bit int + long[] maxPoints = new long[nMax]; //value (int) is in the upper 32 bit, pixel offset in the lower + int iMax = 0; + for (int y=roi.y; y=0; iMax--) { //process all maxima now, starting from the highest + if (iMax%100 == 0 && Thread.currentThread().isInterrupted()) return; + int offset0 = (int)maxPoints[iMax]; //type cast gets 32 lower bits, where pixel index is encoded + //int offset0 = maxPoints[iMax].offset; + if ((types[offset0]&PROCESSED)!=0) //this maximum has been reached from another one, skip it + continue; + //we create a list of connected points and start the list at the current maximum + int x0 = offset0 % width; + int y0 = offset0 / width; + float v0 = isEDM?trueEdmHeight(x0,y0,ip):ip.getPixelValue(x0,y0); + boolean sortingError; + do { //repeat if we have encountered a sortingError + pList[0] = offset0; + types[offset0] |= (EQUAL|LISTED); //mark first point as equal height (to itself) and listed + int listLen = 1; //number of elements in the list + int listI = 0; //index of current element in the list + boolean isEdgeMaximum = (x0==0 || x0==width-1 || y0==0 || y0==height-1); + sortingError = false; //if sorting was inaccurate: a higher maximum was not handled so far + boolean maxPossible = true; //it may be a true maximum + double xEqual = x0; //for creating a single point: determine average over the + double yEqual = y0; // coordinates of contiguous equal-height points + int nEqual = 1; //counts xEqual/yEqual points that we use for averaging + do { //while neigbor list is not fully processed (to listLen) + int offset = pList[listI]; + int x = offset % width; + int y = offset / width; + boolean isInner = (y!=0 && y!=height-1) && (x!=0 && x!=width-1); //not necessary, but faster than isWithin + for (int d=0; d<8; d++) { //analyze all neighbors (in 8 directions) at the same level + int offset2 = offset+dirOffset[d]; + if ((isInner || isWithin(x, y, d)) && (types[offset2]&LISTED)==0) { + if (isEDM && edmPixels[offset2]<=0) + continue; //ignore the background (non-particles) + if ((types[offset2]&PROCESSED)!=0) { + maxPossible = false; //we have reached a point processed previously, thus it is no maximum now + break; + } + int x2 = x+DIR_X_OFFSET[d]; + int y2 = y+DIR_Y_OFFSET[d]; + float v2 = isEDM ? trueEdmHeight(x2, y2, ip) : ip.getPixelValue(x2, y2); + if (v2 > v0 + maxSortingError) { + maxPossible = false; //we have reached a higher point, thus it is no maximum + break; + } else if (v2 >= v0-(float)tolerance) { + if (v2 > v0) { //maybe this point should have been treated earlier + sortingError = true; + offset0 = offset2; + v0 = v2; + x0 = x2; + y0 = y2; + + } + pList[listLen] = offset2; + listLen++; //we have found a new point within the tolerance + types[offset2] |= LISTED; + if ((x2==0 || x2==width-1 || y2==0 || y2==height-1) && (strict || v2 >= v0)) { + isEdgeMaximum = true; + if (excludeEdgesNow) { + maxPossible = false; + break; //we have an edge maximum + } + } + if (v2==v0) { //prepare finding center of equal points (in case single point needed) + types[offset2] |= EQUAL; + xEqual += x2; + yEqual += y2; + nEqual ++; + } + } + } // if isWithin & not LISTED + } // for directions d + listI++; + } while (listI < listLen); + + if (sortingError) { //if x0,y0 was not the true maximum but we have reached a higher one + for (listI=0; listI0) { + PointRoi points = new PointRoi(xyCoordinates); + if (npoints<15 && points.getSize()<3) + points.setSize(3); + if (npoints==1) + points.setSize(4); + imp.setRoi(points); + } + } else if (outputType==LIST) { + Analyzer.resetCounter(); + ResultsTable rt = ResultsTable.getResultsTable(); + for (int i=0; i1) { + ImageStack stack = imp.getStack(); + int currentSlice = imp.getCurrentSlice(); + String label = stack.getShortSliceLabel(currentSlice); + String colon = s.equals("")?"":":"; + if (label!=null && !label.equals("")) + s += colon+label; + else + s += colon+currentSlice; + } + rt.setLabel(s, rt.size()-1); + } + rt.show("Results"); + } + } + if (previewing && messageArea!=null) + messageArea.setText((xyCoordinates==null ? 0 : xyCoordinates.npoints)+" Maxima"); + } //void analyzeAndMarkMaxima + + /** Create an 8-bit image by scaling the pixel values of ip to 1-254 ((byte)0 + double factor = 253/(globalMax-minValue); + if (isEDM && factor>1) + factor = 1; // with EDM, no better resolution + ByteProcessor outIp = new ByteProcessor(width, height); + //convert possibly calibrated image to byte without damaging threshold (setMinAndMax would kill threshold) + byte[] pixels = (byte[])outIp.getPixels(); + long v; + for (int y=0, i=0; y=v1 && v>=v2) { + ridgeOrMax = true; + h = (v1 + v2)/2; + } else { + h = Math.min(v1, v2); + } + h += (d%2==0) ? 1 : SQRT2; //in diagonal directions, distance is sqrt2 + if (trueH > h) trueH = h; + } + if (!ridgeOrMax) trueH = v; + return trueH; + } + } + + /** eliminate unmarked maxima for use by watershed. Starting from each previous maximum, + * explore the surrounding down to successively lower levels until a marked maximum is + * touched (or the plateau of a previously eliminated maximum leads to a marked maximum). + * Then set all the points above this value to this value + * @param outIp the image containing the pixel values + * @param typeP the types of the pixels are marked here + * @param maxPoints array containing the coordinates of all maxima that might be relevant + */ + void cleanupMaxima(ByteProcessor outIp, ByteProcessor typeP, long[] maxPoints) { + byte[] pixels = (byte[])outIp.getPixels(); + byte[] types = (byte[])typeP.getPixels(); + int nMax = maxPoints.length; + int[] pList = new int[width*height]; + for (int iMax = nMax-1; iMax>=0; iMax--) { + int offset0 = (int)maxPoints[iMax]; //type cast gets lower 32 bits where pixel offset is encoded + if ((types[offset0]&(MAX_AREA|ELIMINATED))!=0) continue; + int level = pixels[offset0]&255; + int loLevel = level+1; + pList[0] = offset0; //we start the list at the current maximum + types[offset0] |= LISTED; //mark first point as listed + int listLen = 1; //number of elements in the list + int lastLen = 1; + int listI = 0; //index of current element in the list + boolean saddleFound = false; + while (!saddleFound && loLevel >0) { + loLevel--; + lastLen = listLen; //remember end of list for previous level + listI = 0; //in each level, start analyzing the neighbors of all pixels + do { //for all pixels listed so far + int offset = pList[listI]; + int x = offset % width; + int y = offset / width; + boolean isInner = (y!=0 && y!=height-1) && (x!=0 && x!=width-1); //not necessary, but faster than isWithin + for (int d=0; d<8; d++) { //analyze all neighbors (in 8 directions) at the same level + int offset2 = offset+dirOffset[d]; + if ((isInner || isWithin(x, y, d)) && (types[offset2]&LISTED)==0) { + if ((types[offset2]&MAX_AREA)!=0 || (((types[offset2]&ELIMINATED)!=0) && (pixels[offset2]&255)>=loLevel)) { + saddleFound = true; //we have reached a point touching a "true" maximum... + break; //...or a level not lower, but touching a "true" maximum + } else if ((pixels[offset2]&255)>=loLevel && (types[offset2]&ELIMINATED)==0) { + pList[listLen] = offset2; + //xList[listLen] = x+DIR_X_OFFSET[d]; + //yList[listLen] = x+DIR_Y_OFFSET[d]; + listLen++; //we have found a new point to be processed + types[offset2] |= LISTED; + } + } // if isWithin & not LISTED + } // for directions d + if (saddleFound) break; //no reason to search any further + listI++; + } while (listI < listLen); + } // while !levelFound && loLevel>=0 + for (listI=0; listI0 + } // for x + } // for y + } // void cleanupExtraLines + + /** delete a line starting at x, y up to the next (4-connected) vertex */ + void removeLineFrom (byte[] pixels, int x, int y) { + //IJ.log("del line from "+x+","+y); + pixels[x + width*y] = (byte)255; //delete the first point + boolean continues; + do { + continues = false; + boolean isInner = (y!=0 && y!=height-1) && (x!=0 && x!=width-1); //not necessary, but faster than isWithin + for (int d=0; d<8; d+=2) { //analyze 4-connected neighbors + if (isInner || isWithin(x, y, d)) { + int v = pixels[x + width*y + dirOffset[d]]; + if (v!=(byte)255 && v!=0) { + int nRadii = nRadii(pixels, x+DIR_X_OFFSET[d], y+DIR_Y_OFFSET[d]); + if (nRadii<=1) { //found a point or line end + x += DIR_X_OFFSET[d]; + y += DIR_Y_OFFSET[d]; + pixels[x + width*y] = (byte)255; //delete the point + continues = nRadii==1; //continue along that line + break; + } + } + } + } // for directions d + } while (continues); + //IJ.log("deleted to "+x+","+y); + } // void removeLineFrom + + /** Analyze the neighbors of a pixel (x, y) in a byte image; pixels <255 ("non-white") are + * considered foreground. Edge pixels are considered foreground. + * @param ip + * @param x coordinate of the point + * @param y coordinate of the point + * @return Number of 4-connected lines emanating from this point. Zero if the point is + * embedded in either foreground or background + */ + int nRadii (byte[] pixels, int x, int y) { + int offset = x + y*width; + int countTransitions = 0; + boolean prevPixelSet = true; + boolean firstPixelSet = true; //initialize to make the compiler happy + boolean isInner = (y!=0 && y!=height-1) && (x!=0 && x!=width-1); //not necessary, but faster than isWithin + for (int d=0; d<8; d++) { //walk around the point and note every no-line->line transition + boolean pixelSet = prevPixelSet; + if (isInner || isWithin(x, y, d)) { + boolean isSet = (pixels[offset+dirOffset[d]]!=(byte)255); + if ((d&1)==0) pixelSet = isSet; //non-diagonal directions: always regarded + else if (!isSet) //diagonal directions may separate two lines, + pixelSet = false; // but are insufficient for a 4-connected line + } else { + pixelSet = true; + } + if (pixelSet && !prevPixelSet) + countTransitions ++; + prevPixelSet = pixelSet; + if (d==0) + firstPixelSet = pixelSet; + } + if (firstPixelSet && !prevPixelSet) + countTransitions ++; + return countTransitions; + } // int nRadii + + /** after watershed, set all pixels in the background and segmentation lines to 0 + */ + private void watershedPostProcess(ImageProcessor ip) { + //new ImagePlus("before postprocess",ip.duplicate()).show(); + byte[] pixels = (byte[])ip.getPixels(); + int size = ip.getWidth()*ip.getHeight(); + for (int i=0; i 0) highestValue = v; + if (histogram[v] > maxBinSize) maxBinSize = histogram[v]; + } + int[] levelOffset = new int[highestValue + 1]; + for (int y=0, i=0; y0 && v<255) { + offset = levelStart[v] + levelOffset[v]; + coordinates[offset] = x | y<=1; level--) { + int remaining = histogram[level]; //number of points in the level that have not been processed + int idle = 0; + while (remaining>0 && idle<8) { + int sumN = 0; + int dIndex = 0; + do { // expand each level in 8 directions + int n = processLevel(directionSequence[dIndex%8], ip, table, + levelStart[level], remaining, coordinates, setPointList); + //IJ.log("level="+level+" direction="+directionSequence[dIndex%8]+" remain="+remaining+"-"+n); + remaining -= n; // number of points processed + sumN += n; + if (n > 0) idle = 0; // nothing processed in this direction? + dIndex++; + } while (remaining>0 && idle++<8); + addProgress(sumN/(double)arraySize); + if (IJ.escapePressed()) { // cancelled by the user + IJ.beep(); + IJ.showProgress(1.0); + return false; + } + } + if (remaining>0 && level>1) { // any pixels that we have not reached? + int nextLevel = level; // find the next level to process + do + nextLevel--; + while (nextLevel>1 && histogram[nextLevel]==0); + // in principle we should add all unprocessed pixels of this level to the + // tasklist of the next level. This would make it very slow for some images, + // however. Thus we only add the pixels if they are at the border (of the + // image or a thresholded area) and correct unprocessed pixels at the very + // end by CleanupExtraLines + if (nextLevel > 0) { + int newNextLevelEnd = levelStart[nextLevel] + histogram[nextLevel]; + for (int i=0, p=levelStart[level]; i>intEncodeShift; + int pOffset = x + y*width; + if ((pixels[pOffset]&255)==255) IJ.log("ERROR"); + boolean addToNext = false; + if (x==0 || y==0 || x==width-1 || y==height-1) + addToNext = true; //image border + else for (int d=0; d<8; d++) + if (isWithin(x, y, d) && pixels[pOffset+dirOffset[d]]==0) { + addToNext = true; //border of area below threshold + break; + } + if (addToNext) + coordinates[newNextLevelEnd++] = xy; + } + //IJ.log("level="+level+": add "+(newNextLevelEnd-levelStart[nextLevel+1])+" points to "+nextLevel); + //tasklist for the next level to process becomes longer by this: + histogram[nextLevel] = newNextLevelEnd - levelStart[nextLevel]; + } + } + if (debug && (level>170 || level>100 && level<110 || level<10)) + movie.addSlice("level "+level, ip.duplicate()); + } + if (debug) + new ImagePlus("Segmentation Movie", movie).show(); + return true; + } // boolean watershedSegment + + + /** dilate the UEP on one level by one pixel in the direction specified by step, i.e., set pixels to 255 + * @param pass gives direction of dilation, see makeFateTable + * @param ip the EDM with the segmeted blobs successively getting set to 255 + * @param table The fateTable + * @param levelStart offsets of the level in pixelPointers[] + * @param levelNPoints number of points in the current level + * @param pixelPointers[] list of pixel coordinates (x+y*width) sorted by level (in sequence of y, x within each level) + * @param xCoordinates list of x Coorinates for the current level only (no offset levelStart) + * @return number of pixels that have been changed + */ + private int processLevel(int pass, ImageProcessor ip, int[] fateTable, + int levelStart, int levelNPoints, int[] coordinates, int[] setPointList) { + int xmax = width - 1; + int ymax = height - 1; + byte[] pixels = (byte[])ip.getPixels(); + //byte[] pixels2 = (byte[])ip2.getPixels(); + int nChanged = 0; + int nUnchanged = 0; + for (int i=0, p=levelStart; i>intEncodeShift; + int offset = x + y*width; + int index = 0; //neighborhood pixel ocupation: index in fateTable + if (y>0 && (pixels[offset-width]&255)==255) + index ^= 1; + if (x0 && (pixels[offset-width+1]&255)==255) + index ^= 2; + if (x0 && y0 && (pixels[offset-1]&255)==255) + index ^= 64; + if (x>0 && y>0 && (pixels[offset-width-1]&255)==255) + index ^= 128; + int mask = 1< +*/ +public class ParticleAnalyzer implements PlugInFilter, Measurements { + + /** Display results in the ImageJ console. */ + public static final int SHOW_RESULTS = 1; + + /** Obsolete, replaced by DISPLAY_SUMMARY */ + public static final int SHOW_SUMMARY = 2; + + /** Display image containing outlines of measured particles. */ + public static final int SHOW_OUTLINES = 4; + + /** Do not measure particles touching edge of image. */ + public static final int EXCLUDE_EDGE_PARTICLES = 8; + + /** Display image containing grayscales masks that identify measured particles. */ + public static final int SHOW_ROI_MASKS = 16; + + /** Display a progress bar. */ + public static final int SHOW_PROGRESS = 32; + + /** Clear "Results" window before starting. */ + public static final int CLEAR_WORKSHEET = 64; + + /** Record starting coordinates so outline can be recreated later using doWand(x,y). */ + public static final int RECORD_STARTS = 128; + + /** Display a summary. */ + public static final int DISPLAY_SUMMARY = 256; + + /** Do not display particle outline image. */ + public static final int SHOW_NONE = 512; + + /** Flood fill to ignore interior holes. */ + public static final int INCLUDE_HOLES = 1024; + + /** Add particles to ROI Manager. */ + public static final int ADD_TO_MANAGER = 2048; + + /** Display image containing binary masks of measured particles. */ + public static final int SHOW_MASKS = 4096; + + /** Use 4-connected particle tracing. */ + public static final int FOUR_CONNECTED = 8192; + + /** Replace original image with masks. */ + public static final int IN_SITU_SHOW = 16384; + + /** Display particle outlines as an overlay. */ + public static final int SHOW_OVERLAY_OUTLINES = 32768; + + /** Display filled particle as an overlay. */ + public static final int SHOW_OVERLAY_MASKS = 65536; + + /** Use composite ROIs for particles with holes. */ + public static final int COMPOSITE_ROIS = 131072; + + /** Use "Overlay" checkbox to display overlay. */ + public static final int OVERLAY = 262144; + + static final String OPTIONS = "ap.options"; + + static final int BYTE=0, SHORT=1, FLOAT=2, RGB=3; + static final double DEFAULT_MIN_SIZE = 0.0; + static final double DEFAULT_MAX_SIZE = Double.POSITIVE_INFINITY; + + private static double staticMinSize = 0.0; + private static double staticMaxSize = DEFAULT_MAX_SIZE; + private static boolean pixelUnits; + private static int staticOptions = Prefs.getInt(OPTIONS,CLEAR_WORKSHEET); + private static String[] showStrings = {"Nothing", "Outlines", "Bare Outlines", "Ellipses", "Masks", "Count Masks", "Overlay", "Overlay Masks"}; + private static String[] showStrings2 = {"Nothing", "Overlay", "Overlay Masks", "Outlines", "Bare Outlines", "Ellipses", "Masks", "Count Masks"}; + private static int[] showStringOrder = {0, 6, 7, 1, 2, 3, 4, 5}; + private static double staticMinCircularity=0.0, staticMaxCircularity=1.0; + + protected static final int NOTHING=0, OUTLINES=1, BARE_OUTLINES=2, ELLIPSES=3, MASKS=4, ROI_MASKS=5, + OVERLAY_OUTLINES=6, OVERLAY_MASKS=7; + protected static int staticShowChoice; + protected ImagePlus imp; + protected ResultsTable rt; + protected Analyzer analyzer; + protected int slice; + protected boolean processStack; + protected boolean showResults,excludeEdgeParticles,showSizeDistribution, + resetCounter,showProgress, recordStarts, displaySummary, floodFill, + addToManager, inSituShow, compositeRois, showOverlay; + + private boolean showResultsTable = true; + private boolean showSummaryTable = true; + private double level1, level2; + private double minSize, maxSize; + private double minCircularity, maxCircularity; + private int showChoice; + private int options; + private int measurements; + private Calibration calibration; + private String arg; + private double fillColor; + private boolean thresholdingLUT; + private ImageProcessor drawIP; + private int width,height; + private boolean canceled; + private ImageStack outlines; + private IndexColorModel customLut; + private int particleCount; + private int maxParticleCount = 0; + private int totalCount; + private ResultsTable summaryTable; + private Wand wand; + private int imageType, imageType2; + private boolean roiNeedsImage; + private int minX, maxX, minY, maxY; + private ImagePlus redirectImp; + private ImageProcessor redirectIP; + private PolygonFiller pf; + private Roi saveRoi; + private int saveSlice; + private int beginningCount; + private Rectangle r; + private ImageProcessor mask; + private double totalArea; + private FloodFiller ff; + private Roi exclusionRoi; + private RoiManager roiManager; + private static RoiManager staticRoiManager; + private static ResultsTable staticResultsTable, staticSummaryTable; + private ImagePlus outputImage; + private boolean hideOutputImage; + private int roiType; + private int wandMode = Wand.LEGACY_MODE; + private Overlay overlay; + boolean blackBackground; + private static int defaultFontSize = 9; + private static int nextFontSize = defaultFontSize; + private static Color defaultFontColor = Color.red; + private static Color nextFontColor = defaultFontColor; + private static int nextLineWidth = 1; + private int fontSize = nextFontSize; + private Color fontColor = nextFontColor; + private int lineWidth = nextLineWidth; + private boolean noThreshold; + private boolean calledByPlugin; + private boolean hyperstack; + private static LUT glasbeyLut; + + + /** Constructs a ParticleAnalyzer. + @param options a flag word created by Oring SHOW_RESULTS, EXCLUDE_EDGE_PARTICLES, etc. + @param measurements a flag word created by ORing constants defined in the Measurements interface + @param rt a ResultsTable where the measurements will be stored + @param minSize the smallest particle size in pixels + @param maxSize the largest particle size in pixels + @param minCirc minimum circularity + @param maxCirc maximum circularity + */ + public ParticleAnalyzer(int options, int measurements, ResultsTable rt, double minSize, double maxSize, double minCirc, double maxCirc) { + this.options = options; + this.measurements = measurements; + this.rt = rt; + if (this.rt==null) + this.rt = new ResultsTable(); + this.minSize = minSize; + this.maxSize = maxSize; + this.minCircularity = minCirc; + this.maxCircularity = maxCirc; + slice = 1; + if ((options&SHOW_ROI_MASKS)!=0) + showChoice = ROI_MASKS; + if ((options&SHOW_OVERLAY_OUTLINES)!=0) + showChoice = OVERLAY_OUTLINES; + if ((options&SHOW_OVERLAY_MASKS)!=0) + showChoice = OVERLAY_MASKS; + if ((options&SHOW_OUTLINES)!=0) + showChoice = OUTLINES; + if ((options&SHOW_MASKS)!=0) + showChoice = MASKS; + if ((options&SHOW_NONE)!=0) + showChoice = NOTHING; + if ((options&FOUR_CONNECTED)!=0) { + wandMode = Wand.FOUR_CONNECTED; + options |= INCLUDE_HOLES; + } + nextFontSize = defaultFontSize; + nextFontColor = defaultFontColor; + nextLineWidth = 1; + calledByPlugin = true; + } + + /** Constructs a ParticleAnalyzer using the default min and max circularity values (0 and 1). */ + public ParticleAnalyzer(int options, int measurements, ResultsTable rt, double minSize, double maxSize) { + this(options, measurements, rt, minSize, maxSize, 0.0, 1.0); + } + + /** Default constructor */ + public ParticleAnalyzer() { + slice = 1; + } + + public int setup(String arg, ImagePlus imp) { + this.arg = arg; + this.imp = imp; + IJ.register(ParticleAnalyzer.class); + if (imp==null) { + IJ.noImage(); + return DONE; + } + if (imp.getBitDepth()==24 && !isThresholdedRGB(imp)) { + IJ.error("Particle Analyzer", + "RGB images must be thresholded using\n" + +"Image>Adjust>Color Threshold."); + return DONE; + } + if (!showDialog()) + return DONE; + int baseFlags = DOES_ALL+NO_CHANGES+NO_UNDO; + int flags = IJ.setupDialog(imp, baseFlags); + processStack = (flags&DOES_STACKS)!=0; + slice = 0; + saveRoi = imp.getRoi(); + saveSlice = imp.getCurrentSlice(); + if (saveRoi!=null && saveRoi.isArea()) + exclusionRoi = saveRoi; + imp.startTiming(); + nextFontSize = defaultFontSize; + nextFontColor = defaultFontColor; + nextLineWidth = 1; + return flags; + } + + public void run(ImageProcessor ip) { + if (canceled) + return; + slice++; + if (imp.getStackSize()>1 && processStack) + imp.setSlice(slice); + if (imp.getType()==ImagePlus.COLOR_RGB) { + ip = (ImageProcessor)imp.getProperty("Mask"); + ip.setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE); + ip.setRoi(imp.getRoi()); + } + if (!analyze(imp, ip)) + canceled = true; + if (slice==imp.getStackSize()) { + imp.updateAndDraw(); + if (saveRoi!=null) imp.setRoi(saveRoi); + if (processStack) imp.setSlice(saveSlice); + } + } + + /** Displays a modal options dialog. */ + public boolean showDialog() { + Calibration cal = imp!=null?imp.getCalibration():(new Calibration()); + double unitSquared = cal.pixelWidth*cal.pixelHeight; + if (pixelUnits) + unitSquared = 1.0; + String mOptions = Macro.getOptions(); + if (mOptions!=null) { + boolean oldMacro = updateMacroOptions(); + if (oldMacro) unitSquared = 1.0; + if (mOptions.contains("in_situ")) + inSituShow = true; + if (mOptions.contains("record")) + recordStarts = true; + staticMinSize = 0.0; staticMaxSize = DEFAULT_MAX_SIZE; + staticMinCircularity=0.0; staticMaxCircularity=1.0; + staticShowChoice = NOTHING; + } + GenericDialog gd = new GenericDialog("Analyze Particles"); + minSize = staticMinSize; + maxSize = staticMaxSize; + minCircularity = staticMinCircularity; + maxCircularity = staticMaxCircularity; + showChoice = staticShowChoice; + if (maxSize==999999) + maxSize = DEFAULT_MAX_SIZE; + options = staticOptions; + String unit = cal.getUnit(); + boolean scaled = cal.scaled(); + String units = unit+"^2"; + int places = 0; + double cmin = minSize*unitSquared; + if ((int)cmin!=cmin) places = 2; + double cmax = maxSize*unitSquared; + if ((int)cmax!=cmax && cmax!=DEFAULT_MAX_SIZE) places = 2; + String minStr = ResultsTable.d2s(cmin,places); + if (minStr.indexOf("-")!=-1) { + for (int i=places; i<=6; i++) { + minStr = ResultsTable.d2s(cmin, i); + if (minStr.indexOf("-")==-1) break; + } + } + String maxStr = ResultsTable.d2s(cmax, places); + if (maxStr.indexOf("-")!=-1) { + for (int i=places; i<=6; i++) { + maxStr = ResultsTable.d2s(cmax, i); + if (maxStr.indexOf("-")==-1) break; + } + } + if (scaled) + gd.setInsets(5, 0, 0); + gd.addStringField("Size ("+units+"):", minStr+"-"+maxStr, 12); + if (scaled) { + gd.setInsets(0, 40, 5); + gd.addCheckbox("Pixel units", pixelUnits); + } + gd.addStringField("Circularity:", IJ.d2s(minCircularity)+"-"+IJ.d2s(maxCircularity), 12); + gd.addChoice("Show:", showStrings2, showStrings[showChoice]); + String[] labels = new String[8]; + boolean[] states = new boolean[8]; + labels[0]="Display results"; states[0] = (options&SHOW_RESULTS)!=0; + labels[1]="Exclude on edges"; states[1]=(options&EXCLUDE_EDGE_PARTICLES)!=0; + labels[2]="Clear results"; states[2]=(options&CLEAR_WORKSHEET)!=0; + labels[3]="Include holes"; states[3]=(options&INCLUDE_HOLES)!=0; + labels[4]="Summarize"; states[4]=(options&DISPLAY_SUMMARY)!=0; + labels[5]="Overlay"; states[5]=(options&OVERLAY)!=0; + labels[6]="Add to Manager"; states[6]=(options&ADD_TO_MANAGER)!=0; + labels[7]="Composite ROIs"; states[7]=(options&COMPOSITE_ROIS)!=0; + gd.addCheckboxGroup(4, 2, labels, states); + gd.addHelp(IJ.URL+"/docs/menus/analyze.html#ap"); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + + gd.setSmartRecording(minSize==0.0&&maxSize==Double.POSITIVE_INFINITY); + String size = gd.getNextString(); // min-max size + if (scaled) + pixelUnits = gd.getNextBoolean(); + if (pixelUnits) + unitSquared = 1.0; + else + unitSquared = cal.pixelWidth*cal.pixelHeight; + String[] minAndMax = Tools.split(size, " -"); + double mins = minAndMax.length>=1?gd.parseDouble(minAndMax[0]):0.0; + double maxs = minAndMax.length==2?gd.parseDouble(minAndMax[1]):Double.NaN; + minSize = Double.isNaN(mins)?DEFAULT_MIN_SIZE:mins/unitSquared; + maxSize = Double.isNaN(maxs)?DEFAULT_MAX_SIZE:maxs/unitSquared; + if (minSize=1?gd.parseDouble(minAndMax[0]):0.0; + double maxc = minAndMax.length==2?gd.parseDouble(minAndMax[1]):Double.NaN; + minCircularity = Double.isNaN(minc)?0.0:minc; + maxCircularity = Double.isNaN(maxc)?1.0:maxc; + if (minCircularity<0.0) minCircularity = 0.0; + if (minCircularity>maxCircularity && maxCircularity==1.0) minCircularity = 0.0; + if (minCircularity>maxCircularity) minCircularity = maxCircularity; + if (maxCircularity1 && depth==imp.getStackSize()) { + ImageStack redirectStack = redirectImp.getStack(); + redirectIP = redirectStack.getProcessor(imp.getCurrentSlice()); + } else + redirectIP = redirectImp.getProcessor(); + } else if (imp.getType()==ImagePlus.COLOR_RGB) { + ImagePlus original = (ImagePlus)imp.getProperty("OriginalImage"); + if (original!=null && original.getWidth()==imp.getWidth() && original.getHeight()==imp.getHeight()) { + redirectImp = original; + redirectIP = original.getProcessor(); + } + } + if (!setThresholdLevels(imp, ip)) + return false; + width = ip.getWidth(); + height = ip.getHeight(); + if (inSituShow && showChoice==NOTHING) + showChoice = OUTLINES; + if (!(showChoice==NOTHING||showChoice==OVERLAY_OUTLINES||showChoice==OVERLAY_MASKS)) { + blackBackground = Prefs.blackBackground && inSituShow; + if (slice==1) + outlines = new ImageStack(width, height); + if (showChoice==ROI_MASKS) + drawIP = new ShortProcessor(width, height); + else + drawIP = new ByteProcessor(width, height); + drawIP.setLineWidth(lineWidth); + if (showChoice==ROI_MASKS) + {} // Place holder for now... + else if (showChoice==MASKS&&!blackBackground) + drawIP.invertLut(); + else if (showChoice==OUTLINES) { + if (!inSituShow) { + if (customLut==null) + makeCustomLut(); + drawIP.setColorModel(customLut); + } + drawIP.setFont(new Font("SansSerif", Font.PLAIN, fontSize)); + if (fontSize>12 && inSituShow) + drawIP.setAntialiasedText(true); + } + outlines.addSlice(null, drawIP); + + if (showChoice==ROI_MASKS || blackBackground) { + drawIP.setColor(Color.black); + drawIP.fill(); + drawIP.setColor(Color.white); + } else { + drawIP.setColor(Color.white); + drawIP.fill(); + drawIP.setColor(Color.black); + } + } + calibration = redirectImp!=null?redirectImp.getCalibration():imp.getCalibration(); + + if (measurements==0) + measurements = Analyzer.getMeasurements(); + measurements &= ~LIMIT; // ignore "Limit to Threshold" + if (rt==null) { + Frame table = WindowManager.getFrame("Results"); + if (!showResults && table!=null) { + rt = new ResultsTable(); + if (resetCounter && table instanceof TextWindow) { + IJ.run("Clear Results"); + ((TextWindow)table).close(); + rt = Analyzer.getResultsTable(); + } + } else + rt = Analyzer.getResultsTable(); + } + analyzer = new Analyzer(imp, measurements, rt); + if (resetCounter && slice==1 && rt.size()>0) { + if (!Analyzer.resetCounter()) + return false; + } + beginningCount = Analyzer.getCounter(); + + byte[] pixels = null; + if (ip instanceof ByteProcessor) + pixels = (byte[])ip.getPixels(); + if (r==null) { + r = ip.getRoi(); + mask = ip.getMask(); + if (displaySummary) { + if (mask!=null) + totalArea = ImageStatistics.getStatistics(ip, AREA, calibration).area; + else + totalArea = r.width*calibration.pixelWidth*r.height*calibration.pixelHeight; + } + } + minX=r.x; maxX=r.x+r.width; minY=r.y; maxY=r.y+r.height; + if (r.width=level1 && value<=level2 && !done) { + analyzeParticle(x, y, imp, ip); + done = level1==0.0&&level2==255.0&&imp.getBitDepth()==8; + } + } + if (showProgress && ((y%inc)==0)) + IJ.showProgress((double)(y-r.y)/r.height); + if (win!=null) + canceled = !win.running; + if (canceled) { + Macro.abort(); + break; + } + } + if (showProgress) + IJ.showProgress(1.0); + if (showResults && showResultsTable && rt.size()>0) + rt.updateResults(); + imp.deleteRoi(); + ip.resetRoi(); + ip.reset(); + if (displaySummary) + updateSliceSummary(); + if (addToManager && roiManager!=null) + roiManager.setEditMode(imp, true); + maxParticleCount = (particleCount > maxParticleCount) ? particleCount : maxParticleCount; + totalCount += particleCount; + if (!canceled) + showResults(); + return true; + } + + void updateSliceSummary() { + int slices = imp.getStackSize(); + if (slices==1) { + Frame frame = WindowManager.getFrame("Summary"); + if (frame!=null && (frame instanceof TextWindow)) { + TextWindow tw = (TextWindow)frame; + ResultsTable table = tw.getTextPanel().getResultsTable(); + if (table!= null) + summaryTable = table; + } + } else { + Frame frame = WindowManager.getFrame("Summary of "+imp.getTitle()); + if (frame!=null && (frame instanceof TextWindow)) { + TextWindow tw = (TextWindow)frame; + ResultsTable table = tw.getTextPanel().getResultsTable(); + if (table!= null) + summaryTable = table; + } + } + if (summaryTable==null) + summaryTable = new ResultsTable(); + float[] areas = rt.getColumn(ResultsTable.AREA); + if (areas==null) + areas = new float[0]; + String label = imp.getTitle(); + if (slices>1) { + if (processStack) + label = imp.getStack().getShortSliceLabel(slice); + else + label = imp.getStack().getShortSliceLabel(imp.getCurrentSlice()); + label = label!=null&&!label.equals("")?label:""+slice; + } + summaryTable.incrementCounter(); + summaryTable.addValue("Slice", label); + + double sum = 0.0; + int start = areas.length-particleCount; + if (start<0) + return; + for (int i=start; i0?start:-1); + String title = slices==1?"Summary":"Summary of "+imp.getTitle(); + if (showSummaryTable) + summaryTable.show(title); + } + + void addMeans(int start) { + if ((measurements&MEAN)!=0) addMean(ResultsTable.MEAN, start); + if ((measurements&MODE)!=0) addMean(ResultsTable.MODE, start); + if ((measurements&PERIMETER)!=0) + addMean(ResultsTable.PERIMETER, start); + if ((measurements&ELLIPSE)!=0) { + addMean(ResultsTable.MAJOR, start); + addMean(ResultsTable.MINOR, start); + addMean(ResultsTable.ANGLE, start); + } + if ((measurements&SHAPE_DESCRIPTORS)!=0) { + addMean(ResultsTable.CIRCULARITY, start); + addMean(ResultsTable.SOLIDITY, start); + } + if ((measurements&FERET)!=0) { + addMean(ResultsTable.FERET, start); + addMean(ResultsTable.FERET_X, start); + addMean(ResultsTable.FERET_Y, start); + addMean(ResultsTable.FERET_ANGLE, start); + addMean(ResultsTable.MIN_FERET, start); + } + if ((measurements&INTEGRATED_DENSITY)!=0) + addMean(ResultsTable.INTEGRATED_DENSITY, start); + if ((measurements&MEDIAN)!=0) + addMean(ResultsTable.MEDIAN, start); + if ((measurements&SKEWNESS)!=0) + addMean(ResultsTable.SKEWNESS, start); + if ((measurements&KURTOSIS)!=0) + addMean(ResultsTable.KURTOSIS, start); + } + + private void addMean(int column, int start) { + double value = Double.NaN; + if (start!=-1) { + float[] c = column>=0?rt.getColumn(column):null; + if (c!=null) { + ImageProcessor ip = new FloatProcessor(c.length, 1, c, null); + if (ip==null) return; + ip.setRoi(start, 0, ip.getWidth()-start, 1); + ip = ip.crop(); + ImageStatistics stats = new FloatStatistics(ip); + if (stats==null) + return; + value = stats.mean; + } + } + summaryTable.addValue(ResultsTable.getDefaultHeading(column), value); + } + + boolean eraseOutsideRoi(ImageProcessor ip, Rectangle r, ImageProcessor mask) { + int width = ip.getWidth(); + int height = ip.getHeight(); + ip.setRoi(r); + if (excludeEdgeParticles && exclusionRoi!=null) { + ImageStatistics stats = ImageStatistics.getStatistics(ip, MIN_MAX, null); + if (fillColor>=stats.min && fillColor<=stats.max) { + double replaceColor = level1-1.0; + if (replaceColor<0.0 || replaceColor==fillColor) { + replaceColor = level2+1.0; + int maxColor = imageType==BYTE?255:65535; + if (replaceColor>maxColor || replaceColor==fillColor) { + IJ.error("Particle Analyzer", "Unable to remove edge particles"); + return false; + } + } + for (int y=minY; yAdjust->Threshold tool or the \n" + +"setThreshold(min,max) macro function."); + canceled = true; + return false; + } + level1 = 255; + level2 = 255; + fillColor = 64; + if (!Prefs.blackBackground && !imp.isInvertedLut()) { + level1 = 0; + level2 = 0; + fillColor = 192; + } + if (!IJ.isMacro()) + IJ.log("ParticleAnalyzer: threshold not set; assumed to be "+(int)level1+"-"+(int)level2); + } else { + level1 = t1; + level2 = t2; + if (imageType==BYTE) { + if (level1>0) + fillColor = 0; + else if (level2<255) + fillColor = 255; + } else if (imageType==SHORT) { + if (level1>0) + fillColor = 0; + else if (level2<65535) + fillColor = 65535; + } else if (imageType==FLOAT) + fillColor = -Float.MAX_VALUE; + else + return false; + } + imageType2 = imageType; + if (redirectIP!=null) { + if (redirectIP instanceof ShortProcessor) + imageType2 = SHORT; + else if (redirectIP instanceof FloatProcessor) + imageType2 = FLOAT; + else if (redirectIP instanceof ColorProcessor) + imageType2 = RGB; + else + imageType2 = BYTE; + } + return true; + } + + void analyzeParticle(int x, int y, ImagePlus imp, ImageProcessor ip) { + ImageProcessor ip2 = redirectIP!=null?redirectIP:ip; + wand.autoOutline(x, y, level1, level2, wandMode); + if (wand.npoints==0) + {IJ.log("wand error: "+x+" "+y); return;} + Roi roi = new PolygonRoi(wand.xpoints, wand.ypoints, wand.npoints, roiType); + Rectangle r = roi.getBounds(); + if (r.width>1 && r.height>1) { + PolygonRoi proi = (PolygonRoi)roi; + pf.setPolygon(proi.getXCoordinates(), proi.getYCoordinates(), proi.getNCoordinates()); + ip2.setMask(pf.getMask(r.width, r.height)); + if (floodFill) ff.particleAnalyzerFill(x, y, level1, level2, ip2.getMask(), r); + } + ip2.setRoi(r); + ip.setValue(fillColor); + ImageStatistics stats = getStatistics(ip2, measurements, calibration); + boolean include = true; + if (excludeEdgeParticles) { + if (r.x==minX||r.y==minY||r.x+r.width==maxX||r.y+r.height==maxY) + include = false; + if (exclusionRoi!=null && include) { + // Exclude particle if any point along boundary is not contained in roi. + Rectangle bounds = roi.getBounds(); + int x1=bounds.x+wand.xpoints[wand.npoints-1]; + int y1=bounds.y+wand.ypoints[wand.npoints-1]; + int x2, y2; + for (int i=0; i0.0 || maxCircularity!=1.0) { + double perimeter = roi.getLength(); + double circularity = perimeter==0.0?0.0:4.0*Math.PI*(stats.pixelCount/(perimeter*perimeter)); + if (circularity>1.0 && maxCircularity<=1.0) circularity = 1.0; + if (circularitymaxCircularity) include = false; + } + if (stats.pixelCount>=minSize && stats.pixelCount<=maxSize && include) { + particleCount++; + if (roiNeedsImage) + roi.setImage(imp); + stats.xstart=x; stats.ystart=y; + saveResults(stats, roi); + if (addToManager) + addToRoiManager(roi, mask, particleCount); + int saveShowChoice = showChoice; + if (showOverlay && showChoice==NOTHING) + showChoice = OVERLAY_OUTLINES; + if (showChoice!=NOTHING) + drawParticle(drawIP, roi, stats, mask); + showChoice = saveShowChoice; + } + ip.setRoi(r); + ip.fill(mask); + } + + ImageStatistics getStatistics(ImageProcessor ip, int mOptions, Calibration cal) { + switch (imageType2) { + case BYTE: + return new ByteStatistics(ip, mOptions, cal); + case SHORT: + return new ShortStatistics(ip, mOptions, cal); + case FLOAT: + return new FloatStatistics(ip, mOptions, cal); + case RGB: + return new ColorStatistics(ip, mOptions, cal); + default: + return null; + } + } + + /** Saves statistics for one particle in a results table. This is + a method subclasses can override. */ + protected void saveResults(ImageStatistics stats, Roi roi) { + analyzer.saveResults(stats, roi); + if (maxCircularity>1.0 && rt.columnExists("Circ.") && rt.getValue("Circ.", rt.size()-1)==1.0) { + double perimeter = roi.getLength(); + double circularity = perimeter==0.0?0.0:4.0*Math.PI*(stats.pixelCount/(perimeter*perimeter)); + rt.addValue("Circ.", circularity); + } + if (recordStarts) { + rt.addValue("XStart", stats.xstart); + rt.addValue("YStart", stats.ystart); + } + if (showResultsTable && showResults) + rt.addResults(); + } + + /** Adds the ROI to the ROI Manager. */ + private void addToRoiManager(Roi roi, ImageProcessor mask, int particleNumber) { + if (roiManager==null) { + if (Macro.getOptions()!=null && Interpreter.isBatchMode()) + roiManager = Interpreter.getBatchModeRoiManager(); + if (roiManager==null) { + Frame frame = WindowManager.getFrame("ROI Manager"); + if (frame==null) + IJ.run("ROI Manager..."); + frame = WindowManager.getFrame("ROI Manager"); + if (frame==null || !(frame instanceof RoiManager)) + {addToManager=false; return;} + roiManager = (RoiManager)frame; + } + if (resetCounter) + roiManager.runCommand("reset"); + } + if (imp.getStackSize()>1) { + int n = imp.getCurrentSlice(); + if (hyperstack) { + int[] pos = imp.convertIndexToPosition(n); + roi.setPosition(pos[0],pos[1],pos[2]); + } else + roi.setPosition(n); + } + if (lineWidth!=1) + roi.setStrokeWidth(lineWidth); + roiManager.add(imp, roi, particleNumber); + } + + /** Draws a selected particle in a separate image. This is + another method subclasses may want to override. */ + protected void drawParticle(ImageProcessor drawIP, Roi roi, ImageStatistics stats, ImageProcessor mask) { + switch (showChoice) { + case MASKS: drawFilledParticle(drawIP, roi, mask); break; + case OUTLINES: case BARE_OUTLINES: case OVERLAY_OUTLINES: case OVERLAY_MASKS: + drawOutline(drawIP, roi, mask, rt.size()); break; + case ELLIPSES: drawEllipse(drawIP, stats, rt.size()); break; + case ROI_MASKS: drawRoiFilledParticle(drawIP, roi, mask, rt.size()); break; + default: + } + } + + void drawFilledParticle(ImageProcessor ip, Roi roi, ImageProcessor mask) { + ip.setRoi(roi.getBounds()); + ip.fill(mask); + } + + void drawOutline(ImageProcessor ip, Roi roi, ImageProcessor mask, int count) { + if (showChoice==OVERLAY_OUTLINES || showChoice==OVERLAY_MASKS) { + if (overlay==null) { + overlay = new Overlay(); + overlay.drawLabels(true); + overlay.setLabelFont(new Font("SansSerif", Font.PLAIN, fontSize)); + overlay.setDraggable(false); + } + Roi roi2 = (Roi)roi.clone(); + roi2.setStrokeColor(Color.cyan); + if (lineWidth!=1) + roi2.setStrokeWidth(lineWidth); + if (showChoice==OVERLAY_MASKS) + roi2.setFillColor(getMaskColor(count-1)); + if (processStack || imp.getStackSize()>1) { + int currentSlice = slice; + if (!processStack) + currentSlice = imp.getCurrentSlice(); + if (hyperstack) { + int[] pos = imp.convertIndexToPosition(currentSlice); + roi2.setPosition(pos[0],pos[1],pos[2]); + } else + roi2.setPosition(currentSlice); + } + if (showResults) + roi2.setName(""+count); + overlay.add(roi2); + } else { + Rectangle r = roi.getBounds(); + if (!inSituShow) + ip.setValue(0.0); + if (roi instanceof PolygonRoi) { + int nPoints = ((PolygonRoi)roi).getNCoordinates(); + int[] xp = ((PolygonRoi)roi).getXCoordinates(); + int[] yp = ((PolygonRoi)roi).getYCoordinates(); + int x=r.x, y=r.y; + ip.moveTo(x+xp[0], y+yp[0]); + for (int i=1; i0 && (!processStack||slice==imp.getStackSize())) { + if (processStack) + imp.setOverlay(overlay); + else { + Overlay overlay0 = imp.getOverlay(); + if (overlay0==null || imp.getStackSize()==1) + imp.setOverlay(overlay); + else { + for (int i=0; i1 && outputStack.getSize()==1 && imp.getBitDepth()==8) + imp.setProcessor(outputStack.getProcessor(1)); + else + imp.setStack(null, outputStack); + } else if (!hideOutputImage) + outputImage.show(); + } + if (showResults && !processStack) { + if (showResultsTable && rt.size()>0) { + TextPanel tp = IJ.getTextPanel(); + if (beginningCount>0 && tp!=null && tp.getLineCount()!=count) + rt.show("Results"); + } + Analyzer.firstParticle = beginningCount; + Analyzer.lastParticle = Analyzer.getCounter()-1; + } else + Analyzer.firstParticle = Analyzer.lastParticle = 0; + if (showResults && rt.size()==0 && !(IJ.isMacro()||calledByPlugin) && (!processStack||slice==imp.getStackSize())) { + int digits = (int)level1==level1&&(int)level2==level2?0:2; + String range = IJ.d2s(level1,digits)+"-"+IJ.d2s(level2,digits); + String assummed = noThreshold?"assumed":""; + IJ.showMessage("Particle Analyzer", "No particles were detected. The "+assummed+"\nthreshold ("+range+") may not be correct."); + } + } + + /** Returns the "Outlines", "Masks", "Elipses" or "Count Masks" image, + or null if "Nothing" is selected in the "Show:" menu. */ + public ImagePlus getOutputImage() { + return outputImage; + } + + /** Set 'hideOutputImage' true to not display the "Show:" image. */ + public void setHideOutputImage(boolean hideOutputImage) { + this.hideOutputImage = hideOutputImage; + } + + /** Sets the size of the font used to label outlines in the next particle analyzer instance. */ + public static void setFontSize(int size) { + nextFontSize = size; + } + + /** Sets the color ("blue", "black", etc.) of the font used to label outlines in the next particle analyzer instance. */ + public static void setFontColor(String color) { + nextFontColor = Colors.decode(color, defaultFontColor); + } + + /** Sets the outline line width for the next ParticleAnalyzer instance. */ + public static void setLineWidth(int width) { + nextLineWidth = width; + } + + /** Sets the RoiManager to be used by the next ParticleAnalyzer + instance. There is a JavaScript example at + http://imagej.nih.gov/ij/macros/js/HiddenRoiManager.js + */ + public static void setRoiManager(RoiManager manager) { + staticRoiManager = manager; + } + + /** Sets the ResultsTable to be used by the next + ParticleAnalyzer instance. */ + public static void setResultsTable(ResultsTable rt) { + staticResultsTable = rt; + } + + /** Sets the ResultsTable to be used by the next + ParticleAnalyzer instance for the summary. */ + public static void setSummaryTable(ResultsTable rt) { + staticSummaryTable = rt; + } + + int getColumnID(String name) { + int id = rt.getFreeColumn(name); + if (id==ResultsTable.COLUMN_IN_USE) + id = rt.getColumnIndex(name); + return id; + } + + void makeCustomLut() { + IndexColorModel cm = (IndexColorModel)LookUpTable.createGrayscaleColorModel(false); + byte[] reds = new byte[256]; + byte[] greens = new byte[256]; + byte[] blues = new byte[256]; + cm.getReds(reds); + cm.getGreens(greens); + cm.getBlues(blues); + reds[1] =(byte)fontColor.getRed(); + greens[1] = (byte)fontColor.getGreen();; + blues[1] = (byte)fontColor.getBlue();; + customLut = new IndexColorModel(8, 256, reds, greens, blues); + } + + /** Called once when ImageJ quits. */ + public static void savePreferences(Properties prefs) { + prefs.put(OPTIONS, Integer.toString(staticOptions)); + } + +} diff --git a/src/ij/plugin/filter/PlugInFilter.java b/src/ij/plugin/filter/PlugInFilter.java new file mode 100644 index 0000000..45c144e --- /dev/null +++ b/src/ij/plugin/filter/PlugInFilter.java @@ -0,0 +1,93 @@ +package ij.plugin.filter; +import ij.*; +import ij.process.*; + +/** ImageJ plugins that process an image should implement this interface. + * For filters that have a dialog asking for options or parameters as well + * as for filters that have a progress bar and process stacks the + * ExtendedPlugInFilter interface is recommended. + */ +public interface PlugInFilter { + + /** This method is called once when the filter is loaded. 'arg', + which may be blank, is the argument specified for this plugin + in IJ_Props.txt or in the plugins.config file of a jar archive + containing the plugin. 'imp' is the currently active image. + This method should return a flag word that specifies the + filters capabilities. +

+ For Plugin-filters specifying the {@link #FINAL_PROCESSING} flag, + the setup method will be called again, this time with + arg = "final" after all other processing is done. + */ + public int setup(String arg, ImagePlus imp); + + /** Filters use this method to process the image. If the + {@link #DOES_STACKS} flag was set, it is called for each slice in + a stack. With {@link #CONVERT_TO_FLOAT}, the filter is called with + the image data converted to a FloatProcessor (3 times per + image for RGB images). ImageJ will lock the image before calling + this method and unlock it when the filter is finished. + For PlugInFilters specifying the {@link #NO_IMAGE_REQUIRED} flag + and not the {@link #DONE} flag, run(ip) is called once with the + argument null. + */ + public void run(ImageProcessor ip); + + /** Set this flag if the filter handles 8-bit grayscale images. */ + public int DOES_8G = 1; + /** Set this flag if the filter handles 8-bit indexed color images. */ + public int DOES_8C = 2; + /** Set this flag if the filter handles 16-bit images. */ + public int DOES_16 = 4; + /** Set this flag if the filter handles float images. */ + public int DOES_32 = 8; + /** Set this flag if the filter handles RGB images. */ + public int DOES_RGB = 16; + /** Set this flag if the filter handles all types of images. */ + public int DOES_ALL = DOES_8G+DOES_8C+DOES_16+DOES_32+DOES_RGB; + /** Set this flag if the filter wants its run() method to be + called for all the slices in a stack. */ + public int DOES_STACKS = 32; + /** Set this flag if the filter wants ImageJ, for non-rectangular + ROIs, to restore that part of the image that's inside the bounding + rectangle but outside of the ROI. */ + public int SUPPORTS_MASKING = 64; + /** Set this flag if the filter makes no changes to the pixel data and does not require undo. */ + public int NO_CHANGES = 128; + /** Set this flag if the filter does not require undo. */ + public int NO_UNDO = 256; + /** Set this flag if the filter does not require that an image be open. */ + public int NO_IMAGE_REQUIRED = 512; + /** Set this flag if the filter requires an ROI. */ + public int ROI_REQUIRED = 1024; + /** Set this flag if the filter requires a stack. */ + public int STACK_REQUIRED = 2048; + /** Set this flag if the filter does not want its run method called. */ + public int DONE = 4096; + /** Set this flag to have the ImageProcessor that is passed to the run() method + converted to a FloatProcessor. With RGB images, the run() method is + called three times, once for each channel. */ + public int CONVERT_TO_FLOAT = 8192; + /** Set this flag if the filter requires a snapshot (copy of the pixels array). */ + public int SNAPSHOT = 16384; + /** Set this flag if the filter wants to be called with arg = "dialog" after + being invocated the first time */ + /** Set this flag if the slices of a stack may be processed in parallel threads */ + public final int PARALLELIZE_STACKS = 32768; + /** Set this flag if the setup method of the filter should be called again after + * the calls to the run(ip) have finished. The argument arg of setup + * will be "final" in that case. */ + public final int FINAL_PROCESSING = 65536; + /** Set this flag to keep the invisible binary threshold from being reset. */ + public final int KEEP_THRESHOLD = 131072; + /** Set this flag if images may be processed in parallel threads. Overrides PARALLELIZE_STACKS. + The plugin's run() method is called in parallel threads, with the ROI rectangle of the + ImageProcessor set according to the area that should be processed. Use the + Edit/Options/Memory & Threads command to view or set the thread count. */ + public final int PARALLELIZE_IMAGES = 262144; + /** Set this flag to prevent Undo from being reset when processing a stack. */ + public final int NO_UNDO_RESET = 524288; + + // flags 0x01000000 and above are reserved for ExtendedPlugInFilter +} diff --git a/src/ij/plugin/filter/PlugInFilterRunner.java b/src/ij/plugin/filter/PlugInFilterRunner.java new file mode 100644 index 0000000..a488677 --- /dev/null +++ b/src/ij/plugin/filter/PlugInFilterRunner.java @@ -0,0 +1,634 @@ +package ij.plugin.filter; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.plugin.filter.PlugInFilter.*; +import ij.plugin.filter.*; +import ij.measure.Calibration; +import ij.macro.Interpreter; +import java.awt.*; +import java.util.*; + +public class PlugInFilterRunner implements Runnable, DialogListener { + private String command; // the command, can be but need not be the name of the PlugInFilter + private Object theFilter; // the instance of the PlugInFilter + private ImagePlus imp; + private int flags; // the flags returned by the PlugInFilter + private Overlay originalOverlay; // overlay before pressing 'preview', to revert + private boolean previewCheckboxOn; // the state of the preview checkbox (true = on) + private boolean bgPreviewOn; // tells the background thread that preview is allowed + private boolean bgKeepPreview; // tells the background thread to keep the result of preview + private Thread previewThread; // the thread of the preview + private GenericDialog gd; // the dialog (if it registers here by setDialog) + private Checkbox previewCheckbox; // reference to the preview Checkbox of the dialog + private long previewTime; // time (ms) needed for preview processing + private boolean ipChanged; // whether the image data have been changed + private int processedAsPreview; // the slice processed during preview (if non-zero) + private Object snapshotPixels; // the snapshot to show we have one and for undo in case of parallel actions intervening + private Hashtable slicesForThread; // gives first&last slice that a given thread should process + private Hashtable roisForThread;// gives ROI that a given thread should process + Hashtable sliceForThread = new Hashtable(); // here the stack slice currently processed is stored. + private int nPasses; // the number of calls to the run(ip) method of the filter + private int pass; // passes done so far + private boolean doStack; + + /** The constructor runs a PlugInFilter or ExtendedPlugInFilter by calling its + * setup, run, etc. methods. For details, see the documentation of interfaces + * PlugInFilter and ExtendedPlugInFilter. + * @param theFilter The PlugInFilter to be run + * @param command The command that has caused running the PlugInFilter + * @param arg The argument specified for this PlugInFilter in IJ_Props.txt or in the + * plugins.config file of a .jar archive conatining a collection of plugins. arg + * may be a string of length zero. + */ + public PlugInFilterRunner(Object theFilter, String command, String arg) { + this.theFilter = theFilter; + this.command = command; + imp = WindowManager.getCurrentImage(); + flags = ((PlugInFilter)theFilter).setup(arg, imp); // S E T U P + if ((flags&PlugInFilter.DONE)!=0) return; + if (!checkImagePlus(imp, flags, command)) return; // check whether the PlugInFilter can handle this image type + if ((flags&PlugInFilter.NO_IMAGE_REQUIRED)!=0) + imp = null; // if the plugin does not want an image, it should not get one + Roi roi = null; + if (imp != null) { + roi = imp.getRoi(); + if (roi!=null) roi.endPaste(); // prepare the image: finish previous paste operation (if any) + if (!imp.lock()) + return; // exit if image is in use + nPasses = ((flags&PlugInFilter.CONVERT_TO_FLOAT)!=0) ? imp.getProcessor().getNChannels():1; + } + if (theFilter instanceof ExtendedPlugInFilter) { // calling showDialog required? + try { + flags = ((ExtendedPlugInFilter)theFilter).showDialog(imp, command, this); // D I A L O G (may include preview) + } catch(Exception e) { + killPreview(); + if (Macro.MACRO_CANCELED.equals(e.getMessage())) + throw new RuntimeException(Macro.MACRO_CANCELED); + } + if (snapshotPixels != null) + Undo.setup(Undo.FILTER, imp); // ip has a snapshot that may be used for Undo + boolean keepPreviewFlag = (flags&ExtendedPlugInFilter.KEEP_PREVIEW)!=0; + if (keepPreviewFlag && imp!=null && previewThread!=null && ipChanged && + previewCheckbox!=null && previewCheckboxOn) { + bgKeepPreview = true; + waitForPreviewDone(); + processedAsPreview = imp.getCurrentSlice(); + } else { + killPreview(); + previewTime = 0; + } + } // if ExtendedPlugInFilter + if ((flags&PlugInFilter.DONE)!=0) { + if (imp != null) + imp.unlock(); + return; + } else if (imp==null) { + ((PlugInFilter)theFilter).run(null); // not DONE, but NO_IMAGE_REQUIRED + return; + } + /* preparing for the run(ip) method of the PlugInFilter... */ + int slices = imp.getStackSize(); + //IJ.log("processedAsPreview="+processedAsPreview+"; slices="+slices+"; doesStacks="+((flags&PlugInFilter.DOES_STACKS)!=0)); + if ((flags&PlugInFilter.PARALLELIZE_IMAGES)!=0) + flags &= ~PlugInFilter.PARALLELIZE_STACKS; + doStack = slices>1 && (flags&PlugInFilter.DOES_STACKS)!=0; + imp.startTiming(); + if (doStack || processedAsPreview==0) { // if processing during preview was not enough + //IJ.showStatus(command + (doStack ? " (Stack)..." : "...")); + ImageProcessor ip = imp.getProcessor(); + pass = 0; + if (!doStack) { // single image + FloatProcessor fp = null; + prepareProcessor(ip, imp); + announceSliceNumber(imp.getCurrentSlice()); + if (theFilter instanceof ExtendedPlugInFilter) + ((ExtendedPlugInFilter)theFilter).setNPasses(nPasses); + if ((flags&PlugInFilter.NO_CHANGES)==0) { // for filters modifying the image + boolean disableUndo = Prefs.disableUndo || (flags&PlugInFilter.NO_UNDO)!=0; + if (!disableUndo || ((ip instanceof ColorProcessor)&&WindowManager.getWindow("B&C")!=null)) { + ip.snapshot(); + snapshotPixels = ip.getSnapshotPixels(); + } + } + processOneImage(ip, fp, snapshotPixels); // may also set class variable snapshotPixels + if ((flags&PlugInFilter.NO_CHANGES)==0) { // (filters doing no modifications don't change undo status) + if (snapshotPixels != null) { + ip.setSnapshotPixels(snapshotPixels); + Undo.setup(Undo.FILTER, imp); + } else + Undo.reset(); + } + if ((flags&PlugInFilter.NO_CHANGES)==0&&(flags&PlugInFilter.KEEP_THRESHOLD)==0) + ip.resetBinaryThreshold(); + } else { // stack + if ((flags&PlugInFilter.NO_UNDO_RESET)==0) + Undo.reset(); // no undo for processing a complete stack + IJ.resetEscape(); + int slicesToDo = processedAsPreview!=0 ? slices-1 : slices; + nPasses *= slicesToDo; + if (theFilter instanceof ExtendedPlugInFilter) + ((ExtendedPlugInFilter)theFilter).setNPasses(nPasses); + int threads = 1; + if ((flags&PlugInFilter.PARALLELIZE_STACKS)!=0) { + threads = Prefs.getThreads(); // multithread support for multiprocessor machines + if (threads>slicesToDo) threads = slicesToDo; + if (threads>1) slicesForThread = new Hashtable(threads-1); + } + int startSlice = 1; + for (int i=1; i0) { // for all other threads: + Thread theThread = (Thread)slicesForThread.keys().nextElement(); + try { + theThread.join(); // wait until thread has finished + } catch (InterruptedException e) {} + slicesForThread.remove(theThread); // and remove it from the list. + } + } + } + } // end processing: + if ((flags&PlugInFilter.FINAL_PROCESSING)!=0 && !IJ.escapePressed()) + ((PlugInFilter)theFilter).setup("final", imp); + if (IJ.escapePressed()) { + IJ.showStatus(command + " INTERRUPTED"); + IJ.showProgress(1,1); + } else + IJ.showTime(imp, imp.getStartTime()-previewTime, command + ": ", doStack?slices:1); + IJ.showProgress(1.0); + if (ipChanged) { + imp.changes = true; + imp.updateAndDraw(); + } + ImageWindow win = imp.getWindow(); + if (win!=null) { + win.running = false; + win.running2 = false; + } + imp.unlock(); + } + + /** Process a stack or part of it. The slice given by class variable + * processedAsPreview remains unchanged. + * @param firstSlice Slice number of the first slice to be processed + * @param endSlice Slice number of the last slice to be processed + */ + private void processStack(int firstSlice, int endSlice) { + ImageStack stack = imp.getStack(); + ImageProcessor ip = stack.getProcessor(firstSlice); + prepareProcessor(ip, imp); + ip.setLineWidth(Line.getWidth()); //in contrast to imp.getProcessor, stack.getProcessor does not do this + FloatProcessor fp = null; + int slices = imp.getNSlices(); + for (int i=firstSlice; i<=endSlice; i++) { + if (i != processedAsPreview) { + announceSliceNumber(i); + ip.setPixels(stack.getPixels(i)); + ip.setSliceNumber(i); + ip.setSnapshotPixels(null); + processOneImage(ip, fp, null); + if (IJ.escapePressed()) {IJ.beep(); break;} + } + } + } + + /** prepare an ImageProcessor by setting roi and CalibrationTable. + */ + private void prepareProcessor(ImageProcessor ip, ImagePlus imp) { + ImageProcessor mask = imp.getMask(); + Roi roi = imp.getRoi(); + if (roi!=null && roi.isArea()) + ip.setRoi(roi); + else + ip.setRoi((Roi)null); + if (imp.getStackSize()>1) { + ImageProcessor ip2 = imp.getProcessor(); + double min1 = ip2.getMinThreshold(); + double max1 = ip2.getMaxThreshold(); + double min2 = ip.getMinThreshold(); + double max2 = ip.getMaxThreshold(); + if (min1!=ImageProcessor.NO_THRESHOLD && (min1!=min2||max1!=max2)) + ip.setThreshold(min1, max1, ImageProcessor.NO_LUT_UPDATE); + } + //float[] cTable = imp.getCalibration().getCTable(); + //ip.setCalibrationTable(cTable); + } + + /** + * Process a single image with the PlugInFilter. + * @param ip The image data that should be processed + * @param fp A Floatprocessor as a target for conversion to Float. May be null. + * @param snapshotPixels Valid snapshotPixels for the current ImageProcessor (null when processing a stack) + * Class variables used: flags (input), snapshotPixels (set if a snapshot of ip + * is taken), ipChanged (set if ip was probably changed). + */ + private void processOneImage(ImageProcessor ip, FloatProcessor fp, Object snapshotPixels) { + if ((flags&PlugInFilter.PARALLELIZE_IMAGES)!=0) { + processImageUsingThreads(ip, fp, snapshotPixels); + return; + } + Thread thread = Thread.currentThread(); + boolean convertToFloat = (flags&PlugInFilter.CONVERT_TO_FLOAT)!=0 && !(ip instanceof FloatProcessor); + boolean doMasking = (flags&PlugInFilter.SUPPORTS_MASKING)!=0 && ip.getMask() != null; + if (snapshotPixels==null && (doMasking || ((flags&PlugInFilter.SNAPSHOT)!=0) && !convertToFloat)) { + ip.snapshot(); + this.snapshotPixels = ip.getSnapshotPixels(); + } + if (convertToFloat) { + for (int i=0; iroi.height) threads = roi.height; + if (threads>1) roisForThread = new Hashtable(threads-1); + int y1 = roi.y; + for (int i=1; i en = roisForThread.keys(); en.hasMoreElements();) { + Thread theThread = en.nextElement(); + try { + theThread.join(); // wait until thread has finished + } catch (InterruptedException e) { // if preview cancelled: + interruptRoiThreads(roisForThread); //interrupt all threads and join + Thread.currentThread().interrupt(); //restore 'interrupted' state + break; + } + } + } + roisForThread = null; + ip.setMask(mask); // restore ROI + ip.setRoi(roi); + } + + ImageProcessor duplicateProcessor(ImageProcessor ip, Rectangle roi) { + ImageProcessor ip2 = (ImageProcessor)ip.clone(); + ip2.setRoi(roi); + return ip2; + } + + /** interrupt threads processing the rois of an image and wait till they have finished */ + void interruptRoiThreads(Hashtable roisForThread) { + if (roisForThread==null) return; //class variable may become null in other thread + for (Enumeration en = roisForThread.keys(); en.hasMoreElements();) + ((Thread)en.nextElement()).interrupt(); //interrupt all threads + for (Enumeration en = roisForThread.keys(); en.hasMoreElements();) + try { + ((Thread)en.nextElement()).join(); + } catch (Exception e){} + } + + /** test whether an ImagePlus can be processed based on the flags specified + * and display an error message if not. + */ + private boolean checkImagePlus(ImagePlus imp, int flags, String cmd) { + boolean imageRequired = (flags&PlugInFilter.NO_IMAGE_REQUIRED)==0; + if (imageRequired && imp==null) + {IJ.noImage(); return false;} + if (imageRequired) { + if (imp.getProcessor()==null) + {wrongType(flags, cmd); return false;} + int type = imp.getType(); + switch (type) { + case ImagePlus.GRAY8: + if ((flags&PlugInFilter.DOES_8G)==0) + {wrongType(flags, cmd); return false;} + break; + case ImagePlus.COLOR_256: + if ((flags&PlugInFilter.DOES_8C)==0) + {wrongType(flags, cmd); return false;} + break; + case ImagePlus.GRAY16: + if ((flags&PlugInFilter.DOES_16)==0) + {wrongType(flags, cmd); return false;} + break; + case ImagePlus.GRAY32: + if ((flags&PlugInFilter.DOES_32)==0) + {wrongType(flags, cmd); return false;} + break; + case ImagePlus.COLOR_RGB: + if ((flags&PlugInFilter.DOES_RGB)==0) + {wrongType(flags, cmd); return false;} + break; + } + if ((flags&PlugInFilter.ROI_REQUIRED)!=0 && imp.getRoi()==null) + {IJ.error(cmd, "This command requires a selection"); return false;} + if ((flags&PlugInFilter.STACK_REQUIRED)!=0 && imp.getStackSize()==1) + {IJ.error(cmd, "This command requires a stack"); return false;} + } // if imageRequired + return true; + } + + /** Display an error message, telling the allowed image types + */ + static void wrongType(int flags, String cmd) { + String s = "\""+cmd+"\" requires an image of type:\n \n"; + if ((flags&PlugInFilter.DOES_8G)!=0) s += " 8-bit grayscale\n"; + if ((flags&PlugInFilter.DOES_8C)!=0) s += " 8-bit color\n"; + if ((flags&PlugInFilter.DOES_16)!=0) s += " 16-bit grayscale\n"; + if ((flags&PlugInFilter.DOES_32)!=0) s += " 32-bit (float) grayscale\n"; + if ((flags&PlugInFilter.DOES_RGB)!=0) s += " RGB color\n"; + IJ.error(s); + } + + /** Make the slice number accessible to the PlugInFilter by putting it + * into the appropriate hashtable. + */ + private void announceSliceNumber(int slice) { + synchronized(sliceForThread){ + Integer number = new Integer(slice); + sliceForThread.put(Thread.currentThread(), number); + } + } + + /** Return the slice number currently processed by the calling thread. + * @return The slice number. Returns -1 on error (when not processing). + */ + public int getSliceNumber() { + synchronized(sliceForThread){ + Integer number = (Integer)sliceForThread.get(Thread.currentThread()); + return (number == null) ? -1 : number.intValue(); + } + } + + /** The dispatcher for the background threads + */ + public void run() { + Thread thread = Thread.currentThread(); + try { + if (thread==previewThread) + runPreview(); + else if (roisForThread!=null && roisForThread.containsKey(thread)) { + ImageProcessor ip = (ImageProcessor)roisForThread.get(thread); + ((PlugInFilter)theFilter).run(ip); + ip.setPixels(null); + ip.setSnapshotPixels(null); + } else if (slicesForThread!=null && slicesForThread.containsKey(thread)) { + int[] range = (int[])slicesForThread.get(thread); + processStack(range[0], range[1]); + } else + IJ.error("PlugInFilterRunner internal error:\nunsolicited background thread"); + } catch (Exception err) { + if (thread==previewThread) { + gd.previewRunning(false); + IJ.wait(100); // needed on Macs + previewCheckbox.setState(false); + bgPreviewOn = false; + previewThread = null; + } + String msg = ""+err; + if (msg.indexOf(Macro.MACRO_CANCELED)==-1) { + IJ.beep(); + IJ.log("ERROR: "+msg+"\nin "+thread.getName()+ + "\nat "+(err.getStackTrace()[0])+"\nfrom "+(err.getStackTrace()[1])); + } + } + } + + /** The background thread for preview */ + private void runPreview() { + if (IJ.debugMode) + IJ.log("preview thread started; imp="+imp.getTitle()); + Thread thread = Thread.currentThread(); + ImageProcessor ip = imp.getProcessor(); + Roi originalRoi = imp.getRoi(); + originalOverlay = imp.getOverlay(); + FloatProcessor fp = null; + prepareProcessor(ip, imp); + announceSliceNumber(imp.getCurrentSlice()); + if (snapshotPixels==null && (flags&PlugInFilter.NO_CHANGES)==0) { + ip.snapshot(); + snapshotPixels = ip.getSnapshotPixels(); + } + boolean previewDataOk = false; + while(bgPreviewOn) { + if (previewCheckboxOn) gd.previewRunning(true); // visual feedback + interruptable: { + if (imp.getRoi() != originalRoi) { + imp.setRoi(originalRoi); // restore roi; the PlugInFilter may have affected it + if (originalRoi!=null && originalRoi.isArea()) + ip.setRoi(originalRoi); + else + ip.setRoi((Roi)null); + } + if (ipChanged) { // restore image data if necessary + ip.setSnapshotPixels(snapshotPixels); + ip.reset(); + } + ipChanged = false; + previewDataOk = false; + long startTime = System.currentTimeMillis(); + pass = 0; + if (theFilter instanceof ExtendedPlugInFilter) + ((ExtendedPlugInFilter)theFilter).setNPasses(nPasses); //this should reset pass in the filter + if (thread.isInterrupted()) + break interruptable; + //IJ.log("process preview start now"); + processOneImage(ip, fp, snapshotPixels); // P R O C E S S (sets ipChanged) + IJ.showProgress(1.0); + if (thread.isInterrupted()) + break interruptable; + previewDataOk = true; + previewTime = System.currentTimeMillis() - startTime; + imp.updateAndDraw(); + if (IJ.debugMode) + IJ.log("preview processing done"); + } + gd.previewRunning(false); // optical feedback + IJ.showStatus(""); //delete last status messages from processing + synchronized(this) { + if (!bgPreviewOn) + break; //thread should stop and possibly keep the data + try { + wait(); //wait for interrupted (don't keep preview) or notify (keep preview) + } catch (InterruptedException e) { + previewDataOk = false; + } + } // synchronized + } // while bgPreviewOn + if (thread.isInterrupted()) + previewDataOk = false; //interrupted always means "don't keep preview" + if (!previewDataOk || !bgKeepPreview) { //no need to keep the result + imp.setRoi(originalRoi); //restore roi + if (ipChanged) { //revert the image data + ip.setSnapshotPixels(snapshotPixels); + ip.reset(); + ipChanged = false; + } + } + imp.updateAndDraw(); //display current state of image (reset or result of preview) + sliceForThread.remove(thread); //no need to announce the slice number any more + } + + /** stop the background process responsible for preview as fast as possible + and wait until the preview thread has finished */ + private void killPreview() { + if (previewThread == null) return; + synchronized (this) { + previewThread.interrupt(); //ask for premature finishing (interrupt first -> no keepPreview) + bgPreviewOn = false; //tell a possible background thread to terminate when it has finished + if (roisForThread!=null) + interruptRoiThreads(roisForThread); + } + waitForPreviewDone(); + imp.setOverlay(originalOverlay); + } + + /** stop the background process responsible for preview and wait until the preview thread has finished */ + private void waitForPreviewDone() { + if (previewThread.isAlive()) try { //a NullPointerException is possible if the thread finishes in the meanwhile + previewThread.setPriority(Thread.currentThread().getPriority()); + } catch (Exception e) {} + synchronized (this) { + bgPreviewOn = false; //tell a possible background thread to terminate + notify(); //(but finish processing unless interrupted) + } + try {previewThread.join();} //wait until the background thread is done + catch (InterruptedException e){} + previewThread = null; + } + + /* set the GenericDialog gd where the preview comes from (if gd is + * suitable for listening, i.e., if it has a preview checkbox). + */ + public void setDialog(GenericDialog gd) { + if (gd != null && imp != null) { + previewCheckbox = gd.getPreviewCheckbox(); + if (previewCheckbox != null) { + gd.addDialogListener(this); + this.gd = gd; + } + } //IJ.log("setDialog done"); + } + + /** The listener to any change in the dialog. It is used for preview. + * It is invoked every time the user changes something in the dialog + * (except OK and cancel buttons), provided that all previous + * listeners (parameter checking) have returned true. + * + * @param e The event that has happened in the dialog. This method may + * be also called with e=null, e.g. to start preview already + * when the dialog appears. + * @return Always true. (The return value determines whether the + * dialog will enable the OK button) + */ + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (previewCheckbox == null || imp == null) return true; + previewCheckboxOn = previewCheckbox.getState(); + if (previewCheckboxOn && previewThread == null) { + bgPreviewOn = true; //need to start a background thread for preview + previewThread = new Thread(this, command+" Preview"); + int priority = Thread.currentThread().getPriority() - 2; + if (priority < Thread.MIN_PRIORITY) priority = Thread.MIN_PRIORITY; + previewThread.setPriority(priority); //preview on lower priority than dialog + previewThread.start(); + if (IJ.debugMode) + IJ.log(command+" Preview thread was started"); + return true; + } + if (previewThread != null) { //thread runs already + if (!previewCheckboxOn) { //preview toggled off + killPreview(); + return true; + } else + previewThread.interrupt(); //all other changes: restart calculating preview (with new parameters) + } + return true; + } + +} diff --git a/src/ij/plugin/filter/Printer.java b/src/ij/plugin/filter/Printer.java new file mode 100644 index 0000000..ce7dd84 --- /dev/null +++ b/src/ij/plugin/filter/Printer.java @@ -0,0 +1,154 @@ +package ij.plugin.filter; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.Calibration; +import java.awt.*; +import java.util.Properties; +import java.awt.print.*; + +/** This plugin implements the File/Page Setup and File/Print commands. */ +public class Printer implements PlugInFilter, Printable { + private ImagePlus imp; + private static double scaling = 100.0; + private static boolean drawBorder; + private static boolean center = true; + private static boolean label; + private static boolean printSelection; + private static boolean rotate; + private static boolean actualSize; + private static int fontSize = 12; + + public int setup(String arg, ImagePlus imp) { + if (arg.equals("setup")) + {pageSetup(); return DONE;} + this.imp = imp; + IJ.register(Printer.class); + return DOES_ALL+NO_CHANGES; + } + + public void run(ImageProcessor ip) { + print(imp); + } + + void pageSetup() { + ImagePlus imp = WindowManager.getCurrentImage(); + Roi roi = imp!=null?imp.getRoi():null; + boolean isRoi = roi!=null && roi.isArea(); + GenericDialog gd = new GenericDialog("Page Setup"); + gd.addNumericField("Scale:", scaling, 0, 3, "%"); + gd.addCheckbox("Draw border", drawBorder); + gd.addCheckbox("Center on page", center); + gd.addCheckbox("Print title", label); + if (isRoi) + gd.addCheckbox("Selection only", printSelection); + gd.addCheckbox("Rotate 90"+IJ.degreeSymbol, rotate); + gd.addCheckbox("Print_actual size", actualSize); + if (imp!=null) + gd.enableYesNoCancel(" OK ", "Print"); + gd.showDialog(); + if (gd.wasCanceled()) + return; + scaling = gd.getNextNumber(); + if (scaling<5.0) scaling = 5; + drawBorder = gd.getNextBoolean(); + center = gd.getNextBoolean(); + label = gd.getNextBoolean(); + if (isRoi) + printSelection = gd.getNextBoolean(); + else + printSelection = false; + rotate = gd.getNextBoolean(); + actualSize = gd.getNextBoolean(); + if (!gd.wasOKed() && imp!=null) { + this.imp = imp; + print(imp); + } + } + + void print(ImagePlus imp) { + PrinterJob pj = PrinterJob.getPrinterJob(); + pj.setPrintable(this); + //pj.pageDialog(pj.defaultPage()); + if (IJ.macroRunning() || pj.printDialog()) { + imp.startTiming(); + try {pj.print(); } + catch (PrinterException e) { + IJ.log(""+e); + } + } + } + + @Override + public int print(Graphics g, PageFormat pf, int pageIndex) { + if (pageIndex != 0) return NO_SUCH_PAGE; + Roi roi = imp.getRoi(); + ImagePlus imp2 = imp; + if (imp2.getOverlay()!=null && !imp2.getHideOverlay()) { + imp2.deleteRoi(); + imp2 = imp2.flatten(); + } + ImageProcessor ip = imp2.getProcessor(); + if (printSelection && roi!=null && roi.isArea() ) + ip.setRoi(roi); + ip = ip.crop(); + if (rotate) + ip = ip.rotateLeft(); + int width = ip.getWidth(); + int height = ip.getHeight(); + int margin = 0; + if (drawBorder) margin = 1; + double scale = scaling/100.0; + int dstWidth = (int)(width*scale); + int dstHeight = (int)(height*scale); + int pageX = (int)pf.getImageableX(); + int pageY = (int)pf.getImageableY(); + int dstX = pageX+margin; + int dstY = pageY+margin; + Image img = ip.createImage(); + double pageWidth = pf.getImageableWidth()-2*margin; + double pageHeight = pf.getImageableHeight()-2*margin; + if (label && pageWidth-dstWidthpageWidth || dstHeight>pageHeight) { + // scale to fit page + double hscale = pageWidth/dstWidth; + double vscale = pageHeight/dstHeight; + double scale2 = hscale<=vscale?hscale:vscale; + dstWidth = (int)(dstWidth*scale2); + dstHeight = (int)(dstHeight*scale2); + } else if (center) { + dstX += (pageWidth-dstWidth)/2; + dstY += (pageHeight-dstHeight)/2; + } + g.drawImage(img, + dstX, dstY, dstX+dstWidth, dstY+dstHeight, + 0, 0, width, height, + null); + if (drawBorder) + g.drawRect(dstX-1, dstY-1, dstWidth+1, dstHeight+1); + if (label) { + g.setFont(new Font("SanSerif", Font.PLAIN, fontSize)); + g.setColor(Color.black); + g.drawString(imp.getTitle(), pageX+5, pageY+fontSize); + } + return PAGE_EXISTS; + } + +} diff --git a/src/ij/plugin/filter/RGBStackSplitter.java b/src/ij/plugin/filter/RGBStackSplitter.java new file mode 100644 index 0000000..8a94870 --- /dev/null +++ b/src/ij/plugin/filter/RGBStackSplitter.java @@ -0,0 +1,37 @@ +package ij.plugin.filter; +import ij.*; +import ij.process.*; +import ij.plugin.ChannelSplitter; + +/** Deprecated; replaced by ij.plugin.ChannelSplitter. */ +public class RGBStackSplitter implements PlugInFilter { + ImagePlus imp; + public ImageStack red, green, blue; + + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + (new ChannelSplitter()).run(arg); + return DONE; + } + + public void run(ImageProcessor ip) { + } + + /** Deprecated; replaced by ij.plugin.ChannelSplitter. */ + public void split(ImagePlus imp) { + WindowManager.setTempCurrentImage(imp); + (new ChannelSplitter()).run(""); + } + + /** Deprecated; replaced by ChannelSplitter.splitRGB(). */ + public void split(ImageStack rgb, boolean keepSource) { + ImageStack[] channels = ChannelSplitter.splitRGB(rgb, keepSource); + red = channels[0]; + green = channels[1]; + blue = channels[2]; + } + +} + + + diff --git a/src/ij/plugin/filter/RankFilters.java b/src/ij/plugin/filter/RankFilters.java new file mode 100644 index 0000000..0acec37 --- /dev/null +++ b/src/ij/plugin/filter/RankFilters.java @@ -0,0 +1,947 @@ +package ij.plugin.filter; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.plugin.ContrastEnhancer; +import ij.util.ThreadUtil; +import java.awt.*; +import java.awt.event.*; +import java.util.Arrays; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + + +/** This plugin implements the Mean, Minimum, Maximum, Variance, Median, + * Remove Outliers, Remove NaNs and Despeckle commands. + */ + // Version 2012-07-15 M. Schmid: Fixes a bug that could cause preview not to work correctly + // Version 2012-12-23 M. Schmid: Test for inverted LUT only once (not in each slice) + // Version 2014-10-10 M. Schmid: Fixes a bug that caused Threshold=0 when calling from API + // Version 2019-07-26 M. Schmid: Nonblocking dialog enabled + // Version 2020-07-17 M. Schmid: bugfix: changing preview parameters could lead to partial filtering with different parameters. + // Uses AtomicIntegers. Top-hat filter added + +public class RankFilters implements ExtendedPlugInFilter, DialogListener { + public static final int MEAN=0, MIN=1, MAX=2, VARIANCE=3, MEDIAN=4, OUTLIERS=5, DESPECKLE=6, REMOVE_NAN=7, + OPEN=8, CLOSE=9, TOP_HAT=10; //when adding a new filter, set HIGHEST_FILTER below. + public static final int BRIGHT_OUTLIERS = 0, DARK_OUTLIERS = 1; + private static final String[] outlierStrings = {"Bright","Dark"}; + private static int HIGHEST_FILTER = TOP_HAT; + // Filter parameters + private int filterType; + private double radius; + private double threshold; //this and the next for 'remove outliers' only + private int whichOutliers; + private boolean lightBackground = Prefs.get("bs.background", true); //this and the next for top hat only + private boolean dontSubtract; + // Remember filter parameters for the next time + private static double[] lastRadius = new double[HIGHEST_FILTER+1]; //separate for each filter type + private static double lastThreshold = 50.; + private static int lastWhichOutliers = BRIGHT_OUTLIERS; + private static boolean lastLightBackground = false; + private static boolean lastDontSubtract = false; + // + // F u r t h e r c l a s s v a r i a b l e s + int flags = DOES_ALL|SUPPORTS_MASKING|KEEP_PREVIEW; + private ImagePlus imp; + private int nPasses = 1; // The number of passes (color channels * stack slices) + private PlugInFilterRunner pfr; + private int pass; + private boolean previewing = false; + // M u l t i t h r e a d i n g - r e l a t e d + private int numThreads = Prefs.getThreads(); + // The current state of multithreaded processing is in class variables. + // Thus, stack parallelization must be done ONLY with one thread for the image + // (not using these class variables). + // Atomic objects are used to avoid caching (i.e., to ensure that always the current state of the variable is read). + private AtomicInteger highestYinCache = new AtomicInteger(Integer.MIN_VALUE); // the highest line read into the cache so far + private AtomicInteger nThreadsWaiting = new AtomicInteger(0); // number of threads waiting until they may read data + private AtomicBoolean copyingToCache = new AtomicBoolean(false); // whether a thread is currently copying data to the cache + + /** OPEN, CLOSE, TOPHAT need more than one run of the underlying filter */ + private boolean isMultiStepFilter(int filterType) { + return filterType>=OPEN && filterType<=TOP_HAT; + } + + /** Setup of the PlugInFilter. Returns the flags specifying the capabilities and needs + * of the filter. + * + * @param arg Defines type of filter operation + * @param imp The ImagePlus to be processed + * @return Flags specifying further action of the PlugInFilterRunner + */ + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + if (arg.equals("mean")) + filterType = MEAN; + else if (arg.equals("min")) + filterType = MIN; + else if (arg.equals("max")) + filterType = MAX; + else if (arg.equals("variance")) { + filterType = VARIANCE; + flags |= FINAL_PROCESSING; + } else if (arg.equals("median")) + filterType = MEDIAN; + else if (arg.equals("outliers")) + filterType = OUTLIERS; + else if (arg.equals("despeckle")) + filterType = DESPECKLE; + else if (arg.equals("tophat")) + filterType = TOP_HAT; + else if (arg.equals("nan")) { + filterType = REMOVE_NAN; + if (imp!=null && imp.getBitDepth()!=32) { + IJ.error("RankFilters","\"Remove NaNs\" requires a 32-bit image"); + return DONE; + } + } else if (arg.equals("final")) { //after variance && tophat filter, adjust brightness&contrast + setDisplayRange(imp.getProcessor()); + } else if (arg.equals("masks")) { + showMasks(); + return DONE; + } else { + IJ.error("RankFilters","Argument missing or undefined: "+arg); + return DONE; + } + if (isMultiStepFilter(filterType) && imp!=null) { //composite filter: 'open maxima' etc: + Roi roi = imp.getRoi(); + if (roi!=null && !roi.getBounds().contains(new Rectangle(imp.getWidth(), imp.getHeight()))) + //Roi < image? (actually tested: NOT (Roi>=image)) + flags |= SNAPSHOT; //snapshot for resetRoiBoundary, also for top-hat subtraction + } + return flags; + } + + public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) { + if (filterType == DESPECKLE) { + filterType = MEDIAN; + radius = 1.0; + } else { + GenericDialog gd = GUI.newNonBlockingDialog(command,imp); + radius = lastRadius[filterType]<=0 ? 2 : lastRadius[filterType]; + gd.addNumericField("Radius", radius, 1, 6, "pixels"); + if (filterType==OUTLIERS) { + int digits = imp.getType() == ImagePlus.GRAY32 ? 2 : 0; + gd.addNumericField("Threshold", lastThreshold, digits); + gd.addChoice("Which outliers", outlierStrings, outlierStrings[lastWhichOutliers]); + gd.addHelp(IJ.URL+"/docs/menus/process.html#outliers"); + } else if (filterType==REMOVE_NAN) { + gd.addHelp(IJ.URL+"/docs/menus/process.html#nans"); + } else if (filterType==TOP_HAT) { + gd.addCheckbox("Light Background", lastLightBackground); + gd.addCheckbox("Don't subtract (grayscale open)", lastDontSubtract); + } + gd.addPreviewCheckbox(pfr); //passing pfr makes the filter ready for preview + gd.addDialogListener(this); //the DialogItemChanged method will be called on user input + previewing = true; + gd.showDialog(); //display the dialog; preview may run now + previewing = false; + if (gd.wasCanceled()) return DONE; + IJ.register(this.getClass()); //protect static class variables (filter parameters) from garbage collection + if (Macro.getOptions() == null) { //interactive only: remember parameters entered + lastRadius[filterType] = radius; + if (filterType == OUTLIERS) { + lastThreshold = threshold; + lastWhichOutliers = whichOutliers; + } else if (filterType==TOP_HAT) { + lastLightBackground = lightBackground; + lastDontSubtract = dontSubtract; + } + } + } + this.pfr = pfr; + flags = IJ.setupDialog(imp, flags); //ask whether to process all slices of stack (if a stack) + if ((flags&DOES_STACKS)!=0) { + int size = imp.getWidth() * imp.getHeight(); + Roi roi = imp.getRoi(); + if (roi != null) { + Rectangle roiRect = roi.getBounds(); + size = roiRect.width * roiRect.height; + } + double workToDo = size*(double)radius; //estimate computing time (arb. units) + if (filterType==MEAN || filterType==VARIANCE) workToDo *= 0.5; + else if (filterType==MEDIAN) workToDo *= radius*0.5; + if (workToDo < 1e6 && imp.getImageStackSize()>=2*numThreads) { + numThreads = 1; //for fast operations, avoid overhead of multi-threading in each image + flags |= PARALLELIZE_STACKS; + } + } + return flags; + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + radius = gd.getNextNumber(); + if (filterType == OUTLIERS) { + threshold = gd.getNextNumber(); + whichOutliers = gd.getNextChoiceIndex(); + } else if (filterType==TOP_HAT || filterType==OPEN || filterType==CLOSE) { + lightBackground = gd.getNextBoolean(); + dontSubtract = gd.getNextBoolean(); + } + int maxRadius = (filterType==MEDIAN || filterType==OUTLIERS || filterType==REMOVE_NAN) ? 100 : 1000; + if (gd.invalidNumber() || radius<0 || radius>maxRadius || (filterType==OUTLIERS && threshold <0)) + return false; + if (filterType == TOP_HAT) { + if (dontSubtract) + flags &= ~FINAL_PROCESSING; //grayscale open: keep display range + else + flags |= FINAL_PROCESSING; //top-hat: readjust display range + } + return true; + } + + public void run(ImageProcessor ip) { + rank(ip, radius, filterType, whichOutliers, (float)threshold, lightBackground, dontSubtract); + if (IJ.escapePressed()) // interrupted by user? + ip.reset(); + else if (previewing && (flags&FINAL_PROCESSING)!=0) + setDisplayRange(ip); + } + + /** Filters an image by any method except 'despecle', 'remove outliers', or top-hat + * @param ip The ImageProcessor that should be filtered (all 4 types supported) + * @param radius Determines the kernel size, see Process>Filters>Show Circular Masks. + * Must not be negative. No checking is done for large values that would + * lead to excessive computing times. + * @param filterType May be MEAN, MIN, MAX, VARIANCE, or MEDIAN. + */ + public void rank(ImageProcessor ip, double radius, int filterType) { + rank(ip, radius, filterType, 0, 50f); + } + + /** Filters an image by any method except 'despecle' and top-hat (for 'despeckle', use 'median' and radius=1) + * @param ip The image subject to filtering + * @param radius The kernel radius + * @param filterType as defined above; DESPECKLE is not a valid type here; use median and + * a radius of 1.0 instead + * @param whichOutliers BRIGHT_OUTLIERS or DARK_OUTLIERS for 'outliers' filter + * @param threshold Threshold for 'outliers' filter + */ + public void rank(ImageProcessor ip, double radius, int filterType, int whichOutliers, float threshold) { + rank(ip, radius, filterType, whichOutliers, threshold, false, false); + } + + /** Filters an image by any method except 'despecle' (for 'despeckle', use 'median' and radius=1) + * @param ip The image subject to filtering + * @param radius The kernel radius + * @param filterType as defined above; DESPECKLE is not a valid type here; use median and + * a radius of 1.0 instead + * @param whichOutliers BRIGHT_OUTLIERS or DARK_OUTLIERS for 'outliers' filter + * @param threshold Threshold for 'outliers' filter + * @param lightBackground for top-hat background subtraction, background is light, not dark + * @param dontSubtract fpr top-hat filter, performs a grayscale open or close instead of top-hat, + * where the result of grayscale open/close is subtracted from the original. + */ + public void rank(ImageProcessor ip, double radius, int filterType, int whichOutliers, float threshold, boolean lightBackground, boolean dontSubtract) { + Rectangle roi = ip.getRoi(); + ImageProcessor mask = ip.getMask(); + Rectangle roi1 = null; + int[] lineRadii = makeLineRadii(radius); + + boolean snapshotRequired = (filterType==TOP_HAT && !dontSubtract) || + (isMultiStepFilter(filterType) && (roi.width!=ip.getWidth() || roi.height!=ip.getHeight())); + if (snapshotRequired && ip.getSnapshotPixels()==null) + ip.snapshot(); + + float minMaxOutliersSign = filterType==MIN || filterType==OPEN ? -1f : 1f; //open is minimum first + if (filterType == OUTLIERS) //sign is -1 for high outliers: compare number with minimum + minMaxOutliersSign = (ip.isInvertedLut()==(whichOutliers==DARK_OUTLIERS)) ? -1f : 1f; + + if (filterType == TOP_HAT) { + boolean invertedLut = ip.isInvertedLut(); + boolean invert = (invertedLut && !lightBackground) || (!invertedLut && lightBackground); + minMaxOutliersSign = invert ? 1f : -1f; + } + + ImageProcessor snapIp = null; + FloatProcessor fp = null, snapFp = null; + boolean isImagePart = (roi.width1 ? 2*numThreads : 0); + // 'cache' is the input buffer. Each line y in the image is mapped onto cache line y%cacheHeight + final float[] cache = new float[cacheWidth*cacheHeight]; + highestYinCache.set(Math.max(roi.y-kHeight/2, 0) - 1); //this line+1 will be read into the cache first + + final AtomicIntegerArray yForThread = new AtomicIntegerArray(numThreads); //threads announce here which line they currently process + for (int i=0; i() { + final public Void call() { + doFiltering(ip, lineRadii, cache, cacheWidth, cacheHeight, + filterType, minMaxOutliersSign, threshold, colorChannel, + yForThread, threadNumber, nextY); + return null; + } + }; + } + Future[] futures = ThreadUtil.start(callables); + + doFiltering(ip, lineRadii, cache, cacheWidth, cacheHeight, + filterType, minMaxOutliersSign, threshold, colorChannel, + yForThread, /*threadNumber=*/0, nextY); + ThreadUtil.joinAll(futures); + showProgress(1.0, ip instanceof ColorProcessor); + pass++; + } + + // Filter a grayscale image or one channel of an RGB image using one thread + // + // Synchronization: unless a thread is waiting, we avoid the overhead of 'synchronized' + // statements. That's because a thread waiting for another one should be rare. + // + // Data handling: The area needed for processing a line is written into the array 'cache'. + // This is a stripe of sufficient width for all threads to have each thread processing one + // line, and some extra space if one thread is finished to start the next line. + // This array is padded at the edges of the image so that a surrounding with radius kRadius + // for each pixel processed is within 'cache'. Out-of-image + // pixels are set to the value of the nearest edge pixel. When adding a new line, the lines in + // 'cache' are not shifted but rather the smaller array with the start and end pointers of the + // kernel area is modified to point at the addresses for the next line. + // + // Algorithm: For mean and variance, except for very small radius, usually do not calculate the + // sum over all pixels. This sum is calculated for the first pixel of every line only. For the + // following pixels, add the new values and subtract those that are not in the sum any more. + // For min/max, also first look at the new values, use their maximum if larger than the old + // one. The look at the values not in the area any more; if it does not contain the old + // maximum, leave the maximum unchanged. Otherwise, determine the maximum inside the area. + // For outliers, calculate the median only if the pixel deviates by more than the threshold + // from any pixel in the area. Therfore min or max is calculated; this is a much faster + // operation than the median. + private void doFiltering(ImageProcessor ip, int[] lineRadii, float[] cache, int cacheWidth, int cacheHeight, + int filterType, float minMaxOutliersSign, float threshold, int colorChannel, + AtomicIntegerArray yForThread, int threadNumber, AtomicInteger nextY) { + if (nextY.get() < 0 || Thread.currentThread().isInterrupted()) return; + int width = ip.getWidth(); + int height = ip.getHeight(); + Rectangle roi = ip.getRoi(); + + int kHeight = kHeight(lineRadii); + int kRadius = kRadius(lineRadii); + int kNPoints = kNPoints(lineRadii); + + int xmin = roi.x - kRadius; + int xmax = roi.x + roi.width + kRadius; + int[]cachePointers = makeCachePointers(lineRadii, cacheWidth); + + int padLeft = xmin<0 ? -xmin : 0; + int padRight = xmax>width? xmax-width : 0; + int xminInside = xmin>0 ? xmin : 0; + int xmaxInside = xmax= 0) + yForThread.set(threadNumber, y); + //IJ.log("thread "+threadNumber+" @y="+y+" needs"+(y-kHeight/2)+"-"+(y+kHeight/2)+" highestYinC="+highestYinCache.get()); + boolean threadFinished = y >= roi.y+roi.height || y < 0; // y<0 if aborted + if (numThreads>1 && (nThreadsWaiting.get()>0 || threadFinished)) // 'if' is not synchronized to avoid overhead + //If nThreadsWaiting gets incremented to 1 after the 'if' by another thread, there will be no notifyAll. + //This is not an issue since the the other thread rechecking for a slow thread (synchronized, ~30 lines below) + //will not see the current thread being slow any more, so that other thread will not wait. + synchronized(this) { + notifyAll(); // we may have blocked another thread + //IJ.log("thread "+threadNumber+" @y="+y+" notifying"); + } + if (threadFinished) + return; // all done, break the loop + + if (threadNumber==0) { // main thread checks for abort and ProgressBar + long time = System.currentTimeMillis(); + if (time-lastTime>100) { + lastTime = time; + showProgress((y-roi.y)/(double)(roi.height), rgb); + if (Thread.currentThread().isInterrupted() || (imp!= null && IJ.escapePressed())) { + nextY.set(Integer.MIN_VALUE); + synchronized(this) {notifyAll();} + return; + } + } + } + + for (int i=0; i1) { // thread synchronization + int slowestThreadY = arrayMinNonNegative(yForThread); // non-synchronized check to avoid overhead + + if (y - slowestThreadY + kHeight > cacheHeight) { // we would overwrite data needed by another thread + nThreadsWaiting.incrementAndGet(); + synchronized(this) { + slowestThreadY = arrayMinNonNegative(yForThread); //recheck whether we have to wait + if (y - slowestThreadY + kHeight > cacheHeight) { + do { + //notifyAll(); // avoid deadlock: wake up others waiting + //IJ.log("Thread "+threadNumber+" waiting @y="+y+" slowest@y="+slowestThreadY); + try { + wait(); + if (nextY.get() < 0) return; + } catch (InterruptedException e) { + nextY.set(Integer.MIN_VALUE); + notifyAll(); + Thread.currentThread().interrupt(); //keep interrupted status (PlugInFilterRunner needs it) + return; + } + slowestThreadY = arrayMinNonNegative(yForThread); + } while (y - slowestThreadY + kHeight > cacheHeight); + } //if + } + nThreadsWaiting.decrementAndGet(); + } + } + + if (numThreads==1) { // R E A D + int yStartReading = y==roi.y ? Math.max(roi.y-kHeight/2, 0) : y+kHeight/2; + for (int yNew = yStartReading; yNew<=y+kHeight/2; yNew++) { //only 1 line except at start + readLineToCacheOrPad(pixels, width, height, roi.y, xminInside, widthInside, + cache, cacheWidth, cacheHeight, padLeft, padRight, colorChannel, kHeight, yNew); + } + } else { // if no other thread is copying or if the own thread needs the data + if (!copyingToCache.get() || highestYinCache.get() < y+kHeight/2) synchronized(cache) { + copyingToCache.set(true); // copy as many new line(s) as possible into the cache + while (highestYinCache.get() < arrayMinNonNegative(yForThread) - kHeight/2 + cacheHeight - 1) { + int yNew = highestYinCache.get() + 1; + readLineToCacheOrPad(pixels, width, height, roi.y, xminInside, widthInside, + cache, cacheWidth, cacheHeight, padLeft, padRight, colorChannel, kHeight, yNew); + highestYinCache.set(yNew); + } + copyingToCache.set(false); + } + } + + int cacheLineP = cacheWidth * (y % cacheHeight) + kRadius; //points to pixel (roi.x, y) + filterLine(values, width, cache, cachePointers, kNPoints, cacheLineP, roi, y, // F I L T E R + sums, medianBuf1, medianBuf2, minMaxOutliersSign, maxValue, isFloat, filterType, + smallKernel, sumFilter, minOrMax, minOrMaxOrOutliers, threshold); + if (!isFloat) //Float images: data are written already during 'filterLine' + writeLineToPixels(values, pixels, roi.x+y*width, roi.width, colorChannel); // W R I T E + //IJ.log("thread "+threadNumber+" @y="+y+" line done"); + } // while (true); loop over y (lines) + } + + //returns the minimum of the array, which may be modified concurrently, but not less than 0 + private int arrayMinNonNegative(AtomicIntegerArray array) { + int min = Integer.MAX_VALUE; + for (int i=0; i= max) { //compare with previous maximum 'max' + max = newPointsMax; + } else { + float removedPointsMax = getSideMax(cache, x, cachePointers, false, minMaxOutliersSign); + if (removedPointsMax >= max) + max = getAreaMax(cache, x, cachePointers, 1, newPointsMax, minMaxOutliersSign); + } + if (minOrMax) { + values[valuesP] = max*minMaxOutliersSign; + continue; + } + } else if (sumFilter) { + addSideSums(cache, x, cachePointers, sums); + if (Double.isNaN(sums[0])) //avoid perpetuating NaNs into remaining line + fullCalculation = true; + } + } + if (sumFilter) { + if (filterType == MEAN) + values[valuesP] = (float)(sums[0]/kNPoints); + else {// Variance: sum of squares - square of sums + float value = (float)((sums[1] - sums[0]*sums[0]/kNPoints)/kNPoints); + if (value>maxValue) value = maxValue; + if (value < 0) value = 0; // numeric noise can cause values < 0 + values[valuesP] = value; + } + } else if (filterType == MEDIAN) { + if (isFloat) { + median = Float.isNaN(values[valuesP]) ? Float.NaN : values[valuesP]; // a first guess + median = getNaNAwareMedian(cache, x, cachePointers, medianBuf1, medianBuf2, kNPoints, median); + } else + median = getMedian(cache, x, cachePointers, medianBuf1, medianBuf2, kNPoints, median); + values[valuesP] = median; + } else if (filterType == OUTLIERS) { + float v = cache[cacheLineP+x]; + if (v*minMaxOutliersSign+threshold < max) { //for low outliers: median can't be higher than max (minMaxOutliersSign is +1) + median = getMedian(cache, x, cachePointers, medianBuf1, medianBuf2, kNPoints, median); + if (v*minMaxOutliersSign+threshold < median*minMaxOutliersSign) + v = median; //beyond threshold (below if minMaxOutliersSign=+1), replace outlier by median + } + values[valuesP] = v; + } else if (filterType == REMOVE_NAN) { //float only; then 'values' is pixels array + if (Float.isNaN(values[valuesP])) + values[valuesP] = getNaNAwareMedian(cache, x, cachePointers, medianBuf1, medianBuf2, kNPoints, median); + else + median = values[valuesP]; //initial guess for the next point + } + } // for x + } + + /** Read a line into the cache (including padding in x). + * If y>=height, instead of reading new data, it duplicates the line y=height-1. + * If y==0, it also creates the data for y<0, as far as necessary, thus filling the cache with + * more than one line (padding by duplicating the y=0 row). + */ + private static void readLineToCacheOrPad(Object pixels, int width, int height, int roiY, int xminInside, int widthInside, + float[]cache, int cacheWidth, int cacheHeight, int padLeft, int padRight, int colorChannel, + int kHeight, int y) { + int lineInCache = y%cacheHeight; + if (y < height) { + readLineToCache(pixels, y*width, xminInside, widthInside, + cache, lineInCache*cacheWidth, padLeft, padRight, colorChannel); + if (y==0) for (int prevY = roiY-kHeight/2; prevY<0; prevY++) { //for y<0, pad with y=0 border pixels + int prevLineInCache = cacheHeight+prevY; + System.arraycopy(cache, 0, cache, prevLineInCache*cacheWidth, cacheWidth); + } + } else + System.arraycopy(cache, cacheWidth*((height-1)%cacheHeight), cache, lineInCache*cacheWidth, cacheWidth); + } + + /** Read a line into the cache (includes conversion to flaot). Pad with edge pixels in x if necessary */ + private static void readLineToCache(Object pixels, int pixelLineP, int xminInside, int widthInside, + float[] cache, int cacheLineP, int padLeft, int padRight, int colorChannel) { + if (pixels instanceof byte[]) { + byte[] bPixels = (byte[])pixels; + for (int pp=pixelLineP+xminInside, cp=cacheLineP+padLeft; pp>shift; + } + for (int cp=cacheLineP; cp guess) { + aboveBuf[nAbove] = v; + nAbove++; + } + else if (v < guess) { + belowBuf[nBelow] = v; + nBelow++; + } + } + } + int half = kNPoints/2; + if (nAbove>half) + return findNthLowestNumber(aboveBuf, nAbove, nAbove-half-1); + else if (nBelow>half) + return findNthLowestNumber(belowBuf, nBelow, half); + else + return guess; + } + + /** Get median of values within kernel-sized neighborhood. + * NaN data values are ignored; the output is NaN only if there are only NaN values in the + * kernel-sized neighborhood */ + private static float getNaNAwareMedian(float[] cache, int xCache0, int[] kernel, + float[] aboveBuf, float[]belowBuf, int kNPoints, float guess) { + int nAbove = 0, nBelow = 0; + for (int kk=0; kk guess) { + aboveBuf[nAbove] = v; + nAbove++; + } + else if (v < guess) { + belowBuf[nBelow] = v; + nBelow++; + } + } + } + if (kNPoints == 0) return Float.NaN; //only NaN data in the neighborhood? + int half = kNPoints/2; + if (nAbove>half) + return findNthLowestNumber(aboveBuf, nAbove, nAbove-half-1); + else if (nBelow>half) + return findNthLowestNumber(belowBuf, nBelow, half); + else + return guess; + } + + /** Find the n-th lowest number in part of an array + * @param buf The input array. Only values 0 ... bufLength are read. buf will be modified. + * @param bufLength Number of values in buf that should be read + * @param n which value should be found; n=0 for the lowest, n=bufLength-1 for the highest + * @return the value */ + public final static float findNthLowestNumber(float[] buf, int bufLength, int n) { + // Hoare's find, algorithm, based on http://www.geocities.com/zabrodskyvlada/3alg.html + // Contributed by Heinz Klar + int i,j; + int l=0; + int m=bufLength-1; + float med=buf[n]; + float dum ; + + while (l=n) && (i<=n)) ; + if (j 0) + System.arraycopy(snapshot, pL, pixels, pL, leftWidth); + if (rightWidth > 0) + System.arraycopy(snapshot, pR, pixels, pR, rightWidth); + } + for (int y=roi.y+roi.height, p = roi1.x+y*width; y=1.5 && radius<1.75) //this code creates the same sizes as the previous RankFilters + radius = 1.75; + else if (radius>=2.5 && radius<2.85) + radius = 2.85; + int r2 = (int) (radius*radius) + 1; + int kRadius = (int)(Math.sqrt(r2+1e-10)); + int kHeight = 2*kRadius + 1; + int[] kernel = new int[2*kHeight + 2]; + kernel[2*kRadius] = -kRadius; + kernel[2*kRadius+1] = kRadius; + int nPoints = 2*kRadius+1; + for (int y=1; y<=kRadius; y++) { //lines above and below center together + int dx = (int)(Math.sqrt(r2-y*y+1e-10)); + kernel[2*(kRadius-y)] = -dx; + kernel[2*(kRadius-y)+1] = dx; + kernel[2*(kRadius+y)] = -dx; + kernel[2*(kRadius+y)+1] = dx; + nPoints += 4*dx+2; //2*dx+1 for each line, above&below + } + kernel[kernel.length-2] = nPoints; + kernel[kernel.length-1] = kRadius; + //for (int i=0; i0) + imp.setOverlay(overlay2); + } + if (isEnlarged && imp.getStackSize()==1) { + imp.changes = true; + imp.updateAndDraw(); + Undo.setup(Undo.COMPOUND_FILTER_DONE, imp); + } + if (done) { // remove grid + Overlay ovly = imp.getOverlay(); + if (ovly!=null) { + ovly.remove(GRID); + if (ovly.size()==0) imp.setOverlay(null); + } + } + } + + void enlargeCanvas() { + imp.unlock(); + IJ.run(imp, "Select All", ""); + IJ.run(imp, "Rotate...", "angle="+angle); + Roi roi = imp.getRoi(); + Rectangle r = roi.getBounds(); + if (r.width0) { + overlay.remove(GRID); + imp.setOverlay(overlay); + } + return DONE; + } + Overlay ovly = imp.getOverlay(); + if (ovly!=null) { + ovly.remove(GRID); + if (ovly.size()==0) imp.setOverlay(null); + } + if (enlarge) + flags |= NO_CHANGES; // undoable as a "compound filter" + else if (imp.getStackSize()==1) + flags |= KEEP_PREVIEW; // standard filter without enlarge + done = true; + return IJ.setupDialog(imp, flags); + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + angle = gd.getNextNumber(); + //only check for invalid input to "angle", don't care about gridLines + if (gd.invalidNumber()) { + if (gd.wasOKed()) IJ.error("Angle is invalid."); + return false; + } + gridLines = (int)gd.getNextNumber(); + interpolationMethod = gd.getNextChoiceIndex(); + if (bitDepth==8 || bitDepth==24) + fillWithBackground = gd.getNextBoolean(); + if (canEnlarge) + enlarge = gd.getNextBoolean(); + return true; + } + + /** Returns the current angle. */ + public static double getAngle() { + return angle; + } + + public void setNPasses(int nPasses) { + } + +} + diff --git a/src/ij/plugin/filter/SaltAndPepper.java b/src/ij/plugin/filter/SaltAndPepper.java new file mode 100644 index 0000000..4089a5b --- /dev/null +++ b/src/ij/plugin/filter/SaltAndPepper.java @@ -0,0 +1,44 @@ +package ij.plugin.filter; +import java.awt.*; +import java.util.*; +import ij.*; +import ij.process.*; + +/** Implements ImageJ's Process/Noise/Salt and Pepper command. */ +public class SaltAndPepper implements PlugInFilter { + + Random r = new Random(); + + public int setup(String arg, ImagePlus imp) { + return IJ.setupDialog(imp, DOES_8G+DOES_8C+SUPPORTS_MASKING); + } + + public void run(ImageProcessor ip) { + add(ip, 0.05); + } + + public int rand(int min, int max) { + return min + r.nextInt(max-min); + } + + public void add(ImageProcessor ip, double percent) { + Rectangle roi = ip.getRoi(); + int n = (int)(percent*roi.width*roi.height); + byte[] pixels = (byte[])ip.getPixels(); + int rx, ry; + int width = ip.getWidth(); + int xmin = roi.x; + int xmax = roi.x+roi.width; + int ymin = roi.y; + int ymax = roi.y+roi.height; + for (int i=0; i0.0 && noUnit && e.getSource()==numberField.elementAt(1)) { + unit = "unit"; + ((TextField)stringField.elementAt(0)).setText(unit); + } + boolean noScale = measured<=0||known<=0||noUnit; + if (noScale) + theScale = NO_SCALE; + else { + double scale = measured/known; + int digits = Tools.getDecimalPlaces(scale); + theScale = IJ.d2s(scale,digits)+(scale==1.0?" pixel/":" pixels/")+unit; + } + setScale(theScale); + } + + public void actionPerformed(ActionEvent e) { + super.actionPerformed(e); + if (e.getSource()==unscaleButton) { + ((TextField)numberField.elementAt(0)).setText(length); + ((TextField)numberField.elementAt(1)).setText("0.00"); + ((TextField)numberField.elementAt(2)).setText("1.0"); + ((TextField)stringField.elementAt(0)).setText("pixel"); + setScale(NO_SCALE); + scaleChanged = true; + if (IJ.isMacOSX()) + {setVisible(false); setVisible(true);} + } + } + + void setScale(String theScale) { + ((Label)theLabel).setText("Scale: "+theScale); + } + +} diff --git a/src/ij/plugin/filter/Shadows.java b/src/ij/plugin/filter/Shadows.java new file mode 100644 index 0000000..c330fc6 --- /dev/null +++ b/src/ij/plugin/filter/Shadows.java @@ -0,0 +1,99 @@ +package ij.plugin.filter; +import ij.*; +import ij.gui.*; +import ij.process.*; +import java.awt.*; + +/** Implements the commands in the Process/Shadows submenu. */ +public class Shadows implements PlugInFilter { + + String arg; + ImagePlus imp; + + public int setup(String arg, ImagePlus imp) { + this.arg = arg; + this.imp = imp; + if (imp!=null && imp.getStackSize()>1 && arg.equals("demo")) { + IJ.error("Shadows Demo does not work with stacks."); + return DONE; + } + return IJ.setupDialog(imp, DOES_ALL+SUPPORTS_MASKING); + } + + public void run(ImageProcessor ip) { + if (arg.equals("demo")) { + int iterations = 20; + IJ.resetEscape(); + for (int i=0; i80) fontSize = 80; + } + if (IJ.macroRunning()) { + format = NUMBER; + decimalPlaces = 0; + interval=1; + text = ""; + start = 0; + useOverlay = false; + useTextToolFont = false; + String options = Macro.getOptions(); + if (options!=null) { + if (options.indexOf("interval=0")!=-1 && options.indexOf("format=")==-1) + format = TEXT; + if (options.indexOf(" slice=")!=-1) { + options = options.replaceAll(" slice=", " range="); + Macro.setOptions(options); + } + } + } + if (format<0||format>LABEL) format = NUMBER; + int defaultLastFrame = imp.getStackSize(); + if (imp.isHyperStack()) { + if (imp.getNFrames()>1) + defaultLastFrame = imp.getNFrames(); + else if (imp.getNSlices()>1) + defaultLastFrame = imp.getNSlices(); + } + GenericDialog gd = new GenericDialog("Label Stacks"); + gd.setInsets(2, 5, 0); + gd.addChoice("Format:", formats, formats[format]); + gd.addStringField("Starting value:", IJ.d2s(start,decimalPlaces)); + gd.addStringField("Interval:", ""+IJ.d2s(interval,decimalPlaces)); + gd.addNumericField("X location:", x, 0); + gd.addNumericField("Y location:", y, 0); + gd.addNumericField("Font size:", fontSize, 0); + gd.addStringField("Text:", text, 10); + addRange(gd, "Range:", 1, defaultLastFrame); + gd.setInsets(10,20,0); + gd.addCheckbox(" Use overlay", useOverlay); + gd.addCheckbox(" Use_text tool font", useTextToolFont); + gd.addPreviewCheckbox(pfr); + gd.addHelp(IJ.URL+"/docs/menus/image.html#label"); + gd.addDialogListener(this); + previewing = true; + gd.showDialog(); + previewing = false; + if (gd.wasCanceled()) + return DONE; + else + return flags; + } + + void addRange(GenericDialog gd, String label, int start, int end) { + gd.addStringField(label, start+"-"+end); + } + + double[] getRange(GenericDialog gd, int start, int end) { + String[] range = Tools.split(gd.getNextString(), " -"); + double d1 = Tools.parseDouble(range[0]); + double d2 = range.length==2?Tools.parseDouble(range[1]):Double.NaN; + double[] result = new double[2]; + result[0] = Double.isNaN(d1)?1:(int)d1; + result[1] = Double.isNaN(d2)?end:(int)d2; + if (result[0]end) result[1] = end; + if (result[0]>result[1]) { + result[0] = start; + result[1] = end; + } + return result; + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + format = gd.getNextChoiceIndex(); + start = Tools.parseDouble(gd.getNextString()); + String str = gd.getNextString(); + interval = Tools.parseDouble(str); + x = (int)gd.getNextNumber(); + y = (int)gd.getNextNumber(); + fontSize = (int)gd.getNextNumber(); + text = gd.getNextString(); + double[] range = getRange(gd, 1, defaultLastFrame); + useOverlay = gd.getNextBoolean(); + useTextToolFont = gd.getNextBoolean(); + if (virtualStack) useOverlay = true; + firstFrame=(int)range[0]; lastFrame=(int)range[1]; + int index = str.indexOf("."); + if (index!=-1) + decimalPlaces = str.length()-index-1; + else + decimalPlaces = 0; + if (gd.invalidNumber()) return false; + if (useTextToolFont) + font = new Font(TextRoi.getDefaultFontName(), TextRoi.getDefaultFontStyle(), fontSize); + else + font = new Font("SansSerif", Font.PLAIN, fontSize); + if (y=10) fieldWidth = 2; + if (size>=100) fieldWidth = 3; + if (size>=1000) fieldWidth = 4; + if (size>=10000) fieldWidth = 5; + Prefs.set("label.format", format); + return true; + } + + public void run(ImageProcessor ip) { + int image = ip.getSliceNumber(); + int n = image - 1; + if (imp.isHyperStack()) n = updateIndex(n); + if (virtualStack) { + int nSlices = imp.getStackSize(); + if (previewing) nSlices = 1; + for (int i=1; i<=nSlices; i++) { + image=i; n=i-1; + if (imp.isHyperStack()) n = updateIndex(n); + drawLabel(ip, image, n); + } + } else { + if (previewing && overlay!=null) { + imp.setOverlay(baseOverlay); + overlay = null; + } + drawLabel(ip, image, n); + } + } + + int updateIndex(int n) { + if (imp.getNFrames()>1) + return (int)(n*((double)(imp.getNFrames())/imp.getStackSize())); + else if (imp.getNSlices()>1) + return (int)(n*((double)(imp.getNSlices())/imp.getStackSize())); + else + return n; + } + + void drawLabel(ImageProcessor ip, int image, int n) { + String s = getString(n, interval, format); + ip.setFont(font); + int textWidth = ip.getStringWidth(s); + if (color==null) { + color = Toolbar.getForegroundColor(); + if ((color.getRGB()&0xffffff)==0) { + ip.setRoi(x, y-fontSize, maxWidth+textWidth, fontSize); + double mean = ImageStatistics.getStatistics(ip, Measurements.MEAN, null).mean; + if (mean<50.0 && !ip.isInvertedLut()) color=Color.white; + ip.resetRoi(); + } + } + int frame = image; + int[] pos = new int[]{0, 0, 0}; + if (imp.isHyperStack()) { + pos = imp.convertIndexToPosition(image); + if (imp.getNFrames()>1) + frame = pos[2]; + else if (imp.getNSlices()>1) + frame = pos[1]; + } + if (useOverlay) { + if (image==1) { + overlay = new Overlay(); + if (baseOverlay!=null) { + for (int i=0; i=firstFrame&&frame<=lastFrame) { + int xloc = format==LABEL?x:x+maxWidth-textWidth; + Roi roi = new TextRoi(xloc, y-yoffset, s, font); + roi.setStrokeColor(color); + roi.setNonScalable(true); + if (imp.isHyperStack()) + roi.setPosition(pos[0], pos[1], pos[2]); + else + roi.setPosition(image); + overlay.add(roi); + } + if (image==imp.getStackSize()||previewing) + imp.setOverlay(overlay); + } else if (frame>=firstFrame&&frame<=lastFrame) { + ip.setColor(color); + ip.setAntialiasedText(fontSize>=18); + int xloc = format==LABEL?x:x+maxWidth-textWidth; + ip.moveTo(xloc, y); + ip.drawString(s); + } + } + + String getString(int index, double interval, int format) { + double time = start + (index+1-firstFrame)*interval; + int itime = (int)Math.floor(time); + int sign = 1; + if (itime < 0) sign = -1; + itime = itime*sign; + String str = ""; + switch (format) { + case NUMBER: str=IJ.d2s(time, decimalPlaces)+" "+text; break; + case ZERO_PADDED_NUMBER: + if (decimalPlaces==0) + str=zeroFill((int)time); + else + str=IJ.d2s(time, decimalPlaces); + str = text +" " + str; + break; + case MIN_SEC: + str=pad((int)Math.floor((itime/60)%60))+":"+pad(itime%60)+" "+text; + if (sign == -1) str = "-"+str; + break; + case HOUR_MIN_SEC: + str=pad((int)Math.floor(itime/3600))+":"+pad((int)Math.floor((itime/60)%60))+":"+pad(itime%60)+" "+text; + if (sign == -1) str = "-"+str; + break; + case TEXT: + str=text; + break; + case LABEL: + if (0<=index && index=min && v<=max; + } + + /* + * This class implements a Cartesian polygon in progress. + * The edges are supposed to be parallel to the x or y axis. + * It is implemented as a deque to be able to add points to both + * sides. + */ + static class Outline { + int[] x, y; + int first, last, reserved; + final int GROW = 10; // default extra (spare) space when enlarging arrays (similar performance with 6-20) + + public Outline() { + reserved = GROW; + x = new int[reserved]; + y = new int[reserved]; + first = last = GROW / 2; + } + + /** Makes sure that enough free space is available at the beginning and end of the list, by enlarging the arrays if required */ + private void needs(int neededAtBegin, int neededAtEnd) { + if (neededAtBegin > first || neededAtEnd > reserved - last) { + int extraSpace = Math.max(GROW, Math.abs(x[last-1] - x[first])); //reserve more space for outlines that span large x range + int newSize = reserved + neededAtBegin + neededAtEnd + extraSpace; + int newFirst = neededAtBegin + extraSpace/2; + int[] newX = new int[newSize]; + int[] newY = new int[newSize]; + System.arraycopy(x, first, newX, newFirst, last-first); + System.arraycopy(y, first, newY, newFirst, last-first); + x = newX; + y = newY; + last += newFirst - first; + first = newFirst; + reserved = newSize; + } + } + + /** Adds point x, y at the end of the list */ + public void append(int x, int y) { + if (last-first>=2 && collinear(this.x[last-2], this.y[last-2], this.x[last-1], this.y[last-1], x , y)) { + this.x[last-1] = x; //replace previous point + this.y[last-1] = y; + } else { + needs(0, 1); //new point + this.x[last] = x; + this.y[last] = y; + last++; + } + } + + /** Adds point x, y at the beginning of the list */ + public void prepend(int x, int y) { + if (last-first>=2 && collinear(this.x[first+1], this.y[first+1], this.x[first], this.y[first], x , y)) { + this.x[first] = x; //replace previous point + this.y[first] = y; + } else { + needs(1, 0); //new point + first--; + this.x[first] = x; + this.y[first] = y; + } + } + + /** Merge with another Outline by adding it at the end. Thereafter, the other outline must not be used any more. */ + public void append(Outline o) { + int size = last - first; + int oSize = o.last - o.first; + if (size <= o.first && oSize > reserved - last) { // we don't have enough space in our own array but in that of 'o' + System.arraycopy(x, first, o.x, o.first - size, size); // so prepend our own data to that of 'o' + System.arraycopy(y, first, o.y, o.first - size, size); + x = o.x; + y = o.y; + first = o.first - size; + last = o.last; + reserved = o.reserved; + } else { // append to our own array + needs(0, oSize); + System.arraycopy(o.x, o.first, x, last, oSize); + System.arraycopy(o.y, o.first, y, last, oSize); + last += oSize; + } + } + + /** Merge with another Outline by adding it at the beginning. Thereafter, the other outline must not be used any more. */ + public void prepend(Outline o) { + int size = last - first; + int oSize = o.last - o.first; + if (size <= o.reserved - o.last && oSize > first) { // we don't have enough space in our own array but in that of 'o' + System.arraycopy(x, first, o.x, o.last, size); // so append our own data to that of 'o' + System.arraycopy(y, first, o.y, o.last, size); + x = o.x; + y = o.y; + first = o.first; + last = o.last + size; + reserved = o.reserved; + } else { // prepend to our own array + needs(oSize, 0); + first -= oSize; + System.arraycopy(o.x, o.first, x, first, oSize); + System.arraycopy(o.y, o.first, y, first, oSize); + } + } + + public Polygon getPolygon() { + // optimize out intermediate points of straight lines (created, e.g., by merging outlines) + int i, j=first+1; + for (i=first+1; i+1 2 && collinear(x[last - 1], y[last - 1], x[first], y[first], x[first + 1], y[first + 1])) + first++; + + int count = last - first; + int[] xNew = new int[count]; + int[] yNew = new int[count]; + System.arraycopy(x, first, xNew, 0, count); + System.arraycopy(y, first, yNew, 0, count); + return new Polygon(xNew, yNew, count); + } + + /** Returns whether three points are on one straight line */ + boolean collinear (int x1, int y1, int x2, int y2, int x3, int y3) { + return (x2-x1)*(y3-y2) == (y2-y1)*(x3-x2); + } + + public String toString() { + String res = "[first:" + first + ",last:" + last + + ",reserved:" + reserved + ":"; + if (last > x.length) System.err.println("ERROR!"); + int nmax = 10; //don't print more coordinates than this + for (int i = first; i < last && i < x.length; i++) { + if (last-first > nmax && i-first > nmax/2) { + i = last - nmax/2; + res += "..."; + nmax = last-first; //dont check again + } else + res += "(" + x[i] + "," + y[i] + ")"; + } + return res + "]"; + } + } + + /* + * Construct all outlines simultaneously by traversing the rows from top to bottom. + * The points are added such that for each pair of consecutive points, the inner + * part is on the left, i.e., the outline encloses filled areas in the + * counterclockwise direction. The outline of empty areas (holes) runs clockwise. + * + * thisRow[x + 1] indicates whether the pixel at (x, y) is selected (inside threshold bounds). + * prevRow[x + 1] indicates whether the pixel at (x, y - 1) is selected. + * + * outline[x] is the outline that is currently unclosed at the top-left corner of pixel(x, y); + * outline[x + 1] is at the top-right corner of pixel(x, y). + * + * If the pixel (x, y - 1) has an outline at its bottom and right sides (merging in its + * lower right corner) and this outline should continue as left & top edges of pixel (x + 1, y), + * xAfterLowerRightCorner is set to x + 1 (the pixel coordinate where this + * has to be taken into account), and oAfterLowerRightCorner is the outline that + * should continue at the left side of pixel (xAfterLowerRightCorner, y) to higher y. + * (Without the code with xAfterLowerRightCorner, etc., this case of 8-connected pixels + * would result in disjunct outlines, e.g. a one-pixel-wide line with angle between + * 0 and -90 deg would be converted to many separate rectangular segments). + */ + Roi getRoi() { + if (showStatus) + IJ.showStatus("Converting threshold to selection"); + boolean[] prevRow, thisRow; + ArrayList polygons = new ArrayList(); + Outline[] outline; + int progressInc = Math.max(h/50, 1); + + prevRow = new boolean[w + 2]; + thisRow = new boolean[w + 2]; + outline = new Outline[w + 1]; + + for (int y = 0; y <= h; y++) { + boolean[] b = prevRow; prevRow = thisRow; thisRow = b; + int xAfterLowerRightCorner = -1; //x at right of 8-connected (not 4-connected) pixels NW-SE + Outline oAfterLowerRightCorner = null; //there, continue this outline towards south + thisRow[1] = y < h ? selected(0, y) : false; + for (int x = 0; x <= w; x++) { + if (y < h && x < w - 1) + thisRow[x + 2] = selected(x + 1, y); //we need to read one pixel ahead + else if (x < w - 1) + thisRow[x + 2] = false; + //IJ.log(x+","+y+": "+thisRow[x + 1]+(x==xAfterLowerRightCorner ? " Corner" : "")+" left="+outline[x]+(x < w ? " right="+outline[x+1] : "")); + if (thisRow[x + 1]) { // i.e., pixel (x,y) is selected + if (!prevRow[x + 1]) { + // Upper edge of selected area: + // - left and right outlines are null: new outline + // - left null: append (line to left) + // - right null: prepend (line to right), or prepend&append (after lower right corner, two borders from one corner) + // - left == right: close (end of hole above) unless we can continue at the right + // - left != right: merge (prepend) unless we can continue at the right + if (outline[x] == null) { + if (outline[x + 1] == null) { + outline[x + 1] = outline[x] = new Outline(); + outline[x].append(x + 1, y); + outline[x].append(x, y); + } else { + outline[x] = outline[x + 1]; // line from top-right to top-left + outline[x + 1] = null; + outline[x].append(x, y); + } + } else if (outline[x + 1] == null) { + if (x == xAfterLowerRightCorner) { + outline[x + 1] = outline[x]; + outline[x] = oAfterLowerRightCorner; + outline[x].append(x, y); + outline[x + 1].prepend(x + 1, y); + } else { + outline[x + 1] = outline[x]; + outline[x] = null; + outline[x + 1].prepend(x + 1, y); + } + } else if (outline[x + 1] == outline[x]) { + if (x < w - 1 && y < h && x != xAfterLowerRightCorner + && !thisRow[x + 2] && prevRow[x + 2]) { //at lower right corner & next pxl deselected + outline[x] = null; + //outline[x+1] unchanged + outline[x + 1].prepend(x + 1,y); + xAfterLowerRightCorner = x + 1; + oAfterLowerRightCorner = outline[x + 1]; + } else { + //System.err.println("subtract " + outline[x]); + polygons.add(outline[x].getPolygon()); // MINUS (add inner hole) + outline[x + 1] = null; + outline[x] = (x == xAfterLowerRightCorner) ? oAfterLowerRightCorner : null; + } + } else { + outline[x].prepend(outline[x + 1]); // merge + for (int x1 = 0; x1 <= w; x1++) + if (x1 != x + 1 && outline[x1] == outline[x + 1]) { + outline[x1] = outline[x]; // after merging, replace old with merged + outline[x + 1] = null; // no line continues at the right + outline[x] = (x == xAfterLowerRightCorner) ? oAfterLowerRightCorner : null; + break; + } + if (outline[x + 1] != null) + throw new RuntimeException("assertion failed"); + } + } + if (!thisRow[x]) { + // left edge + if (outline[x] == null) + throw new RuntimeException("assertion failed"); + outline[x].append(x, y + 1); + } + } else { // !thisRow[x + 1], i.e., pixel (x,y) is deselected + if (prevRow[x + 1]) { + // Lower edge of selected area: + // - left and right outlines are null: new outline + // - left == null: prepend + // - right == null: append, or append&prepend (after lower right corner, two borders from one corner) + // - right == left: close unless we can continue at the right + // - right != left: merge (append) unless we can continue at the right + if (outline[x] == null) { + if (outline[x + 1] == null) { + outline[x] = outline[x + 1] = new Outline(); + outline[x].append(x, y); + outline[x].append(x + 1, y); + } else { + outline[x] = outline[x + 1]; + outline[x + 1] = null; + outline[x].prepend(x, y); + } + } else if (outline[x + 1] == null) { + if (x == xAfterLowerRightCorner) { + outline[x + 1] = outline[x]; + outline[x] = oAfterLowerRightCorner; + outline[x].prepend(x, y); + outline[x + 1].append(x + 1, y); + } else { + outline[x + 1] = outline[x]; + outline[x] = null; + outline[x + 1].append(x + 1, y); + } + } else if (outline[x + 1] == outline[x]) { + //System.err.println("add " + outline[x]); + if (x < w - 1 && y < h && x != xAfterLowerRightCorner + && thisRow[x + 2] && !prevRow[x + 2]) { //at lower right corner & next pxl selected + outline[x] = null; + //outline[x+1] unchanged + outline[x + 1].append(x + 1,y); + xAfterLowerRightCorner = x + 1; + oAfterLowerRightCorner = outline[x + 1]; + } else { + polygons.add(outline[x].getPolygon()); // PLUS (add filled outline) + outline[x + 1] = null; + outline[x] = x == xAfterLowerRightCorner ? oAfterLowerRightCorner : null; + } + } else { + if (x < w - 1 && y < h && x != xAfterLowerRightCorner + && thisRow[x + 2] && !prevRow[x + 2]) { //at lower right corner && next pxl selected + outline[x].append(x + 1, y); + outline[x + 1].prepend(x + 1,y); + xAfterLowerRightCorner = x + 1; + oAfterLowerRightCorner = outline[x]; + // outline[x + 1] unchanged (the one at the right-hand side of (x, y-1) to the top) + outline[x] = null; + } else { + outline[x].append(outline[x + 1]); // merge + for (int x1 = 0; x1 <= w; x1++) + if (x1 != x + 1 && outline[x1] == outline[x + 1]) { + outline[x1] = outline[x]; // after merging, replace old with merged + outline[x + 1] = null; // no line continues at the right + outline[x] = (x == xAfterLowerRightCorner) ? oAfterLowerRightCorner : null; + break; + } + if (outline[x + 1] != null) + throw new RuntimeException("assertion failed"); + } + } + } + if (thisRow[x]) { + // right edge + if (outline[x] == null) + throw new RuntimeException("assertion failed"); + outline[x].prepend(x, y + 1); + } + } + } + if (y%progressInc==0) { + if (Thread.currentThread().isInterrupted()) return null; + if (showStatus) + IJ.showProgress(y*(PROGRESS_FRACTION_OUTLINING/h)); + } + } + + if (polygons.size()==0) + return null; + if (showStatus) IJ.showStatus("Converting threshold to selection..."); + GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD); + progressInc = Math.max(polygons.size()/10, 1); + for (int i = 0; i < polygons.size(); i++) { + path.append((Polygon)polygons.get(i), false); + if (Thread.currentThread().isInterrupted()) return null; + if (showStatus && i%progressInc==0) + IJ.showProgress(PROGRESS_FRACTION_OUTLINING + i*(1.-PROGRESS_FRACTION_OUTLINING)/polygons.size()); + } + + ShapeRoi shape = new ShapeRoi(path); + Roi roi = shape!=null ? shape.trySimplify():null; // try to convert to non-composite ROI + if (showStatus) + IJ.showProgress(1.0); + return roi; + } + + public int setup(String arg, ImagePlus imp) { + image = imp; + return DOES_8G | DOES_16 | DOES_32 | NO_CHANGES; + } + + /** Determines whether to show status messages and a progress bar */ + public void showStatus(boolean showStatus) { + this.showStatus = showStatus; + } +} diff --git a/src/ij/plugin/filter/Transformer.java b/src/ij/plugin/filter/Transformer.java new file mode 100644 index 0000000..27301e8 --- /dev/null +++ b/src/ij/plugin/filter/Transformer.java @@ -0,0 +1,92 @@ +package ij.plugin.filter; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.Calibration; +import java.awt.*; +import java.awt.image.*; + +/** Implements the Flip and Rotate commands in the Image/Transform submenu. */ +public class Transformer implements PlugInFilter { + private ImagePlus imp; + private String arg; + private Overlay overlay; + private boolean firstSlice = true; + + public int setup(String arg, ImagePlus imp) { + this.arg = arg; + this.imp = imp; + if (imp!=null) + overlay = imp.getOverlay(); + if (arg.equals("fliph") || arg.equals("flipv")) + return IJ.setupDialog(imp, DOES_ALL+NO_UNDO); + else + return DOES_ALL+NO_UNDO+NO_CHANGES; + } + + public void run(ImageProcessor ip) { + Calibration cal = imp.getCalibration(); + boolean transformOrigin = cal.xOrigin!=0 || cal.yOrigin!=0; + if (arg.equals("fliph")) { + ip.flipHorizontal(); + Rectangle r = ip.getRoi(); + if (transformOrigin && r.x==0 && r.y==0 && r.width==ip.getWidth() && r.height==ip.getHeight()) + cal.xOrigin = imp.getWidth()-1 - cal.xOrigin; + return; + } + if (arg.equals("flipv")) { + ip.flipVertical(); + Rectangle r = ip.getRoi(); + if (transformOrigin && r.x==0 && r.y==0 && r.width==ip.getWidth() && r.height==ip.getHeight()) + cal.yOrigin = imp.getHeight()-1 - cal.yOrigin; + return; + } + if (arg.equals("right") || arg.equals("left")) { + StackProcessor sp = new StackProcessor(imp.getStack(), ip); + ImageStack s2 = null; + if (arg.equals("right")) { + s2 = sp.rotateRight(); + rotateOverlay(90); + if (transformOrigin) { + double xOrigin = imp.getHeight()-1 - cal.yOrigin; + double yOrigin = cal.xOrigin; + cal.xOrigin = xOrigin; + cal.yOrigin = yOrigin; + } + } else { + s2 = sp.rotateLeft(); + rotateOverlay(-90); + if (transformOrigin) { + double xOrigin = cal.yOrigin; + double yOrigin = imp.getWidth()-1 - cal.xOrigin; + cal.xOrigin = xOrigin; + cal.yOrigin = yOrigin; + } + } + imp.setStack(null, s2); + double pixelWidth = cal.pixelWidth; + cal.pixelWidth = cal.pixelHeight; + cal.pixelHeight = pixelWidth; + if (!cal.getXUnit().equals(cal.getYUnit())) { + String xUnit = cal.getXUnit(); + cal.setXUnit(cal.getYUnit()); + cal.setYUnit(xUnit); + } + return; + } + } + + private void rotateOverlay(int angle) { + if (overlay!=null && firstSlice) { + double xcenter = imp.getWidth()/2.0; + double ycenter = imp.getHeight()/2.0; + double diff1 = xcenter-ycenter; + double diff2 = ycenter-xcenter; + Overlay overlay2 = overlay.rotate(angle,xcenter,ycenter); + overlay2.translate(diff2,diff1); + imp.setOverlay(overlay2); + } + firstSlice = false; + } + +} diff --git a/src/ij/plugin/filter/Translator.java b/src/ij/plugin/filter/Translator.java new file mode 100644 index 0000000..635674b --- /dev/null +++ b/src/ij/plugin/filter/Translator.java @@ -0,0 +1,83 @@ +package ij.plugin.filter; +import ij.*; +import ij.gui.*; +import ij.process.*; +import java.awt.*; +import java.awt.geom.*; + + +/** This plugin implements the Image/Translate command. */ +public class Translator implements ExtendedPlugInFilter, DialogListener { + private int flags = DOES_ALL|PARALLELIZE_STACKS; + private static double xOffset = 15; + private static double yOffset = 15; + private ImagePlus imp; + private GenericDialog gd; + private PlugInFilterRunner pfr; + private static int interpolationMethod = ImageProcessor.NONE; + private String[] methods = ImageProcessor.getInterpolationMethods(); + private boolean previewing; + private Overlay origOverlay; + private boolean overlayOnly; + + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + if (imp!=null) { + origOverlay = imp.getOverlay(); + Undo.saveOverlay(imp); + } + return flags; + } + + public void run(ImageProcessor ip) { + ip.setInterpolationMethod(interpolationMethod); + if (!overlayOnly || origOverlay==null) + ip.translate(xOffset, yOffset); + if (origOverlay!=null) { + Overlay overlay = origOverlay.duplicate(); + overlay.translate(xOffset, yOffset); + imp.setOverlay(overlay); + } + } + + public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) { + this.pfr = pfr; + int digits = xOffset==(int)xOffset&&yOffset==(int)yOffset?1:3; + if (IJ.isMacro()) + interpolationMethod = ImageProcessor.NONE; + gd = new GenericDialog("Translate"); + gd.addSlider("X offset:", -100, 100, xOffset, 0.1); + gd.addSlider("Y offset:", -100, 100, xOffset, 0.1); + gd.addChoice("Interpolation:", methods, methods[interpolationMethod]); + if (origOverlay!=null) + gd.addCheckbox("Overlay only", false); + gd.addPreviewCheckbox(pfr); + gd.addDialogListener(this); + previewing = true; + gd.showDialog(); + if (gd.wasCanceled()) { + imp.setOverlay(origOverlay); + return DONE; + } + previewing = false; + return IJ.setupDialog(imp, flags); + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + xOffset = gd.getNextNumber(); + yOffset = gd.getNextNumber(); + interpolationMethod = gd.getNextChoiceIndex(); + if (origOverlay!=null) + overlayOnly = gd.getNextBoolean(); + if (gd.invalidNumber()) { + if (gd.wasOKed()) IJ.error("Offset is invalid."); + return false; + } + return true; + } + + public void setNPasses(int nPasses) { + } + +} + diff --git a/src/ij/plugin/filter/UnsharpMask.java b/src/ij/plugin/filter/UnsharpMask.java new file mode 100644 index 0000000..22ff2f3 --- /dev/null +++ b/src/ij/plugin/filter/UnsharpMask.java @@ -0,0 +1,99 @@ +package ij.plugin.filter; +import ij.*; +import ij.gui.GenericDialog; +import ij.gui.DialogListener; +import ij.process.*; +import ij.plugin.filter.GaussianBlur; +import ij.measure.Measurements; +import java.awt.*; + +/** This plugin-filter implements ImageJ's Unsharp Mask command. + * Unsharp masking subtracts a blurred copy of the image and rescales the image + * to obtain the same contrast of large (low-frequency) structures as in the + * input image. This is equivalent to adding a high-pass filtered image and + * thus sharpens the image. + * "Radius (Sigma)" is the standard deviation (blur radius) of the Gaussian blur that + * is subtracted. "Mask Weight" determines the strength of filtering, where "Mask Weight"=1 + * would be an infinite weight of the high-pass filtered image that is added. + */ +public class UnsharpMask implements ExtendedPlugInFilter, DialogListener { + private static double sigma = 1.0; // standard deviation of the Gaussian + private static double weight = 0.6; // weight of the mask + private final int flags = DOES_ALL|SUPPORTS_MASKING|CONVERT_TO_FLOAT|SNAPSHOT|KEEP_PREVIEW; + private GaussianBlur gb; + + /** Method to return types supported + * @param arg Not used by this plugin + * @param imp The image to be filtered + * @return Code describing supported formats etc. + * (see ij.plugin.filter.PlugInFilter & ExtendedPlugInFilter) + */ + public int setup(String arg, ImagePlus imp) { + return flags; + } + + /** This method is invoked for each slice or color channel. It filters + * an image by enhancing high-frequency components. Since this + * PlugInFilter specifies the CONVERT_TO_FLOAT and SNAPHOT + * flags, 'ip' is always a FloatProcessor with a valid snapshot. + * @param ip The image, slice or channel to filter + */ + public void run(ImageProcessor ip) { + sharpenFloat((FloatProcessor)ip, sigma, (float)weight); + } + + /** Unsharp Mask filtering of a float image. 'fp' must have a valid snapshot. */ + public void sharpenFloat(FloatProcessor fp, double sigma, float weight) { + if (gb == null) gb = new GaussianBlur(); + gb.blurGaussian(fp, sigma, sigma, 0.01); + if (Thread.currentThread().isInterrupted()) return; + float[] pixels = (float[])fp.getPixels(); + float[] snapshotPixels = (float[])fp.getSnapshotPixels(); + int width = fp.getWidth(); + Rectangle roi = fp.getRoi(); + for (int y=roi.y; y 0.99 || gd.invalidNumber()) + return false; + else return true; + } + + /** Since most computing time is spent in GaussianBlur, forward the + * information about the number of passes to Gaussian Blur. The + * ProgressBar will be handled by GaussianBlur. */ + public void setNPasses(int nPasses) { + if (gb == null) gb = new GaussianBlur(); + gb.setNPasses(nPasses); + } +} diff --git a/src/ij/plugin/filter/Writer.java b/src/ij/plugin/filter/Writer.java new file mode 100644 index 0000000..27b2b3d --- /dev/null +++ b/src/ij/plugin/filter/Writer.java @@ -0,0 +1,47 @@ +package ij.plugin.filter; +import ij.*; +import ij.process.*; +import ij.io.*; + + +/** Obsolete +* @deprecated +*/ +public class Writer implements PlugInFilter { + private String arg; + private ImagePlus imp; + + public int setup(String arg, ImagePlus imp) { + this.arg = arg; + this.imp = imp; + return DOES_ALL+NO_CHANGES; + } + + public void run(ImageProcessor ip) { + if (arg.equals("tiff")) + new FileSaver(imp).saveAsTiff(); + else if (arg.equals("gif")) + new FileSaver(imp).saveAsGif(); + else if (arg.equals("jpeg")) + new FileSaver(imp).saveAsJpeg(); + else if (arg.equals("text")) + new FileSaver(imp).saveAsText(); + else if (arg.equals("lut")) + new FileSaver(imp).saveAsLut(); + else if (arg.equals("raw")) + new FileSaver(imp).saveAsRaw(); + else if (arg.equals("zip")) + new FileSaver(imp).saveAsZip(); + else if (arg.equals("bmp")) + new FileSaver(imp).saveAsBmp(); + else if (arg.equals("png")) + new FileSaver(imp).saveAsPng(); + else if (arg.equals("pgm")) + new FileSaver(imp).saveAsPgm(); + else if (arg.equals("fits")) + new FileSaver(imp).saveAsFits(); + } + +} + + diff --git a/src/ij/plugin/filter/XYWriter.java b/src/ij/plugin/filter/XYWriter.java new file mode 100644 index 0000000..37f66d6 --- /dev/null +++ b/src/ij/plugin/filter/XYWriter.java @@ -0,0 +1,65 @@ +package ij.plugin.filter; + +import java.awt.*; +import java.awt.image.*; +import java.util.Vector; +import java.io.*; +import ij.*; +import ij.process.*; +import ij.io.*; +import ij.gui.*; +import ij.measure.*; +import java.awt.geom.*; + + +/** Saves the XY coordinates of the current ROI boundary. */ +public class XYWriter implements PlugInFilter { + ImagePlus imp; + + public int setup(String arg, ImagePlus imp) { + this.imp = imp; + return DOES_ALL+ROI_REQUIRED+NO_CHANGES; + } + + public void run(ImageProcessor ip) { + saveXYCoordinates(imp); + } + + public void saveXYCoordinates(ImagePlus imp) { + Roi roi = imp.getRoi(); + if (roi==null) + throw new IllegalArgumentException("ROI required"); + SaveDialog sd = new SaveDialog("Save Coordinates as Text...", imp.getTitle(), ".txt"); + String name = sd.getFileName(); + if (name == null) + return; + String directory = sd.getDirectory(); + PrintWriter pw = null; + try { + pw = new PrintWriter(new BufferedOutputStream(new FileOutputStream(directory+name))); + } + catch (IOException e) { + IJ.error("XYWriter", "Unable to save coordinates:\n "+e.getMessage()); + return; + } + + Calibration cal = imp.getCalibration(); + String ls = System.getProperty("line.separator"); + if (roi.subPixelResolution()) { + FloatPolygon p = roi.getFloatPolygon(); + for (int i=0; iCompositeImage.MAX_CHANNELS) + nCheckBoxes = CompositeImage.MAX_CHANNELS; + checkbox = new Checkbox[nCheckBoxes]; + for (int i=0; i1&&channels>16; + int g = (p&0xff00)>>8; + int b = p&0xff; + Color c = new Color(r, g, b); + if (setBackground) { + Toolbar.setBackgroundColor(c); + if (Recorder.record) + Recorder.setBackgroundColor(c); + } else { + Toolbar.setForegroundColor(c); + if (Recorder.record) + Recorder.setForegroundColor(c); + } + } + + void editColor() { + Color c = background?Toolbar.getBackgroundColor():Toolbar.getForegroundColor(); + ColorChooser cc = new ColorChooser((background?"Background":"Foreground")+" Color", c, false); + c = cc.getColor(); + if (background) + Toolbar.setBackgroundColor(c); + else + Toolbar.setForegroundColor(c); + } + + public void refreshColors() { + ip.refreshBackground(false); + ip.refreshForeground(false); + repaint(); + } + + private void showStatus(String msg, int rgb) { + if (msg.length()>1) + IJ.showStatus(msg); + else { + int r = (rgb&0xff0000)>>16; + int g = (rgb&0xff00)>>8; + int b = rgb&0xff; + String hex = Colors.colorToString(new Color(r,g,b)); + IJ.showStatus("red="+pad(r)+", green="+pad(g)+", blue="+pad(b)+" ("+hex+") "+msg); + } + } + + public void mouseExited(MouseEvent e) { + IJ.showStatus(""); + setCursor(defaultCursor); + } + + public void mouseEntered(MouseEvent e) { + setCursor(crosshairCursor); + } + + public void mouseReleased(MouseEvent e) {} + public void mouseClicked(MouseEvent e) {} + public void mouseDragged(MouseEvent e) {} + +} + diff --git a/src/ij/plugin/frame/ColorThresholder.java b/src/ij/plugin/frame/ColorThresholder.java new file mode 100644 index 0000000..376f6df --- /dev/null +++ b/src/ij/plugin/frame/ColorThresholder.java @@ -0,0 +1,1568 @@ +package ij.plugin.frame; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import java.awt.image.*; +import java.util.*; +import java.awt.event.*; +import ij.measure.*; +import ij.plugin.*; +import ij.plugin.filter.ThresholdToSelection; + + +/* This plugin isolates pixels in an RGB image or stack according to a range of Hue. + Original PassBand2 by Bob Dougherty. Some code borrowed from ThresholdAdjuster by Wayne Rasband. + + Version 0 5/12/2002. + Version 1 5/13 Filtered pixels set to foreground color. Speed improved. + Version 2 5/13. Fixed a bug in setting the restore pixels that was causing problems with stacks. + Explicitly get the foreground color from the toolbar in apply. + + Modifications by G. Landini. + 17/Feb/2004. The changes are seen as the sliders/checkboxes are adjusted. + Added hue strip to histogram window, changed histogram scale factor + 19/Feb/2004. Added Saturation and Brightness histograms, + Added Pass/Stop checkboxes for each HSB channel. + Added threshold, added inversion of threshold + Cleaned some variables. Changed name to Threshold_HSB + 22/Feb/2004 Threshold in RGB or HSB space + Changed name to Threshold_Colour, changed button names. + Added thresholding by "sampling". Hue band sampled selection may not' + always work if there are 0 valued histograms. Thread now finishes properly + 23/Feb/2004 Java 1.4 on Mac OS X bug (thanks Wayne) + 25/Feb/2004 Any type of ROI supported for [Sample] + 26/Feb/2004 Modified ROI handling (thanks Wayne) + 28/Feb/2004 Improved Hue sampling, changed histogram background colour + 29/Feb/2004 Added CIE Lab colour space + 6/Mar/2004 v1.4 Requires ImageJ 1.32c (thanks Wayne) + 8/Mar/2004 v1.5 ColourProcessor bug (thanks Wayne), filter checkboxes detect a new image + 23/Jun/2004 v1.6 Added YUV colour space + 1/May/2006 v1.7 Minor changes to Lab coding, added macro recorder button + 5/Jan/2007 v1.8 added warning and commented lines for back/foreground colours + 2/Feb/2008 v1.9 closing does not apply the filter if Original was being displayed. Thanks for the hint Bob! + */ + +/** Selects pixels according to hsb or rgb components. */ +public class ColorThresholder extends PlugInFrame implements PlugIn, Measurements, + ActionListener, AdjustmentListener, FocusListener, ItemListener, Runnable{ + + private static final int HSB=0, RGB=1, LAB=2, YUV=3; + private static final String[] colorSpaces = {"HSB", "RGB", "Lab", "YUV"}; + private boolean flag = false; + private int colorSpace = HSB; + private Thread thread; + private static Frame instance; + + private BandPlot plot = new BandPlot(); + private BandPlot splot = new BandPlot(); + private BandPlot bplot = new BandPlot(); + private int sliderRange = 256; + private Panel panel, panelt; + private Button originalB, filteredB, stackB, helpB, sampleB, resetallB, newB, macroB, selectB; + private Checkbox bandPassH, bandPassS, bandPassB, darkBackground; + private CheckboxGroup colourMode; + private Choice colorSpaceChoice, methodChoice, modeChoice; + private int previousImageID = -1; + private int previousSlice = -1; + private ImageJ ij; + private int minHue = 0, minSat = 0, minBri = 0; + private int maxHue = 255, maxSat = 255, maxBri = 255; + private Scrollbar minSlider, maxSlider, minSlider2, maxSlider2, minSlider3, maxSlider3; + private Label label1, label2, label3, label4, label5, label6, labelh, labels, labelb, labelf; + private boolean done; + private byte[] hSource, sSource, bSource; + private boolean applyingStack; + + private static final int DEFAULT = 0; + private static String[] methodNames = AutoThresholder.getMethods(); + private static String method = methodNames[DEFAULT]; + private static AutoThresholder thresholder = new AutoThresholder(); + private static final int RED=0, WHITE=1, BLACK=2, BLACK_AND_WHITE=3; + private static final String[] modes = {"Red", "White", "Black", "B&W"}; + private static int mode = RED; + + private int numSlices; + private ImageStack stack; + private int width, height, numPixels; + + public ColorThresholder() { + super("Threshold Color"); + if (instance!=null) { + WindowManager.toFront(instance); + return; + } + thread = new Thread(this, "BandAdjuster"); + WindowManager.addWindow(this); + instance = this; + + ij = IJ.getInstance(); + Font font = IJ.font10; + GridBagLayout gridbag = new GridBagLayout(); + GridBagConstraints c = new GridBagConstraints(); + setLayout(gridbag); + + int y = 0; + c.gridx = 0; + c.gridy = y; + c.gridwidth = 1; + c.weightx = 0; + c.insets = new Insets(5, 0, 0, 0); + labelh = new Label("Hue", Label.CENTER); + add(labelh, c); + + c.gridx = 1; + c.gridy = y++; + c.gridwidth = 1; + c.weightx = 0; + c.insets = new Insets(7, 0, 0, 0); + labelf = new Label("", Label.RIGHT); + add(labelf, c); + + // plot + c.gridx = 0; + c.gridy = y; + c.gridwidth = 1; + c.fill = c.BOTH; + c.anchor = c.CENTER; + c.insets = new Insets(0, 5, 0, 0); + add(plot, c); + + // checkboxes + bandPassH = new Checkbox("Pass"); + bandPassH.addItemListener(this); + bandPassH.setState(true); + c.gridx = 1; + c.gridy = y++; + c.gridwidth = 2; + c.insets = new Insets(5, 5, 0, 5); + add(bandPassH, c); + + // minHue slider + minSlider = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, sliderRange); + GUI.fixScrollbar(minSlider); + c.gridx = 0; + c.gridy = y++; + c.gridwidth = 1; + c.weightx = IJ.isMacintosh()?90:100; + c.fill = c.HORIZONTAL; + c.insets = new Insets(5, 5, 0, 0); + + add(minSlider, c); + minSlider.addAdjustmentListener(this); + minSlider.setUnitIncrement(1); + + // minHue slider label + c.gridx = 1; + c.gridwidth = 1; + c.weightx = IJ.isMacintosh()?10:0; + c.insets = new Insets(5, 0, 0, 0); + label1 = new Label(" ", Label.LEFT); + label1.setFont(font); + add(label1, c); + + // maxHue sliderHue + maxSlider = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, sliderRange); + GUI.fixScrollbar(maxSlider); + c.gridx = 0; + c.gridy = y; + c.gridwidth = 1; + c.weightx = 100; + c.insets = new Insets(5, 5, 0, 0); + add(maxSlider, c); + maxSlider.addAdjustmentListener(this); + maxSlider.setUnitIncrement(1); + + // maxHue slider label + c.gridx = 1; + c.gridwidth = 1; + c.gridy = y++; + c.weightx = 0; + c.insets = new Insets(5, 0, 0, 0); + label2 = new Label(" ", Label.LEFT); + label2.setFont(font); + add(label2, c); + + //===== + c.gridx = 0; + c.gridy = y++; + c.gridwidth = 1; + c.weightx = 0; + c.insets = new Insets(10, 0, 0, 0); + labels = new Label("Saturation", Label.CENTER); + add(labels, c); + + // plot + c.gridx = 0; + c.gridy = y; + c.gridwidth = 1; + c.fill = c.BOTH; + c.anchor = c.CENTER; + c.insets = new Insets(0, 5, 0, 0); + add(splot, c); + + // checkboxes + bandPassS = new Checkbox("Pass"); + bandPassS.addItemListener(this); + bandPassS.setState(true); + c.gridx = 1; + c.gridy = y++; + c.gridwidth = 2; + c.insets = new Insets(5, 5, 0, 5); + add(bandPassS, c); + + // minSat slider + minSlider2 = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, sliderRange); + GUI.fixScrollbar(minSlider2); + c.gridx = 0; + c.gridy = y++; + c.gridwidth = 1; + c.weightx = IJ.isMacintosh()?90:100; + c.fill = c.HORIZONTAL; + c.insets = new Insets(5, 5, 0, 0); + add(minSlider2, c); + minSlider2.addAdjustmentListener(this); + minSlider2.setUnitIncrement(1); + + // minSat slider label + c.gridx = 1; + c.gridwidth = 1; + c.weightx = IJ.isMacintosh()?10:0; + c.insets = new Insets(5, 0, 0, 0); + label3 = new Label(" ", Label.LEFT); + label3.setFont(font); + add(label3, c); + + // maxSat slider + maxSlider2 = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, sliderRange); + GUI.fixScrollbar(maxSlider2); + c.gridx = 0; + c.gridy = y++; + c.gridwidth = 1; + c.weightx = 100; + c.insets = new Insets(5, 5, 0, 0); + add(maxSlider2, c); + maxSlider2.addAdjustmentListener(this); + maxSlider2.setUnitIncrement(1); + + // maxSat slider label + c.gridx = 1; + c.gridwidth = 1; + c.weightx = 0; + c.insets = new Insets(5, 0, 0, 0); + label4 = new Label(" ", Label.LEFT); + label4.setFont(font); + add(label4, c); + + //===== + c.gridx = 0; + c.gridwidth = 1; + c.gridy = y++; + c.weightx = 0; + c.insets = new Insets(10, 0, 0, 0); + labelb = new Label("Brightness", Label.CENTER); + add(labelb, c); + + c.gridx = 0; + c.gridwidth = 1; + c.gridy = y; + c.fill = c.BOTH; + c.anchor = c.CENTER; + c.insets = new Insets(0, 5, 0, 0); + add(bplot, c); + + // checkboxes + bandPassB = new Checkbox("Pass"); + bandPassB.addItemListener(this); + bandPassB.setState(true); + c.gridx = 1; + c.gridy = y++; + c.gridwidth = 2; + c.insets = new Insets(5, 5, 0, 5); + add(bandPassB, c); + + // minBri slider + minSlider3 = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, sliderRange); + GUI.fixScrollbar(minSlider3); + c.gridx = 0; + c.gridy = y++; + c.gridwidth = 1; + c.weightx = IJ.isMacintosh()?90:100; + c.fill = c.HORIZONTAL; + c.insets = new Insets(5, 5, 0, 0); + add(minSlider3, c); + minSlider3.addAdjustmentListener(this); + minSlider3.setUnitIncrement(1); + + // minBri slider label + c.gridx = 1; + c.gridwidth = 1; + c.weightx = IJ.isMacintosh()?10:0; + c.insets = new Insets(5, 0, 0, 0); + label5 = new Label(" ", Label.LEFT); + label5.setFont(font); + add(label5, c); + + // maxBri slider + maxSlider3 = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, sliderRange); + GUI.fixScrollbar(maxSlider3); + c.gridx = 0; + c.gridy = y++; + c.gridwidth = 1; + c.weightx = 100; + c.insets = new Insets(5, 5, 0, 0); + add(maxSlider3, c); + maxSlider3.addAdjustmentListener(this); + maxSlider3.setUnitIncrement(1); + + // maxBri slider label + c.gridx = 1; + c.gridwidth = 1; + c.weightx = 0; + c.insets = new Insets(5, 0, 0, 0); + label6 = new Label(" ", Label.LEFT); + label6.setFont(font); + add(label6, c); + + GridBagLayout gridbag2 = new GridBagLayout(); + GridBagConstraints c2 = new GridBagConstraints(); + int y2 = 0; + Panel panel = new Panel(); + panel.setLayout(gridbag2); + + // threshoding method choice + c2.gridx = 0; c2.gridy = y2; + c2.anchor = GridBagConstraints.EAST; + c2.gridwidth = 1; + c2.insets = new Insets(5, 0, 0, 0); + Label theLabel = new Label("Thresholding method:"); + gridbag2.setConstraints(theLabel, c2); + panel.add(theLabel); + methodChoice = new Choice(); + for (int i=0; iimaxhue) imaxhue=(hsSource[i]&255); + if ((hsSource[i]&255)imaxsat) imaxsat=(ssSource[i]&255); + if ((ssSource[i]&255)imaxbri) imaxbri=(bsSource[i]&255); + if ((bsSource[i]&255)0){ + gap=1; + gapst=i; + } + else { + gap++; + } + if (gap>maxgap){ + maxgap=gap; + maxgapst=gapst; + maxgapen=i; + } + } + } + + for (i = 0; i < 256; i++){ + if (bin[i]>0){ + rangePassL = i; + break; + } + } + for (i = 255; i >= 0; i--){ + if (bin[i]>0){ + rangePassH = i; + break; + } + } + if ((rangePassH-rangePassL)maxHue) { + minHue = maxHue; + minSlider.setValue((int)minHue); + } + } + + void adjustMinSat(int value) { + minSat = value; + if (maxSatmaxSat) { + minSat = maxSat; + minSlider2.setValue((int)minSat); + } + } + + void adjustMinBri(int value) { + minBri = value; + if (maxBrimaxBri) { + minBri = maxBri; + minSlider3.setValue((int)minBri); + } + } + + void apply(ImagePlus imp) { + if (IJ.debugMode) IJ.log("ColorThresholder.apply"); + ImageProcessor fillMaskIP = (ImageProcessor)imp.getProperty("Mask"); + if (fillMaskIP==null) return; + byte[] fillMask = (byte[])fillMaskIP.getPixels(); + byte fill = (byte)255; + byte keep = (byte)0; + + if (bandPassH.getState() && bandPassS.getState() && bandPassB.getState()){ //PPP All pass + for (int j = 0; j < numPixels; j++){ + int hue = hSource[j]&0xff; + int sat = sSource[j]&0xff; + int bri = bSource[j]&0xff; + if (((hue < minHue)||(hue > maxHue)) || ((sat < minSat)||(sat > maxSat)) || ((bri < minBri)||(bri > maxBri))) + fillMask[j] = keep; + else + fillMask[j] = fill; + } + } else if(!bandPassH.getState() && !bandPassS.getState() && !bandPassB.getState()){ //SSS All stop + for (int j = 0; j < numPixels; j++){ + int hue = hSource[j]&0xff; + int sat = sSource[j]&0xff; + int bri = bSource[j]&0xff; + if (((hue >= minHue)&&(hue <= maxHue)) || ((sat >= minSat)&&(sat <= maxSat)) || ((bri >= minBri)&&(bri <= maxBri))) + fillMask[j] = keep; + else + fillMask[j] = fill; + } + } else if(bandPassH.getState() && bandPassS.getState() && !bandPassB.getState()){ //PPS + for (int j = 0; j < numPixels; j++){ + int hue = hSource[j]&0xff; + int sat = sSource[j]&0xff; + int bri = bSource[j]&0xff; + if (((hue < minHue)||(hue > maxHue)) || ((sat < minSat)||(sat > maxSat)) || ((bri >= minBri) && (bri <= maxBri))) + fillMask[j] = keep; + else + fillMask[j] = fill; + } + } else if(!bandPassH.getState() && !bandPassS.getState() && bandPassB.getState()){ //SSP + for (int j = 0; j < numPixels; j++){ + int hue = hSource[j]&0xff; + int sat = sSource[j]&0xff; + int bri = bSource[j]&0xff; + if (((hue >= minHue) && (hue <= maxHue)) || ((sat >= minSat) && (sat <= maxSat)) || ((bri < minBri) || (bri > maxBri))) + fillMask[j] = keep; + else + fillMask[j] = fill; + } + } else if (bandPassH.getState() && !bandPassS.getState() && !bandPassB.getState()){ //PSS + for (int j = 0; j < numPixels; j++){ + int hue = hSource[j]&0xff; + int sat = sSource[j]&0xff; + int bri = bSource[j]&0xff; + if (((hue < minHue) || (hue > maxHue)) || ((sat >= minSat) && (sat <= maxSat)) || ((bri >= minBri) && (bri <= maxBri))) + fillMask[j] = keep; + else + fillMask[j] = fill; + } + } else if(!bandPassH.getState() && bandPassS.getState() && bandPassB.getState()){ //SPP + for (int j = 0; j < numPixels; j++){ + int hue = hSource[j]&0xff; + int sat = sSource[j]&0xff; + int bri = bSource[j]&0xff; + if (((hue >= minHue) && (hue <= maxHue))|| ((sat < minSat) || (sat > maxSat)) || ((bri < minBri) || (bri > maxBri))) + fillMask[j] = keep; + else + fillMask[j] = fill; + } + } else if (!bandPassH.getState() && bandPassS.getState() && !bandPassB.getState()){ //SPS + for (int j = 0; j < numPixels; j++){ + int hue = hSource[j]&0xff; + int sat = sSource[j]&0xff; + int bri = bSource[j]&0xff; + if (((hue >= minHue)&& (hue <= maxHue)) || ((sat < minSat)||(sat > maxSat)) || ((bri >= minBri) && (bri <= maxBri))) + fillMask[j] = keep; + else + fillMask[j] = fill; + } + } else if(bandPassH.getState() && !bandPassS.getState() && bandPassB.getState()){ //PSP + for (int j = 0; j < numPixels; j++){ + int hue = hSource[j]&0xff; + int sat = sSource[j]&0xff; + int bri = bSource[j]&0xff; + if (((hue < minHue) || (hue > maxHue)) || ((sat >= minSat)&&(sat <= maxSat)) || ((bri < minBri) || (bri > maxBri))) + fillMask[j] = keep; + else + fillMask[j] = fill; + } + } + + ImageProcessor ip = imp.getProcessor(); + if (ip==null) return; + if (mode==BLACK_AND_WHITE) { + int[] pixels = (int[])ip.getPixels(); + int fcolor = Prefs.blackBackground?0xffffffff:0xff000000; + int bcolor = Prefs.blackBackground?0xff000000:0xffffffff; + for (int i=0; i255?255:L1)) & 0xff); + a[i] = (byte)((int)(a1<0?0:(a1>255?255:a1)) & 0xff); + b[i] = (byte)((int)(b1<0?0:(b1>255?255:b1)) & 0xff); + } + } + + public void getYUV(ImageProcessor ip, byte[] Y, byte[] U, byte[] V) { + // Returns YUV in 3 byte arrays. + + //RGB <--> YUV Conversion Formulas from http://www.cse.msu.edu/~cbowen/docs/yuvtorgb.html + //R = Y + (1.4075 * (V - 128)); + //G = Y - (0.3455 * (U - 128) - (0.7169 * (V - 128)); + //B = Y + (1.7790 * (U - 128); + // + //Y = R * .299 + G * .587 + B * .114; + //U = R * -.169 + G * -.332 + B * .500 + 128.; + //V = R * .500 + G * -.419 + B * -.0813 + 128.; + + int c, x, y, i=0, r, g, b; + double yf; + + int width=ip.getWidth(); + int height=ip.getHeight(); + + for(y=0;y>16);//R + g = ((c&0x00ff00)>>8);//G + b = ( c&0x0000ff); //B + + // Kai's plugin + yf = (0.299 * r + 0.587 * g + 0.114 * b); + Y[i] = (byte)((int)Math.floor(yf + 0.5)) ; + U[i] = (byte)(128+(int)Math.floor((0.493 *(b - yf))+ 0.5)); + V[i] = (byte)(128+(int)Math.floor((0.877 *(r - yf))+ 0.5)); + + //Y[i] = (byte) (Math.floor( 0.299 * r + 0.587 * g + 0.114 * b)+.5); + //U[i] = (byte) (Math.floor(-0.169 * r - 0.332 * g + 0.500 * b + 128.0)+.5); + //V[i] = (byte) (Math.floor( 0.500 * r - 0.419 * g - 0.0813 * b + 128.0)+.5); + + i++; + } + } + } + + /** Converts the current image from RGB to CIE L*a*b* and stores the results + * in the same RGB image R=L*, G=a*, B=b*. Values are therfore offset and rescaled. + */ + public static void RGBtoLab() { + ImagePlus imp = IJ.getImage(); + if (imp.getBitDepth()==24) + imp.setProcessor(RGBtoLab(imp.getProcessor())); + } + + private static ImageProcessor RGBtoLab(ImageProcessor ip) { + int n = ip.getPixelCount(); + byte[] L = new byte[n]; + byte[] a = new byte[n]; + byte[] b = new byte[n]; + ColorThresholder.getLab(ip, L, a, b); + ColorProcessor cp = new ColorProcessor(ip.getWidth(),ip.getHeight()); + cp.setRGB(L,a,b); + return cp; + } + + /** Converts the current image from RGB to YUV and stores + * the results in the same RGB image R=Y, G=U, B=V. + * Author: Gabriel Landini, G.Landini@bham.ac.uk + */ + public static void RGBtoYUV() { + ImagePlus imp = IJ.getImage(); + if (imp.getBitDepth()==24) { + RGBtoYUV(imp.getProcessor()); + imp.updateAndDraw(); + } + } + + static void RGBtoYUV(ImageProcessor ip) { + int xe = ip.getWidth(); + int ye = ip.getHeight(); + int c, x, y, i=0, Y, U, V, r, g, b; + double yf; + + ImagePlus imp = WindowManager.getCurrentImage(); + + for(y=0;y>16);//R + g = ((c&0x00ff00)>>8);//G + b = ( c&0x0000ff); //B + + // Kai's plugin + yf = (0.299 * r + 0.587 * g + 0.114 * b); + Y = ((int)Math.floor(yf + 0.5)) ; + U = (128+(int)Math.floor((0.493 *(b - yf))+ 0.5)); + V = (128+(int)Math.floor((0.877 *(r - yf))+ 0.5)); + + ip.putPixel(x,y, (((Y<0?0:Y>255?255:Y) & 0xff) << 16)+ + (((U<0?0:U>255?255:U) & 0xff) << 8) + + ((V<0?0:V>255?255:V) & 0xff)); + + ip.putPixel(x,y, ((Y & 0xff) <<16) + ((U & 0xff) << 8) + ( V & 0xff)); + } + } + } + + + + class BandPlot extends Canvas implements Measurements, MouseListener { + + final int WIDTH = 256, HEIGHT=64; + double minHue = 0, minSat=0, minBri=0; + double maxHue = 255, maxSat= 255, maxBri=255; + int[] histogram; + Color[] hColors; + int hmax; + Image os; + Graphics osg; + + public BandPlot() { + addMouseListener(this); + setSize(WIDTH+1, HEIGHT+1); + } + + /** Overrides Component getPreferredSize(). Added to work + around a bug in Java 1.4 on Mac OS X.*/ + public Dimension getPreferredSize() { + return new Dimension(WIDTH+1, HEIGHT+1); + } + + void setHistogram(ImagePlus imp, int j) { + ImageProcessor ip = imp.getProcessor(); + ImageStatistics stats = ImageStatistics.getStatistics(ip, AREA+MODE, null); + int maxCount2 = 0; + histogram = stats.histogram; + for (int i = 0; i < stats.nBins; i++) + if ((histogram[i] > maxCount2) ) maxCount2 = histogram[i]; + //if ((histogram[i] > maxCount2) && (i != stats.mode)) maxCount2 = histogram[i]; + + hmax = (int)(maxCount2 * 1.15);//GL was 1.5 + os = null; + ColorModel cm = ip.getColorModel(); + if (!(cm instanceof IndexColorModel)) + return; + IndexColorModel icm = (IndexColorModel)cm; + int mapSize = icm.getMapSize(); + if (mapSize!=256) + return; + byte[] r = new byte[256]; + byte[] g = new byte[256]; + byte[] b = new byte[256]; + icm.getReds(r); + icm.getGreens(g); + icm.getBlues(b); + hColors = new Color[256]; + + if (colorSpace==RGB){ + if (j==0){ + for (int i=0; i<256; i++) + hColors[i] = new Color(i&255, 0&255, 0&255); + } + else if (j==1){ + for (int i=0; i<256; i++) + hColors[i] = new Color(0&255, i&255, 0&255); + } + else if (j==2){ + for (int i=0; i<256; i++) + hColors[i] = new Color(0&255, 0&255, i&255); + } + } + else if (colorSpace==HSB){ + if (j==0){ + for (int i=0; i<256; i++) + hColors[i] = new Color(r[i]&255, g[i]&255, b[i]&255); + } + else if (j==1){ + for (int i=0; i<256; i++) + hColors[i] = new Color(255&255, 255-i&255, 255-i&255); + //hColors[i] = new Color(192-i/4&255, 192+i/4&255, 192-i/4&255); + } + else if (j==2){ + for (int i=0; i<256; i++) + //hColors[i] = new Color(i&255, i&255, 0&255); + hColors[i] = new Color(i&255, i&255, i&255); + } + } + else if (colorSpace==LAB){ + if (j==0){ + for (int i=0; i<256; i++) + hColors[i] = new Color(i&255, i&255, i&255); + } + else if (j==1){ + for (int i=0; i<256; i++) + hColors[i] = new Color(i&255, 255-i&255, 0&255); + } + else if (j==2){ + for (int i=0; i<256; i++) + hColors[i] = new Color(i&255, i&255, 255-i&255); + } + } + else if (colorSpace==YUV){ + if (j==0){ + for (int i=0; i<256; i++) + hColors[i] = new Color(i&255, i&255, i&255); + } + else if (j==1){ + for (int i=0; i<256; i++) + hColors[i] = new Color((int)(36+(255-i)/1.4)&255, 255-i&255, i&255); + } + else if (j==2){ + for (int i=0; i<256; i++) + hColors[i] = new Color(i&255, 255-i&255, (int)(83+(255-i)/2.87)&255); + } + } + + } + + int[] getHistogram() { + return histogram; + } + + public void update(Graphics g) { + paint(g); + } + + public void paint(Graphics g ) { + int hHist=0; + if (histogram!=null) { + if (os==null) { + os = createImage(WIDTH,HEIGHT); + osg = os.getGraphics(); + //osg.setColor(Color.white); + osg.setColor(new Color(140,152,144)); + osg.fillRect(0, 0, WIDTH, HEIGHT); + for (int i = 0; i < WIDTH; i++) { + if (hColors!=null) osg.setColor(hColors[i]); + hHist=HEIGHT - ((int)(HEIGHT * histogram[i])/hmax)-6; + osg.drawLine(i, HEIGHT, i, hHist); + osg.setColor(Color.black); + osg.drawLine(i, hHist, i, hHist); + } + osg.dispose(); + } + if (os!=null) g.drawImage(os, 0, 0, this); + } else { + g.setColor(Color.white); + g.fillRect(0, 0, WIDTH, HEIGHT); + } + g.setColor(Color.black); + g.drawLine(0, HEIGHT -6, 256, HEIGHT-6); + g.drawRect(0, 0, WIDTH, HEIGHT); + g.drawRect((int)minHue, 1, (int)(maxHue-minHue), HEIGHT-7); + } + + public void mousePressed(MouseEvent e) {} + public void mouseReleased(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + public void mouseClicked(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + } // BandPlot class + +} // BandAdjuster class + + + + diff --git a/src/ij/plugin/frame/Commands.java b/src/ij/plugin/frame/Commands.java new file mode 100644 index 0000000..d963671 --- /dev/null +++ b/src/ij/plugin/frame/Commands.java @@ -0,0 +1,192 @@ +package ij.plugin.frame; +import ij.*; +import ij.gui.*; +import java.awt.*; +import java.awt.event.*; + + +/** This plugin implements the Plugins>Utiltiees>Recent Commands command. */ +public class Commands extends PlugInFrame implements ActionListener, ItemListener, CommandListener { + public static final String LOC_KEY = "commands.loc"; + public static final String CMDS_KEY = "commands.cmds"; + public static final int MAX_COMMANDS = 20; + private static Frame instance; + private static final String divider = "---------------"; + private static final String[] commands = { + "Blobs", + "Open...", + "Show Info...", + "Close", + "Close All", + "Appearance...", + "Histogram", + "Gaussian Blur...", + "Record...", + "Capture Screen", + "Find Commands..." + }; + private List list; + private String command; + private Button button; + + public Commands() { + super("Commands"); + if (instance!=null) { + WindowManager.toFront(instance); + return; + } + instance = this; + WindowManager.addWindow(this); + list = new List(MAX_COMMANDS); + list.addItemListener(this); + String cmds = Prefs.get(CMDS_KEY, null); + if (cmds!=null) { + String[] cmd = cmds.split(","); + int len = cmd.length<=MAX_COMMANDS?cmd.length:MAX_COMMANDS; + boolean isDivider = false; + for (int i=0; i=MAX_COMMANDS) + list.remove(getDividerIndex()-1); + list.add(cmd2, 0); + } + command = null; + return cmd2; + } + + private int getDividerIndex() { + int index = 0; + for (int i=0; i1.0) { + sanFont = sanFont.deriveFont((float)(sanFont.getSize()*scale)); + monoFont = monoFont.deriveFont((float)(monoFont.getSize()*scale)); + } + + // plot + c.gridx = 0; + y = 0; + c.gridy = y++; + c.fill = GridBagConstraints.BOTH; + c.anchor = GridBagConstraints.CENTER; + c.insets = new Insets(10, 10, 0, 10); + gridbag.setConstraints(plot, c); + add(plot); + plot.addKeyListener(ij); + // min and max labels + + if (!windowLevel) { + panel = new Panel(); + c.gridy = y++; + c.insets = new Insets(0, 10, 0, 10); + gridbag.setConstraints(panel, c); + panel.setLayout(new BorderLayout()); + minLabel = new Label(blankMinLabel, Label.LEFT); + minLabel.setFont(monoFont); + if (IJ.debugMode) minLabel.setBackground(Color.yellow); + panel.add("West", minLabel); + maxLabel = new Label(blankMaxLabel, Label.RIGHT); + maxLabel.setFont(monoFont); + if (IJ.debugMode) maxLabel.setBackground(Color.yellow); + panel.add("East", maxLabel); + add(panel); + blankMinLabel = " "; + blankMaxLabel = " "; + } + + // min slider + if (!windowLevel) { + minSlider = new Scrollbar(Scrollbar.HORIZONTAL, sliderRange/2, 1, 0, sliderRange); + GUI.fixScrollbar(minSlider); + c.gridy = y++; + c.insets = new Insets(2, 10, 0, 10); + gridbag.setConstraints(minSlider, c); + add(minSlider); + minSlider.addAdjustmentListener(this); + minSlider.addKeyListener(ij); + minSlider.setUnitIncrement(1); + minSlider.setFocusable(false); // prevents blinking on Windows + addLabel("Minimum", null); + } + + // max slider + if (!windowLevel) { + maxSlider = new Scrollbar(Scrollbar.HORIZONTAL, sliderRange/2, 1, 0, sliderRange); + GUI.fixScrollbar(maxSlider); + c.gridy = y++; + c.insets = new Insets(2, 10, 0, 10); + gridbag.setConstraints(maxSlider, c); + add(maxSlider); + maxSlider.addAdjustmentListener(this); + maxSlider.addKeyListener(ij); + maxSlider.setUnitIncrement(1); + maxSlider.setFocusable(false); + addLabel("Maximum", null); + } + + // brightness slider + brightnessSlider = new Scrollbar(Scrollbar.HORIZONTAL, sliderRange/2, 1, 0, sliderRange); + GUI.fixScrollbar(brightnessSlider); + c.gridy = y++; + c.insets = new Insets(windowLevel?12:2, 10, 0, 10); + gridbag.setConstraints(brightnessSlider, c); + add(brightnessSlider); + brightnessSlider.addAdjustmentListener(this); + brightnessSlider.addKeyListener(ij); + brightnessSlider.setUnitIncrement(1); + brightnessSlider.setFocusable(false); + if (windowLevel) + addLabel("Level: ", levelLabel=new TrimmedLabel(" ")); + else + addLabel("Brightness", null); + + // contrast slider + if (!balance) { + contrastSlider = new Scrollbar(Scrollbar.HORIZONTAL, sliderRange/2, 1, 0, sliderRange); + GUI.fixScrollbar(contrastSlider); + c.gridy = y++; + c.insets = new Insets(2, 10, 0, 10); + gridbag.setConstraints(contrastSlider, c); + add(contrastSlider); + contrastSlider.addAdjustmentListener(this); + contrastSlider.addKeyListener(ij); + contrastSlider.setUnitIncrement(1); + contrastSlider.setFocusable(false); + if (windowLevel) + addLabel("Window: ", windowLabel=new TrimmedLabel(" ")); + else + addLabel("Contrast", null); + } + + // color channel popup menu + if (balance) { + c.gridy = y++; + c.insets = new Insets(5, 10, 0, 10); + choice = new Choice(); + addBalanceChoices(); + gridbag.setConstraints(choice, c); + choice.addItemListener(this); + add(choice); + } + + // buttons + if (scale>1.0) { + Font font = getFont(); + if (font!=null) + font = font.deriveFont((float)(font.getSize()*scale)); + else + font = new Font("SansSerif", Font.PLAIN, (int)(12*scale)); + setFont(font); + } + int trim = IJ.isMacOSX()?20:0; + panel = new Panel(); + panel.setLayout(new GridLayout(0,2, 0, 0)); + autoB = new TrimmedButton("Auto",trim); + autoB.addActionListener(this); + autoB.addKeyListener(ij); + panel.add(autoB); + resetB = new TrimmedButton("Reset",trim); + resetB.addActionListener(this); + resetB.addKeyListener(ij); + panel.add(resetB); + setB = new TrimmedButton("Set",trim); + setB.addActionListener(this); + setB.addKeyListener(ij); + panel.add(setB); + applyB = new TrimmedButton("Apply",trim); + applyB.addActionListener(this); + applyB.addKeyListener(ij); + panel.add(applyB); + c.gridy = y++; + c.insets = new Insets(8, 5, 10, 5); + gridbag.setConstraints(panel, c); + add(panel); + + addKeyListener(ij); // ImageJ handles keyboard shortcuts + pack(); + Point loc = Prefs.getLocation(LOC_KEY); + if (loc!=null) + setLocation(loc); + else + GUI.centerOnImageJScreen(this); + if (IJ.isMacOSX()) setResizable(false); + show(); + + thread = new Thread(this, "ContrastAdjuster"); + //thread.setPriority(thread.getPriority()-1); + thread.start(); + setup(); + } + + void addBalanceChoices() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null && imp.isComposite()) { + for (int i=0; i640 && newSliderRange<1280) + newSliderRange /= 2; + else if (newSliderRange>=1280) + newSliderRange /= 5; + if (newSliderRange<256) newSliderRange = 256; + if (newSliderRange>1024) newSliderRange = 1024; + double displayRange = max-min; + if (valueRange>=1280 && valueRange!=0 && displayRange/valueRange<0.25) + newSliderRange *= 1.6666; + if (newSliderRange!=sliderRange) { + sliderRange = newSliderRange; + updateScrollBars(null, true); + } else + updateScrollBars(null, false); + if (balance) { + if (imp.isComposite()) { + int channel = imp.getChannel(); + if (channel<=4) { + choice.select(channel-1); + channels = channelConstants[channel-1]; + } + if (choice.getItem(0).equals("Red")) { + choice.removeAll(); + addBalanceChoices(); + } + } else { // not composite + if (choice.getItem(0).equals("Channel 1")) { + choice.removeAll(); + addBalanceChoices(); + } + } + } + if (!doReset) + plotHistogram(imp); + autoThreshold = 0; + if (imp.isComposite()) + IJ.setKeyUp(KeyEvent.VK_SHIFT); + } + + void setMinAndMax(ImagePlus imp, double min, double max) { + boolean rgb = imp.getType()==ImagePlus.COLOR_RGB; + if (channels!=7 && rgb) + imp.setDisplayRange(min, max, channels); + else + imp.setDisplayRange(min, max); + if (rgb) + plotHistogram(imp); + } + + void updatePlot() { + plot.min = min; + plot.max = max; + plot.repaint(); + } + + void updateLabels(ImagePlus imp) { + double min = imp.getDisplayRangeMin(); + double max = imp.getDisplayRangeMax();; + int type = imp.getType(); + Calibration cal = imp.getCalibration(); + boolean realValue = type==ImagePlus.GRAY32; + if (cal.calibrated()) { + min = cal.getCValue((int)min); + max = cal.getCValue((int)max); + realValue = true; + } + if (windowLevel) { + digits = realValue?2:0; + double window = max-min; + double level = min+(window)/2.0; + windowLabel.setText(IJ.d2s(window, digits)); + levelLabel.setText(IJ.d2s(level, digits)); + } else { + digits = realValue?4:0; + if (realValue) { + double s = min<0||max<0?0.1:1.0; + double amin = Math.abs(min); + double amax = Math.abs(max); + if (amin>99.0*s||amax>99.0*s) digits = 3; + if (amin>999.0*s||amax>999.0*s) digits = 2; + if (amin>9999.0*s||amax>9999.0*s) digits = 1; + if (amin>99999.0*s||amax>99999.0*s) digits = 0; + if (amin>9999999.0*s||amax>9999999.0*s) digits = -2; + } + String minString = IJ.d2s(min, min==0.0?0:digits) + blankMinLabel; + minLabel.setText(minString.substring(0,blankMinLabel.length())); + String maxString = blankMaxLabel + IJ.d2s(max, digits); + maxString = maxString.substring(maxString.length()-blankMaxLabel.length(), maxString.length()); + maxLabel.setText(maxString); + } + } + + void updateScrollBars(Scrollbar sb, boolean newRange) { + if (sb==null || sb!=contrastSlider) { + double mid = sliderRange/2; + double c = ((defaultMax-defaultMin)/(max-min))*mid; + if (c>mid) + c = sliderRange - ((max-min)/(defaultMax-defaultMin))*mid; + contrast = (int)c; + if (contrastSlider!=null) { + if (newRange) + contrastSlider.setValues(contrast, 1, 0, sliderRange); + else + contrastSlider.setValue(contrast); + } + } + if (sb==null || sb!=brightnessSlider) { + double level = min + (max-min)/2.0; + double normalizedLevel = 1.0 - (level - defaultMin)/(defaultMax-defaultMin); + brightness = (int)(normalizedLevel*sliderRange); + if (newRange) + brightnessSlider.setValues(brightness, 1, 0, sliderRange); + else + brightnessSlider.setValue(brightness); + } + if (minSlider!=null && (sb==null || sb!=minSlider)) { + if (newRange) + minSlider.setValues(scaleDown(min), 1, 0, sliderRange); + else + minSlider.setValue(scaleDown(min)); + } + if (maxSlider!=null && (sb==null || sb!=maxSlider)) { + if (newRange) + maxSlider.setValues(scaleDown(max), 1, 0, sliderRange); + else + maxSlider.setValue(scaleDown(max)); + } + } + + int scaleDown(double v) { + if (vdefaultMax) v = defaultMax; + return (int)((v-defaultMin)*(sliderRange-1.0)/(defaultMax-defaultMin)); + } + + /** Restore image outside non-rectangular roi. */ + void doMasking(ImagePlus imp, ImageProcessor ip) { + ImageProcessor mask = imp.getMask(); + if (mask!=null) { + Rectangle r = ip.getRoi(); + if (mask.getWidth()!=r.width||mask.getHeight()!=r.height) { + ip.setRoi(imp.getRoi()); + mask = ip.getMask(); + } + ip.reset(mask); + } + } + + void adjustMin(ImagePlus imp, ImageProcessor ip, double minvalue) { + resetRGB(ip); + min = defaultMin + minvalue*(defaultMax-defaultMin)/(sliderRange-1.0); + if (max>defaultMax) + max = defaultMax; + if (min>max) + max = min; + setMinAndMax(imp, min, max); + if (min==max) + setThreshold(ip); + if (RGBImage) doMasking(imp, ip); + updateScrollBars(minSlider, false); + } + + void adjustMax(ImagePlus imp, ImageProcessor ip, double maxvalue) { + resetRGB(ip); + max = defaultMin + maxvalue*(defaultMax-defaultMin)/(sliderRange-1.0); + //IJ.log("adjustMax: "+maxvalue+" "+max); + if (min0.0) { + min = center-(0.5*range)/slope; + max = center+(0.5*range)/slope; + } + setMinAndMax(imp, min, max); + if (RGBImage) doMasking(imp, ip); + updateScrollBars(contrastSlider, false); + } + + void reset(ImagePlus imp, ImageProcessor ip) { + if (RGBImage) + ip.reset(); + int bitDepth = imp.getBitDepth(); + if (bitDepth==16 || bitDepth==32) { + imp.resetDisplayRange(); + defaultMin = imp.getDisplayRangeMin(); + defaultMax = imp.getDisplayRangeMax(); + plot.defaultMin = defaultMin; + plot.defaultMax = defaultMax; + } + min = defaultMin; + max = defaultMax; + setMinAndMax(imp, min, max); + updateScrollBars(null, false); + plotHistogram(imp); + autoThreshold = 0; + } + + void plotHistogram(ImagePlus imp) { + ImageStatistics stats; + if (balance && (channels==4 || channels==2 || channels==1) && imp.getType()==ImagePlus.COLOR_RGB) { + int w = imp.getWidth(); + int h = imp.getHeight(); + byte[] r = new byte[w*h]; + byte[] g = new byte[w*h]; + byte[] b = new byte[w*h]; + ((ColorProcessor)imp.getProcessor()).getRGB(r,g,b); + byte[] pixels=null; + if (channels==4) + pixels = r; + else if (channels==2) + pixels = g; + else if (channels==1) + pixels = b; + ImageProcessor ip = new ByteProcessor(w, h, pixels, null); + stats = ImageStatistics.getStatistics(ip, 0, imp.getCalibration()); + } else { + int range = imp.getType()==ImagePlus.GRAY16?ImagePlus.getDefault16bitRange():0; + if (range!=0 && imp.getProcessor().getMax()==Math.pow(2,range)-1 && !(imp.getCalibration().isSigned16Bit())) { + ImagePlus imp2 = new ImagePlus("Temp", imp.getProcessor()); + stats = new StackStatistics(imp2, 256, 0, Math.pow(2,range)); + } else + stats = imp.getStatistics(); + } + Color color = Color.gray; + if (imp.isComposite() && !(balance&&channels==7)) + color = ((CompositeImage)imp).getChannelColor(); + plot.setHistogram(stats, color); + } + + void apply(ImagePlus imp, ImageProcessor ip) { + if (balance && imp.isComposite()) + return; + int bitDepth = imp.getBitDepth(); + if ((bitDepth==8||bitDepth==16) && !IJ.isMacro()) { + String msg = "WARNING: the pixel values will\nchange if you click \"OK\"."; + if (!IJ.showMessageWithCancel("Apply Lookup Table?", msg)) + return; + } + String option = null; + if (RGBImage) + imp.unlock(); + if (!imp.lock()) + return; + if (RGBImage) { + if (imp.getStackSize()>1) + applyRGBStack(imp); + else + applyRGB(imp,ip); + return; + } + if (bitDepth==32) { + IJ.beep(); + IJ.error("\"Apply\" does not work with 32-bit images"); + imp.unlock(); + return; + } + int range = 256; + if (bitDepth==16) { + range = 65536; + int defaultRange = imp.getDefault16bitRange(); + if (defaultRange>0) + range = (int)Math.pow(2,defaultRange)-1; + } + int tableSize = bitDepth==16?65536:256; + int[] table = new int[tableSize]; + int min = (int)imp.getDisplayRangeMin(); + int max = (int)imp.getDisplayRangeMax(); + if (IJ.debugMode) IJ.log("Apply: mapping "+min+"-"+max+" to 0-"+(range-1)); + for (int i=0; i=max) + table[i] = range-1; + else + table[i] = (int)(((double)(i-min)/(max-min))*range); + } + ip.setRoi(imp.getRoi()); + if (imp.getStackSize()>1 && !imp.isComposite()) { + ImageStack stack = imp.getStack(); + YesNoCancelDialog d = new YesNoCancelDialog(new Frame(), + "Entire Stack?", "Apply LUT to all "+stack.size()+" stack slices?"); + if (d.cancelPressed()) + {imp.unlock(); return;} + if (d.yesPressed()) { + if (imp.getStack().isVirtual()) { + imp.unlock(); + IJ.error("\"Apply\" does not work with virtual stacks. Use\nImage>Duplicate to convert to a normal stack."); + return; + } + int current = imp.getCurrentSlice(); + ImageProcessor mask = imp.getMask(); + for (int i=1; i<=imp.getStackSize(); i++) { + imp.setSlice(i); + ip = imp.getProcessor(); + if (mask!=null) ip.snapshot(); + ip.applyTable(table); + ip.reset(mask); + } + imp.setSlice(current); + option = "stack"; + } else { + ip.snapshot(); + ip.applyTable(table); + ip.reset(ip.getMask()); + option = "slice"; + } + } else { + ip.snapshot(); + ip.applyTable(table); + ip.reset(ip.getMask()); + } + reset(imp, ip); + imp.changes = true; + imp.unlock(); + if (Recorder.record) { + if (Recorder.scriptMode()) { + if (option==null) option = ""; + Recorder.recordCall("IJ.run(imp, \"Apply LUT\", \""+option+"\");"); + } else { + if (option!=null) + Recorder.record("run", "Apply LUT", option); + else + Recorder.record("run", "Apply LUT"); + } + } + } + + void applyRGB(ImagePlus imp, ImageProcessor ip) { + recordSetMinAndMax(ip.getMin(), ip.getMax()); + ip.snapshot(); + ip.setMinAndMax(0, 255); + reset(imp, ip); + /* + double min = imp.getDisplayRangeMin(); + double max = imp.getDisplayRangeMax(); + ip.setRoi(imp.getRoi()); + ip.reset(); + if (channels!=7) + ((ColorProcessor)ip).setMinAndMax(min, max, channels); + else + ip.setMinAndMax(min, max); + ip.reset(ip.getMask()); + imp.changes = true; + previousImageID = 0; + ((ColorProcessor)ip).caSnapshot(false); + setup(); + if (Recorder.record) { + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.run(imp, \"Apply LUT\", \"\");"); + else + Recorder.record("run", "Apply LUT"); + } + */ + } + + private void applyRGBStack(ImagePlus imp) { + double min = imp.getDisplayRangeMin(); + double max = imp.getDisplayRangeMax(); + if (IJ.debugMode) IJ.log("applyRGBStack: "+min+"-"+max); + int current = imp.getCurrentSlice(); + int n = imp.getStackSize(); + if (!IJ.showMessageWithCancel("Update Entire Stack?", + "Apply brightness and contrast settings\n"+ + "to all "+n+" slices in the stack?\n \n"+ + "NOTE: There is no Undo for this operation.")) + return; + ImageProcessor mask = imp.getMask(); + Rectangle roi = imp.getRoi()!=null?imp.getRoi().getBounds():null; + ImageStack stack = imp.getStack(); + for (int i=1; i<=n; i++) { + IJ.showProgress(i, n); + IJ.showStatus(i+"/"+n); + if (i!=current) { + ImageProcessor ip = stack.getProcessor(i); + ip.setRoi(roi); + if (mask!=null) ip.snapshot(); + if (channels!=7) + ((ColorProcessor)ip).setMinAndMax(min, max, channels); + else + ip.setMinAndMax(min, max); + if (mask!=null) ip.reset(mask); + } + } + imp.setStack(null, stack); + imp.setSlice(current); + imp.changes = true; + previousImageID = 0; + setup(); + if (Recorder.record) { + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.run(imp, \"Apply LUT\", \"stack\");"); + else + Recorder.record("run", "Apply LUT", "stack"); + } + } + + void setThreshold(ImageProcessor ip) { + if (!(ip instanceof ByteProcessor)) + return; + if (((ByteProcessor)ip).isInvertedLut()) + ip.setThreshold(max, 255, ImageProcessor.NO_LUT_UPDATE); + else + ip.setThreshold(0, max, ImageProcessor.NO_LUT_UPDATE); + } + + void autoAdjust(ImagePlus imp, ImageProcessor ip) { + if (RGBImage) + ip.reset(); + ImageStatistics stats = imp.getRawStatistics(); + int limit = stats.pixelCount/10; + int[] histogram = stats.histogram; + if (autoThreshold<10) + autoThreshold = AUTO_THRESHOLD; + else + autoThreshold /= 2; + int threshold = stats.pixelCount/autoThreshold; + int i = -1; + boolean found = false; + int count; + do { + i++; + count = histogram[i]; + if (count>limit) count = 0; + found = count> threshold; + } while (!found && i<255); + int hmin = i; + i = 256; + do { + i--; + count = histogram[i]; + if (count>limit) count = 0; + found = count > threshold; + } while (!found && i>0); + int hmax = i; + Roi roi = imp.getRoi(); + if (hmax>=hmin) { + if (RGBImage) imp.deleteRoi(); + min = stats.histMin+hmin*stats.binSize; + max = stats.histMin+hmax*stats.binSize; + if (min==max) + {min=stats.min; max=stats.max;} + setMinAndMax(imp, min, max); + if (RGBImage && roi!=null) imp.setRoi(roi); + } else { + reset(imp, ip); + return; + } + updateScrollBars(null, false); + if (Recorder.record) { + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.run(imp, \"Enhance Contrast\", \"saturated=0.35\");"); + else + Recorder.record("run", "Enhance Contrast", "saturated=0.35"); + } + } + + void setMinAndMax(ImagePlus imp, ImageProcessor ip) { + min = imp.getDisplayRangeMin(); + max = imp.getDisplayRangeMax(); + Calibration cal = imp.getCalibration(); + //int digits = (ip instanceof FloatProcessor)||cal.calibrated()?2:0; + double minValue = cal.getCValue(min); + double maxValue = cal.getCValue(max); + int channels = imp.getNChannels(); + GenericDialog gd = new GenericDialog("Set Display Range"); + gd.addNumericField("Minimum displayed value: ", minValue, digits, 7, ""); + gd.addNumericField("Maximum displayed value: ", maxValue, digits, 7, ""); + gd.addChoice("Unsigned 16-bit range:", sixteenBitRanges, sixteenBitRanges[get16bitRangeIndex()]); + String label = "Propagate to all other "; + label = imp.isComposite()?label+channels+" channel images":label+"open images"; + gd.addCheckbox(label, false); + boolean allChannels = false; + if (imp.isComposite() && channels>1) { + label = "Propagate to the other "; + label = channels==2?label+"channel of this image":label+(channels-1)+" channels of this image"; + gd.addCheckbox(label, allChannels); + } + gd.showDialog(); + if (gd.wasCanceled()) + return; + minValue = gd.getNextNumber(); + maxValue = gd.getNextNumber(); + minValue = cal.getRawValue(minValue); + maxValue = cal.getRawValue(maxValue); + int rangeIndex = gd.getNextChoiceIndex(); + int range1 = ImagePlus.getDefault16bitRange(); + int range2 = set16bitRange(rangeIndex); + if (range1!=range2 && imp.getType()==ImagePlus.GRAY16 && !cal.isSigned16Bit()) { + reset(imp, ip); + minValue = imp.getDisplayRangeMin(); + maxValue = imp.getDisplayRangeMax(); + } + boolean propagate = gd.getNextBoolean(); + if (imp.isComposite() && channels>1) + allChannels = gd.getNextBoolean(); + if (maxValue>=minValue) { + min = minValue; + max = maxValue; + setMinAndMax(imp, min, max); + updateScrollBars(null, false); + if (RGBImage) doMasking(imp, ip); + if (allChannels) { + int channel = imp.getChannel(); + for (int c=1; c<=channels; c++) { + imp.setPositionWithoutUpdate(c, imp.getSlice(), imp.getFrame()); + imp.setDisplayRange(min, max); + //IJ.log("setDisplayRange: "+c+" "+min+" "+max); + } + ((CompositeImage)imp).reset(); + imp.setPosition(channel, imp.getSlice(), imp.getFrame()); + } + if (propagate) + propagate(imp); + if (Recorder.record) { + if (imp.getBitDepth()==32) + recordSetMinAndMax(min, max); + else { + int imin = (int)min; + int imax = (int)max; + if (cal.isSigned16Bit()) { + imin = (int)cal.getCValue(imin); + imax = (int)cal.getCValue(imax); + } + recordSetMinAndMax(imin, imax); + } + if (range2>0) { + if (Recorder.scriptMode()) + Recorder.recordCall("ImagePlus.setDefault16bitRange("+range2+");"); + else + Recorder.recordString("call(\"ij.ImagePlus.setDefault16bitRange\", "+range2+");\n"); + } + } + } + } + + private void propagate(ImagePlus img) { + if (img.getBitDepth()==24) { + GenericDialog gd = new GenericDialog("Contrast Adjuster"); + gd.addMessage( "Propagation of RGB images not supported. As a work-around,\nconvert images to multi-channel composite color."); + gd.hideCancelButton(); + gd.showDialog(); + return; + } + int[] list = WindowManager.getIDList(); + if (list==null) return; + int nImages = list.length; + if (nImages<=1) return; + ImageProcessor ip = img.getProcessor(); + double min = ip.getMin(); + double max = ip.getMax(); + int depth = img.getBitDepth(); + if (depth==24) return; + int id = img.getID(); + if (img.isComposite()) { + int nChannels = img.getNChannels(); + for (int i=0; i=minValue) { + min = minValue; + max = maxValue; + setMinAndMax(imp, minValue, maxValue); + updateScrollBars(null, false); + if (RGBImage) doMasking(imp, ip); + if (propagate) + propagate(imp); + if (Recorder.record) { + if (imp.getBitDepth()==32) + recordSetMinAndMax(min, max); + else { + int imin = (int)min; + int imax = (int)max; + if (cal.isSigned16Bit()) { + imin = (int)cal.getCValue(imin); + imax = (int)cal.getCValue(imax); + } + recordSetMinAndMax(imin, imax); + } + } + } + } + + public static void recordSetMinAndMax(double min, double max) { + if ((int)min==min && (int)max==max) { + int imin=(int)min, imax = (int)max; + if (Recorder.scriptMode()) + Recorder.recordCall("imp.setDisplayRange("+imin+", "+imax+");"); + else + Recorder.record("setMinAndMax", imin, imax); + } else { + if (Recorder.scriptMode()) + Recorder.recordCall("imp.setDisplayRange("+IJ.d2s(min,2)+", "+IJ.d2s(max,2)+");"); + else + Recorder.record("setMinAndMax", IJ.d2s(min,2), IJ.d2s(max,2)); + } + } + + static final int RESET=0, AUTO=1, SET=2, APPLY=3, THRESHOLD=4, MIN=5, MAX=6, + BRIGHTNESS=7, CONTRAST=8, UPDATE=9; + + // Separate thread that does the potentially time-consuming processing + public void run() { + while (!done) { + synchronized(this) { + try {wait();} + catch(InterruptedException e) {} + } + doUpdate(); + } + } + + void doUpdate() { + ImagePlus imp; + ImageProcessor ip; + int action; + int minvalue = minSliderValue; + int maxvalue = maxSliderValue; + int bvalue = brightnessValue; + int cvalue = contrastValue; + if (doReset) action = RESET; + else if (doAutoAdjust) action = AUTO; + else if (doSet) action = SET; + else if (doApplyLut) action = APPLY; + else if (minSliderValue>=0) action = MIN; + else if (maxSliderValue>=0) action = MAX; + else if (brightnessValue>=0) action = BRIGHTNESS; + else if (contrastValue>=0) action = CONTRAST; + else return; + minSliderValue = maxSliderValue = brightnessValue = contrastValue = -1; + doReset = doAutoAdjust = doSet = doApplyLut = false; + imp = WindowManager.getCurrentImage(); + if (imp==null) { + IJ.beep(); + IJ.showStatus("No image"); + return; + } else if (imp.getOverlay()!=null && imp.getOverlay().isCalibrationBar()) { + IJ.beep(); + IJ.showStatus("Has calibration bar"); + return; + } + ip = imp.getProcessor(); + if (RGBImage && !imp.lock()) + {imp=null; return;} + switch (action) { + case RESET: + reset(imp, ip); + if (Recorder.record) { + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.resetMinAndMax(imp);"); + else + Recorder.record("resetMinAndMax"); + } + break; + case AUTO: autoAdjust(imp, ip); break; + case SET: if (windowLevel) setWindowLevel(imp, ip); else setMinAndMax(imp, ip); break; + case APPLY: apply(imp, ip); break; + case MIN: adjustMin(imp, ip, minvalue); break; + case MAX: adjustMax(imp, ip, maxvalue); break; + case BRIGHTNESS: adjustBrightness(imp, ip, bvalue); break; + case CONTRAST: adjustContrast(imp, ip, cvalue); break; + } + updatePlot(); + updateLabels(imp); + if ((IJ.shiftKeyDown()||(balance&&channels==7)) && imp.isComposite()) + ((CompositeImage)imp).updateAllChannelsAndDraw(); + else + imp.updateChannelAndDraw(); + if (RGBImage) + imp.unlock(); + } + + /** Overrides close() in PlugInDialog. */ + public void close() { + super.close(); + instance = null; + done = true; + Prefs.saveLocation(LOC_KEY, getLocation()); + synchronized(this) { + notify(); + } + } + + public void windowActivated(WindowEvent e) { + super.windowActivated(e); + Window owin = e.getOppositeWindow(); + if (owin==null || !(owin instanceof ImageWindow)) + return; + if (IJ.debugMode) IJ.log("windowActivated: "+owin); + if (IJ.isMacro()) { + // do nothing if macro and RGB image + ImagePlus imp2 = WindowManager.getCurrentImage(); + if (imp2!=null && imp2.getBitDepth()==24) { + return; + } + } + previousImageID = 0; // user may have modified image + setup(); + WindowManager.setWindow(this); + } + + public synchronized void itemStateChanged(ItemEvent e) { + int index = choice.getSelectedIndex(); + channels = channelConstants[index]; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null && imp.isComposite()) { + if (index+1<=imp.getNChannels()) + imp.setPosition(index+1, imp.getSlice(), imp.getFrame()); + else { + choice.select(channelLabels.length-1); + channels = 7; + } + } else { + imp.getProcessor().snapshot(); + doReset = true; + } + notify(); + } + + /** Resets this ContrastAdjuster and brings it to the front. */ + public void updateAndDraw() { + previousImageID = 0; + toFront(); + } + + /** Updates the ContrastAdjuster. */ + public static void update() { + if (instance!=null) { + instance.previousImageID = 0; + instance.setup(); + } + } + +} // ContrastAdjuster class + + +class ContrastPlot extends Canvas implements MouseListener { + + static final int WIDTH=128, HEIGHT=64; + double defaultMin = 0; + double defaultMax = 255; + double min = 0; + double max = 255; + int[] histogram; + int hmax; + Image os; + Graphics osg; + Color color = Color.gray; + double scale = Prefs.getGuiScale(); + int width = WIDTH; + int height = HEIGHT; + + public ContrastPlot() { + addMouseListener(this); + if (scale>1.0) { + width = (int)(width*scale); + height = (int)(height*scale); + } + setSize(width+1, height+1); + } + + /** Overrides Component getPreferredSize(). Added to work + around a bug in Java 1.4.1 on Mac OS X.*/ + public Dimension getPreferredSize() { + return new Dimension(width+1, height+1); + } + + void setHistogram(ImageStatistics stats, Color color) { + this.color = color; + histogram = stats.histogram; + if (histogram.length!=256) { + histogram=null; + return; + } + int maxCount = 0; + int mode = 0; + for (int i=0; i<256; i++) { + if (histogram[i]>maxCount) { + maxCount = histogram[i]; + mode = i; + } + } + int maxCount2 = 0; + for (int i=0; i<256; i++) { + if ((histogram[i]>maxCount2) && (i!=mode)) + maxCount2 = histogram[i]; + } + hmax = stats.maxCount; + if ((hmax>(maxCount2*2)) && (maxCount2!=0)) { + hmax = (int)(maxCount2*1.5); + histogram[mode] = hmax; + } + os = null; + } + + public void update(Graphics g) { + paint(g); + } + + public void paint(Graphics g) { + int x1, y1, x2, y2; + double scale = (double)width/(defaultMax-defaultMin); + double slope = 0.0; + if (max!=min) + slope = height/(max-min); + if (min>=defaultMin) { + x1 = (int)(scale*(min-defaultMin)); + y1 = height; + } else { + x1 = 0; + if (max>min) + y1 = height-(int)((defaultMin-min)*slope); + else + y1 = height; + } + if (max<=defaultMax) { + x2 = (int)(scale*(max-defaultMin)); + y2 = 0; + } else { + x2 = width; + if (max>min) + y2 = height-(int)((defaultMax-min)*slope); + else + y2 = 0; + } + if (histogram!=null) { + if (os==null && hmax!=0) { + os = createImage(width,height); + osg = os.getGraphics(); + osg.setColor(Color.white); + osg.fillRect(0, 0, width, height); + osg.setColor(color); + double scale2 = width/256.0; + for (int i = 0; i < 256; i++) { + int x =(int)(i*scale2); + osg.drawLine(x, height, x, height - ((int)(height*histogram[i])/hmax)); + } + osg.dispose(); + } + if (os!=null) g.drawImage(os, 0, 0, this); + } else { + g.setColor(Color.white); + g.fillRect(0, 0, width, height); + } + g.setColor(Color.black); + g.drawLine(x1, y1, x2, y2); + g.drawLine(x2, height-5, x2, height); + g.drawRect(0, 0, width, height); + } + + public void mousePressed(MouseEvent e) {} + public void mouseReleased(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + public void mouseClicked(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + +} // ContrastPlot class + +class TrimmedLabel extends Label { + int trim = IJ.isMacOSX()?0:6; + + public TrimmedLabel(String title) { + super(title); + } + + public Dimension getMinimumSize() { + return new Dimension(super.getMinimumSize().width, super.getMinimumSize().height-trim); + } + + public Dimension getPreferredSize() { + return getMinimumSize(); + } + +} // TrimmedLabel class + + diff --git a/src/ij/plugin/frame/Editor.java b/src/ij/plugin/frame/Editor.java new file mode 100644 index 0000000..e7edf8e --- /dev/null +++ b/src/ij/plugin/frame/Editor.java @@ -0,0 +1,1773 @@ +package ij.plugin.frame; +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.util.*; +import java.awt.datatransfer.*; +import ij.*; +import ij.gui.*; +import ij.util.Tools; +import ij.text.*; +import ij.macro.*; +import ij.plugin.MacroInstaller; +import ij.plugin.Commands; +import ij.plugin.Macro_Runner; +import ij.plugin.JavaScriptEvaluator; +import ij.io.SaveDialog; + +/** This is a simple TextArea based editor for editing and compiling plugins. */ +public class Editor extends PlugInFrame implements ActionListener, ItemListener, + TextListener, KeyListener, MouseListener, ClipboardOwner, MacroConstants, Runnable, Debugger { + + /** ImportPackage statements added in front of scripts. Contains no + newlines so that lines numbers in error messages are not changed. */ + public static String JavaScriptIncludes = + "importPackage(Packages.ij);"+ + "importPackage(Packages.ij.gui);"+ + "importPackage(Packages.ij.process);"+ + "importPackage(Packages.ij.measure);"+ + "importPackage(Packages.ij.util);"+ + "importPackage(Packages.ij.macro);"+ + "importPackage(Packages.ij.plugin);"+ + "importPackage(Packages.ij.io);"+ + "importPackage(Packages.ij.plugin.filter);"+ + "importPackage(Packages.ij.plugin.frame);"+ + "importPackage(Packages.ij.plugin.tool);"+ + "importPackage(java.lang);"+ + "importPackage(java.awt);"+ + "importPackage(java.awt.image);"+ + "importPackage(java.awt.geom);"+ + "importPackage(java.util);"+ + "importPackage(java.io);"+ + "function print(s) {IJ.log(s);};"; + + private static String JS_EXAMPLES = + "img = IJ.openImage(\"http://imagej.net/images/blobs.gif\")\n" + +"img = IJ.createImage(\"Untitled\", \"16-bit ramp\", 500, 500, 1)\n" + +"img.show()\n" + +"ip = img.getProcessor()\n" + +"ip.getStats()\n" + +"IJ.setAutoThreshold(img, \"IsoData\")\n" + +"IJ.run(img, \"Analyze Particles...\", \"show=Overlay display clear\")\n" + +"ip.invert()\n" + +"ip.blurGaussian(5)\n" + +"ip.get(10,10)\n" + +"ip.set(10,10,222)\n" + +"(To run, move cursor to end of a line and press 'enter'.\n" + +"Visible images are automatically updated.)\n"; + + public static final int MAX_SIZE=28000, XINC=10, YINC=18; + public static final int MONOSPACED=1, MENU_BAR=2, RUN_BAR=4, INSTALL_BUTTON=8; + public static final int MACROS_MENU_ITEMS = 15; + public static final String INTERACTIVE_NAME = "Interactive Interpreter"; + static final String FONT_SIZE = "editor.font.size"; + static final String FONT_MONO= "editor.font.mono"; + static final String CASE_SENSITIVE= "editor.case-sensitive"; + static final String DEFAULT_DIR= "editor.dir"; + static final String INSERT_SPACES= "editor.spaces"; + static final String TAB_INC= "editor.tab-inc"; + private final static int MACRO=0, JAVASCRIPT=1, BEANSHELL=2, PYTHON=3; + private final static String[] languages = {"Macro", "JavaScript", "BeanShell", "Python"}; + private final static String[] extensions = {".ijm", ".js", ".bsh", ".py"}; + public static Editor currentMacroEditor; + private TextArea ta; + private String path; + protected boolean changes; + private static String searchString = ""; + private static boolean caseSensitive = Prefs.get(CASE_SENSITIVE, true); + private static int lineNumber = 1; + private static int xoffset, yoffset; + private static int nWindows; + private Menu fileMenu, editMenu; + private Properties p = new Properties(); + private int[] macroStarts; + private String[] macroNames; + private MenuBar mb; + private Menu macrosMenu; + private int nMacros; + private Program pgm; + private int eventCount; + private String shortcutsInUse; + private int inUseCount; + private MacroInstaller installer; + private static String defaultDir = Prefs.get(DEFAULT_DIR, null);; + private boolean dontShowWindow; + private int[] sizes = {9, 10, 11, 12, 13, 14, 16, 18, 20, 24, 36, 48, 60, 72}; + private int fontSizeIndex = (int)Prefs.get(FONT_SIZE, 6); // defaults to 16-point + private CheckboxMenuItem monospaced; + private static boolean wholeWords; + private boolean isMacroWindow; + private int debugStart, debugEnd; + private static TextWindow debugWindow; + private boolean step; + private int previousLine; + private static Editor instance; + private int runToLine; + private String downloadUrl; + private boolean downloading; + private FunctionFinder functionFinder; + private ArrayList undoBuffer = new ArrayList(); + private boolean performingUndo; + private boolean checkForCurlyQuotes; + private static int tabInc = (int)Prefs.get(TAB_INC, 3); + private static boolean insertSpaces = Prefs.get(INSERT_SPACES, false); + private CheckboxMenuItem insertSpacesItem; + private boolean interactiveMode; + private Interpreter interpreter; + private JavaScriptEvaluator evaluator; + private int messageCount; + private String rejectMacrosMsg; + private Button runButton, installButton; + private Choice language; + + public Editor() { + this(24, 80, 0, MENU_BAR); + } + + public Editor(String name) { + this(24, 80, 0, getOptions(name)); + } + + public Editor(int rows, int columns, int fontSize, int options) { + super("Editor"); + WindowManager.addWindow(this); + addMenuBar(options); + if ((options&RUN_BAR)!=0) { + Panel panel = new Panel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + runButton = new Button("Run"); + runButton.addActionListener(this); + panel.setFont(new Font("SansSerif", Font.PLAIN, sizes[fontSizeIndex])); + panel.add(runButton); + if ((options&INSTALL_BUTTON)!=0) { + installButton = new Button("Install"); + installButton.addActionListener(this); + panel.add(installButton); + } + language = new Choice(); + for (int i=0; i8*XINC) + {xoffset=0; yoffset=0;} + setLocation(left+xoffset, top+yoffset); + xoffset+=XINC; yoffset+=YINC; + nWindows++; + } + + void setWindowTitle(String title) { + Menus.updateWindowMenuItem(getTitle(), title); + setTitle(title); + } + + public void create(String name, String text) { + ta.append(text); + if (IJ.isMacOSX()) IJ.wait(25); // needed to get setCaretPosition() on OS X + ta.setCaretPosition(0); + setWindowTitle(name); + boolean macroExtension = name.endsWith(".txt") || name.endsWith(".ijm"); + if (macroExtension || name.endsWith(".js") || name.endsWith(".bsh") || name.endsWith(".py") || name.indexOf(".")==-1) { + macrosMenu = new Menu("Macros"); + macrosMenu.add(new MenuItem("Run Macro", new MenuShortcut(KeyEvent.VK_R))); + macrosMenu.add(new MenuItem("Evaluate Line", new MenuShortcut(KeyEvent.VK_Y))); + macrosMenu.add(new MenuItem("Abort Macro")); + macrosMenu.add(new MenuItem("Install Macros", new MenuShortcut(KeyEvent.VK_I))); + macrosMenu.add(new MenuItem("Macro Functions...", new MenuShortcut(KeyEvent.VK_M, true))); + macrosMenu.add(new MenuItem("Function Finder...", new MenuShortcut(KeyEvent.VK_F, true))); + macrosMenu.add(new MenuItem("Enter Interactive Mode")); + macrosMenu.add(new MenuItem("Assign to Repeat Cmd",new MenuShortcut(KeyEvent.VK_A, true))); + macrosMenu.addSeparator(); + macrosMenu.add(new MenuItem("Evaluate Macro")); + macrosMenu.add(new MenuItem("Evaluate JavaScript", new MenuShortcut(KeyEvent.VK_J, false))); + macrosMenu.add(new MenuItem("Evaluate BeanShell", new MenuShortcut(KeyEvent.VK_B, true))); + macrosMenu.add(new MenuItem("Evaluate Python", new MenuShortcut(KeyEvent.VK_P, false))); + macrosMenu.add(new MenuItem("Show Log Window", new MenuShortcut(KeyEvent.VK_L, true))); + macrosMenu.addSeparator(); + // MACROS_MENU_ITEMS must be updated if items are added to this menu + macrosMenu.addActionListener(this); + mb.add(macrosMenu); + if (!(name.endsWith(".js")||name.endsWith(".bsh")||name.endsWith(".py"))) { + Menu debugMenu = new Menu("Debug"); + debugMenu.add(new MenuItem("Debug Macro", new MenuShortcut(KeyEvent.VK_D))); + debugMenu.add(new MenuItem("Step", new MenuShortcut(KeyEvent.VK_E))); + debugMenu.add(new MenuItem("Trace", new MenuShortcut(KeyEvent.VK_T))); + debugMenu.add(new MenuItem("Fast Trace", new MenuShortcut(KeyEvent.VK_T,true))); + debugMenu.add(new MenuItem("Run")); + debugMenu.add(new MenuItem("Run to Insertion Point", new MenuShortcut(KeyEvent.VK_E, true))); + debugMenu.add(new MenuItem("Abort")); + debugMenu.addActionListener(this); + mb.add(debugMenu); + } + } else { + fileMenu.addSeparator(); + fileMenu.add(new MenuItem("Compile and Run", new MenuShortcut(KeyEvent.VK_R))); + } + if (language!=null) { + for (int i=0; i 0) + IJ.showMessage("", rejectMacrosMsg); + return; + } + String functions = Interpreter.getAdditionalFunctions(); + if (functions!=null && text!=null) { + if (!(text.endsWith("\n") || functions.startsWith("\n"))) + text = text + "\n" + functions; + else + text = text + functions; + } + installer = new MacroInstaller(); + installer.setFileName(getTitle()); + int nShortcuts = installer.install(text, macrosMenu); + if (installInPluginsMenu || nShortcuts>0) + installer.install(null); + dontShowWindow = installer.isAutoRunAndHide(); + currentMacroEditor = this; + } + + /** Opens a file and replaces the text (if any) by the contents of the file. */ + public void open(String dir, String name) { + path = dir+name; + File file = new File(path); + if (!file.exists()) { + IJ.error("File not found: "+path); + return; + } + try { + StringBuffer sb = new StringBuffer(5000); + BufferedReader r = new BufferedReader(new FileReader(file)); + while (true) { + String s=r.readLine(); + if (s==null) + break; + else + sb.append(s+"\n"); + } + r.close(); + if (ta!=null && ta.getText().length()>0) { + ta.setText(""); //delete previous contents (if any) + eventCount = 0; + } + create(name, new String(sb)); + changes = false; + } + catch (Exception e) { + IJ.handleException(e); + return; + } + } + + public String getText() { + if (ta==null) + return ""; + else + return ta.getText(); + } + + public TextArea getTextArea() { + return ta; + } + + public void display(String title, String text) { + ta.selectAll(); + ta.replaceRange(text, ta.getSelectionStart(), ta.getSelectionEnd()); + ta.setCaretPosition(0); + setWindowTitle(title); + changes = false; + if (IJ.getInstance()!=null) + show(); + WindowManager.setWindow(this); + } + + void save() { + if (path==null) { + saveAs(); + return; + } + File f = new File(path); + if (f.exists() && !f.canWrite()) { + IJ.showMessage("Editor", "Unable to save because file is write-protected. \n \n" + path); + return; + } + String text = ta.getText(); + char[] chars = new char[text.length()]; + text.getChars(0, text.length(), chars, 0); + try { + BufferedReader br = new BufferedReader(new CharArrayReader(chars)); + BufferedWriter bw = new BufferedWriter(new FileWriter(path)); + while (true) { + String s = br.readLine(); + if (s==null) break; + bw.write(s, 0, s.length()); + bw.newLine(); + } + bw.close(); + IJ.showStatus(text.length()+" chars saved to " + path); + changes = false; + } catch + (IOException e) {} + } + + void compileAndRun() { + if (path==null) + saveAs(); + if (path!=null) { + save(); + String text = ta.getText(); + if (text.contains("implements PlugInFilter") && text.contains("IJ.run(")) + IJ.log("<>"); + IJ.runPlugIn("ij.plugin.Compiler", path); + } + } + + final void runMacro(boolean debug) { + if (path!=null) + Macro_Runner.setFilePath(path); + if (getTitle().endsWith(".js")) + {evaluateJavaScript(); return;} + else if (getTitle().endsWith(".bsh")) + {evaluateScript(".bsh"); return;} + else if (getTitle().endsWith(".py")) + {evaluateScript(".py"); return;} + int start = ta.getSelectionStart(); + int end = ta.getSelectionEnd(); + String text; + if (start==end) + text = ta.getText(); + else + text = ta.getSelectedText(); + Interpreter.abort(); // abort any currently running macro + if (checkForCurlyQuotes && text.contains("\u201D")) { + // replace curly quotes with standard quotes + text = text.replaceAll("\u201C", "\""); + text = text.replaceAll("\u201D", "\""); + if (start==end) + ta.setText(text); + else { + String text2 = ta.getText(); + text2 = text2.replaceAll("\u201C", "\""); + text2 = text2.replaceAll("\u201D", "\""); + ta.setText(text2); + } + changes = true; + checkForCurlyQuotes = false; + } + currentMacroEditor = this; + new MacroRunner(text, debug?this:null); + } + + void evaluateMacro() { + String title = getTitle(); + if (title.endsWith(".js")||title.endsWith(".bsh")||title.endsWith(".py")) + setWindowTitle(title.substring(0,title.length()-3)+".ijm"); + runMacro(false); + } + + void evaluateJavaScript() { + if (!getTitle().endsWith(".js")) + setWindowTitle(SaveDialog.setExtension(getTitle(), ".js")); + int start = ta.getSelectionStart(); + int end = ta.getSelectionEnd(); + String text; + if (start==end) + text = ta.getText(); + else + text = ta.getSelectedText(); + if (text.equals("")) + return; + boolean strictMode = false; + if (IJ.isJava18()) { + // text.matches("^( |\t)*(\"use strict\"|'use strict')"); + String text40 = text.substring(0,Math.min(40,text.length())); + strictMode = text40.contains("'use strict'") || text40.contains("\"use strict\""); + } + text = getJSPrefix("") + text; + if (IJ.isJava18()) { + text = "load(\"nashorn:mozilla_compat.js\");" + text; + if (strictMode) + text = "'use strict';" + text; + } + if (!(IJ.isMacOSX()&&!IJ.is64Bit())) { + // Use JavaScript engine built into Java 6 and later. + IJ.runPlugIn("ij.plugin.JavaScriptEvaluator", text); + } else { + Object js = IJ.runPlugIn("JavaScript", text); + if (js==null) + download("/download/tools/JavaScript.jar"); + } + } + + public void evaluateScript(String ext) { + if (downloading) { + IJ.beep(); + IJ.showStatus("Download in progress"); + return; + } + if (ext.endsWith(".js")) { + evaluateJavaScript(); + return; + } + if (!getTitle().endsWith(ext)) + setWindowTitle(SaveDialog.setExtension(getTitle(), ext)); + int start = ta.getSelectionStart(); + int end = ta.getSelectionEnd(); + String text; + if (start==end) + text = ta.getText(); + else + text = ta.getSelectedText(); + if (text.equals("")) return; + String plugin, url; + if (ext.equals(".bsh")) { + plugin = "bsh"; + url = "/plugins/bsh/BeanShell.jar"; + } else { + // download Jython from http://imagej.nih.gov/ij/plugins/jython/ + plugin = "Jython"; + url = "/plugins/jython/Jython.jar"; + } + Object obj = IJ.runPlugIn(plugin, text); + if (obj==null) + download(url); + } + + private void download(String url) { + this.downloadUrl = url; + Thread thread = new Thread(this, "Downloader"); + thread.setPriority(Math.max(thread.getPriority()-2, Thread.MIN_PRIORITY)); + thread.start(); + } + + void evaluateLine() { + int start = ta.getSelectionStart(); + int end = ta.getSelectionEnd(); + if (end>start) + {runMacro(false); return;} + String text = ta.getText(); + while (start>0) { + start--; + if (text.charAt(start)=='\n') + {start++; break;} + } + while (end pageHeight) { + // New Page + pageNum++; + linesForThisPage = 0; + pg.dispose(); + pg = pjob.getGraphics(); + if (pg != null) + pg.setFont (helv); + curHeight = topMargin; + } + curHeight += fontHeight; + if (pg != null) { + pg.drawString (nextLine, leftMargin, curHeight - fontDescent); + linesForThisPage++; + linesForThisJob++; + } + } + } while (nextLine != null); + } catch (EOFException eof) { + // Fine, ignore + } catch (Throwable t) { // Anything else + IJ.handleException(t); + } + } + + String detabLine(String s) { + if (s.indexOf('\t')<0) + return s; + int tabSize = 4; + StringBuffer sb = new StringBuffer((int)(s.length()*1.25)); + char c; + for (int i=0; i1) { + undoBuffer.remove(undoBuffer.size()-1); + String text = (String)undoBuffer.get(undoBuffer.size()-1); + performingUndo = true; + ta.setText(text); + if (position<=text.length()) + ta.setCaretPosition(position-offset(position)); + if (IJ.debugMode) IJ.log("Undo2: "+undoBuffer.size()+" "+text); + } + } + + boolean copy() { + String s; + s = ta.getSelectedText(); + Clipboard clip = getToolkit().getSystemClipboard(); + if (clip!=null) { + StringSelection cont = new StringSelection(s); + clip.setContents(cont,this); + return true; + } else + return false; + } + + void cut() { + if (copy()) { + int start = ta.getSelectionStart(); + int end = ta.getSelectionEnd(); + ta.replaceRange("", start-offset(start), end-offset(end-2>=start?end-2:start)); + if (IJ.isMacOSX()) + ta.setCaretPosition(start); + } + } + + private void assignToRepeatCommand() { + String title = getTitle(); + if (!(title.endsWith(".ijm")||title.endsWith(".txt")||!title.contains("."))) { + IJ.error("Assign to Repeat Command", "One or more lines of macro code required."); + return; + } + int start = ta.getSelectionStart(); + int end = ta.getSelectionEnd(); + String text; + if (start==end) + text = ta.getText(); + else + text = ta.getSelectedText(); + Executer.setAsRepeatCommand(text); + } + + void paste() { + String s; + s = ta.getSelectedText(); + Clipboard clipboard = getToolkit( ). getSystemClipboard(); + Transferable clipData = clipboard.getContents(s); + try { + s = (String)(clipData.getTransferData(DataFlavor.stringFlavor)); + } catch (Exception e) { + s = e.toString( ); + } + int start = ta.getSelectionStart( ); + int end = ta.getSelectionEnd( ); + ta.replaceRange(s, start-offset(start), end-offset(end-2>=start?end-2:start)); + if (IJ.isMacOSX()) + ta.setCaretPosition(start+s.length()); + checkForCurlyQuotes = true; + } + + // workaround for TextArea.getCaretPosition() bug on Windows + private int offset(int pos) { + if (!IJ.isWindows()) + return 0; + String text = ta.getText(); + int rcount = 0; + for (int i=0; i<=pos; i++) { + if (text.charAt(i)=='\r') + rcount++; + } + if (IJ.debugMode) IJ.log("offset: "+pos+" "+rcount); + return pos-rcount>=0?rcount:0; + } + + void copyToInfo() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) { + IJ.noImage(); + return; + } + int start = ta.getSelectionStart(); + int end = ta.getSelectionEnd(); + String text; + if (start==end) + text = ta.getText(); + else + text = ta.getSelectedText(); + imp.setProperty("Info", text); + } + + public void actionPerformed(ActionEvent e) { + String what = e.getActionCommand(); + int flags = e.getModifiers(); + boolean altKeyDown = (flags & Event.ALT_MASK)!=0; + if (e.getSource()==runButton) { + runMacro(false); + return; + } else if (e.getSource()==installButton) { + String text = ta.getText(); + if (text.contains("macro \"") || text.contains("macro\"")) + installMacros(text, true); + else + IJ.error("Editor", "File must contain at least one macro or macro tool."); + return; + } + if ("Save".equals(what)) + save(); + else if ("Compile and Run".equals(what)) + compileAndRun(); + else if ("Run Macro".equals(what)) { + if (altKeyDown) { + enableDebugging(); + runMacro(true); + } else + runMacro(false); + } else if ("Debug Macro".equals(what)) { + enableDebugging(); + runMacro(true); + } else if ("Step".equals(what)) + setDebugMode(STEP); + else if ("Trace".equals(what)) + setDebugMode(TRACE); + else if ("Fast Trace".equals(what)) + setDebugMode(FAST_TRACE); + else if ("Run".equals(what)) + setDebugMode(RUN_TO_COMPLETION); + else if ("Run to Insertion Point".equals(what)) + runToInsertionPoint(); + else if ("Abort".equals(what) || "Abort Macro".equals(what)) { + Interpreter.abort(); + IJ.beep(); + } else if ("Evaluate Line".equals(what)) + evaluateLine(); + else if ("Install Macros".equals(what)) + installMacros(ta.getText(), true); + else if ("Macro Functions...".equals(what)) + showMacroFunctions(); + else if ("Function Finder...".equals(what)) + functionFinder = new FunctionFinder(this); + else if ("Evaluate Macro".equals(what)) + evaluateMacro(); + else if ("Evaluate JavaScript".equals(what)) + evaluateJavaScript(); + else if ("Evaluate BeanShell".equals(what)) + evaluateScript(".bsh"); + else if ("Evaluate Python".equals(what)) + evaluateScript(".py"); + else if ("Show Log Window".equals(what)) + showLogWindow(); + else if ("Revert".equals(what)) + revert(); + else if ("Print...".equals(what)) + print(); + else if (what.startsWith("Undo")) + undo(); + else if (what.startsWith("Paste")) + paste(); + else if (what.equals("Copy to Image Info")) + copyToInfo(); + else if (what.startsWith("Copy")) + copy(); + else if (what.startsWith("Cut")) + cut(); + else if ("Save As...".equals(what)) + saveAs(); + else if ("Select All".equals(what)) + selectAll(); + else if ("Find...".equals(what)) + find(null); + else if ("Find Next".equals(what)) + find(searchString); + else if ("Go to Line...".equals(what)) + gotoLine(); + else if ("Balance".equals(what)) + balance(); + else if ("Detab...".equals(what)) + detab(); + else if ("Zap Gremlins".equals(what)) + zapGremlins(); + else if ("Make Text Larger".equals(what)) + changeFontSize(true); + else if ("Make Text Smaller".equals(what)) + changeFontSize(false); + else if ("Save Settings".equals(what)) + saveSettings(); + else if ("New...".equals(what)) + IJ.run("Text Window"); + else if ("Open...".equals(what)) + IJ.open(); + else if (what.equals("Enter Interactive Mode")) + enterInteractiveMode(); + else if (what.equals("Assign to Repeat Cmd")) + assignToRepeatCommand(); + else if (what.endsWith(".ijm") || what.endsWith(".java") || what.endsWith(".js") || what.endsWith(".bsh") || what.endsWith(".py")) + openExample(what); + else { + if (altKeyDown) { + enableDebugging(); + installer.runMacro(what, this); + } else + installer.runMacro(what, null); + } + } + + /** Opens an example from the Help/Examples menu + and runs if "Autorun Exampes" is checked. */ + public static boolean openExample(String name) { + boolean isJava = name.endsWith(".java"); + boolean isJavaScript = name.endsWith(".js"); + boolean isBeanShell = name.endsWith(".bsh"); + boolean isPython = name.endsWith(".py"); + boolean isMacro = name.endsWith(".ijm"); + if (!(isMacro||isJava||isJavaScript||isBeanShell||isPython)) + return false; + boolean run = !isJava && !name.contains("_Tool") && Prefs.autoRunExamples; + int rows = 24; + int columns = 70; + int options = MENU_BAR + RUN_BAR; + if (isMacro) + options += INSTALL_BUTTON; + String text = null; + Editor ed = new Editor(rows, columns, 0, options); + String dir = "Macro/"; + if (isJava) + dir = "Java/"; + else if (isJavaScript) + dir = "JavaScript/"; + else if (isBeanShell) + dir = "BeanShell/"; + else if (isPython) + dir = "Python/"; + String url = "http://wsr.imagej.net/download/Examples/"+dir+name; + text = IJ.openUrlAsString(url); + if (text.startsWith("0) + url += "#" +selectedWords[0];//append selection as hash tag + IJ.runPlugIn("ij.plugin.BrowserLauncher", IJ.URL+url); + } + + final void runToInsertionPoint() { + Interpreter interp = Interpreter.getInstance(); + if (interp==null) + IJ.beep(); + else { + runToLine = getCurrentLine(); + setDebugMode(RUN_TO_CARET); + } + } + + final int getCurrentLine() { + int pos = ta.getCaretPosition(); + int currentLine = 0; + String text = ta.getText(); + if (IJ.isWindows()) + text = text.replaceAll("\r\n", "\n"); + char[] chars = new char[text.length()]; + chars = text.toCharArray(); + int count=0; + int start=0, end=0; + int len = chars.length; + for (int i=0; i=start && posend) + currentLine = count; + return currentLine; + } + + final void enableDebugging() { + step = true; + int start = ta.getSelectionStart(); + int end = ta.getSelectionEnd(); + if (start==debugStart && end==debugEnd) + ta.select(start, start); + } + + final void setDebugMode(int mode) { + step = true; + Interpreter interp = Interpreter.getInstance(); + if (interp!=null) { + if (interp.getDebugger()==null) + fixLineEndings(); + interp.setDebugger(this); + interp.setDebugMode(mode); + } + } + + public void textValueChanged(TextEvent e) { + String text = ta.getText(); + //if (undo2==null || text.length()!=undo2.length()+1 || text.charAt(text.length()-1)=='\n') + int length = 0; + if (!performingUndo) { + for (int i=0; i2 || !IJ.isMacOSX() && eventCount>1) + changes = true; + if (IJ.isMacOSX()) // screen update bug work around + ta.setCaretPosition(ta.getCaretPosition()); + } + + public void keyPressed(KeyEvent e) { } + public void mousePressed (MouseEvent e) {} + public void mouseExited (MouseEvent e) {} + public void mouseEntered (MouseEvent e) {} + + public void mouseReleased (MouseEvent e) { + showLinePos(); + } + public void mouseClicked (MouseEvent e) { + showLinePos(); + } + + private void showLinePos() { // show line numbers in status bar (Norbert Vischer) + char[] chars = ta.getText().toCharArray(); + if(chars.length > 1e6) + return; + int selStart = ta.getSelectionStart(); + int selEnd = ta.getSelectionEnd(); + int line=0; + int startLine = 1; + int endLine = 1; + for (int i=1; i<=chars.length; i++) { + if (chars[i-1]=='\n') line++; + if (i==selStart) + startLine=line + 1; + if (i<=selEnd) + endLine=line + 1; + if (i>=selEnd) + break; + } + String msg = "Line " + startLine; + if (startLine != endLine) { + msg += "-" + endLine; + } + IJ.showStatus(msg); + } + + public void keyReleased(KeyEvent e) { + int pos = ta.getCaretPosition(); + showLinePos(); + if (insertSpaces && pos>0 && e.getKeyCode()==KeyEvent.VK_TAB) { + String spaces = " "; + for (int i=1; i=0; i--) { + if (i==0 || text.charAt(i)=='\n') { + pos1 = i; + break; + } + } + if (isScript) { + if (evaluator==null) { + interpreter = null; + evaluator = new JavaScriptEvaluator(); + } + } else { + if (interpreter==null) { + evaluator = null; + interpreter = new Interpreter(); + } + } + String code = text.substring(pos1,pos2+1); + if (code.length()==0 || code.equals("\n")) + return; + else if (code.length()<=6 && code.contains("help")) { + ta.appendText(" Type a statement (e.g., \"run('Invert')\") to run it.\n"); + ta.appendText(" Enter an expression (e.g., \"x/2\" or \"log(2)\") to evaluate it.\n"); + ta.appendText(" Move cursor to end of line and press 'enter' to repeat.\n"); + ta.appendText(" \"quit\" - exit interactive mode\n"); + ta.appendText(" "+(IJ.isMacOSX()?"cmd":"ctrl")+"+M - enter interactive mode\n"); + if (isScript) { + ta.appendText(" \"macro\" - switch language to macro\n"); + ta.appendText(" \"examples\" - show JavaScript examples\n"); + } else { + ta.appendText(" "+(IJ.isMacOSX()?"cmd":"ctrl")+"+shift+F - open Function Finder\n"); + ta.appendText(" \"js\" - switch language to JavaScript\n"); + } + } else if (isScript && code.length()==9 && code.contains("examples")) { + ta.appendText(JS_EXAMPLES); + } else if (code.length()<=3 && code.contains("js")) { + interactiveMode = false; + interpreter = null; + evaluator = null; + changeExtension(".js"); + enterInteractiveMode(); + } else if (code.length()<=6 && code.contains("macro")) { + interactiveMode = false; + interpreter = null; + evaluator = null; + changeExtension(".txt"); + enterInteractiveMode(); + } else if (code.length()<=6 && code.contains("quit")) { + interactiveMode = false; + interpreter = null; + evaluator = null; + ta.appendText("[Exiting interactive mode.]\n"); + } else if (isScript) { + boolean updateImage = code.contains("ip."); + code = "load(\"nashorn:mozilla_compat.js\");"+JavaScriptIncludes+code; + String rtn = evaluator.eval(code); + if (rtn!=null && rtn.length()>0) { + int index = rtn.indexOf("at line number "); + if (index>-1) + rtn = rtn.substring(0,index); + insertText(rtn); + } + if (updateImage && (rtn==null||rtn.length()==0)) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) + imp.updateAndDraw(); + } + } else if (!code.startsWith("[Macro ")) { + String rtn = interpreter.eval(code); + if (rtn!=null) + insertText(rtn); + } + } + + private void changeExtension(String ext) { + String title = getTitle(); + int index = title.indexOf("."); + if (index>-1) + title = title.substring(0,index); + setTitle(title+ext); + } + + private void enterInteractiveMode() { + if (interactiveMode) + return; + String title = getTitle(); + if (ta!=null && ta.getText().length()>400 && !(title.startsWith("Untitled")||title.startsWith(INTERACTIVE_NAME))) { + GenericDialog gd = new GenericDialog("Enter Interactive Mode"); + gd.addMessage("Enter mode that supports interactive\nediting and running of macros and scripts?"); + gd.setOKLabel("Enter"); + gd.showDialog(); + if (gd.wasCanceled()) + return; + } + String language = title.endsWith(".js")?"JavaScript ":"Macro "; + messageCount++; + String help = messageCount<=2?" Type \"help\" for info.":""; + ta.appendText("["+language+"interactive mode."+help+"]\n"); + interactiveMode = true; + } + + public void insertText(String text) { + if (ta==null) return; + int start = ta.getSelectionStart( ); + ta.replaceRange(" "+text+"\n", start, start); + } + + public void keyTyped(KeyEvent e) { + } + + public void itemStateChanged(ItemEvent e) { + String cmd = e.getItem().toString(); + if (e.getSource()==language) { + setExtension(cmd); + return; + } + CheckboxMenuItem item = (CheckboxMenuItem)e.getSource(); + if ("Tab Key Inserts Spaces".equals(cmd)) { + insertSpaces = e.getStateChange()==1; + Prefs.set(INSERT_SPACES, insertSpaces); + } else + setFont(); + } + + private void setExtension(String language) { + String title = getTitle(); + int dot = title.lastIndexOf("."); + if (dot>=0) + title = title.substring(0, dot); + for (int i=0; i=text.length()-1) + {index=-1; break;} + } + } else + index = text.indexOf(s, ta.getCaretPosition()+1); + searchString = s2; + if (index<0) + {IJ.beep(); return;} + ta.setSelectionStart(index); + ta.setSelectionEnd(index+s.length()); + } + + boolean isWholeWordMatch(String text, String word, int index) { + char c = index==0?' ':text.charAt(index-1); + if (Character.isLetterOrDigit(c) || c=='_') return false; + c = index+word.length()>=text.length()?' ':text.charAt(index+word.length()); + if (Character.isLetterOrDigit(c) || c=='_') return false; + return true; + } + + void gotoLine() { + GenericDialog gd = new GenericDialog("Go to Line", this); + gd.addNumericField("Go to line number: ", lineNumber, 0); + gd.showDialog(); + if (gd.wasCanceled()) + return; + int n = (int)gd.getNextNumber(); + if (n<1) return; + String text = ta.getText(); + char[] chars = new char[text.length()]; + chars = text.toCharArray(); + int count=1, loc=0; + for (int i=0; i= 0; i--) { + char ch = chars[i]; + if ("({[]})".indexOf(ch) >= 0) { + leftBows = ch + leftBows; + leftBows = leftBows.replace("[]", "");//skip nested pairs + leftBows = leftBows.replace("()", ""); + leftBows = leftBows.replace("{}", ""); + if (leftBows.equals ("[") || leftBows.equals ("{") || leftBows.equals ("(")) { + start = i; + break; + } + } + } + String rightBows = ""; + for (int i = position ; i < chars.length; i++) { + char ch = chars[i]; + if ("({[]})".indexOf(ch) >= 0) { + rightBows += ch; + rightBows = rightBows.replace("[]", "");//skip nested pairs + rightBows = rightBows.replace("()", ""); + rightBows = rightBows.replace("{}", ""); + String pair = leftBows + rightBows; + if (pair.equals("[]") || pair.equals("{}") || pair.equals("()")) { + stop = i; + break; + } + } + } + if (start == -1 || stop == -1) { + IJ.beep(); + return; + } + ta.setSelectionStart(start); + ta.setSelectionEnd(stop + 1); + IJ.showStatus(chars.length + " " + position + " " + start + " " + stop); + } + + // replaces contents of comments with blanks + private void maskComments(char[] chars) { + int n = chars.length; + boolean inSlashSlashComment = false; + boolean inSlashStarComment = false; + for (int i=0; i 0 && chars[i - 1] == '\\'; + if (chars[i] == '\n') + inQuotes = false; + if (chars[i] == quote && !escaped) { + if (!inQuotes) { + startMask = i; + inQuotes = true; + } else { + stopMask = i; + for (int jj = startMask; jj <= stopMask; jj++) { + chars[jj] = ' '; + } + inQuotes = false; + } + } + } + } + } + + //replaces contents of comments with blanks + private void rmaskComments(char[] chars) { + int n = chars.length; + boolean inSlashSlashComment = false; + boolean inSlashStarComment = false; + for (int i=0; i127)) { + count++; + chars[i] = ' '; + } + } + if (count>0) { + text = new String(chars); + ta.setText(text); + } + if (count>0) + IJ.showMessage("Zap Gremlins", count+" invalid characters converted to spaces"); + else + IJ.showMessage("Zap Gremlins", "No invalid characters found"); + } + + + private void detab() { + GenericDialog gd = new GenericDialog("Detab", this); + gd.addNumericField("Spaces per tab: ", tabInc, 0); + gd.addCheckbox("Tab key inserts spaces: ", insertSpaces); + gd.showDialog(); + if (gd.wasCanceled()) + return; + int tabInc2 = tabInc; + tabInc = (int)gd.getNextNumber(); + if (tabInc<1) tabInc=1; + if (tabInc>8) tabInc=8; + if (tabInc!=tabInc2) + Prefs.set(TAB_INC, tabInc); + boolean insertSpaces2 = insertSpaces; + insertSpaces = gd.getNextBoolean(); + if (insertSpaces!=insertSpaces2) { + Prefs.set(INSERT_SPACES, insertSpaces); + insertSpacesItem.setState(insertSpaces); + } + int nb = 0; + int pos = 1; + String text = ta.getText(); + if (text.indexOf('\t')<0) + return; + char[] chars = new char[text.length()]; + chars = text.toCharArray(); + StringBuffer sb = new StringBuffer((int)(chars.length*1.25)); + for (int i=0; i0) { + sb.append(' '); + ++pos; + --nb; + } + } else if (c=='\n') { + sb.append(c); + pos = 1; + } else { + sb.append(c); + ++pos; + } + } + ta.setText(sb.toString()); + } + + void selectAll() { + ta.selectAll(); + showLinePos(); + } + + void changeFontSize(boolean larger) { + int in = fontSizeIndex; + if (larger) { + fontSizeIndex++; + if (fontSizeIndex==sizes.length) + fontSizeIndex = sizes.length-1; + } else { + fontSizeIndex--; + if (fontSizeIndex<0) + fontSizeIndex = 0; + } + IJ.showStatus(sizes[fontSizeIndex]+" point"); + setFont(); + } + + void saveSettings() { + Prefs.set(FONT_SIZE, fontSizeIndex); + Prefs.set(FONT_MONO, monospaced.getState()); + IJ.showStatus("Font settings saved (size="+sizes[fontSizeIndex]+", monospaced="+monospaced.getState()+")"); + } + + void setFont() { + ta.setFont(new Font(getFontName(), Font.PLAIN, sizes[fontSizeIndex])); + } + + String getFontName() { + return monospaced.getState()?"Monospaced":"SansSerif"; + } + + public void setFont(Font font) { + ta.setFont(font); + } + + public int getFontSize() { + return sizes[fontSizeIndex]; + } + + public void append(String s) { + ta.append(s); + } + + public void setIsMacroWindow(boolean mw) { + isMacroWindow = mw; + } + + public static void setDefaultDirectory(String dir) { + dir = IJ.addSeparator(dir); + defaultDir = dir; + } + + public void lostOwnership (Clipboard clip, Transferable cont) {} + + public int debug(Interpreter interp, int mode) { + if (IJ.debugMode) + IJ.log("debug: "+interp.getLineNumber()+" "+mode+" "+interp); + if (mode==RUN_TO_COMPLETION) + return 0; + int n = interp.getLineNumber(); + if (mode==RUN_TO_CARET) { + if (n==runToLine) { + mode = STEP; + interp.setDebugMode(mode); + } else + return 0; + } + if (!isVisible()) { // abort macro if user closes window + interp.abortMacro(); + return 0; + } + if (n==previousLine) { + previousLine=0; + return 0; + } + Window win = WindowManager.getActiveWindow(); + if (win!=this) + IJ.wait(50); + toFront(); + previousLine = n; + String text = ta.getText(); + if (IJ.isWindows()) + text = text.replaceAll("\r\n", "\n"); + char[] chars = new char[text.length()]; + chars = text.toCharArray(); + int count=1; + debugStart=0; + int len = chars.length; + debugEnd = len; + for (int i=0; i100)) + break; + textArea.append(s+"\n"); + } + r.close(); + } + catch (Exception e) { + IJ.error(e.getMessage()); + return; + } + } + + public void itemStateChanged(ItemEvent e) { + fitTypeStr = fit.getSelectedItem(); + } + + public void actionPerformed(ActionEvent e) { + if (e.getSource() instanceof MenuItem) { + String cmd = e.getActionCommand(); + if (cmd==null) return; + if (cmd.equals("Cut")) + cut(); + else if (cmd.equals("Copy")) + copy(); + else if (cmd.equals("Paste")) + paste(); + return; + } + try { + if (e.getSource()==doIt) { + final int fitType = CurveFitter.getFitCode(fit.getSelectedItem()); + Thread thread = new Thread( + new Runnable() { + final public void run() { + doFit(fitType); + } + }, "CurveFitting" + ); + thread.setPriority(Thread.currentThread().getPriority()); + thread.start(); + } else if (e.getSource()==apply) + applyFunction(); + else + open(); + } catch (Exception ex) {IJ.log(""+ex);} + } + + String zapGremlins(String text) { + char[] chars = new char[text.length()]; + chars = text.toCharArray(); + int count=0; + for (int i=0; i127)) { + count++; + chars[i] = ' '; + } + } + if (count>0) + return new String(chars); + else + return text; + } + + public void keyTyped (KeyEvent e) {} + public void keyReleased (KeyEvent e) {} + public void keyPressed (KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) + IJ.getInstance().keyPressed(e); + } + + private boolean copy() { + String s = textArea.getSelectedText(); + Clipboard clip = getToolkit().getSystemClipboard(); + if (clip!=null) { + StringSelection cont = new StringSelection(s); + clip.setContents(cont,this); + return true; + } else + return false; + } + + + private void cut() { + if (copy()) { + int start = textArea.getSelectionStart(); + int end = textArea.getSelectionEnd(); + textArea.replaceRange("", start, end); + } + } + + private void paste() { + String s; + s = textArea.getSelectedText(); + Clipboard clipboard = getToolkit( ). getSystemClipboard(); + Transferable clipData = clipboard.getContents(s); + try { + s = (String)(clipData.getTransferData(DataFlavor.stringFlavor)); + } catch (Exception e) { + s = e.toString( ); + } + int start = textArea.getSelectionStart( ); + int end = textArea.getSelectionEnd( ); + textArea.replaceRange(s, start, end); + if (IJ.isMacOSX()) + textArea.setCaretPosition(start+s.length()); + } + + public void lostOwnership (Clipboard clip, Transferable cont) {} + +} \ No newline at end of file diff --git a/src/ij/plugin/frame/LineWidthAdjuster.java b/src/ij/plugin/frame/LineWidthAdjuster.java new file mode 100644 index 0000000..d0a720b --- /dev/null +++ b/src/ij/plugin/frame/LineWidthAdjuster.java @@ -0,0 +1,204 @@ +package ij.plugin.frame; +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; +import ij.*; +import ij.plugin.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.*; +import ij.plugin.frame.Recorder; +import ij.util.Tools; + +/** Adjusts the width of line selections. */ +public class LineWidthAdjuster extends PlugInFrame implements PlugIn, + Runnable, AdjustmentListener, TextListener, ItemListener { + + public static final String LOC_KEY = "line.loc"; + int sliderRange = 300; + Scrollbar slider; + int value; + boolean setText; + static LineWidthAdjuster instance; + Thread thread; + boolean done; + TextField tf; + Checkbox checkbox; + + public LineWidthAdjuster() { + super("Line Width"); + if (instance!=null) { + WindowManager.toFront(instance); + return; + } + WindowManager.addWindow(this); + instance = this; + slider = new Scrollbar(Scrollbar.HORIZONTAL, Line.getWidth(), 1, 1, sliderRange+1); + GUI.fixScrollbar(slider); + slider.setFocusable(false); // prevents blinking on Windows + + Panel panel = new Panel(); + int margin = IJ.isMacOSX()?5:0; + GridBagLayout grid = new GridBagLayout(); + GridBagConstraints c = new GridBagConstraints(); + panel.setLayout(grid); + c.gridx = 0; c.gridy = 0; + c.gridwidth = 1; + c.ipadx = 100; + c.insets = new Insets(margin, 15, margin, 5); + c.anchor = GridBagConstraints.CENTER; + grid.setConstraints(slider, c); + panel.add(slider); + c.ipadx = 0; // reset + c.gridx = 1; + c.insets = new Insets(margin, 5, margin, 15); + tf = new TextField(""+Line.getWidth(), 4); + tf.addTextListener(this); + grid.setConstraints(tf, c); + panel.add(tf); + + c.gridx = 2; + c.insets = new Insets(margin, 25, margin, 5); + checkbox = new Checkbox("Spline fit", isSplineFit()); + checkbox.addItemListener(this); + panel.add(checkbox); + + add(panel, BorderLayout.CENTER); + slider.addAdjustmentListener(this); + slider.setUnitIncrement(1); + + GUI.scale(this); + pack(); + Point loc = Prefs.getLocation(LOC_KEY); + if (loc!=null) + setLocation(loc); + else + GUI.centerOnImageJScreen(this); + setResizable(false); + show(); + thread = new Thread(this, "LineWidthAdjuster"); + thread.start(); + setup(); + addKeyListener(IJ.getInstance()); + } + + public synchronized void adjustmentValueChanged(AdjustmentEvent e) { + value = slider.getValue(); + setText = true; + notify(); + } + + public synchronized void textValueChanged(TextEvent e) { + int width = (int)Tools.parseDouble(tf.getText(), -1); + //IJ.log(""+width); + if (width==-1) return; + if (width<0) width=1; + if (width!=Line.getWidth()) { + slider.setValue(width); + value = width; + notify(); + } + } + void setup() { + } + + // Separate thread that does the potentially time-consuming processing + public void run() { + while (!done) { + synchronized(this) { + try {wait();} + catch(InterruptedException e) {} + if (done) return; + } + if (setText) tf.setText(""+value); + setText = false; + Line.setWidth(value); + updateRoi(); + } + } + + private static void updateRoi() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + Roi roi = imp.getRoi(); + if (roi!=null && roi.isLine()) { + roi.updateWideLine(Line.getWidth()); + imp.draw(); + return; + } + } + Roi previousRoi = Roi.getPreviousRoi(); + if (previousRoi==null) return; + int id = previousRoi.getImageID(); + if (id>=0) return; + imp = WindowManager.getImage(id); + if (imp==null) return; + Roi roi = imp.getRoi(); + if (roi!=null && roi.isLine()) { + roi.updateWideLine(Line.getWidth()); + imp.draw(); + } + } + + boolean isSplineFit() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) return false; + Roi roi = imp.getRoi(); + if (roi==null) return false; + if (!(roi instanceof PolygonRoi)) return false; + return ((PolygonRoi)roi).isSplineFit(); + } + + /** Overrides close() in PlugInFrame. */ + public void close() { + super.close(); + instance = null; + done = true; + Prefs.saveLocation(LOC_KEY, getLocation()); + synchronized(this) {notify();} + } + + public void windowActivated(WindowEvent e) { + super.windowActivated(e); + checkbox.setState(isSplineFit()); + } + + public void itemStateChanged(ItemEvent e) { + boolean selected = e.getStateChange()==ItemEvent.SELECTED; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) + {checkbox.setState(false); return;}; + Roi roi = imp.getRoi(); + int type = roi!=null ? roi.getType() : -1; + + if (roi==null || !(roi instanceof PolygonRoi) || type==Roi.FREEROI || type==Roi.FREELINE || type==Roi.ANGLE) { + checkbox.setState(false); + return; + }; + PolygonRoi poly = (PolygonRoi)roi; + boolean splineFit = poly.isSplineFit(); + if (selected && !splineFit) { + poly.fitSpline(); //this must not call roi.notifyListeners (live plot would trigger it continuously) + Prefs.splineFitLines = true; + imp.draw(); + roi.notifyListeners(RoiListener.MODIFIED); + } else if (!selected && splineFit) { + poly.removeSplineFit(); + Prefs.splineFitLines = false; + imp.draw(); + roi.notifyListeners(RoiListener.MODIFIED); + } + } + + public static void update() { + if (instance==null) return; + instance.checkbox.setState(instance.isSplineFit()); + int sliderWidth = instance.slider.getValue(); + int lineWidth = Line.getWidth(); + if (lineWidth!=sliderWidth && lineWidth<=200) { + instance.slider.setValue(lineWidth); + instance.tf.setText(""+lineWidth); + } + } + +} diff --git a/src/ij/plugin/frame/MemoryMonitor.java b/src/ij/plugin/frame/MemoryMonitor.java new file mode 100644 index 0000000..ab53a4a --- /dev/null +++ b/src/ij/plugin/frame/MemoryMonitor.java @@ -0,0 +1,134 @@ +package ij.plugin.frame; +import ij.*; +import ij.gui.*; +import ij.process.*; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; + +/** This plugin continuously plots ImageJ's memory utilization. + Click on the plot to force the JVM to do garbage collection. */ +public class MemoryMonitor extends PlugInFrame { + private static final double scale = Prefs.getGuiScale(); + private static final int width = (int)(250*scale); + private static final int height = (int)(90*scale); + private static final String LOC_KEY = "memory.loc"; + private static MemoryMonitor instance; + private Image image; + private Graphics2D g; + private int frames; + private double[] mem; + private int index; + private long value; + private double defaultMax = 20*1024*1024; // 20MB + private double max = defaultMax; + private long maxMemory = IJ.maxMemory(); + private boolean done; + + public MemoryMonitor() { + super("Memory"); + if (instance!=null) { + WindowManager.toFront(instance); + return; + } + instance = this; + WindowManager.addWindow(this); + + setLayout(new BorderLayout()); + Canvas ic = new PlotCanvas(); + ic.setSize(width, height); + add(ic); + setResizable(false); + pack(); + Point loc = Prefs.getLocation(LOC_KEY); + if (loc!=null) + setLocation(loc); + else + GUI.centerOnImageJScreen(this); + image = createImage(width,height); + g = (Graphics2D)image.getGraphics(); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setColor(Color.white); + g.fillRect(0, 0, width, height); + g.setFont(new Font("SansSerif",Font.PLAIN,(int)(12*Prefs.getGuiScale()))); + show(); + ImageJ ij = IJ.getInstance(); + if (ij!=null) { + addKeyListener(ij); + ic.addKeyListener(ij); + ic.addMouseListener(ij); + } + mem = new double[width+1]; + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + while (!done) { + updatePlot(); + addText(); + ic.repaint(); + IJ.wait(50); + frames++; + } + } + + void addText() { + double value2 = (double)value/1048576L; + String s = IJ.d2s(value2,value2>50?0:2)+"MB"; + if (maxMemory>0L) { + double percent = value*100/maxMemory; + s += " ("+(percent<1.0?"<1":IJ.d2s(percent,0)) + "%)"; + } + g.drawString(s, 2, 15); + String images = ""+WindowManager.getImageCount(); + g.drawString(images, width-(5+images.length()*8), 15); + } + + void updatePlot() { + double used = IJ.currentMemory(); + if (frames%10==0) value = (long)used; + if (used>0.86*max) max *= 2.0; + mem[index++] = used; + if (index==mem.length) index = 0; + double maxmax = 0.0; + for (int i=0; imaxmax) maxmax= mem[i]; + } + if (maxmax '~' && c < 0xa0) { + sb.append('\\'); + String octal = Integer.toString(c,8); + while (octal.length()<3) + octal = '0' + octal; + sb.append(octal); + } else + sb.append(c); + } + return new String(sb); + } + + public static void record(String method, String arg) { + if (IJ.debugMode) IJ.log("record: "+method+" "+arg); + boolean sw = method.equals("selectWindow"); + if (textArea!=null && !(scriptMode&&sw||commandName!=null&&sw)) { + if (scriptMode && method.equals("roiManager")) + textArea.append("rm.runCommand(imp,\""+arg+"\");\n"); + else if (scriptMode && method.equals("run")) + textArea.append("IJ."+method+"(\""+arg+"\");\n"); + else { + if (method.equals("setTool")) + method = "//"+(scriptMode?"IJ.":"")+method; + textArea.append(method+"(\""+arg+"\");\n"); + } + } + } + + public static void record(String method, String arg1, String arg2) { + if (textArea==null) return; + if (arg1.equals("Open")||arg1.equals("Save")||method.equals("saveAs")) + arg2 = fixPath(arg2); + if (scriptMode&&method.equals("roiManager")) + textArea.append("rm.runCommand(\""+arg1+"\", \""+arg2+"\");\n"); + else { + if (scriptMode && method.equals("saveAs")) + method = "IJ." + method; + textArea.append(method+"(\""+arg1+"\", \""+arg2+"\");\n"); + } + } + + public static void record(String method, String arg1, String arg2, String arg3) { + if (textArea==null) return; + textArea.append(method+"(\""+arg1+"\", \""+arg2+"\",\""+arg3+"\");\n"); + } + + public static void record(String method, int a1) { + if (textArea==null) return; + textArea.append(method+"("+a1+");\n"); + } + + public static void record(String method, int a1, int a2) { + if (textArea==null) return; + textArea.append(method+"("+a1+", "+a2+");\n"); + } + + public static void record(String method, int a1, int a2, String a3) { + if (textArea==null) return; + textArea.append(method+"("+a1+", "+a2+", \""+a3+"\");\n"); + } + + public static void record(String method, double a1, double a2) { + if (textArea==null) return; + int places = Math.abs(a1)<0.0001||Math.abs(a2)<0.0001?9:4; + textArea.append(method+"("+IJ.d2s(a1,places)+", "+IJ.d2s(a2,places)+");\n"); + } + + public static void record(String method, int a1, int a2, int a3) { + if (textArea==null) return; + if (scriptMode&&method.endsWith("groundColor")) method = "IJ."+method; + textArea.append(method+"("+a1+", "+a2+", "+a3+");\n"); + } + + public static void record(String method, String a1, int a2) { + textArea.append(method+"(\""+a1+"\", "+a2+");\n"); + } + + public static void record(String method, String args, int a1, int a2) { + if (textArea==null) return; + textArea.append(method+"(\""+args+"\", "+a1+", "+a2+");\n"); + } + + public static void record(String method, int a1, int a2, int a3, int a4) { + if (textArea==null) return; + if (scriptMode&&method.startsWith("make")) { + if (method.equals("makeRectangle")) + recordString("imp.setRoi("+a1+","+a2+","+a3+","+a4+");\n"); + else if (method.equals("makeOval")) + recordString("imp.setRoi(new OvalRoi("+a1+","+a2+","+a3+","+a4+"));\n"); + else if (method.equals("makeLine")) + recordString("imp.setRoi(new Line("+a1+","+a2+","+a3+","+a4+"));\n"); + else if (method.equals("makeArrow")) + recordString("imp.setRoi(new Arrow("+a1+","+a2+","+a3+","+a4+"));\n"); + } else { + if (method.equals("makeArrow")) { + ImagePlus imp = WindowManager.getCurrentImage(); + Roi roi = imp!=null?imp.getRoi():null; + if (roi!=null && (roi instanceof Line)) { + Arrow arrow = (Arrow)roi; + String options = Arrow.styles[arrow.getStyle()]; + if (arrow.getOutline()) + options += " outline"; + if (arrow.getDoubleHeaded()) + options += " double"; + if (arrow.getHeadSize()<=5) + options += " small"; + else if (arrow.getHeadSize()>=15) + options += " large"; + options = options.toLowerCase(); + int strokeWidth = (int)arrow.getStrokeWidth(); + textArea.append(method+"("+a1+", "+a2+", "+a3+", "+a4+", \""+options+"\");\n"); + if (strokeWidth!=1) + textArea.append("Roi.setStrokeWidth("+strokeWidth+");\n"); + Color color = arrow.getStrokeColor(); + if (color!=null) + textArea.append("Roi.setStrokeColor(\""+Colors.colorToString(color)+"\");\n"); + return; + } + } + textArea.append(method+"("+a1+", "+a2+", "+a3+", "+a4+");\n"); + } + } + + public static void record(String method, int a1, int a2, int a3, int a4, int a5) { + textArea.append(method+"("+a1+", "+a2+", "+a3+", "+a4+", "+a5+");\n"); + } + + public static void record(String method, int a1, int a2, int a3, int a4, double a5) { + textArea.append(method+"("+a1+", "+a2+", "+a3+", "+a4+", "+IJ.d2s(a5,2)+");\n"); + } + + public static void record(String method, String path, String args, int a1, int a2, int a3, int a4, int a5) { + if (textArea==null) + return; + path = fixPath(path); + method = "//"+method; + textArea.append(method+"(\""+path+"\", "+"\""+args+"\", "+a1+", "+a2+", "+a3+", "+a4+", "+a5+");\n"); + } + + public static void recordString(String str) { + if (textArea!=null) + textArea.append(str); + } + + public static void disableCommandRecording() { + commandName = null; + } + + public static void recordCall(String call) { + recordCall(call, false); + } + + public static void recordCall(String call, boolean recordCommand) { + if (IJ.debugMode) IJ.log("recordCall: "+call+" "+commandName); + boolean isMacro = Thread.currentThread().getName().endsWith("Macro$") && !recordInMacros; + if (textArea!=null && scriptMode && !IJ.macroRunning() && !isMacro) { + if (javaMode() && call.startsWith("rm.setSelected")) { + call = call.replace("[", "new int[]{"); + call = call.replace("])", "})"); + } + if (javaMode() && call.startsWith("rt = ")) + call = "ResultTable " + call; + textArea.append(call+"\n"); + if (!recordCommand) + commandName = null; + } + } + + public static void recordCall(String className, String call) { + recordCall(javaMode()?className+" "+call:call); + } + + public static void recordRoi(Roi roi) { + if (roi==null) + return; + Polygon polygon = roi.getPolygon(); + recordRoi(polygon, roi.getType()); + } + + public static void recordRoi(Polygon p, int type) { + if (textArea==null) + return; + if (scriptMode) + {recordScriptRoi(p,type); return;} + if (type==Roi.ANGLE||type==Roi.POINT) { + String xarr = "newArray(", yarr="newArray("; + xarr += p.xpoints[0]+","; + yarr += p.ypoints[0]+","; + xarr += p.xpoints[1]+","; + yarr += p.ypoints[1]+","; + xarr += p.xpoints[2]+")"; + yarr += p.ypoints[2]+")"; + String typeStr= type==Roi.ANGLE?"angle":"point"; + textArea.append("makeSelection(\""+typeStr+"\","+xarr+","+yarr+");\n"); + } else { + String method = type>=Roi.LINE && type<=Roi.FREELINE?"makeLine":"makePolygon"; + StringBuffer args = new StringBuffer(); + for (int i=0; i-1) + key = key.substring(0,index); + index = key.indexOf(":"); + if (index>-1) + key = key.substring(0,index); + key = key.toLowerCase(Locale.US); + return key; + } + + /** Writes the current command and options to the Recorder window. */ + public static void saveCommand() { + String name = commandName; + ImagePlus imp = WindowManager.getCurrentImage(); + //IJ.log("saveCommand: "+name+" "+isSaveAs()+" "+scriptMode+" "+commandOptions); + if (name!=null) { + if (name.equals("Duplicate Image...")) + name = "Duplicate..."; + if (name.equals("Make Binary") && imp!=null && imp.getStackSize()==1) { + name = "Convert to Mask"; + commandOptions = null; + } + if (commandOptions==null && (name.equals("Fill")||name.equals("Clear")||name.equals("Draw"))) { + Roi roi = imp!=null?imp.getRoi():null; + if (!(roi!=null && (roi instanceof TextRoi) && (name.equals("Draw")))) + commandOptions = "slice"; + } + if (!fgColorSet && (name.equals("Fill")||name.equals("Draw"))) + setForegroundColor(Toolbar.getForegroundColor()); + else if (!bgColorSet && (name.equals("Clear")||name.equals("Clear Outside"))) + setBackgroundColor(Toolbar.getBackgroundColor()); + if (!bbSet && (name.equals("Convert to Mask")||name.equals("Erode") + ||name.equals("Dilate")||name.equals("Skeletonize"))) + setBlackBackground(); + if (name.equals("Add Shortcut by Name... ")) + name = "Add Shortcut... "; + if (commandOptions!=null) { + if (name.equals("Open...") || name.equals("URL...")) + recordOpen(strip(commandOptions)); + else if (name.equals("TIFF Virtual Stack...") && scriptMode) { + String s = "imp = IJ.openVirtual"; + String path = strip(commandOptions); + textArea.append(s+"(\""+path+"\");\n"); + } else if (isSaveAs()) { + if (name.endsWith("...")) + name= name.substring(0, name.length()-3); + if (name.equals("Save")) + name = "Tiff"; + String path = strip(commandOptions); + String s = scriptMode?"IJ.saveAs(imp, ":"saveAs("; + textArea.append(s+"\""+name+"\", \""+path+"\");\n"); + } else if (name.equals("Image...")) + appendNewImage(false); + else if (name.equals("Hyperstack...")||name.equals("New Hyperstack...")) + appendNewImage(true); + else if (name.equals("Set Slice...")) + textArea.append((scriptMode?"imp.":"")+"setSlice("+strip(commandOptions)+");\n"); + else if (name.equals("Rename...")) + textArea.append((scriptMode?"imp.setTitle":"rename")+"(\""+strip(commandOptions)+"\");\n"); + else if (name.equals("Wand Tool...")) + textArea.append("//run(\""+name+"\", \""+commandOptions+"\");\n"); + else if (name.equals("Results... ")&&commandOptions.indexOf(".txt")==-1) + textArea.append((scriptMode?"IJ.":"")+"open(\""+strip(commandOptions)+"\");\n"); + else if (name.equals("Results...")) // Save As>Results + ; + else if (name.equals("Run...")) // Plugins>Macros>Run + ; + else if (scriptMode && name.equals("Text Image... ")) // File>Import>Text Image + ; + else { + if (name.equals("Calibrate...")) { + if (commandOptions.startsWith("function=None unit=[Gray Value]")) + commandOptions = commandOptions.substring(0,13); + else if (commandOptions.startsWith("function=None")) { + int index = commandOptions.indexOf(" text1="); + if (index>0) + commandOptions = commandOptions.substring(0,index); + } + } + String prefix = "run("; + if (scriptMode) { + boolean addImp = imageUpdated || (WindowManager.getCurrentImage()!=null + &&(name.equals("Properties... ")||name.equals("Fit Spline")||commandOptions.contains("save="))); + if (commandOptions.contains("open=")) + addImp = false; + prefix = addImp?"IJ.run(imp, ":"IJ.run("; + } + textArea.append(prefix+"\""+name+"\", \""+commandOptions+"\");\n"); + if (nonAscii(commandOptions)) { + if (commandOptions!=null && !commandOptions.contains("="+IJ.micronSymbol+"m")) + textArea.append(" <>\n"); + } + } + } else { + Roi roi = imp!=null?imp.getRoi():null; + if (name.equals("Threshold...") || name.equals("Fonts...") || name.equals("Brightness/Contrast...") || name.equals("Channels Tool...")) + textArea.append((scriptMode?"//IJ.":"//")+"run(\""+name+"\");\n"); + else if (name.equals("Start Animation [\\]")) + textArea.append("doCommand(\"Start Animation [\\\\]\");\n"); + else if (name.equals("Blobs")) + textArea.append("run(\"Blobs (25K)\");\n"); + else if (name.equals("Split Channels") && scriptMode) { + String text = "channels = ChannelSplitter.split(imp);\n"; + if (javaMode()) + text = "ImagePlus[] " + text; + textArea.append(text); + } else if (name.equals("Add to Manager")) + ; + else if (name.equals("Find Commands...")) + ; + else if (scriptMode && name.equals("Create Mask")) + ; + else if (roi!=null && (roi instanceof TextRoi) && (name.equals("Draw")||name.equals("Add Selection..."))) + textArea.append(((TextRoi)roi).getMacroCode(name, imp)); + else { + if (IJ.altKeyDown() && (name.equals("Open Next")||name.equals("Plot Profile"))) + textArea.append("setKeyDown(\"alt\"); "); + if (scriptMode) { + boolean addImp = imageUpdated || + (imp!=null&&(name.equals("Select None")||name.equals("Draw")||name.equals("Fit Spline")||name.equals("Add Selection..."))); + String prefix = addImp?"IJ.run(imp, ":"IJ.run("; + textArea.append(prefix+"\""+name+"\", \"\");\n"); + } else + textArea.append("run(\""+name+"\");\n"); + } + } + } + commandName = null; + commandOptions = null; + if (imageID!=0) { + ImagePlus.removeImageListener(instance); + imageID = 0; + } + } + + private static boolean nonAscii(String s) { + int len = s!=null?s.length():0; + for (int i=0; i127) + return true; + } + return false; + } + + static boolean isTextOrTable(String path) { + return path.endsWith(".txt") || path.endsWith(".csv") || path.endsWith(".xls") || path.endsWith(".tsv"); + } + + static boolean isSaveAs() { + return commandName.equals("Tiff...") + || commandName.equals("Gif...") + || commandName.equals("Jpeg...") + || commandName.equals("Text Image...") + || commandName.equals("ZIP...") + || commandName.equals("Raw Data...") + || commandName.equals("BMP...") + || commandName.equals("PNG...") + || commandName.equals("PGM...") + || commandName.equals("FITS...") + || commandName.equals("LUT...") + || commandName.equals("Selection...") + || commandName.equals("XY Coordinates...") + //|| commandName.equals("Results...") + || commandName.equals("Text... ") + || commandName.equals("Save"); + } + + static void appendNewImage(boolean hyperstack) { + String options = getCommandOptions() + " "; + //IJ.log("appendNewImage: "+options); + String title = Macro.getValue(options, "name", "Untitled"); + String type = Macro.getValue(options, "type", "8-bit"); + String fill = Macro.getValue(options, "fill", ""); + if (!fill.equals("")) + type = type +" " + fill.toLowerCase(); + if (hyperstack) { + String mode = Macro.getValue(options, "display", ""); + if (!mode.equals("")) + type = type +" " + mode.toLowerCase() + "-mode"; + if (options.contains(" label")) + type = type +" label"; + } + int width = (int)Tools.parseDouble(Macro.getValue(options, "width", "512")); + int height = (int)Tools.parseDouble(Macro.getValue(options, "height", "512")); + String d1= ", " + (int)Tools.parseDouble(Macro.getValue(options, "slices", "1")); + String d2="", d3=""; + if (hyperstack) { + d1 = ", " + (int)Tools.parseDouble(Macro.getValue(options, "channels", "1")); + d2 = ", " + (int)Tools.parseDouble(Macro.getValue(options, "slices", "1")); + d3 = ", " + (int)Tools.parseDouble(Macro.getValue(options, "frames", "1")); + } + textArea.append((scriptMode?"imp = IJ.createImage":"newImage") + +"(\""+title+"\", "+"\""+type+"\", "+width+", "+height+d1+d2+d3+");\n"); + } + + static String strip(String value) { + int index = value.indexOf('='); + if (index>=0) + value = value.substring(index+1); + if (value.startsWith("[")) { + int index2 = value.indexOf(']'); + if (index2==-1) index2 = value.length(); + value = value.substring(1, index2); + } else { + index = value.indexOf(' '); + if (index!=-1) + value = value.substring(0, index); + } + return value; + } + + static String addQuotes(String value) { + if (value==null) + value = ""; + int index = value.indexOf(' '); + if (index>-1) + value = "["+value+"]"; + return value; + } + + /** Used by GenericDialog to determine if any options have been recorded. */ + public static String getCommandOptions() { + return commandOptions; + } + + /** Used by GenericDialog.notifyListeners() to clear the command options. */ + public static void resetCommandOptions() { + commandOptions = null; + } + + void createMacro() { + String text = textArea.getText(); + if (text==null || text.equals("")) { + IJ.showMessage("Recorder", "A macro cannot be created until at least\none command has been recorded."); + return; + } + String name = fileName.getText(); + Editor ed = new Editor(name); + boolean java = mode.getSelectedItem().equals(modes[JAVA]); + boolean beanshell = mode.getSelectedItem().equals(modes[BEANSHELL]); + boolean python = mode.getSelectedItem().equals(modes[PYTHON]); + int dotIndex = name.lastIndexOf("."); + if (scriptMode) { // JavaScript, BeanShell, Python or Java + if (dotIndex>=0) name = name.substring(0, dotIndex); + if (text.indexOf("rm.")!=-1) { + text = (java?"RoiManager ":"")+ "rm = RoiManager.getRoiManager();\n" + + text; + } + if (text.contains("overlay.add")) + text = (java?"Overlay ":"") + "overlay = new Overlay();\n" + text; + if ((text.contains("imp.")||text.contains("(imp")||text.contains("overlay.add")) && !text.contains("IJ.openImage") + && !text.contains("IJ.openVirtual") && !text.contains("IJ.createImage")) + text = (java?"ImagePlus ":"") + "imp = IJ.getImage();\n" + text; + if (text.contains("overlay.add")) + text = text + "imp.setOverlay(overlay);\n"; + if (text.indexOf("imp =")!=-1 && !(text.indexOf("IJ.getImage")!=-1||text.indexOf("IJ.saveAs")!=-1||text.indexOf("imp.close")!=-1)) + text = text + "imp.show();\n"; + if (python) { + text = text.replaceAll("new ", ""); + text = text.replaceAll("\n//", "\n#"); + } + if (java) { + name += ".java"; + createPlugin(text, name); + return; + } else if (beanshell) + name += ".bsh"; + else if (python) + name += ".py"; + else + name += ".js"; + } else { // ImageJ macro + if (!name.endsWith(".txt")) { + if (dotIndex>=0) name = name.substring(0, dotIndex); + name += ".ijm"; + } + } + ed.createMacro(name, text); + fgColorSet = bgColorSet = false; + bbSet = false; + } + + void createPlugin(String text, String name) { + StringTokenizer st = new StringTokenizer(text, "\n"); + int n = st.countTokens(); + boolean impDeclared = false; + boolean lutDeclared = false; + String line; + StringBuffer sb = new StringBuffer(); + for (int i=0; i3) { + sb.append("\t\t"); + if (line.startsWith("imp =") && !impDeclared) { + sb.append("ImagePlus "); + impDeclared = true; + } + if (line.startsWith("lut =") && !lutDeclared) { + sb.append("LUT "); + lutDeclared = true; + } + sb.append(line); + sb.append('\n'); + } + } + String text2 = new String(sb); + text2 = text2.replaceAll("print", "IJ.log"); + NewPlugin np = (NewPlugin)IJ.runPlugIn("ij.plugin.NewPlugin", text2); + Editor ed = np.getEditor(); + ed.updateClassName(ed.getTitle(), name); + ed.setTitle(name); + } + + /** Temporarily disables path recording. */ + public static void disablePathRecording() { + recordPath = false; + } + + public static boolean scriptMode() { + return scriptMode; + } + + public void actionPerformed(ActionEvent e) { + if (e.getSource()==makeMacro) + createMacro(); + else if (e.getSource()==help) + showHelp(); + } + + public void itemStateChanged(ItemEvent e) { + setFileName(); + Prefs.set("recorder.mode", mode.getSelectedItem()); + } + + void setFileName() { + String name = mode.getSelectedItem(); + scriptMode = !name.equals(modes[MACRO]); + if (name.equals(modes[MACRO])) + fileName.setText("Macro.ijm"); + else if (name.equals(modes[JAVASCRIPT])) + fileName.setText("Script.js"); + else if (name.equals(modes[BEANSHELL])) + fileName.setText("Script.bsh"); + else if (name.equals(modes[PYTHON])) + fileName.setText("Script.py"); + else + fileName.setText("My_Plugin.java"); + fgColorSet = bgColorSet = false; + bbSet = false; + } + + public void imageUpdated(ImagePlus imp) { + if (imp.getID()==imageID) + imageUpdated = true; + } + + public void imageOpened(ImagePlus imp) { } + + public void imageClosed(ImagePlus imp) { } + + void showHelp() { + IJ.showMessage("Recorder", + "Click \"Create\" to open recorded commands\n" + +"as a macro in an editor window.\n" + +" \n" + +"In the editor:\n" + +" \n" + +" Type ctrl+R (Macros>Run Macro) to\n" + +" run the macro.\n" + +" \n" + +" Use File>Save As to save it and\n" + +" ImageJ's Open command to open it.\n" + +" \n" + +" To create a command, save in the plugins\n" + +" folder and run Help>Refresh Menus.\n" + ); + } + + public void close() { + super.close(); + record = false; + textArea = null; + commandName = null; + instance = null; + } + + public String getText() { + if (textArea==null) + return ""; + else + return textArea.getText(); + } + + public static Recorder getInstance() { + return instance; + } + + public static void setForegroundColor(Color c) { + record("setForegroundColor", c.getRed(), c.getGreen(), c.getBlue()); + fgColorSet = true; + } + + public static void setBackgroundColor(Color c) { + record("setBackgroundColor", c.getRed(), c.getGreen(), c.getBlue()); + bgColorSet = true; + } + + public static void setBlackBackground() { + String bb = Prefs.blackBackground?"true":"false"; + if (scriptMode) + recordString("Prefs.blackBackground = "+bb+";\n"); + else + recordString("setOption(\"BlackBackground\", "+bb+");\n"); + bbSet = true; + } + + /** Override windowActivated in PlugInFrame. */ + public void windowActivated(WindowEvent e) { + if (IJ.isMacintosh() && !IJ.isJava17()) + this.setMenuBar(Menus.getMenuBar()); + WindowManager.setWindow(this); + } + +} diff --git a/src/ij/plugin/frame/RoiManager.java b/src/ij/plugin/frame/RoiManager.java new file mode 100644 index 0000000..0fb8bdc --- /dev/null +++ b/src/ij/plugin/frame/RoiManager.java @@ -0,0 +1,2852 @@ +package ij.plugin.frame; +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.util.*; +import java.awt.List; +import java.util.zip.*; +import java.awt.geom.*; + +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; +import javax.swing.ScrollPaneConstants; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.io.*; +import ij.plugin.filter.*; +import ij.plugin.Colors; +import ij.plugin.OverlayLabels; +import ij.plugin.FolderOpener; +import ij.util.*; +import ij.macro.*; +import ij.measure.*; +import ij.plugin.OverlayCommands; + +/** This plugin implements the Analyze/Tools/ROI Manager command. */ +public class RoiManager extends PlugInFrame implements ActionListener, ItemListener, MouseListener, MouseWheelListener, ListSelectionListener, Iterable { + public static final String LOC_KEY = "manager.loc"; + private static final String MULTI_CROP_DIR = "multi-crop.dir"; + private static final int BUTTONS = 11; + private static final int DRAW=0, FILL=1, LABEL=2; + private static final int SHOW_ALL=0, SHOW_NONE=1, LABELS=2, NO_LABELS=3; + private static final int MENU=0, COMMAND=1; + private static final int IGNORE_POSITION=-999; // ignore the ROI's built in position + private static final int CHANNEL=0, SLICE=1, FRAME=2, SHOW_DIALOG=3; + private static int rows = 15; + private static int lastNonShiftClick = -1; + private static boolean allowMultipleSelections = true; + private static String moreButtonLabel = "More "+'\u00bb'; + private Panel panel; + private static Frame instance; + private static int colorIndex = 4; + private JList list; + private DefaultListModel listModel; + private ArrayList rois = new ArrayList(); + private boolean canceled; + private boolean macro; + private boolean ignoreInterrupts; + private PopupMenu pm; + private Button moreButton, colorButton; + private Checkbox showAllCheckbox = new Checkbox("Show All", false); + private Checkbox labelsCheckbox = new Checkbox("Labels", false); + private Overlay overlayTemplate; + + private static boolean measureAll = true; + private static boolean onePerSlice = true; + private static boolean restoreCentered; + private int prevID; + private boolean noUpdateMode; + private int defaultLineWidth = 1; + private Color defaultColor; + private boolean firstTime = true; + private boolean appendResults; + private static ResultsTable mmResults, mmResults2; + private int imageID; + private boolean allowRecording; + private boolean recordShowAll = true; + private boolean allowDuplicates; + private double translateX = 10.0; + private double translateY = 10.0; + private static String errorMessage; + private boolean multiCropShow = true; + private boolean multiCropSave; + private int multiCropFormatIndex; + + /** Opens the "ROI Manager" window, or activates it if it is already open. + * @see #RoiManager(boolean) + * @see #getRoiManager + */ + public RoiManager() { + super("ROI Manager"); + if (instance!=null) { + WindowManager.toFront(instance); + return; + } + if (IJ.isMacro() && Interpreter.getBatchModeRoiManager()!=null) { + list = new JList(); + listModel = new DefaultListModel(); + list.setModel(listModel); + return; + } + instance = this; + list = new JList(); + errorMessage = null; + showWindow(); + } + + /** Constructs an ROIManager without displaying it. The boolean argument is ignored. */ + public RoiManager(boolean b) { + super("ROI Manager"); + list = new JList(); + listModel = new DefaultListModel(); + list.setModel(listModel); + errorMessage = null; + } + + void showWindow() { + ImageJ ij = IJ.getInstance(); + addKeyListener(ij); + addMouseListener(this); + addMouseWheelListener(this); + WindowManager.addWindow(this); + //setLayout(new FlowLayout(FlowLayout.CENTER,5,5)); + setLayout(new BorderLayout()); + listModel = new DefaultListModel(); + list.setModel(listModel); + GUI.scale(list); + list.setPrototypeCellValue("0000-0000-0000 "); + list.addListSelectionListener(this); + list.addKeyListener(ij); + list.addMouseListener(this); + list.addMouseWheelListener(this); + if (IJ.isLinux()) list.setBackground(Color.white); + JScrollPane scrollPane = new JScrollPane(list, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + add("Center", scrollPane); + panel = new Panel(); + int nButtons = BUTTONS; + panel.setLayout(new GridLayout(nButtons, 1, 5, 0)); + addButton("Add [t]"); + addButton("Update"); + addButton("Delete"); + addButton("Rename..."); + addButton("Measure"); + addButton("Deselect"); + addButton("Properties..."); + addButton("Flatten [F]"); + addButton(moreButtonLabel); + showAllCheckbox.addItemListener(this); + panel.add(showAllCheckbox); + labelsCheckbox.addItemListener(this); + panel.add(labelsCheckbox); + add("East", panel); + addPopupMenu(); + GUI.scale(this); + pack(); + Dimension size = getSize(); + if (size.width>270) + setSize(size.width-40, size.height); + list.remove(0); + Point loc = Prefs.getLocation(LOC_KEY); + if (loc!=null) + setLocation(loc); + else + GUI.centerOnImageJScreen(this); + show(); + } + + void addButton(String label) { + Button b = new Button(label); + b.addActionListener(this); + b.addKeyListener(IJ.getInstance()); + b.addMouseListener(this); + if (label.equals(moreButtonLabel)) moreButton = b; + panel.add(b); + } + + void addPopupMenu() { + pm = new PopupMenu(); + GUI.scalePopupMenu(pm); + addPopupItem("Open..."); + addPopupItem("Save..."); + addPopupItem("Fill"); + addPopupItem("Draw"); + addPopupItem("AND"); + addPopupItem("OR (Combine)"); + addPopupItem("XOR"); + addPopupItem("Split"); + addPopupItem("Add Particles"); + addPopupItem("Multi Measure"); + addPopupItem("Multi Plot"); + addPopupItem("Multi Crop"); + addPopupItem("Sort"); + addPopupItem("Specify..."); + addPopupItem("Remove Positions..."); + addPopupItem("Labels..."); + addPopupItem("List"); + addPopupItem("Interpolate ROIs"); + addPopupItem("Translate..."); + addPopupItem("Help"); + addPopupItem("Options..."); + add(pm); + } + + void addPopupItem(String s) { + MenuItem mi=new MenuItem(s); + mi.addActionListener(this); + pm.add(mi); + } + + public void actionPerformed(ActionEvent e) { + String label = e.getActionCommand(); + if (label==null) + return; + String command = label; + allowRecording = true; + if (command.equals("Add [t]")) + runCommand("add"); + else if (command.equals("Update")) + update(true); + else if (command.equals("Delete")) + delete(false); + else if (command.equals("Rename...")) + rename(null); + else if (command.equals("Properties...")) + setProperties(null, -1, null); + else if (command.equals("Flatten [F]")) + flatten(); + else if (command.equals("Measure")) + measure(getImage()); + else if (command.equals("Open...")) + open(null); + else if (command.equals("Save...")) { + Thread t1 = new Thread(new Runnable() { + public void run() { + save(null); + } + }); + t1.start(); + } else if (command.equals("Fill")) + drawOrFill(FILL); + else if (command.equals("Draw")) + drawOrFill(DRAW); + else if (command.equals("Deselect")) + deselect(); + else if (command.equals(moreButtonLabel)) { + Point ploc = panel.getLocation(); + Point bloc = moreButton.getLocation(); + pm.show(this, ploc.x, bloc.y); + } else if (command.equals("OR (Combine)")) { + new MacroRunner("roiManager(\"Combine\");"); + if (Recorder.record) Recorder.record("roiManager", "Combine"); + } else if (command.equals("Split")) + split(); + else if (command.equals("AND")) + and(); + else if (command.equals("XOR")) + xor(); + else if (command.equals("Add Particles")) + addParticles(); + else if (command.equals("Multi Measure")) + multiMeasure(""); + else if (command.equals("Multi Plot")) + multiPlot(); + else if (command.equals("Multi Crop")) + multiCrop(); + else if (command.equals("Sort")) + sort(); + else if (command.equals("Specify...")) + specify(); + else if (command.equals("Remove Positions...")) + removePositions(SHOW_DIALOG); + else if (command.equals("Labels...")) + labels(); + else if (command.equals("List")) + listRois(); + else if (command.equals("Interpolate ROIs")) + interpolateRois(); + else if (command.equals("Translate...")) + translate(); + else if (command.equals("Help")) + help(); + else if (command.equals("Options...")) + options(); + else if (command.equals("\"Show All\" Color...")) + setShowAllColor(); + allowRecording = false; + } + + private void interpolateRois() { + IJ.runPlugIn("ij.plugin.RoiInterpolator", ""); + if (record()) + Recorder.record("roiManager", "Interpolate ROIs"); + } + + public void itemStateChanged(ItemEvent e) { + Object source = e.getSource(); + boolean showAllMode = showAllCheckbox.getState(); + if (source==showAllCheckbox) { + if (firstTime && okToSet()) + labelsCheckbox.setState(true); + showAll(showAllCheckbox.getState()?SHOW_ALL:SHOW_NONE); + if (Recorder.record && recordShowAll) { + if (showAllMode) + Recorder.record("roiManager", "Show All"); + else + Recorder.record("roiManager", "Show None"); + } + recordShowAll = true; + firstTime = false; + return; + } + if (source==labelsCheckbox) { + if (firstTime && okToSet()) + showAllCheckbox.setState(true); + boolean editState = labelsCheckbox.getState(); + boolean showAllState = showAllCheckbox.getState(); + if (!showAllState && !editState) + showAll(SHOW_NONE); + else { + showAll(editState?LABELS:NO_LABELS); + if (Recorder.record) { + if (editState) + Recorder.record("roiManager", "Show All with labels"); + else if (showAllState) + Recorder.record("roiManager", "Show All without labels"); + } + if (editState && !showAllState && okToSet()) { + showAllCheckbox.setState(true); + recordShowAll = false; + } + } + firstTime = false; + return; + } + } + + private boolean okToSet() { + return !(IJ.isMacOSX()&&IJ.isJava18()); + } + + void add(boolean shiftKeyDown, boolean altKeyDown) { + if (shiftKeyDown) + addAndDraw(altKeyDown); + else if (altKeyDown) + addRoi(true); + else + addRoi(false); + } + + /** Adds the specified ROI. */ + public void addRoi(Roi roi) { + allowDuplicates = true; + addRoi(roi, false, null, -1); + } + + boolean addRoi(boolean promptForName) { + return addRoi(null, promptForName, null, IGNORE_POSITION); + } + + boolean addRoi(Roi roi, boolean promptForName, Color color, int lineWidth) { + if (listModel==null) + IJ.log("<>"); + ImagePlus imp = roi==null?getImage():WindowManager.getCurrentImage(); + if (roi==null) { + if (imp==null) + return false; + roi = imp.getRoi(); + if (roi==null) { + error("The active image does not have a selection."); + return false; + } + } + if ((roi instanceof PolygonRoi) && ((PolygonRoi)roi).getNCoordinates()==0) + return false; + if (color==null && roi.getStrokeColor()!=null) + color = roi.getStrokeColor(); + else if (color==null && defaultColor!=null) + color = defaultColor; + boolean ignorePosition = false; + if (lineWidth==IGNORE_POSITION) { + ignorePosition = true; + lineWidth = -1; + } + if (lineWidth<0) { + int sw = (int)roi.getStrokeWidth(); + lineWidth = sw>1?sw:defaultLineWidth; + } + if (lineWidth>100) lineWidth = 1; + int n = getCount(); + int position = imp!=null&&!ignorePosition?roi.getPosition():0; + int saveCurrentSlice = imp!=null?imp.getCurrentSlice():0; + if (position>0 && position!=saveCurrentSlice) { + if (imp.lock()) + imp.setSliceWithoutUpdate(position); + else + return false; //can't lock image, must not change the stack slice + } else + position = 0; //we need to revert to the original stack slice and unlock if position>0 + if (n>0 && !IJ.isMacro() && imp!=null && !allowDuplicates) { + // check for duplicate + Roi roi2 = (Roi)rois.get(n-1); + if (roi2!=null) { + String label = (String)listModel.getElementAt(n-1); + int slice2 = getSliceNumber(roi2, label); + if (roi.equals(roi2) && (slice2==-1||slice2==imp.getCurrentSlice()) && imp.getID()==prevID && !Interpreter.isBatchMode()) { + if (position>0) { + imp.setSliceWithoutUpdate(saveCurrentSlice); + imp.unlock(); + } + return false; + } + } + } + allowDuplicates = false; + prevID = imp!=null?imp.getID():0; + String name = roi.getName(); + if (isStandardName(name)) + name = null; + String label = name!=null?name:getLabel(imp, roi, -1); + if (promptForName) + label = promptForName(label); + if (label==null) { + if (position>0) { + imp.setSliceWithoutUpdate(saveCurrentSlice); + imp.unlock(); + } + return false; + } + listModel.addElement(label); + roi.setName(label); + Roi roiCopy = (Roi)roi.clone(); + if (ignorePosition && imp!=null && imp.getStackSize()>1 && imp.getWindow()!=null && isVisible()) { + // set ROI position to current stack position if image and RoiManager are visible + roiCopy.setPosition(imp); + } + if (lineWidth>1) + roiCopy.setStrokeWidth(lineWidth); + if (color!=null) + roiCopy.setStrokeColor(color); + rois.add(roiCopy); + updateShowAll(); + if (record()) + recordAdd(defaultColor, defaultLineWidth); + if (position>0) { + imp.setSliceWithoutUpdate(saveCurrentSlice); + imp.unlock(); + } + return true; + } + + void recordAdd(Color color, int lineWidth) { + if (Recorder.scriptMode()) + Recorder.recordCall("rm.addRoi(roi);"); + else if (color!=null && lineWidth==1) + Recorder.recordString("roiManager(\"Add\", \""+getHex(color)+"\");\n"); + else if (lineWidth>1) + Recorder.recordString("roiManager(\"Add\", \""+getHex(color)+"\", "+lineWidth+");\n"); + else + Recorder.record("roiManager", "Add"); + } + + String getHex(Color color) { + if (color==null) color = ImageCanvas.getShowAllColor(); + String hex = Integer.toHexString(color.getRGB()); + if (hex.length()==8) hex = hex.substring(2); + return hex; + } + + /** Adds the specified ROI to the list. The second argument ('n') will + * be used to form the first part of the ROI label if it is zero or greater. + * @param roi the Roi to be added + * @param n if zero or greater, will be used to form the first part of the label + */ + public void add(Roi roi, int n) { + add((ImagePlus)null, roi, n); + } + + /** Adds the specified ROI to the list. The third argument ('n') will + * be used to form the first part of the ROI label if it is zero or greater. + * @param imp the image associated with the ROI, or null + * @param roi the Roi to be added + * @param n if zero or greater, will be used to form the first part of the label + */ + public void add(ImagePlus imp, Roi roi, int n) { + if (IJ.debugMode && n<3 && roi!=null) IJ.log("RoiManager.add: "+n+" "+roi.getName()); + if (roi==null) + return; + String label = roi.getName(); + String label2 = label; + if (label==null) + label = getLabel(imp, roi, n); + else { + if (n>=0) + label = n+"-"+label; + } + if (label==null) + return; + listModel.addElement(label); + if (label2!=null) + roi.setName(label2); + else + roi.setName(label); + rois.add((Roi)roi.clone()); + } + + /** Replaces the ROI at the specified index. */ + public void setRoi(Roi roi, int index) { + if (index<0 || index>=rois.size()) + throw new IllegalArgumentException("setRoi: Index out of range"); + rois.set(index, (Roi)roi.clone()); + updateShowAll(); + } + + boolean isStandardName(String name) { + if (name==null) + return false; + int len = name.length(); + if (len<9 || (len>0&&!Character.isDigit(name.charAt(0)))) + return false; + boolean isStandard = false; + if (len>=14 && name.charAt(4)=='-' && name.charAt(9)=='-' ) + isStandard = true; + else if (len>=17 && name.charAt(5)=='-' && name.charAt(11)=='-' ) + isStandard = true; + else if (len>=9 && name.charAt(4)=='-') + isStandard = true; + else if (len>=11 && name.charAt(5)=='-') + isStandard = true; + return isStandard; + } + + String getLabel(ImagePlus imp, Roi roi, int n) { + Rectangle r = roi.getBounds(); + int xc = r.x + r.width/2; + int yc = r.y + r.height/2; + if (n>=0) + {xc = yc; yc=n;} + if (xc<0) xc = 0; + if (yc<0) yc = 0; + int digits = 4; + String xs = "" + xc; + if (xs.length()>digits) digits = xs.length(); + String ys = "" + yc; + if (ys.length()>digits) digits = ys.length(); + if (digits==4 && imp!=null && (imp.getStackSize()>=10000||imp.getHeight()>=10000)) + digits = 5; + xs = "000000" + xc; + ys = "000000" + yc; + String label = ys.substring(ys.length()-digits) + "-" + xs.substring(xs.length()-digits); + if (imp!=null && imp.getStackSize()>1) { + int slice = imp.getCurrentSlice(); + String zs = "000000" + slice; + label = zs.substring(zs.length()-digits) + "-" + label; + } + return label; + } + + void addAndDraw(boolean altKeyDown) { + if (altKeyDown) { + if (!addRoi(true)) return; + } else if (!addRoi(false)) + return; + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + Undo.setup(Undo.COMPOUND_FILTER, imp); + IJ.run(imp, "Draw", "slice"); + Undo.setup(Undo.COMPOUND_FILTER_DONE, imp); + } + if (record()) Recorder.record("roiManager", "Add & Draw"); + } + + boolean delete(boolean replacing) { + int count = getCount(); + if (count==0) + return error("The ROI Manager is empty."); + int index[] = getSelectedIndexes(); + if (index.length==0 || (replacing&&count>1)) { + String msg = "Delete all items on the list?"; + if (replacing) + msg = "Replace items on the list?"; + canceled = false; + if (!IJ.isMacro() && !macro) { + YesNoCancelDialog d = new YesNoCancelDialog(this, "ROI Manager", msg); + if (d.cancelPressed()) + {canceled = true; return false;} + if (!d.yesPressed()) return false; + } + index = getAllIndexes(); + } + if (count==index.length && !replacing) { + rois.clear(); + listModel.removeAllElements(); + } else { + for (int i=count-1; i>=0; i--) { + boolean delete = false; + for (int j=0; j1 && index.length==1 && imp!=null) + // imp.deleteRoi(); + updateShowAll(); + if (record()) + Recorder.record("roiManager", "Delete"); + return true; + } + + // Delete ROI on event dispatch thread + private void deleteOnEDT(final int i) { + try { + EventQueue.invokeAndWait(new Runnable() { + public void run() { + rois.remove(i); + listModel.remove(i); + } + }); + } catch ( + Exception e) { + } + } + + boolean update(boolean clone) { + ImagePlus imp = getImage(); + if (imp==null) + return false; + ImageCanvas ic = imp.getCanvas(); + boolean showingAll = ic!=null && ic.getShowAllROIs(); + Roi roi = imp.getRoi(); + if (roi==null) { + error("The active image does not have a selection."); + return false; + } + int index = list.getSelectedIndex(); + if (index<0 && !showingAll) + return error("Exactly one item in the list must be selected."); + if (index>=0) { + if (clone) { + String name = (String)listModel.getElementAt(index); + Roi roi2 = (Roi)roi.clone(); + roi2.setPosition(imp); + roi.setName(name); + roi2.setName(name); + rois.set(index, roi2); + } else + rois.set(index, roi); + } + if (record()) Recorder.record("roiManager", "Update"); + updateShowAll(); + return true; + } + + boolean rename(String name2) { + int index = list.getSelectedIndex(); + if (index<0) + return error("Exactly one item in the list must be selected."); + String name = (String)listModel.getElementAt(index); + if (name2==null) + name2 = promptForName(name); + if (name2==null) + return false; + if (name2.equals(name)) + return false; + Roi roi = (Roi)rois.get(index); + roi.setName(name2); + int position = getSliceNumber(name2); + if (position>0 && !roi.hasHyperStackPosition()) + roi.setPosition(position); + rois.set(index, roi); + listModel.setElementAt(name2, index); + list.setSelectedIndex(index); + if (Prefs.useNamesAsLabels && labelsCheckbox.getState()) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) imp.draw(); + } + if (record()) + Recorder.record("roiManager", "Rename", name2); + return true; + } + + public void rename(int index, String newName) { + if (index<0 || index>=getCount()) + throw new IllegalArgumentException("Index out of range: "+index); + Roi roi = (Roi)rois.get(index); + roi.setName(newName); + listModel.setElementAt(newName, index); + } + + String promptForName(String name) { + GenericDialog gd = new GenericDialog("ROI Manager"); + gd.addStringField("Rename As:", name, 20); + gd.showDialog(); + if (gd.wasCanceled()) + return null; + else + return gd.getNextString(); + } + + boolean restore(ImagePlus imp, int index, boolean setSlice) { + Roi roi = (Roi)rois.get(index); + if (imp==null || roi==null) + return false; + //IJ.log("restore: "+roi.getPosition()+" "+roi.getZPosition()+" "+imp.getNSlices()+" "+imp.getStackSize()); + if (setSlice) { + boolean hyperstack = imp.isHyperStack(); + int position = roi.getPosition(); + if (hyperstack && roi.hasHyperStackPosition()) + imp.setPosition(roi.getCPosition(), roi.getZPosition(), roi.getTPosition()); + else if (hyperstack && imp.getNSlices()==1) + imp.setPosition(imp.getChannel(), 1, position); + else if (hyperstack) + imp.setPosition(imp.getChannel(), position, imp.getChannel()); + else if (roi.getZPosition()>0 && imp.getNSlices()==imp.getStackSize()) + imp.setSlice(roi.getZPosition()); + else if (position>0 && position<=imp.getStackSize()) + imp.setSlice(position); + else { + String label = (String)listModel.getElementAt(index); + int n = getSliceNumber(roi, label); + if (n>=1 && n<=imp.getStackSize()) { + if (hyperstack) { + if (imp.getNSlices()>1 && n<=imp.getNSlices()) + imp.setPosition(imp.getC(),n,imp.getT()); + else if (imp.getNFrames()>1 && n<=imp.getNFrames()) + imp.setPosition(imp.getC(),imp.getZ(),n); + else + imp.setPosition(n); + } else + imp.setSlice(n); + } + } + } + if (showAllCheckbox.getState() && !restoreCentered && !noUpdateMode) { + roi.setImage(null); + imp.setRoi(roi); + return true; + } + Roi roi2 = (Roi)roi.clone(); + Rectangle r = roi2.getBounds(); + int width= imp.getWidth(), height=imp.getHeight(); + if (restoreCentered) { + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) { + Rectangle r1 = ic.getSrcRect(); + Rectangle r2 = roi2.getBounds(); + roi2.setLocation(r1.x+r1.width/2-r2.width/2, r1.y+r1.height/2-r2.height/2); + } + } + if (r.x>=width || r.y>=height || (r.x+r.width)<0 || (r.y+r.height)<0) { + if (roi2.getType()!=Roi.POINT) + roi2.setLocation((width-r.width)/2, (height-r.height)/2); + } + if (noUpdateMode) { + imp.setRoi(roi2, false); + noUpdateMode = false; + } else + imp.setRoi(roi2, true); + return true; + } + + private boolean restoreWithoutUpdate(ImagePlus imp, int index) { + noUpdateMode = true; + if (imp==null) + imp = getImage(); + return restore(imp, index, false); + } + + /** Returns the slice number associated with the specified name, + or -1 if the name does not include a slice number. */ + public int getSliceNumber(String label) { + int slice = -1; + if (label.length()>=14 && label.charAt(4)=='-' && label.charAt(9)=='-') + slice = (int)Tools.parseDouble(label.substring(0,4),-1); + else if (label.length()>=17 && label.charAt(5)=='-' && label.charAt(11)=='-') + slice = (int)Tools.parseDouble(label.substring(0,5),-1); + else if (label.length()>=20 && label.charAt(6)=='-' && label.charAt(13)=='-') + slice = (int)Tools.parseDouble(label.substring(0,6),-1); + return slice; + } + + /** Returns the slice number associated with the specified ROI or name, + or -1 if the ROI or name does not include a slice number. */ + int getSliceNumber(Roi roi, String label) { + int slice = roi!=null?roi.getPosition():-1; + if (slice==0) + slice=-1; + if (slice==-1) + slice = getSliceNumber(label); + return slice; + } + + /** Opens a single .roi file or a ZIP-compressed set of ROIs. + * Returns 'true' if the operation was succesful. + */ + public boolean open(String path) { + Macro.setOptions(null); + String name = null; + if (path==null || path.equals("")) { + OpenDialog od = new OpenDialog("Open Selection(s)...", ""); + String directory = od.getDirectory(); + name = od.getFileName(); + if (name==null) + return false; + path = directory + name; + } + if (Recorder.record && !IJ.macroRunning()) { + if (Recorder.scriptMode()) + Recorder.recordCall("rm.open(\""+path+"\");"); + else + Recorder.record("roiManager", "Open", path); + } + boolean ok = false; + if (path.endsWith(".zip")) { + boolean wasRecording = Recorder.record; + Recorder.record = false; + ok = openZip(path); + Recorder.record = wasRecording; + return ok; + } + Opener o = new Opener(); + if (name==null) name = o.getName(path); + Roi roi = o.openRoi(path); + if (roi!=null) { + if (roi.getName()!=null) + name = roi.getName(); + if (name.endsWith(".roi")) + name = name.substring(0, name.length()-4); + listModel.addElement(name); + rois.add(roi); + errorMessage = null; + ok = true; + } else { + errorMessage = "Unable to open ROI at "+path; + ok = false; + } + updateShowAll(); + return ok; + } + + // Modified on 2005/11/15 by Ulrik Stervbo to only read .roi files and to not empty the current list + boolean openZip(String path) { + ZipInputStream in = null; + ByteArrayOutputStream out = null; + int nRois = 0; + errorMessage = null; + try { + in = new ZipInputStream(new FileInputStream(path)); + byte[] buf = new byte[1024]; + int len; + ZipEntry entry = in.getNextEntry(); + while (entry!=null) { + String name = entry.getName(); + if (name.endsWith(".roi")) { + out = new ByteArrayOutputStream(); + while ((len = in.read(buf)) > 0) + out.write(buf, 0, len); + out.close(); + byte[] bytes = out.toByteArray(); + RoiDecoder rd = new RoiDecoder(bytes, name); + Roi roi = rd.getRoi(); + if (roi!=null) { + name = name.substring(0, name.length()-4); + listModel.addElement(name); + rois.add(roi); + nRois++; + } + } + entry = in.getNextEntry(); + } + in.close(); + } catch (IOException e) { + errorMessage = e.toString(); + error(errorMessage); + } finally { + if (in!=null) + try {in.close();} catch (IOException e) {} + if (out!=null) + try {out.close();} catch (IOException e) {} + } + if (nRois==0 && errorMessage==null) { + errorMessage = "This ZIP archive does not contain \".roi\" files: " + path; + error(errorMessage); + } + updateShowAll(); + return errorMessage==null; + } + + /** If one ROI is selected, it is saved as a .roi + * file, if multiple (or no) ROIs are selected, + * they are saved as a .zip ROI set. Returns + * 'true' if the save operation was succesful. + * @see #setSelectedIndexes + */ + public boolean save(String path) { + if (getCount()==0) + return error("The selection list is empty."); + int[] indexes = getIndexes(); + if (indexes.length>1) + return saveMultiple(indexes, path); + else + return saveOne(indexes, path); + + } + + boolean saveOne(int[] indexes, String path) { + if (indexes.length==0) + return error("The list is empty"); + Roi roi = (Roi)rois.get(indexes[0]); + if (path==null) { + Macro.setOptions(null); + String name = (String) listModel.getElementAt(indexes[0]); + SaveDialog sd = new SaveDialog("Save Selection...", name, ".roi"); + String name2 = sd.getFileName(); + if (name2 == null) + return false; + String dir = sd.getDirectory(); + if (!name2.endsWith(".roi")) name2 = name2+".roi"; + String newName = name2.substring(0, name2.length()-4); + rois.set(indexes[0], roi); + roi.setName(newName); + listModel.setElementAt(newName, indexes[0]); + path = dir+name2; + } + RoiEncoder re = new RoiEncoder(path); + errorMessage = null; + try { + re.write(roi); + } catch (IOException e) { + errorMessage = e.getMessage(); + IJ.error("ROI Manager", errorMessage); + } + if (Recorder.record && !IJ.isMacro()) { + if (Recorder.scriptMode()) + Recorder.recordCall("rm.save(\""+path+"\");"); + else + Recorder.record("roiManager", "Save", path); + } + return true; + } + + boolean saveMultiple(int[] indexes, String path) { + Macro.setOptions(null); + if (path==null || path.equals("")) { + SaveDialog sd = new SaveDialog("Save ROIs...", "RoiSet", ".zip"); + String name = sd.getFileName(); + if (name == null) + return false; + if (!(name.endsWith(".zip") || name.endsWith(".ZIP"))) + name = name + ".zip"; + String dir = sd.getDirectory(); + path = dir+name; + } + DataOutputStream out = null; + IJ.showStatus("Saving "+indexes.length+" ROIs "+" to "+path); + long t0 = System.currentTimeMillis(); + String[] names = new String[listModel.size()]; + for (int i=0; i 1) // do we have to change the stack slice for one of the rois? + for (int i=0; i1 || roi.hasHyperStackPosition()) { + allSliceOne=false; + break; + } + } + if (!allSliceOne) + if (!imp.lock()) return false; // if we can't lock, we must not change the stack slice + int measurements = Analyzer.getMeasurements(); + if (imp.getStackSize()>1) + Analyzer.setMeasurements(measurements|Measurements.SLICE); + int currentSlice = imp.getCurrentSlice(); + Analyzer.setMeasurements(measurements&(~Measurements.ADD_TO_OVERLAY)); + for (int i=0; i1) + imp.deleteRoi(); + if (record()) Recorder.record("roiManager", "Measure"); + if (!allSliceOne) + imp.unlock(); + return true; + } + + /** This method measures the selected ROIs, or all ROIs if + * none are selected, on all the slices of a stack and returns + * a ResultsTable arranged with one row per slice. + * @see JavaScript example + */ + public ResultsTable multiMeasure(ImagePlus imp) { + Roi[] rois = getSelectedRoisAsArray(); + ResultsTable rt = multiMeasure(imp, rois, false); + imp.deleteRoi(); + return rt; + } + + /** This method performs measurements for several ROI's in a stack + and arranges the results with one line per slice. By contrast, the + measure() method produces several lines per slice. The results + from multiMeasure() may be easier to import into a spreadsheet + program for plotting or additional analysis. Based on the multi() + method in Bob Dougherty's Multi_Measure plugin + (http://www.optinav.com/Multi-Measure.htm). + */ + boolean multiMeasure(String cmd) { + ImagePlus imp = getImage(); + if (imp==null) return false; + int[] indexes = getIndexes(); + if (indexes.length==0) { + error("Multi-measure: no selection"); + return false; + } + if (!imp.lock()) + return false; + int measurements = Analyzer.getMeasurements(); + + int nSlices = imp.getStackSize(); + if (cmd!=null) + appendResults = cmd.contains("append")?true:false; + if (IJ.isMacro()) { + if (cmd.startsWith("multi-measure")) { + measureAll = cmd.contains(" measure") && nSlices>1; // measure-all + onePerSlice = cmd.contains(" one"); + appendResults = cmd.contains(" append"); + } else { + if (nSlices>1) + measureAll = true; + onePerSlice = true; + } + } else { + GenericDialog gd = new GenericDialog("Multi Measure"); + if (nSlices>1) + gd.addCheckbox("Measure all "+nSlices+" slices", measureAll); + gd.addCheckbox("One row per slice", onePerSlice); + gd.addCheckbox("Append results", appendResults); + int columns = getColumnCount(imp, measurements)*indexes.length; + String str = nSlices==1?"this option":"both options"; + gd.setInsets(10, 25, 0); + gd.addMessage( + "Enabling "+str+" will result\n"+ + "in a table with "+columns+" columns." + ); + gd.showDialog(); + if (gd.wasCanceled()) { + imp.unlock(); + return false; + } + if (nSlices>1) + measureAll = gd.getNextBoolean(); + onePerSlice = gd.getNextBoolean(); + appendResults = gd.getNextBoolean(); + } + if (!measureAll) nSlices = 1; + int currentSlice = imp.getCurrentSlice(); + + if (!onePerSlice) { + int measurements2 = nSlices>1?measurements|Measurements.SLICE:measurements; + ResultsTable rt = new ResultsTable(); + rt.showRowNumbers(true); + if (appendResults && mmResults2!=null) + rt = mmResults2; + Analyzer analyzer = new Analyzer(imp, measurements2, rt); + analyzer.disableReset(true); + for (int slice=1; slice<=nSlices; slice++) { + if (nSlices>1) imp.setSliceWithoutUpdate(slice); + for (int i=0; i1) + imp.setSlice(currentSlice); + } else { + Roi[] rois = getSelectedRoisAsArray(); + if ("".equals(cmd)) { // run More>>Multi Measure command in separate thread + MultiMeasureRunner mmr = new MultiMeasureRunner(); + mmr.multiMeasure(imp, rois, appendResults); + } else { + ResultsTable rtMulti = multiMeasure(imp, rois, appendResults); + mmResults = (ResultsTable)rtMulti.clone(); + rtMulti.show("Results"); + imp.setSlice(currentSlice); + if (indexes.length>1) + IJ.run("Select None"); + } + } + if (record()) { + if (Recorder.scriptMode()) { + Recorder.recordCall("rt = rm.multiMeasure(imp);"); + Recorder.recordCall("rt.show(\"Results\");"); + } else { + if ((nSlices==1||measureAll) && onePerSlice && !appendResults) + Recorder.record("roiManager", "Multi Measure"); + else { + String options = ""; + if (measureAll) + options += " measure_all"; + if (onePerSlice) + options += " one"; + if (appendResults) + options += " append"; + Recorder.record("roiManager", "multi-measure"+options); + } + } + } + imp.unlock(); + return true; + } + + private static ResultsTable multiMeasure(ImagePlus imp, Roi[] rois, boolean appendResults) { + int nSlices = imp.getStackSize(); + Analyzer aSys = new Analyzer(imp); // System Analyzer + ResultsTable rtSys = Analyzer.getResultsTable(); + ResultsTable rtMulti = new ResultsTable(); + rtMulti.showRowNumbers(true); + if (appendResults && mmResults!=null) + rtMulti = mmResults; + rtSys.reset(); + int currentSlice = imp.getCurrentSlice(); + for (int slice=1; slice<=nSlices; slice++) { + int sliceUse = slice; + if (nSlices==1) sliceUse = currentSlice; + imp.setSliceWithoutUpdate(sliceUse); + rtMulti.incrementCounter(); + if ((Analyzer.getMeasurements()&Measurements.LABELS)!=0) + rtMulti.addLabel("Label", imp.getTitle()); + int roiIndex = 0; + for (int i=0; i0 && (name.length()<9||!Character.isDigit(name.charAt(0)))) + suffix = "("+name+")"; + } + if (head!=null && col!=null && !head.equals("Slice")) + rtMulti.addValue(head+suffix, rtSys.getValue(j,rtSys.getCounter()-1)); + } + } + if (nSlices>1) IJ.showProgress(slice,nSlices); + } + return rtMulti; + } + + int getColumnCount(ImagePlus imp, int measurements) { + ImageStatistics stats = imp.getStatistics(measurements); + ResultsTable rt = new ResultsTable(); + rt.showRowNumbers(true); + Analyzer analyzer = new Analyzer(imp, measurements, rt); + analyzer.saveResults(stats, null); + int count = 0; + for (int i=0; i<=rt.getLastColumn(); i++) { + float[] col = rt.getColumn(i); + String head = rt.getColumnHeading(i); + if (head!=null && col!=null) + count++; + } + return count; + } + + void multiPlot() { + ImagePlus imp = getImage(); + if (imp==null) return; + int[] indexes = getIndexes(); + int n = indexes.length; + if (n==0) return; + if (!imp.lock()) return; + Color[] colors = {Color.blue, Color.green, Color.magenta, Color.red, Color.cyan, Color.yellow}; + if (n>colors.length) { + colors = new Color[n]; + double c = 0; + double inc =150.0/n; + for (int i=0; imaxX) maxX = y[i].length; + if (freeYScale) { + double[] a = Tools.getMinMax(y[i]); + if (a[0]maxY) maxY = a[1]; + } + double[] xx = new double[y[i].length]; + for (int j=0; j1) + IJ.run("Select None"); + if (record()) Recorder.record("roiManager", "Multi Plot"); + imp.unlock(); + } + + private void multiCrop() { + ImagePlus imp = getImage(); + if (imp==null) + return; + int[] indexes = getIndexes(); + int n = indexes.length; + String directory = Prefs.get(MULTI_CROP_DIR, IJ.getDir("downloads")+"stack/"); + String[] formats = {"tif", "png", "jpg"}; + GenericDialog gd = new GenericDialog("Multi Crop"); + gd.setInsets(5, 0, 0); + gd.addDirectoryField("Dir:", directory); + gd.setInsets(2, 70, 10); + gd.addMessage("drag and drop target", IJ.font10, Color.darkGray); + gd.addChoice("Format:", formats, formats[multiCropFormatIndex]); + gd.addCheckbox("Show "+n+" cropped images:", multiCropShow); + gd.addCheckbox("Save "+n+" cropped images:", multiCropSave); + gd.showDialog(); + if (gd.wasCanceled()) + return; + directory = gd.getNextString(); + directory = IJ.addSeparator(directory); + Prefs.set(MULTI_CROP_DIR, directory); + multiCropFormatIndex = gd.getNextChoiceIndex(); + String format = formats[multiCropFormatIndex]; + multiCropShow = gd.getNextBoolean(); + multiCropSave = gd.getNextBoolean(); + String options = ""; + if (multiCropShow) options += " show"; + if (multiCropSave) { + options += " save"; + options += " "+format; + } + if (record()) { + String dir = Recorder.fixPath(directory); + if (Recorder.scriptMode()) + Recorder.recordCall("rm.multiCrop(\""+dir+"\", \""+options+"\");"); + else + Recorder.record("RoiManager.multiCrop", dir, options); + } + multiCrop(directory, options); + } + + public void multiCrop(String directory, String options) { + ImagePlus imp = getImage(); + if (imp==null) + return; + Roi roiOrig = imp.getRoi(); + Roi[] rois = getSelectedRoisAsArray(); + ImagePlus[] images = imp.crop(rois); + if (options==null) options = ""; + if (options.contains("show")) { + ImageStack stack = ImageStack.create(images); + ImagePlus imgStack = new ImagePlus("CROPPED_"+getTitle(),stack); + Overlay overlay = Overlay.createStackOverlay(rois); + imgStack.setOverlay(overlay); + imgStack.show(); + if (roiOrig==null) + imp.deleteRoi(); + } + if (options.contains("save")) { + String format = "tif"; + if (options.contains("png")) format = "png"; + if (options.contains("jpg")) format = "jpg"; + for (int i=0; iwidth) width = bounds.width; + if (bounds.height>height) height = bounds.height; + } + imp = FolderOpener.open(directory, width, height, "virtual"); + if (imp!=null) imp.show(); + } + */ + } + } + + /** Sets the group for the selected ROIs. */ + public void setGroup(int group) { + int[] indexes = getIndexes(); + for (int i: indexes) { + Roi roi = getRoi(i); + roi.setGroup(group); + } + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) imp.draw(); + } + + /** Sets the position for the selected ROIs. */ + public void setPosition(int position) { + int[] indexes = getIndexes(); + for (int i: indexes) { + Roi roi = getRoi(i); + roi.setPosition(position); + } + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) imp.draw(); + } + + /** Obsolete; replaced by RoiManager.setGroup() macro function. */ + public static void setGroup(String group) { + RoiManager rm = getInstance(); + if (rm==null) return; + int groupInt = (int)Tools.parseDouble(group,0); + if (groupInt>0) + rm.setGroup(groupInt); + } + + boolean drawOrFill(int mode) { + int[] indexes = getIndexes(); + ImagePlus imp = getImage(); + if (imp==null) return false; + if (!imp.lock()) return false; + imp.deleteRoi(); + ImageProcessor ip = imp.getProcessor(); + ip.setColor(Toolbar.getForegroundColor()); + ip.snapshot(); + Undo.setup(Undo.FILTER, imp); + Filler filler = mode==LABEL?new Filler():null; + int slice = imp.getCurrentSlice(); + for (int i=0; i=1 && slice2<=imp.getStackSize()) { + imp.setSlice(slice2); + ip = imp.getProcessor(); + ip.setColor(Toolbar.getForegroundColor()); + if (slice2!=slice) Undo.reset(); + } + switch (mode) { + case DRAW: roi.drawPixels(ip); break; + case FILL: ip.fill(roi); break; + case LABEL: + roi.drawPixels(ip); + filler.drawLabel(imp, ip, i+1, roi.getBounds()); + break; + } + } + if (record() && (mode==DRAW||mode==FILL)) + Recorder.record("roiManager", mode==DRAW?"Draw":"Fill"); + if (showAllCheckbox.getState()) + runCommand("show none"); + imp.updateAndDraw(); + imp.unlock(); + return true; + } + + void setProperties(Color color, int lineWidth, Color fillColor) { + boolean showDialog = color==null && lineWidth==-1 && fillColor==null; + int[] indexes = getIndexes(); + int n = indexes.length; + if (n==0) return; + Roi rpRoi = null; + String rpName = null; + Font font = null; + int justification = TextRoi.LEFT; + String roiText = null; + double opacity = -1; + int pointType = -1; + int pointSize = -1; + int group = -1; + int position = -1; + if (showDialog) { + //String label = (String) listModel.getElementAt(indexes[0]); + rpRoi = (Roi)rois.get(indexes[0]); + if (n==1) { + fillColor = rpRoi.getFillColor(); + rpName = rpRoi.getName(); + } + if (rpRoi.getStrokeColor()==null) + rpRoi.setStrokeColor(Roi.getColor()); + rpRoi = (Roi) rpRoi.clone(); + if (n>1) + rpRoi.setName("range: "+(indexes[0]+1)+"-"+(indexes[n-1]+1)); + rpRoi.setFillColor(fillColor); + RoiProperties rp = new RoiProperties("Properties ", rpRoi); // " "=show "List coordinates" + if (!rp.showDialog()) + return; + // Recover parameters of the Property window that were stored in the "transient" roi + lineWidth = (int)rpRoi.getStrokeWidth(); + defaultLineWidth = lineWidth; + color = rpRoi.getStrokeColor(); + fillColor = rpRoi.getFillColor(); + group = rpRoi.getGroup(); + position = rpRoi.getPosition(); + defaultColor = color; + if (rpRoi instanceof TextRoi) { + font = ((TextRoi)rpRoi).getCurrentFont(); + justification = ((TextRoi)rpRoi).getJustification(); + roiText = ((TextRoi)rpRoi).getText(); + } + if (rpRoi instanceof ImageRoi) + opacity = ((ImageRoi)rpRoi).getOpacity(); + if (rpRoi instanceof PointRoi) { + pointType = ((PointRoi)rpRoi).getPointType(); + pointSize = ((PointRoi)rpRoi).getSize(); + } + } + ImagePlus imp = WindowManager.getCurrentImage(); + if (n==getCount() && n>1 && !IJ.isMacro() && imp!=null && imp.getWindow()!=null) { + GenericDialog gd = new GenericDialog("ROI Manager"); + gd.addMessage("Apply changes to all "+n+" selections?"); + gd.showDialog(); + if (gd.wasCanceled()) return; + } + for (int i=0; i=0) + roi.setStrokeWidth(lineWidth); + roi.setFillColor(fillColor); + if (group>=0) + roi.setGroup(group); + if (rpRoi!=null) { + if (rpRoi.hasHyperStackPosition()) + roi.setPosition(rpRoi.getCPosition(), rpRoi.getZPosition(), rpRoi.getTPosition()); + else + roi.setPosition(rpRoi.getPosition()); + } + if ((roi instanceof TextRoi) && showDialog) { + roi.setImage(imp); + if (font!=null) + ((TextRoi)roi).setCurrentFont(font); + ((TextRoi)roi).setJustification(justification); + if (n==1) ((TextRoi)roi).setText(roiText); + roi.setImage(null); + } + if ((roi instanceof ImageRoi) && opacity!=-1) + ((ImageRoi)roi).setOpacity(opacity); + if (roi instanceof PointRoi) { + if (pointType!=-1) ((PointRoi)roi).setPointType(pointType); + if (pointSize!=-1) ((PointRoi)roi).setSize(pointSize); + } + } + if (rpRoi!=null && rpName!=null && !rpRoi.getName().equals(rpName)) + rename(rpRoi.getName()); + ImageCanvas ic = imp!=null?imp.getCanvas():null; + Roi roi = imp!=null?imp.getRoi():null; + boolean showingAll = ic!=null && ic.getShowAllROIs(); + if (roi!=null && (n==1||!showingAll)) { + if (lineWidth>=0) roi.setStrokeWidth(lineWidth); + if (color!=null) roi.setStrokeColor(color); + if (group>=0) roi.setGroup(group); + if (fillColor!=null) roi.setFillColor(fillColor); + if (roi!=null && (roi instanceof TextRoi)) { + ((TextRoi)roi).setCurrentFont(font); + ((TextRoi)roi).setJustification(justification); + ((TextRoi)roi).setText(roiText); + } + if (roi!=null && (roi instanceof ImageRoi) && opacity!=-1) + ((ImageRoi)roi).setOpacity(opacity); + } + if (lineWidth>1 && !showingAll && roi==null) { + showAll(SHOW_ALL); + showingAll = true; + } + if (imp!=null) imp.draw(); + if (record()) { + if (group>=0) { + if (Recorder.scriptMode()) + Recorder.recordCall("rm.setGroup("+group+");"); + else + Recorder.record("RoiManager.setGroup", group); + } + if (position>=0) { + if (Recorder.scriptMode()) + Recorder.recordCall("rm.setPosition("+position+");"); + else + Recorder.record("RoiManager.setPosition", position); + } + if (fillColor!=null) + Recorder.record("roiManager", "Set Fill Color", Colors.colorToString(fillColor)); + else { + if (group==0) + Recorder.record("roiManager", "Set Color", Colors.colorToString(color!=null?color:Color.red)); + Recorder.record("roiManager", "Set Line Width", lineWidth); + } + } + } + + void flatten() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) + {IJ.noImage(); return;} + ImageCanvas ic = imp.getCanvas(); + if ((ic!=null && ic.getShowAllList()==null) && imp.getOverlay()==null && imp.getRoi()==null) + error("Image does not have an overlay or ROI"); + else + IJ.doCommand("Flatten"); // run Image>Flatten in separate thread + } + + public boolean getDrawLabels() { + return labelsCheckbox.getState(); + } + + private void combine() { + ImagePlus imp = getImage(); + if (imp==null) + return; + Roi[] rois = getSelectedRoisAsArray(); + if (rois.length==1 && !IJ.isMacro()) { + error("More than one item must be selected, or none"); + return; + } + if (countPointRois(rois)==rois.length) + combinePoints(imp, rois); + else + combineRois(imp, rois); + } + + private int countPointRois(Roi[] rois) { + int nPointRois = 0; + for (Roi roi : rois) + if (roi.getType()==Roi.POINT) + nPointRois++; + return nPointRois; + } + + private void combineRois(ImagePlus imp, Roi[] rois) { + if (rois.length==1) { + Roi roi2 = (Roi)rois[0].clone(); + roi2.setPosition(0); + roi2.setGroup(0); + imp.setRoi(roi2); + return; + } + IJ.resetEscape(); + ShapeRoi s1=null, s2=null; + for (int i=0; i0) + error(err); + } + + void sort() { + int n = listModel.size(); + if (n==0) + return; + String[] labels = new String[n]; + for (int i=0; i1) + slice = true; + if (imp.getNFrames()>1 && imp.getNSlices()==1) + frame = true; + } + Font font = new Font("SansSerif", Font.BOLD, 12); + GenericDialog gd = new GenericDialog("Remove"); + gd.setInsets(5,15,0); + gd.addMessage("Remove positions for: ", font); + gd.setInsets(6,25,0); + gd.addCheckbox("Channels:", channel); + gd.setInsets(0,25,0); + gd.addCheckbox("Slices:", slice); + gd.setInsets(0,25,0); + gd.addCheckbox("Frames:", frame); + gd.showDialog(); + if (gd.wasCanceled()) + return; + removeChannels = gd.getNextBoolean(); + removeSlices = gd.getNextBoolean(); + removeFrames = gd.getNextBoolean(); + } + if (!removeChannels && !removeSlices && !removeFrames) { + slice = true; + return; + } + for (int i=0; i0) { + String name2 = name.substring(5, name.length()); + roi.setName(name2); + rois.set(index, roi); + listModel.setElementAt(name2, index); + } + int c = roi.getCPosition(); + int z = roi.getZPosition(); + int t = roi.getTPosition(); + if (c>0 || t>0) { + if (removeChannels) c = 0; + if (removeSlices) z = 0; + if (removeFrames) t = 0; + roi.setPosition(c, z, t); + } else + roi.setPosition(0); + } + if (imp!=null) + imp.draw(); + if (record()) { + if (removeChannels) Recorder.record("roiManager", "Remove Channel Info"); + if (removeSlices) Recorder.record("roiManager", "Remove Slice Info"); + if (removeFrames) Recorder.record("roiManager", "Remove Frame Info"); + } + } + + private void help() { + String macro = "run('URL...', 'url="+IJ.URL+"/docs/menus/analyze.html#manager');"; + new MacroRunner(macro); + } + + private void labels() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + showAllCheckbox.setState(true); + labelsCheckbox.setState(true); + showAll(LABELS); + } + try { + IJ.run("Labels..."); + } catch(Exception e) {} + Overlay defaultOverlay = OverlayLabels.createOverlay(); + Prefs.useNamesAsLabels = defaultOverlay.getDrawNames(); + } + + private void options() { + Color c = ImageCanvas.getShowAllColor(); + GenericDialog gd = new GenericDialog("Options"); + //gd.addPanel(makeButtonPanel(gd), GridBagConstraints.CENTER, new Insets(5, 0, 0, 0)); + gd.addCheckbox("Associate \"Show All\" ROIs with slices", Prefs.showAllSliceOnly); + gd.addCheckbox("Restore ROIs centered", restoreCentered); + gd.addCheckbox("Use ROI names as labels", Prefs.useNamesAsLabels); + gd.showDialog(); + if (gd.wasCanceled()) { + if (c!=ImageCanvas.getShowAllColor()) + ImageCanvas.setShowAllColor(c); + return; + } + Prefs.showAllSliceOnly = gd.getNextBoolean(); + restoreCentered = gd.getNextBoolean(); + Prefs.useNamesAsLabels = gd.getNextBoolean(); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + Overlay overlay = imp.getOverlay(); + if (overlay==null) { + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) + overlay = ic.getShowAllList(); + } + if (overlay!=null) { + overlay.drawNames(Prefs.useNamesAsLabels); + setOverlay(imp, overlay); + } else + imp.draw(); + } + if (record()) { + Recorder.record("roiManager", "Associate", Prefs.showAllSliceOnly?"true":"false"); + Recorder.record("roiManager", "Centered", restoreCentered?"true":"false"); + Recorder.record("roiManager", "UseNames", Prefs.useNamesAsLabels?"true":"false"); + } + } + + Panel makeButtonPanel(GenericDialog gd) { + Panel panel = new Panel(); + //buttons.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 0)); + colorButton = new Button("\"Show All\" Color..."); + colorButton.addActionListener(this); + panel.add(colorButton); + return panel; + } + + void setShowAllColor() { + ColorChooser cc = new ColorChooser("\"Show All\" Color", ImageCanvas.getShowAllColor(), false); + ImageCanvas.setShowAllColor(cc.getColor()); + } + + void split() { + ImagePlus imp = getImage(); + if (imp==null) return; + Roi roi = imp.getRoi(); + if (roi==null || roi.getType()!=Roi.COMPOSITE) { + error("Image with composite selection required"); + return; + } + boolean record = Recorder.record; + Recorder.record = false; + Roi[] rois = ((ShapeRoi)roi).getRois(); + for (int i=0; i0) { + Roi[] rois = getRoisAsArray(); + Overlay overlay = newOverlay(); + for (int i=0; i0) { + Roi[] rois = getRoisAsArray(); + Overlay overlay = newOverlay(); + for (int i=0; i

+		DICOM dcm = new DICOM(is);
+		dcm.run("Name");
+		dcm.show();
+		
+	*/
+	public DICOM(InputStream is) {
+		this(new BufferedInputStream(is));
+	}
+
+	/** Constructs a DICOM reader that using an BufferredInputStream. */
+	public DICOM(BufferedInputStream bis) {
+		inputStream = bis;
+	}
+
+	public void run(String arg) {
+		OpenDialog od = new OpenDialog("Open Dicom...", arg);
+		String directory = od.getDirectory();
+		String fileName = od.getFileName();
+		if (fileName==null)
+			return;
+		DicomDecoder dd = new DicomDecoder(directory, fileName);
+		dd.inputStream = inputStream;
+		FileInfo fi = null;
+		try {
+			fi = dd.getFileInfo();
+		} catch (IOException e) {
+			String msg = e.getMessage();
+			IJ.showStatus("");
+			if (msg.indexOf("EOF")<0&&showErrors) {
+				IJ.error("DICOM Reader", e.getClass().getName()+"\n \n"+msg);
+				return;
+			} else if (!dd.dicmFound()&&showErrors) {
+				msg = "This does not appear to be a valid\n"
+				+ "DICOM file. It does not have the\n"
+				+ "characters 'DICM' at offset 128.";
+				IJ.error("DICOM Reader", msg);
+				return;
+			}
+		}
+		if (gettingInfo) {
+			info = dd.getDicomInfo();
+			return;
+		}
+		if (fi!=null && fi.width>0 && fi.height>0 && fi.offset>0) {
+			FileOpener fo = new FileOpener(fi);
+			ImagePlus imp = fo.openImage();
+			// Avoid opening as float even if slope != 1.0 in case ignoreRescaleSlope or fixedDicomScaling
+			// were checked in the DICOM preferences.
+			boolean openAsFloat = (dd.rescaleSlope!=1.0 && !(Prefs.ignoreRescaleSlope || Prefs.fixedDicomScaling)) 
+				|| Prefs.openDicomsAsFloat;
+			String options = Macro.getOptions();
+			if (openAsFloat) {
+				IJ.run(imp, "32-bit", "");
+				if (dd.rescaleSlope!=1.0)
+					IJ.run(imp, "Multiply...", "value="+dd.rescaleSlope+" stack");
+				if (dd.rescaleIntercept!=0.0)
+					IJ.run(imp, "Add...", "value="+dd.rescaleIntercept+" stack");
+				if (imp.getStackSize()>1) {
+				    imp.setSlice(imp.getStackSize()/2);
+					ImageStatistics stats = imp.getRawStatistics();
+					imp.setDisplayRange(stats.min,stats.max);
+				}
+			} else if (fi.fileType==FileInfo.GRAY16_SIGNED) {
+				if (dd.rescaleIntercept!=0.0 && (dd.rescaleSlope==1.0||Prefs.fixedDicomScaling)) {
+					double[] coeff = new double[2];
+					coeff[0] = dd.rescaleSlope*(-32768) + dd.rescaleIntercept;
+					coeff[1] = dd.rescaleSlope;
+					imp.getCalibration().setFunction(Calibration.STRAIGHT_LINE, coeff, "Gray Value");
+				}
+			} else if (dd.rescaleIntercept!=0.0 && 
+					  (dd.rescaleSlope==1.0||Prefs.fixedDicomScaling||fi.fileType==FileInfo.GRAY8)) {
+				double[] coeff = new double[2];
+				coeff[0] = dd.rescaleIntercept;
+				coeff[1] = dd.rescaleSlope;
+				imp.getCalibration().setFunction(Calibration.STRAIGHT_LINE, coeff, "Gray Value");
+			}
+			Macro.setOptions(options);
+			if (dd.windowWidth>0.0) {
+				double min = dd.windowCenter-dd.windowWidth/2;
+				double max = dd.windowCenter+dd.windowWidth/2;
+				if (!openAsFloat) {
+					Calibration cal = imp.getCalibration();
+					min = cal.getRawValue(min);
+					max = cal.getRawValue(max);
+				}
+				ImageProcessor ip = imp.getProcessor();
+				ip.setMinAndMax(min, max);
+				if (IJ.debugMode) IJ.log("window: "+min+"-"+max);
+			}
+			if (imp.getStackSize()>1)
+				setStack(fileName, imp.getStack());
+			else
+				setProcessor(fileName, imp.getProcessor());
+			setCalibration(imp.getCalibration());
+			setProperty("Info", dd.getDicomInfo());
+			setFileInfo(fi); // needed for revert
+			if (arg.equals("")) show();
+		} else if (showErrors)
+			IJ.error("DICOM Reader","Unable to decode DICOM header.");
+		IJ.showStatus("");
+	}
+
+	/** Opens the specified file as a DICOM. Does not 
+		display a message if there is an error.
+		Here is an example:
+		
+		DICOM dcm = new DICOM();
+		dcm.open(path);
+		if (dcm.getWidth()==0)
+			IJ.log("Error opening '"+path+"'");
+		else
+			dcm.show();
+		
+ */ + public void open(String path) { + showErrors = false; + run(path); + } + + /** Returns the DICOM tags of the specified file as a string. */ + public String getInfo(String path) { + showErrors = false; + gettingInfo = true; + run(path); + return info; + } + + /** Convert 16-bit signed to unsigned if all pixels>=0. */ + void convertToUnsigned(ImagePlus imp, FileInfo fi) { + ImageProcessor ip = imp.getProcessor(); + short[] pixels = (short[])ip.getPixels(); + int min = Integer.MAX_VALUE; + int value; + for (int i=0; i=32768) { + for (int i=0; i60) + s = s.substring(0,60); + return s; + } + + int getByte() throws IOException { + int b = f.read(); + if (b ==-1) + throw new IOException("unexpected EOF"); + ++location; + return b; + } + + int getShort() throws IOException { + int b0 = getByte(); + int b1 = getByte(); + if (littleEndian) + return ((b1 << 8) + b0); + else + return ((b0 << 8) + b1); + } + + int getSShort() throws IOException { + short b0 = (short)getByte(); + short b1 = (short)getByte(); + if (littleEndian) + return ((b1 << 8) + b0); + else + return ((b0 << 8) + b1); + } + + final int getInt() throws IOException { + int b0 = getByte(); + int b1 = getByte(); + int b2 = getByte(); + int b3 = getByte(); + if (littleEndian) + return ((b3<<24) + (b2<<16) + (b1<<8) + b0); + else + return ((b0<<24) + (b1<<16) + (b2<<8) + b3); + } + + long getUInt() throws IOException { + long b0 = getByte(); + long b1 = getByte(); + long b2 = getByte(); + long b3 = getByte(); + if (littleEndian) + return ((b3<<24) + (b2<<16) + (b1<<8) + b0); + else + return ((b0<<24) + (b1<<16) + (b2<<8) + b3); + } + + double getDouble() throws IOException { + int b0 = getByte(); + int b1 = getByte(); + int b2 = getByte(); + int b3 = getByte(); + int b4 = getByte(); + int b5 = getByte(); + int b6 = getByte(); + int b7 = getByte(); + long res = 0; + if (littleEndian) { + res += b0; + res += ( ((long)b1) << 8); + res += ( ((long)b2) << 16); + res += ( ((long)b3) << 24); + res += ( ((long)b4) << 32); + res += ( ((long)b5) << 40); + res += ( ((long)b6) << 48); + res += ( ((long)b7) << 56); + } else { + res += b7; + res += ( ((long)b6) << 8); + res += ( ((long)b5) << 16); + res += ( ((long)b4) << 24); + res += ( ((long)b3) << 32); + res += ( ((long)b2) << 40); + res += ( ((long)b1) << 48); + res += ( ((long)b0) << 56); + } + return Double.longBitsToDouble(res); + } + + float getFloat() throws IOException { + int b0 = getByte(); + int b1 = getByte(); + int b2 = getByte(); + int b3 = getByte(); + int res = 0; + if (littleEndian) { + res += b0; + res += ( ((long)b1) << 8); + res += ( ((long)b2) << 16); + res += ( ((long)b3) << 24); + } else { + res += b3; + res += ( ((long)b2) << 8); + res += ( ((long)b1) << 16); + res += ( ((long)b0) << 24); + } + return Float.intBitsToFloat(res); + } + + byte[] getLut(int length) throws IOException { + if ((length&1)!=0) { // odd + String dummy = getString(length); + return null; + } + length /= 2; + byte[] lut = new byte[length]; + for (int i=0; i>>8); + return lut; + } + + int getLength() throws IOException { + int b0 = getByte(); + int b1 = getByte(); + int b2 = getByte(); + int b3 = getByte(); + + // We cannot know whether the VR is implicit or explicit + // without the full DICOM Data Dictionary for public and + // private groups. + + // We will assume the VR is explicit if the two bytes + // match the known codes. It is possible that these two + // bytes are part of a 32-bit length for an implicit VR. + + vr = (b0<<8) + b1; + + switch (vr) { + case OB: case OW: case SQ: case UN: case UT: + case OF: case OL: case OD: case UC: case UR: + case OV: case SV: case UV: + // Explicit VR with 32-bit length if other two bytes are zero + if ( (b2 == 0) || (b3 == 0) ) return getInt(); + // Implicit VR with 32-bit length + vr = IMPLICIT_VR; + if (littleEndian) + return ((b3<<24) + (b2<<16) + (b1<<8) + b0); + else + return ((b0<<24) + (b1<<16) + (b2<<8) + b3); + case AE: case AS: case AT: case CS: case DA: case DS: case DT: case FD: + case FL: case IS: case LO: case LT: case PN: case SH: case SL: case SS: + case ST: case TM: case UI: case UL: case US: case QQ: + // Explicit vr with 16-bit length + if (littleEndian) + return ((b3<<8) + b2); + else + return ((b2<<8) + b3); + default: + // Implicit VR with 32-bit length... + vr = IMPLICIT_VR; + if (littleEndian) + return ((b3<<24) + (b2<<16) + (b1<<8) + b0); + else + return ((b0<<24) + (b1<<16) + (b2<<8) + b3); + } + } + + int getNextTag() throws IOException { + int groupWord = getShort(); + if (groupWord==0x0800 && bigEndianTransferSyntax) { + littleEndian = false; + groupWord = 0x0008; + } + int elementWord = getShort(); + int tag = groupWord<<16 | elementWord; + elementLength = getLength(); + + // hack needed to read some GE files + // The element length must be even! + if (elementLength==13 && !oddLocations) elementLength = 10; + + // "Undefined" element length. + // This is a sort of bracket that encloses a sequence of elements. + if (elementLength==-1) { + elementLength = 0; + inSequence = true; + } + //IJ.log("getNextTag: "+tag+" "+elementLength); + return tag; + } + + FileInfo getFileInfo() throws IOException { + long skipCount; + FileInfo fi = new FileInfo(); + int bitsAllocated = 16; + fi.fileFormat = fi.RAW; + fi.fileName = fileName; + if (directory.indexOf("://")>0) { // is URL + URL u = new URL(directory+fileName); + inputStream = new BufferedInputStream(u.openStream()); + fi.inputStream = inputStream; + } else if (inputStream!=null) + fi.inputStream = inputStream; + else + fi.directory = directory; + fi.width = 0; + fi.height = 0; + fi.offset = 0; + fi.intelByteOrder = true; + fi.fileType = FileInfo.GRAY16_UNSIGNED; + fi.fileFormat = FileInfo.DICOM; + int samplesPerPixel = 1; + int planarConfiguration = 0; + String photoInterpretation = ""; + + if (inputStream!=null) { + // Use large buffer to allow URL stream to be reset after reading header + f = inputStream; + f.mark(400000); + } else + f = new BufferedInputStream(new FileInputStream(directory + fileName)); + if (IJ.debugMode) { + IJ.log(""); + IJ.log("DicomDecoder: decoding "+fileName); + } + + int[] bytes = new int[ID_OFFSET]; + for (int i=0; i-1||s.indexOf("1.2.5")>-1) { + f.close(); + String msg = "ImageJ cannot open compressed DICOM images.\n \n"; + msg += "Transfer Syntax UID = "+s; + throw new IOException(msg); + } + if (s.indexOf("1.2.840.10008.1.2.2")>=0) + bigEndianTransferSyntax = true; + break; + case MODALITY: + modality = getString(elementLength); + addInfo(tag, modality); + break; + case NUMBER_OF_FRAMES: + s = getString(elementLength); + addInfo(tag, s); + double frames = s2d(s); + if (frames>1.0) + fi.nImages = (int)frames; + break; + case SAMPLES_PER_PIXEL: + samplesPerPixel = getShort(); + addInfo(tag, samplesPerPixel); + break; + case PHOTOMETRIC_INTERPRETATION: + photoInterpretation = getString(elementLength); + addInfo(tag, photoInterpretation); + break; + case PLANAR_CONFIGURATION: + planarConfiguration = getShort(); + addInfo(tag, planarConfiguration); + break; + case ROWS: + fi.height = getShort(); + addInfo(tag, fi.height); + break; + case COLUMNS: + fi.width = getShort(); + addInfo(tag, fi.width); + break; + case IMAGER_PIXEL_SPACING: case PIXEL_SPACING: + String scale = getString(elementLength); + getSpatialScale(fi, scale); + addInfo(tag, scale); + break; + case SLICE_THICKNESS: case SLICE_SPACING: + String spacing = getString(elementLength); + fi.pixelDepth = s2d(spacing); + addInfo(tag, spacing); + break; + case BITS_ALLOCATED: + bitsAllocated = getShort(); + if (bitsAllocated==8) + fi.fileType = FileInfo.GRAY8; + else if (bitsAllocated==32) + fi.fileType = FileInfo.GRAY32_UNSIGNED; + addInfo(tag, bitsAllocated); + break; + case PIXEL_REPRESENTATION: + int pixelRepresentation = getShort(); + if (pixelRepresentation==1) { + fi.fileType = FileInfo.GRAY16_SIGNED; + signed = true; + } + addInfo(tag, pixelRepresentation); + break; + case WINDOW_CENTER: + String center = getString(elementLength); + int index = center.indexOf('\\'); + if (index!=-1) center = center.substring(index+1); + windowCenter = s2d(center); + addInfo(tag, center); + break; + case WINDOW_WIDTH: + String width = getString(elementLength); + index = width.indexOf('\\'); + if (index!=-1) width = width.substring(index+1); + windowWidth = s2d(width); + addInfo(tag, width); + break; + case RESCALE_INTERCEPT: + String intercept = getString(elementLength); + rescaleIntercept = s2d(intercept); + addInfo(tag, intercept); + break; + case RESCALE_SLOPE: + String slop = getString(elementLength); + rescaleSlope = s2d(slop); + addInfo(tag, slop); + break; + case RED_PALETTE: + fi.reds = getLut(elementLength); + addInfo(tag, elementLength/2); + break; + case GREEN_PALETTE: + fi.greens = getLut(elementLength); + addInfo(tag, elementLength/2); + break; + case BLUE_PALETTE: + fi.blues = getLut(elementLength); + addInfo(tag, elementLength/2); + break; + case FLOAT_PIXEL_DATA: + fi.fileType = FileInfo.GRAY32_FLOAT; + // continue without break + case PIXEL_DATA: + // Start of image data... + if (elementLength!=0) { + fi.offset = location; + addInfo(tag, location); + decodingTags = false; + } else + addInfo(tag, null); + break; + case 0x7F880010: + // What is this? - RAK + if (elementLength!=0) { + fi.offset = location+4; + decodingTags = false; + } + break; + default: + // Not used, skip over it... + addInfo(tag, null); + } + } // while(decodingTags) + + if (fi.fileType==FileInfo.GRAY8) { + if (fi.reds!=null && fi.greens!=null && fi.blues!=null + && fi.reds.length==fi.greens.length + && fi.reds.length==fi.blues.length) { + fi.fileType = FileInfo.COLOR8; + fi.lutSize = fi.reds.length; + + } + } + + if (fi.fileType==FileInfo.GRAY32_UNSIGNED && signed) + fi.fileType = FileInfo.GRAY32_INT; + + if (samplesPerPixel==3 && photoInterpretation.startsWith("RGB")) { + if (planarConfiguration==0) + fi.fileType = FileInfo.RGB; + else if (planarConfiguration==1) + fi.fileType = FileInfo.RGB_PLANAR; + } else if (photoInterpretation.endsWith("1 ")) + fi.whiteIsZero = true; + + if (!littleEndian) + fi.intelByteOrder = false; + + if (IJ.debugMode) { + IJ.log("width: " + fi.width); + IJ.log("height: " + fi.height); + IJ.log("images: " + fi.nImages); + IJ.log("bits allocated: " + bitsAllocated); + IJ.log("offset: " + fi.offset); + } + + if (inputStream!=null) + f.reset(); + else + f.close(); + return fi; + } + + String getDicomInfo() { + String s = new String(dicomInfo); + char[] chars = new char[s.length()]; + s.getChars(0, s.length(), chars, 0); + for (int i=0; i>>16; + //if (group!=previousGroup && (previousInfo!=null&&previousInfo.indexOf("Sequence:")==-1)) + // dicomInfo.append("\n"); + previousGroup = group; + previousInfo = info; + dicomInfo.append(tag2hex(tag)+info+"\n"); + } + if (IJ.debugMode) { + if (info==null) info = ""; + vrLetters[0] = (byte)(vr >> 8); + vrLetters[1] = (byte)(vr & 0xFF); + String VR = new String(vrLetters); + IJ.log("(" + tag2hex(tag) + VR + + " " + elementLength + + " bytes from " + + (location-elementLength)+") " + + info); + } + } + + void addInfo(int tag, int value) throws IOException { + addInfo(tag, Integer.toString(value)); + } + + String getHeaderInfo(int tag, String value) throws IOException { + if (tag==ITEM_DELIMINATION || tag==SEQUENCE_DELIMINATION) { + inSequence = false; + if (!IJ.debugMode) return null; + } + String key = i2hex(tag); + //while (key.length()<8) + // key = '0' + key; + String id = (String)dictionary.get(key); + if (id!=null) { + if (vr==IMPLICIT_VR && id!=null) + vr = (id.charAt(0)<<8) + id.charAt(1); + id = id.substring(2); + } + if (tag==ITEM) + return id!=null?id+":":null; + if (value!=null) + return id+": "+value; + switch (vr) { + case FD: + if (elementLength==8) + value = Double.toString(getDouble()); + else + for (int i=0; i44) value=null; + break; + case SQ: + value = ""; + if (tag==ACQUISITION_CONTEXT_SEQUENCE) + acquisitionSequence = true; + if (tag==VIEW_CODE_SEQUENCE) + acquisitionSequence = false; + boolean privateTag = ((tag>>16)&1)!=0; + if (tag!=ICON_IMAGE_SEQUENCE && !privateTag) + break; + // else fall through and skip icon image sequence or private sequence + default: + long skipCount = (long)elementLength; + while (skipCount > 0) skipCount -= f.skip(skipCount); + location += elementLength; + value = ""; + } + if (value!=null && id==null && !value.equals("")) + return "---: "+value; + else if (id==null) + return null; + else + return id+": "+value; + } + + static char[] buf8 = new char[8]; + + /** Converts an int to an 8 byte hex string. */ + String i2hex(int i) { + for (int pos=7; pos>=0; pos--) { + buf8[pos] = Tools.hexDigits[i&0xf]; + i >>>= 4; + } + return new String(buf8); + } + + char[] buf10; + + String tag2hex(int tag) { + if (buf10==null) { + buf10 = new char[11]; + buf10[4] = ','; + buf10[9] = ' '; + } + int pos = 8; + while (pos>=0) { + buf10[pos] = Tools.hexDigits[tag&0xf]; + tag >>>= 4; + pos--; + if (pos==4) pos--; // skip coma + } + return new String(buf10); + } + + double s2d(String s) { + if (s==null) return 0.0; + if (s.startsWith("\\")) + s = s.substring(1); + Double d; + try {d = new Double(s);} + catch (NumberFormatException e) {d = null;} + if (d!=null) + return(d.doubleValue()); + else + return(0.0); + } + + void getSpatialScale(FileInfo fi, String scale) { + double xscale=0, yscale=0; + int i = scale.indexOf('\\'); + if (i>0) { + yscale = s2d(scale.substring(0, i)); + xscale = s2d(scale.substring(i+1)); + } + if (xscale!=0.0 && yscale!=0.0) { + fi.pixelWidth = xscale; + fi.pixelHeight = yscale; + fi.unit = "mm"; + } + } + + boolean dicmFound() { + return dicmFound; + } + +} + + +class DicomDictionary { + + Properties getDictionary() { + Properties p = new Properties(); + for (int i=0; i=0) + data = rt.getColumn(index); + if (data==null) { + IJ.error("Distribution", "No available results: \""+parameter+"\""); + return; + } + + float [] pars = new float [11]; + stats(count, data, pars); + if (autoBinning) { + //sd = 7, min = 3, max = 4 + // use Scott's method (1979 Biometrika, 66:605-610) for optimal binning: 3.49*sd*N^-1/3 + float binWidth = (float)(3.49 * pars[7]*(float)Math.pow((float)count, -1.0/3.0)); + nBins= (int)Math.floor(((pars[4]-pars[3])/binWidth)+.5); + if (nBins<2) nBins = 2; + } + + ImageProcessor ip = new FloatProcessor(count, 1, data, null); + ImagePlus imp = new ImagePlus("", ip); + ImageStatistics stats = new StackStatistics(imp, nBins, nMin, nMax); + int maxCount = 0; + for (int i=0; imaxCount) + maxCount = stats.histogram[i]; + } + stats.histYMax = maxCount; + new HistogramWindow(parameter+" Distribution", imp, stats); + } + + int getIndex(String[] strings) { + for (int i=0; imax) max = data[i]; + } + + ave = totl/nc; + + for(i=0;i 0){ + skew = (float)skew / (nc * (float) Math.pow(sdev,3)); + kurt = (float)kurt / (nc * (float) Math.pow(var, 2)) - 3; + } + pars[1]=(float) nc; + pars[2]=totl; + pars[3]=min; + pars[4]=max; + pars[5]=ave; + pars[6]=adev; + pars[7]=sdev; + pars[8]=var; + pars[9]=skew; + pars[10]=kurt; + + } + +} diff --git a/src/ij/plugin/DragAndDrop.java b/src/ij/plugin/DragAndDrop.java new file mode 100644 index 0000000..a7e7e8c --- /dev/null +++ b/src/ij/plugin/DragAndDrop.java @@ -0,0 +1,220 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.io.*; +import ij.process.ImageProcessor; +import ij.plugin.frame.Recorder; +import java.io.*; +import java.awt.Point; +import java.awt.datatransfer.*; +import java.awt.dnd.*; +import java.util.*; +import java.util.Iterator; +import java.util.ArrayList; + +/** This class opens images, roi's, luts and text files dragged and dropped on the "ImageJ" window. + It is based on the Draw_And_Drop plugin by Eric Kischell (keesh@ieee.org). + + 10 November 2006: Albert Cardona added Linux support and an + option to open all images in a dragged folder as a stack. +*/ + +public class DragAndDrop implements PlugIn, DropTargetListener, Runnable { + private Iterator iterator; + private static boolean convertToRGB; + private static boolean virtualStack; + private boolean openAsVirtualStack; + + public void run(String arg) { + ImageJ ij = IJ.getInstance(); + ij.setDropTarget(null); + new DropTarget(ij, this); + new DropTarget(Toolbar.getInstance(), this); + new DropTarget(ij.getStatusBar(), this); + } + + public void drop(DropTargetDropEvent dtde) { + dtde.acceptDrop(DnDConstants.ACTION_COPY); + DataFlavor[] flavors = null; + try { + Transferable t = dtde.getTransferable(); + iterator = null; + flavors = t.getTransferDataFlavors(); + if (IJ.debugMode) IJ.log("DragAndDrop.drop: "+flavors.length+" flavors"); + for (int i=0; i1 && (int)s.charAt(1)==0) + s = fixLinuxString(s); + ArrayList list = new ArrayList(); + if (s.indexOf("href=\"")!=-1 || s.indexOf("src=\"")!=-1) { + s = parseHTML(s); + if (IJ.debugMode) IJ.log(" url: "+s); + list.add(s); + this.iterator = list.iterator(); + break; + } + BufferedReader br = new BufferedReader(new StringReader(s)); + String tmp; + while (null != (tmp = br.readLine())) { + tmp = java.net.URLDecoder.decode(tmp.replaceAll("\\+","%2b"), "UTF-8"); + if (tmp.startsWith("file://")) tmp = tmp.substring(7); + if (IJ.debugMode) IJ.log(" content: "+tmp); + if (tmp.startsWith("http://")) + list.add(s); + else + list.add(new File(tmp)); + } + this.iterator = list.iterator(); + break; + } + } + if (iterator!=null) { + Thread thread = new Thread(this, "DrawAndDrop"); + thread.setPriority(Math.max(thread.getPriority()-1, Thread.MIN_PRIORITY)); + thread.start(); + } + } catch(Exception e) { + dtde.dropComplete(false); + return; + } + dtde.dropComplete(true); + if (flavors==null || flavors.length==0) { + if (IJ.isMacOSX()) + IJ.error("First drag and drop ignored. Please try again. You can avoid this\n" + +"problem by dragging to the toolbar instead of the status bar."); + else + IJ.error("Drag and drop failed"); + } + } + + private String fixLinuxString(String s) { + StringBuffer sb = new StringBuffer(200); + for (int i=0; i=0) { + int index2 = s.indexOf("\"", index1+5); + if (index2>0) + return s.substring(index1+5, index2); + } + index1 = s.indexOf("href=\""); + if (index1>=0) { + int index2 = s.indexOf("\"", index1+6); + if (index2>0) + return s.substring(index1+6, index2); + } + return s; + } + + public void dragEnter(DropTargetDragEvent e) { + IJ.showStatus("<>"); + if (IJ.debugMode) IJ.log("DragEnter: "+e.getLocation()); + e.acceptDrag(DnDConstants.ACTION_COPY); + openAsVirtualStack = false; + } + + public void dragOver(DropTargetDragEvent e) { + if (IJ.debugMode) IJ.log("DragOver: "+e.getLocation()); + Point loc = e.getLocation(); + int buttonSize = Toolbar.getButtonSize(); + int width = IJ.getInstance().getSize().width; + openAsVirtualStack = width-loc.x<=(buttonSize+buttonSize/3); + if (openAsVirtualStack) + IJ.showStatus("<>"); + else + IJ.showStatus("<>"); + } + + public void dragExit(DropTargetEvent e) { + IJ.showStatus(""); + } + + public void dropActionChanged(DropTargetDragEvent e) {} + + public void run() { + Iterator iterator = this.iterator; + while(iterator.hasNext()) { + Object obj = iterator.next(); + String str = ""+obj; + if (str!=null && str.startsWith("https:/")) { + if (!str.startsWith("https://")) + str = str.replace("https:/", "http://"); + obj = str; + } + if (obj!=null && (obj instanceof String)) + openURL((String)obj); + else + openFile((File)obj); + } + } + + /** Open a URL. */ + private void openURL(String url) { + if (IJ.debugMode) IJ.log("DragAndDrop.openURL: "+url); + if (url!=null) + IJ.open(url); + } + + /** Open a file. If it's a directory, ask to open all images as a sequence in a stack or individually. */ + public void openFile(File f) { + if (IJ.debugMode) IJ.log("DragAndDrop.openFile: "+f); + try { + if (null == f) return; + String path = f.getCanonicalPath(); + if (f.exists()) { + if (f.isDirectory()) { + if (openAsVirtualStack) + IJ.run("Image Sequence...", "open=[" + path + "] sort use"); + else + openDirectory(f, path); + } else { + if (openAsVirtualStack && (path.endsWith(".tif")||path.endsWith(".TIF"))) + (new FileInfoVirtualStack()).run(path); + else if (openAsVirtualStack && (path.endsWith(".avi")||path.endsWith(".AVI"))) + IJ.run("AVI...", "open=["+path+"] use"); + else if (openAsVirtualStack && (path.endsWith(".txt"))) { + ImageProcessor ip = (new TextReader()).open(path); + if (ip!=null) + new ImagePlus(f.getName(),ip).show(); + } else { + Recorder.recordOpen(path); + (new Opener()).openAndAddToRecent(path); + } + OpenDialog.setLastDirectory(f.getParent()+File.separator); + OpenDialog.setLastName(f.getName()); + } + } else { + IJ.log("File not found: " + path); + } + } catch (Throwable e) { + if (!Macro.MACRO_CANCELED.equals(e.getMessage())) + IJ.handleException(e); + } + } + + private void openDirectory(File f, String path) { + if (path==null) return; + path = IJ.addSeparator(path); + String[] names = f.list(); + names = (new FolderOpener()).trimFileList(names); + if (names==null) + return; + FolderOpener fo = new FolderOpener(); + fo.setDirectory(path); + fo.run(""); + } + +} diff --git a/src/ij/plugin/Duplicator.java b/src/ij/plugin/Duplicator.java new file mode 100644 index 0000000..3e73c6b --- /dev/null +++ b/src/ij/plugin/Duplicator.java @@ -0,0 +1,705 @@ +package ij.plugin; +import java.awt.*; +import java.awt.event.*; +import java.util.Vector; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.util.Tools; +import ij.plugin.frame.Recorder; +import ij.measure.Calibration; + +/** This plugin implements the Image/Duplicate command. +
+   // test script
+   img1 = IJ.getImage();
+   img2 = new Duplicator().run(img1);
+   //img2 = new Duplicator().run(img1,1,10);
+   img2.show();
+
+*/ +public class Duplicator implements PlugIn, TextListener, ItemListener { + private static boolean staticDuplicateStack; + private static boolean staticIgnoreSelection; + private static boolean ignoreNextSelection; + private boolean duplicateStack; + private boolean ignoreSelection; + private int first, last; + private Checkbox stackCheckbox; + private TextField titleField, rangeField; + private TextField[] rangeFields; + private int firstC, lastC, firstZ, lastZ, firstT, lastT; + private String defaultTitle; + private String sliceLabel; + private ImagePlus imp; + private boolean legacyMacro; + private boolean titleChanged; + private GenericDialog gd; + + public void run(String arg) { + imp = IJ.getImage(); + Roi roiA = imp.getRoi(); + ImagePlus impA = imp; + boolean isRotatedRect = (roiA!=null && roiA instanceof RotatedRectRoi); + if (isRotatedRect) { + Rectangle bounds = imp.getRoi().getBounds(); + imp.setRoi(bounds); + } + if (roiA!=null) { + Rectangle r = roiA.getBounds(); + if (r.x>=imp.getWidth() || r.y>=imp.getHeight() || r.x+r.width<=0 || r.y+r.height<=0) { + IJ.error("Roi is outside image"); + return; + } + } + int stackSize = imp.getStackSize(); + String title = imp.getTitle(); + String newTitle = WindowManager.getUniqueName(imp, title); + defaultTitle = newTitle; + duplicateStack = staticDuplicateStack && !IJ.isMacro(); + ignoreSelection = (staticIgnoreSelection||ignoreNextSelection) && Macro.getOptions()==null; + if (!IJ.altKeyDown()||stackSize>1) { + if (imp.isHyperStack() || imp.isComposite()) { + duplicateHyperstack(imp, newTitle); + if (isRotatedRect) { + straightenRotatedRect(impA, roiA, IJ.getImage()); + } + return; + } else + newTitle = showDialog(imp, "Duplicate...", "Title: "); + } + if (newTitle==null) { + if (isRotatedRect) + imp.setRoi(roiA); + return; + } + ImagePlus imp2; + Roi roi = imp.getRoi(); + if (ignoreSelection && roi!=null) + imp.deleteRoi(); + if (duplicateStack && (first>1||last1 && imp2.getStackSize()==stackSize) + imp2.setSlice(imp.getCurrentSlice()); + if (isRotatedRect) + straightenRotatedRect(impA, roiA, imp2); + } + + private void recordCrop(ImagePlus imp) { + if (!Recorder.record) + return; + if (imp.getStackSize()==1) { + if (imp.getRoi()==null || ignoreSelection) + Recorder.recordCall("imp = imp.duplicate();"); + else + Recorder.recordCall("imp = imp.crop();"); + } else if (imp.getRoi()==null || ignoreSelection) { + if (duplicateStack) + Recorder.recordCall("imp = imp.duplicate();"); + else + Recorder.recordCall("imp = imp.crop(\"whole-slice\");"); + } else { + if (duplicateStack) + Recorder.recordCall("imp = imp.crop();"); + else + Recorder.recordCall("imp = imp.crop(\"slice\");"); + } + } + + /** Rotates duplicated part of image + - impA is original image, + - roiA is orig rotatedRect + - impB contains duplicated overlapping bounding rectangle + processing steps: + - increase canvas of impB before rotation + - rotate impB + - calculate excentricity + - translate to compensate excentricity + - create orthogonal rectangle in center + - crop to impC + Author: N. Vischer + */ + private void straightenRotatedRect(ImagePlus impA, Roi roiA, ImagePlus impB) { + impB.deleteRoi(); //we have it in roiA + Color colorBack = Toolbar.getBackgroundColor(); + IJ.setBackgroundColor(0,0,0); + String title = impB.getTitle(); + if(impB.getOverlay() != null) + impB.getOverlay().clear(); + int boundLeft = roiA.getBounds().x; + int boundTop = roiA.getBounds().y; + int boundWidth = roiA.getBounds().width; + int boundHeight = roiA.getBounds().height; + + float[] xx = roiA.getFloatPolygon().xpoints; + float[] yy = roiA.getFloatPolygon().ypoints; + + double dx1 = xx[1] - xx[0];//calc sides and angle + double dy1 = yy[1] - yy[0]; + double dx2 = xx[2] - xx[1]; + double dy2 = yy[2] - yy[1]; + + double rrWidth = Math.sqrt(dx1 * dx1 + dy1 * dy1);//width of rot rect + double rrHeight = Math.sqrt(dx2 * dx2 + dy2 * dy2); + double rrDia = Math.sqrt(rrWidth * rrWidth + rrHeight * rrHeight); + + double phi1 = -Math.atan2(dy1, dx1); + double phi0 = phi1 * 180 / Math.PI; + + double usedL = Math.max(boundLeft, 0); //usedrect is orthogonal rect to be rotated + double usedR = Math.min(boundLeft + boundWidth, impA.getWidth()); + double usedT = Math.max(boundTop, 0); + double usedB = Math.min(boundTop + boundHeight, impA.getHeight()); + double usedCX = (usedL + usedR) / 2; + double usedCY = (usedT + usedB) / 2; //Center of UsedRect + + double boundsCX = boundLeft + boundWidth / 2;//Center of Bound = center of RotRect + double boundsCY = boundTop + boundHeight / 2; + + double dx3 = boundsCX - usedCX;//calculate excentricity + double dy3 = boundsCY - usedCY; + double rad3 = Math.sqrt(dx3 * dx3 + dy3 * dy3); + double phi3 = Math.atan2(dy3, dx3); + double phi4 = phi3 + phi1; + double dx4 = -rad3 * Math.cos(phi4); + double dy4 = -rad3 * Math.sin(phi4); + + //Increase canvas to a square large enough for rotation + ImageStack stackOld = impB.getStack(); + int currentSlice = impB.getCurrentSlice(); + double xOff = (rrDia - (usedR - usedL)) / 2;//put img in center + double yOff = (rrDia - (usedB - usedT)) / 2; + + ImageStack stackNew = (new CanvasResizer()).expandStack(stackOld, (int) rrDia, (int) rrDia, (int) xOff, (int) yOff); + impB.setStack(stackNew); + ImageProcessor ip = impB.getProcessor(); + ip.setInterpolationMethod(ImageProcessor.BILINEAR); + ip.setBackgroundValue(0); + + for (int slc = 0; slc < stackNew.size(); slc++) { + impB.setSlice(slc+1); + ip.rotate(phi0); //Rotate + ip.translate(dx4, dy4); //Translate + } + + int x = (impB.getWidth() - (int) rrWidth) / 2; + int y = (impB.getHeight() - (int) rrHeight) / 2; + + impB.setStack(impB.getStack().crop(x, y, 0, (int) rrWidth, (int) rrHeight, impB.getStack().getSize()));//Crop + impB.setSlice(currentSlice); + impB.setTitle(title); + impB.show(); + impB.updateAndDraw(); + impA.setRoi(roiA); //restore rotated rect in source image + Toolbar.setBackgroundColor(colorBack); + } + + /** Returns a copy of the image, stack or hyperstack contained in the specified ImagePlus. + * @see ij.ImagePlus#duplicate + */ + public ImagePlus run(ImagePlus imp) { + if (imp.getStackSize()==1) + return crop(imp); + Rectangle rect = null; + Roi roi = imp.getRoi(); + Roi roi2 = cropRoi(imp, roi); + if (roi2!=null && roi2.isArea()) + rect = roi2.getBounds(); + ImageStack stack = imp.getStack(); + boolean virtualStack = stack.isVirtual(); + double min = imp.getDisplayRangeMin(); + double max = imp.getDisplayRangeMax(); + ImageStack stack2 = null; + int n = stack.size(); + boolean showProgress = virtualStack || ((double)n*stack.getWidth()*stack.getHeight()>=209715200.0); + for (int i=1; i<=n; i++) { + if (showProgress) { + IJ.showStatus("Duplicating: "+i+"/"+n); + IJ.showProgress(i,n); + } + ImageProcessor ip2 = stack.getProcessor(i); + ip2.setRoi(rect); + ip2 = ip2.crop(); + if (stack2==null) + stack2 = new ImageStack(ip2.getWidth(), ip2.getHeight(), imp.getProcessor().getColorModel()); + stack2.addSlice(stack.getSliceLabel(i), ip2); + } + IJ.showProgress(1.0); + ImagePlus imp2 = imp.createImagePlus(); + imp2.setStack("DUP_"+imp.getTitle(), stack2); + String info = (String)imp.getProperty("Info"); + if (info!=null) + imp2.setProperty("Info", info); + imp2.setProperties(imp.getPropertiesAsArray()); + imp2.setCalibration(imp.getCalibration()); + int[] dim = imp.getDimensions(); + imp2.setDimensions(dim[2], dim[3], dim[4]); + if (imp.isComposite()) { + imp2 = new CompositeImage(imp2, 0); + ((CompositeImage)imp2).copyLuts(imp); + } + if (virtualStack) + imp2.setDisplayRange(min, max); + if (imp.isHyperStack()) + imp2.setOpenAsHyperStack(true); + Overlay overlay = imp.getOverlay(); + if (overlay!=null && !imp.getHideOverlay()) + imp2.setOverlay(overlay.crop(rect)); + if (Recorder.record) { + if (imp.getRoi()==null || ignoreSelection) + Recorder.recordCall("imp = imp.duplicate();"); + else + Recorder.recordCall("imp = imp.crop(\"stack\");"); + } + return imp2; + } + + /** Returns a copy the current image or stack slice, cropped if there is a selection. + * @see ij.ImagePlus#crop + * @see ij.ImagePlus#crop(String) + */ + public ImagePlus crop(ImagePlus imp) { + if (imp.getNChannels()>1 && imp.getCompositeMode()==IJ.COMPOSITE) { + int z = imp.getSlice(); + int t = imp.getFrame(); + return run(imp, 1, imp.getNChannels(), z, z, t, t); + } + boolean hyperstack = imp.isHyperStack(); + int displayMode = imp.isComposite()?imp.getDisplayMode():0; + ImageProcessor ip = imp.getProcessor(); + ImageProcessor ip2 = ip.crop(); + ImagePlus imp2 = imp.createImagePlus(); + imp2.setProcessor("DUP_"+imp.getTitle(), ip2); + String info = (String)imp.getProperty("Info"); + if (info!=null) + imp2.setProperty("Info", info); + imp2.setProperties(imp.getPropertiesAsArray()); + if (imp.isStack()) { + ImageStack stack = imp.getStack(); + String label = stack.getSliceLabel(imp.getCurrentSlice()); + if (label!=null) { + if (label.length()>250 && label.indexOf('\n')>0 && label.contains("0002,")) + imp2.setProperty("Info", label); // DICOM metadata + else + imp2.setProp("Slice_Label", label); + } + if (imp.isComposite()) { + LUT lut = ((CompositeImage)imp).getChannelLut(); + if (displayMode==IJ.GRAYSCALE) + imp2.getProcessor().setColorModel(null); + else + imp2.getProcessor().setColorModel(lut); + } + } else { + String label = imp.getProp("Slice_Label"); + if (label!=null) + imp2.setProp("Slice_Label", label); + } + Overlay overlay = imp.getOverlay(); + if (overlay!=null && !imp.getHideOverlay()) { + Overlay overlay2 = overlay.crop(ip.getRoi()); + if (imp.getStackSize()>1) { + if (hyperstack) { + int c = imp.getC(); + int z = imp.getZ(); + int t = imp.getT(); + overlay2.crop(c,c,z,z,t,t); + } else + overlay2.crop(imp.getCurrentSlice(), imp.getCurrentSlice()); + } + imp2.setOverlay(overlay2); + } + return imp2; + } + + /** Returns a new stack containing a subrange of the specified stack. */ + public ImagePlus run(ImagePlus imp, int firstSlice, int lastSlice) { + Rectangle rect = null; + Roi roi = imp.getRoi(); + if (roi!=null && roi.isArea()) + rect = roi.getBounds(); + ImageStack stack = imp.getStack(); + boolean virtualStack = stack.isVirtual(); + double min = imp.getDisplayRangeMin(); + double max = imp.getDisplayRangeMax(); + ImageStack stack2 = null; + int n = lastSlice-firstSlice+1; + boolean showProgress = virtualStack || ((double)n*stack.getWidth()*stack.getHeight()>=209715200.0); + for (int i=firstSlice; i<=lastSlice; i++) { + if (showProgress) { + IJ.showStatus("Duplicating: "+i+"/"+lastSlice); + IJ.showProgress(i-firstSlice,n); + } + ImageProcessor ip2 = stack.getProcessor(i); + ip2.setRoi(rect); + ip2 = ip2.crop(); + if (stack2==null) + stack2 = new ImageStack(ip2.getWidth(), ip2.getHeight(), imp.getProcessor().getColorModel()); + stack2.addSlice(stack.getSliceLabel(i), ip2); + } + IJ.showProgress(1.0); + ImagePlus imp2 = imp.createImagePlus(); + imp2.setStack("DUP_"+imp.getTitle(), stack2); + String info = (String)imp.getProperty("Info"); + if (info!=null) + imp2.setProperty("Info", info); + imp2.setProperties(imp.getPropertiesAsArray()); + int size = stack2.getSize(); + boolean tseries = imp.getNFrames()==imp.getStackSize(); + if (tseries) + imp2.setDimensions(1, 1, size); + else + imp2.setDimensions(1, size, 1); + if (virtualStack) + imp2.setDisplayRange(min, max); + Overlay overlay = imp.getOverlay(); + if (overlay!=null && !imp.getHideOverlay()) { + Overlay overlay2 = overlay.crop(rect); + overlay2.crop(firstSlice, lastSlice); + imp2.setOverlay(overlay2); + } + if (Recorder.record) + Recorder.recordCall("imp = imp.crop(\""+firstSlice+"-"+lastSlice+"\");"); + return imp2; + } + + /** Returns a new hyperstack containing a possibly reduced version of the input image. */ + public ImagePlus run(ImagePlus imp, int firstC, int lastC, int firstZ, int lastZ, int firstT, int lastT) { + Rectangle rect = null; + Roi roi = imp.getRoi(); + Roi roi2 = cropRoi(imp, roi); + if (roi2!=null && roi2.isArea()) + rect = roi2.getBounds(); + ImageStack stack = imp.getStack(); + ImageStack stack2 = null; + for (int t=firstT; t<=lastT; t++) { + for (int z=firstZ; z<=lastZ; z++) { + for (int c=firstC; c<=lastC; c++) { + int n1 = imp.getStackIndex(c, z, t); + ImageProcessor ip = stack.getProcessor(n1); + String label = stack.getSliceLabel(n1); + ip.setRoi(rect); + ip = ip.crop(); + if (stack2==null) + stack2 = new ImageStack(ip.getWidth(), ip.getHeight(), null); + stack2.addSlice(label, ip); + } + } + } + ImagePlus imp2 = imp.createImagePlus(); + imp2.setStack("DUP_"+imp.getTitle(), stack2); + imp2.setDimensions(lastC-firstC+1, lastZ-firstZ+1, lastT-firstT+1); + if (imp.isComposite()) { + int mode =imp.getDisplayMode(); + if (lastC>firstC) { + imp2 = new CompositeImage(imp2, mode); + int i2 = 1; + for (int i=firstC; i<=lastC; i++) { + LUT lut = ((CompositeImage)imp).getChannelLut(i); + ((CompositeImage)imp2).setChannelLut(lut, i2++); + } + if (imp.getNChannels()==imp2.getNChannels()) { + boolean[] active = ((CompositeImage)imp).getActiveChannels(); + boolean[] active2 = ((CompositeImage)imp2).getActiveChannels(); + if (active!=null && active2!=null && active.length==active2.length) { + for (int i=0; i1 && duplicateStack && !isMacro; + legacyMacro = options!=null && (options.contains("duplicate")||!options.contains("use")); + String title = getNewTitle(); + if (title==null) title=defaultTitle; + GenericDialog gd = new GenericDialog(dialogTitle); + this.gd = gd; + gd.addStringField(prompt, title, 15); + if (isRoi) + gd.addCheckbox("Ignore selection", ignoreSelection); + if (stackSize>1) { + gd.addCheckbox("Duplicate stack", duplicateStack); + gd.setInsets(2, 30, 3); + gd.addStringField("Range:", "1-"+stackSize); + if (!isMacro) { + stackCheckbox = (Checkbox)(gd.getCheckboxes().elementAt(gd.getCheckboxes().size()-1)); + stackCheckbox.addItemListener(this); + Vector v = gd.getStringFields(); + titleField = (TextField)v.elementAt(0); + rangeField = (TextField)v.elementAt(1); + titleField.addTextListener(this); + rangeField.addTextListener(this); + } + } + gd.setSmartRecording(true); + gd.showDialog(); + if (gd.wasCanceled()) + return null; + title = gd.getNextString(); + if (isRoi) + ignoreSelection = gd.getNextBoolean(); + if (stackSize>1) { + duplicateStack = gd.getNextBoolean(); + if (duplicateStack) { + String[] range = Tools.split(gd.getNextString(), " -"); + double d1 = gd.parseDouble(range[0]); + double d2 = range.length==2?gd.parseDouble(range[1]):Double.NaN; + first = Double.isNaN(d1)?1:(int)d1; + last = Double.isNaN(d2)?stackSize:(int)d2; + if (first<1) first = 1; + if (last>stackSize) last = stackSize; + if (first>last) {first=1; last=stackSize;} + } else { + first = 1; + last = stackSize; + } + } + if (!isMacro) { + staticDuplicateStack = duplicateStack; + if (!ignoreNextSelection) staticIgnoreSelection=ignoreSelection; + } + ignoreNextSelection = false; + if (Recorder.record && titleField!=null && titleField.getText().equals(sliceLabel)) + Recorder.recordOption("use"); + return title; + } + + private String getNewTitle() { + if (titleChanged) + return null; + String title = defaultTitle; + if (imp.getStackSize()>1 && !duplicateStack && !legacyMacro && (stackCheckbox==null||!stackCheckbox.getState())) { + ImageStack stack = imp.getStack(); + String label = stack.getShortSliceLabel(imp.getCurrentSlice()); + if (label!=null && label.length()==0) + label = null; + if (label!=null) { + title = label; + sliceLabel = label; + } + } + return title; + } + + void duplicateHyperstack(ImagePlus imp, String newTitle) { + newTitle = showHSDialog(imp, newTitle); + if (newTitle==null) + return; + ImagePlus imp2 = null; + Roi roi = imp.getRoi(); + if (!duplicateStack) { + int nChannels = imp.getNChannels(); + boolean singleComposite = imp.isComposite() && nChannels==imp.getStackSize(); + if (!singleComposite && nChannels>1 && imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.COMPOSITE) { + firstC = 1; + lastC = nChannels; + } else + firstC = lastC = imp.getChannel(); + firstZ = lastZ = imp.getSlice(); + firstT = lastT = imp.getFrame(); + } + imp2 = run(imp, firstC, lastC, firstZ, lastZ, firstT, lastT); + if (imp2==null) return; + imp2.setTitle(newTitle); + if (imp2.getWidth()==0 || imp2.getHeight()==0) { + IJ.error("Duplicator", "Selection is outside the image"); + return; + } + if (roi!=null && roi.isArea() && roi.getType()!=Roi.RECTANGLE) { + Roi roi2 = (Roi)cropRoi(imp, roi).clone(); + roi2.setLocation(0, 0); + imp2.setRoi(roi2); + } + imp2.show(); + imp2.setPosition(imp.getC(), imp.getZ(), imp.getT()); + if (IJ.isMacro()&&imp2.getWindow()!=null) + IJ.wait(50); + } + + String showHSDialog(ImagePlus imp, String newTitle) { + int nChannels = imp.getNChannels(); + int nSlices = imp.getNSlices(); + int nFrames = imp.getNFrames(); + boolean composite = imp.isComposite() && nChannels==imp.getStackSize(); + String options = Macro.getOptions(); + boolean isMacro = options!=null; + GenericDialog gd = new GenericDialog("Duplicate"); + gd.addStringField("Title:", newTitle, 15); + gd.setInsets(12, 20, 8); + gd.addCheckbox("Duplicate hyperstack", (duplicateStack&&!isMacro)||composite); + int nRangeFields = 0; + if (nChannels>1) { + gd.setInsets(2, 30, 3); + gd.addStringField("Channels (c):", "1-"+nChannels); + nRangeFields++; + } + if (nSlices>1) { + gd.setInsets(2, 30, 3); + gd.addStringField("Slices (z):", "1-"+nSlices); + nRangeFields++; + } + if (nFrames>1) { + gd.setInsets(2, 30, 3); + gd.addStringField("Frames (t):", "1-"+nFrames); + nRangeFields++; + } + if (!isMacro) { + stackCheckbox = (Checkbox)(gd.getCheckboxes().elementAt(gd.getCheckboxes().size()-1)); + stackCheckbox.addItemListener(this); + Vector v = gd.getStringFields(); + rangeFields = new TextField[3]; + for (int i=0; i1) { + String[] range = Tools.split(gd.getNextString(), " -"); + double c1 = gd.parseDouble(range[0]); + double c2 = range.length==2?gd.parseDouble(range[1]):Double.NaN; + firstC = Double.isNaN(c1)?1:(int)c1; + lastC = Double.isNaN(c2)?firstC:(int)c2; + if (firstC<1) firstC = 1; + if (lastC>nChannels) lastC = nChannels; + if (firstC>lastC) {firstC=1; lastC=nChannels;} + } else + firstC = lastC = 1; + if (nSlices>1) { + String[] range = Tools.split(gd.getNextString(), " -"); + double z1 = gd.parseDouble(range[0]); + double z2 = range.length==2?gd.parseDouble(range[1]):Double.NaN; + firstZ = Double.isNaN(z1)?1:(int)z1; + lastZ = Double.isNaN(z2)?firstZ:(int)z2; + if (firstZ<1) firstZ = 1; + if (lastZ>nSlices) lastZ = nSlices; + if (firstZ>lastZ) {firstZ=1; lastZ=nSlices;} + } else + firstZ = lastZ = 1; + if (nFrames>1) { + String[] range = Tools.split(gd.getNextString(), " -"); + double t1 = gd.parseDouble(range[0]); + double t2 = range.length==2?gd.parseDouble(range[1]):Double.NaN; + firstT= Double.isNaN(t1)?1:(int)t1; + lastT = Double.isNaN(t2)?firstT:(int)t2; + if (firstT<1) firstT = 1; + if (lastT>nFrames) lastT = nFrames; + if (firstT>lastT) {firstT=1; lastT=nFrames;} + } else + firstT = lastT = 1; + if (!isMacro) + staticDuplicateStack = duplicateStack; + return newTitle; + } + + /* + * Returns the part of 'roi' overlaping 'imp' + * Author Marcel Boeglin 2013.12.15 + */ + Roi cropRoi(ImagePlus imp, Roi roi) { + if (roi==null) + return null; + if (imp==null) + return roi; + Rectangle b = roi.getBounds(); + int w = imp.getWidth(); + int h = imp.getHeight(); + if (b.x<0 || b.y<0 || b.x+b.width>w || b.y+b.height>h) { + ShapeRoi shape1 = new ShapeRoi(roi); + ShapeRoi shape2 = new ShapeRoi(new Roi(0, 0, w, h)); + roi = shape2.and(shape1); + } + if (roi.getBounds().width==0 || roi.getBounds().height==0) + throw new IllegalArgumentException("Selection is outside the image"); + return roi; + } + + public static Overlay cropOverlay(Overlay overlay, Rectangle bounds) { + return overlay.crop(bounds); + } + + public void textValueChanged(TextEvent e) { + if (IJ.debugMode) IJ.log("Duplicator.textValueChanged: "+e); + if (e.getSource()==titleField) { + if (!titleField.getText().equals(getNewTitle())) + titleChanged = true; + } else + stackCheckbox.setState(true); + } + + public void itemStateChanged(ItemEvent e) { + duplicateStack = stackCheckbox.getState(); + if (titleField!=null) { + String title = getNewTitle(); + if (title!=null && !title.equals(titleField.getText())) { + titleField.setText(title); + if (gd!=null) gd.setDefaultString(0, title); + } + } + } + + public static void ignoreNextSelection() { + ignoreNextSelection = true; + } + +} diff --git a/src/ij/plugin/EventListener.java b/src/ij/plugin/EventListener.java new file mode 100644 index 0000000..f103c3f --- /dev/null +++ b/src/ij/plugin/EventListener.java @@ -0,0 +1,94 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import java.awt.EventQueue; + +/** This plugin implements the Plugins/Utilities/Monitor Events command. + By implementing the IJEventListener, CommandListener, ImageListener + and RoiListener interfaces, it is able to monitor foreground and background + color changes, tool switches, Log window closings, command executions, image + window openings, closings and updates, and ROI changes. +*/ +public class EventListener implements PlugIn, IJEventListener, ImageListener, RoiListener, CommandListener { + + public void run(String arg) { + IJ.addEventListener(this); + Executer.addCommandListener(this); + ImagePlus.addImageListener(this); + Roi.addRoiListener(this); + IJ.log("EventListener started"); + } + + public void eventOccurred(int eventID) { + switch (eventID) { + case IJEventListener.FOREGROUND_COLOR_CHANGED: + String c = Integer.toHexString(Toolbar.getForegroundColor().getRGB()); + c = "#"+c.substring(2); + IJ.log("Changed foreground color to "+c); + break; + case IJEventListener.BACKGROUND_COLOR_CHANGED: + c = Integer.toHexString(Toolbar.getBackgroundColor().getRGB()); + c = "#"+c.substring(2); + IJ.log("Changed background color to "+c); + break; + case IJEventListener.TOOL_CHANGED: + String name = IJ.getToolName(); + IJ.log("Switched to the "+name+(name.endsWith("Tool")?"":" tool")); + break; + case IJEventListener.COLOR_PICKER_CLOSED: + IJ.log("Color picker closed"); + break; + case IJEventListener.LOG_WINDOW_CLOSED: + IJ.removeEventListener(this); + Executer.removeCommandListener(this); + ImagePlus.removeImageListener(this); + Roi.removeRoiListener(this); + IJ.showStatus("Log window closed; EventListener stopped"); + break; + } + } + + // called when an image is opened + public void imageOpened(ImagePlus imp) { + IJ.log("Image opened: \""+imp.getTitle()+"\""+edt()); + } + + // Called when an image is closed + public void imageClosed(ImagePlus imp) { + IJ.log("Image closed: \""+imp.getTitle()+"\""+edt()); + } + + // Called when an image's pixel data is updated + public void imageUpdated(ImagePlus imp) { + IJ.log("Image updated: \""+imp.getTitle()+"\""+edt()); + } + + // Called when an image is saved + public void imageSaved(ImagePlus imp) { + IJ.log("Image saved: \""+imp.getTitle()+"\""+edt()); + } + + private String edt() { + return EventQueue.isDispatchThread()?" (EDT)":" (not EDT)"; + } + + public String commandExecuting(String command) { + IJ.log("Command executed: \""+command+"\" command"); + return command; + } + + public void roiModified(ImagePlus img, int id) { + String type = "UNKNOWN"; + switch (id) { + case CREATED: type="CREATED"; break; + case MOVED: type="MOVED"; break; + case MODIFIED: type="MODIFIED"; break; + case EXTENDED: type="EXTENDED"; break; + case COMPLETED: type="COMPLETED"; break; + case DELETED: type="DELETED"; break; + } + IJ.log("ROI modified: "+(img!=null?img.getTitle():"")+", "+type); + } + + +} diff --git a/src/ij/plugin/FFT.java b/src/ij/plugin/FFT.java new file mode 100644 index 0000000..4e71780 --- /dev/null +++ b/src/ij/plugin/FFT.java @@ -0,0 +1,614 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.*; +import ij.plugin.ContrastEnhancer; +import ij.measure.Calibration; +import ij.util.Tools; +import ij.plugin.frame.Recorder; +import java.awt.*; +import java.util.*; + +/** +This class implements the FFT, Inverse FFT and Redisplay Power Spectrum commands +in the Process/FFT submenu. It is based on Arlo Reeves' +Pascal implementation of the Fast Hartley Transform from NIH Image +(http://imagej.nih.gov/ij/docs/ImageFFT/). +The Fast Hartley Transform was restricted by U.S. Patent No. 4,646,256, but was placed +in the public domain by Stanford University in 1995 and is now freely available. + +Version 2008-08-25 inverse transform: mask is always symmetrized +*/ +public class FFT implements PlugIn, Measurements { + + // static settings + public static boolean displayRawPS; + public static boolean displayFHT; + public static boolean displayComplex; + private static boolean displayFFT = true; + private static boolean doFFT; + private static boolean reuseWindow; + public static String fileName; + + // settings as instance variables + private boolean iDisplayRawPS; + private boolean iDisplayFHT; + private boolean iDisplayComplex; + private boolean iDisplayFFT; + private boolean iDoFFT; + + private ImagePlus imp, imp2; + private boolean padded; + private int originalWidth; + private int originalHeight; + private int stackSize = 1; + private int slice = 1; + private boolean showOutput = true; + + + public void run(String arg) { + if (arg.equals("options")) { + showDialog(); + if (iDoFFT) + arg="fft"; + else + return; + } + if (imp==null) + imp = IJ.getImage(); + if (arg.equals("fft") && imp.isComposite()) { + if (!GUI.showCompositeAdvisory(imp,"FFT")) + return; + } + if (arg.equals("redisplay")) { + redisplayPowerSpectrum(); + return; + } + if (arg.equals("swap")) { + if (imp.getWidth()==imp.getHeight()) { + swapQuadrants(imp.getStack()); + imp.updateAndDraw(); + } else + IJ.error("Swap Quadrants","Image must be square"); + return; + } + if (arg.equals("inverse")) { + if (imp.getTitle().startsWith("FHT of")) { + doFHTInverseTransform(); + return; + } + if (imp.getStackSize()==2) { + doComplexInverseTransform(); + return; + } + } + ImageProcessor ip = imp.getProcessor(); + Object obj = imp.getProperty("FHT"); + FHT fht = (obj instanceof FHT)?(FHT)obj:null; + stackSize = imp.getStackSize(); + boolean inverse; + if (fht==null && arg.equals("inverse")) { + IJ.error("FFT", "Frequency domain image required"); + return; + } + if (fht!=null) { + inverse = true; + imp.deleteRoi(); + } else { + if (imp.getRoi()!=null) + ip = ip.crop(); + fht = newFHT(ip); + inverse = false; + } + if (inverse) + doInverseTransform(fht); + else { + fileName = imp.getTitle(); + doForwardTransform(fht); + } + IJ.showProgress(1.0); + if (Recorder.record) { + if (inverse) + Recorder.recordCall("imp = FFT.inverse(imp);"); + else + Recorder.recordCall("imp = FFT.forward(imp); //see Help/Examples/JavaScript/FFT Filter"); + } + } + + /** + * Performs a forward FHT transform. + * @param imp A spatial domain image, which is not modified + * @return A frequency domain version of the input image + * @see #filter + * @see #inverse + */ + public static ImagePlus forward(ImagePlus imp) { + FFT fft = new FFT(); + fft.imp = imp; + fft.showOutput = false; + fft.run("forward"); + return fft.imp2; + } + + /** + * Multiplies a Fourier domain image by a filter + * @param imp A frequency domain image, which is modified. + * @param filter The filter, 32-bits (0-1) or 8-bits (0-255) + * @see #forward + * @see #inverse + * @see #filter + */ + public static void multiply(ImagePlus imp, ImageProcessor filter) { + Object obj = imp.getProperty("FHT"); + FHT fht = obj!=null&&(obj instanceof FHT)?(FHT)obj:null; + if (fht==null) + return; + int size = fht.getWidth(); + boolean isFloat = filter.getBitDepth()==32; + if (!isFloat) + filter = filter.convertToByte(true); + filter = filter.resize(size, size); + swapQuadrants(filter); + float[] fhtPixels = (float[])fht.getPixels(); + for (int i=0; i0) { + fht.setRoi(0, 0, fht.originalWidth, fht.originalHeight); + ip2 = fht.crop(); + } + int bitDepth = fht.originalBitDepth>0?fht.originalBitDepth:imp.getBitDepth(); + if (!showOutput && bitDepth!=24) + bitDepth = 32; + switch (bitDepth) { + case 8: ip2 = ip2.convertToByte(false); break; + case 16: ip2 = ip2.convertToShort(false); break; + case 24: + showStatus("Setting brightness"); + if (fht.rgb==null || ip2==null) { + IJ.error("FFT", "Unable to set brightness"); + return; + } + ColorProcessor rgb = (ColorProcessor)fht.rgb.duplicate(); + rgb.setBrightness((FloatProcessor)ip2); + ip2 = rgb; + fht.rgb = null; + break; + case 32: break; + } + if (bitDepth!=24 && fht.originalColorModel!=null) + ip2.setColorModel(fht.originalColorModel); + String title = imp.getTitle(); + if (title.startsWith("FFT of ")) + title = title.substring(7, title.length()); + ImagePlus imp2 = new ImagePlus("Inverse FFT of "+title, ip2); + imp2.setCalibration(imp.getCalibration()); + if (showOutput) + imp2.show(); + else + this.imp2 = imp2; + } + + void doForwardTransform(FHT fht) { + showStatus("Forward transform"); + long t0 = System.currentTimeMillis(); + fht.transform(); + if (iDisplayRawPS || (displayRawPS&&!IJ.isMacro())) { + ImageProcessor rawps = fht.getRawPowerSpectrum(); + if (rawps!=null) { + swapQuadrants(rawps); + ImagePlus imp2 = new ImagePlus("PS of "+fileName, rawps); + enhanceContrast(imp2); + imp2.show(); + } + } + if (iDisplayFHT || (displayFHT&&!IJ.isMacro())) { + ImageProcessor ip2 = fht.duplicate(); + swapQuadrants(ip2); + ImagePlus imp2 = new ImagePlus("FHT of "+FFT.fileName, ip2); + enhanceContrast(imp2); + setImageProperties(imp2, "Fast Hartley Transform"); + imp2.show(); + } + if (iDisplayComplex || (displayComplex&&!IJ.isMacro())) { + ImageStack ct = fht.getComplexTransform(); + ImagePlus imp2 = new ImagePlus("Complex of "+FFT.fileName, ct); + enhanceContrast(imp2); + setImageProperties(imp2, "Complex Fourier Transform"); + imp2.show(); + } + if (!(iDisplayFHT || iDisplayComplex || iDisplayRawPS)) + iDisplayFFT = true; + if (iDisplayFFT) { + showStatus("Calculating power spectrum"); + ImageProcessor ps = fht.getPowerSpectrum(); + String title = "FFT of "+imp.getTitle(); + ImagePlus imp2 = new ImagePlus(title, ps); + if (showOutput) { + ImagePlus fftImage = reuseWindow?WindowManager.getImage(title):null; + if (fftImage!=null) + fftImage.setImage(imp2); + else + imp2.show((System.currentTimeMillis()-t0)+" ms"); + } + fht.powerSpectrumMean = ps.getStats().mean; + imp2.setProperty("FHT", fht); + imp2.setCalibration(imp.getCalibration()); + String properties = "Fast Hartley Transform\n"; + properties += "width: "+fht.originalWidth + "\n"; + properties += "height: "+fht.originalHeight + "\n"; + properties += "bitdepth: "+fht.originalBitDepth + "\n"; + imp2.setProperty("Info", properties); + if (!showOutput) + this.imp2 = imp2; + } + } + + private void setImageProperties(ImagePlus imp, String type) { + imp.setProp(" ", type); + imp.setProp("Original width", originalWidth); + imp.setProp("Original height", originalHeight); + } + + private void enhanceContrast(ImagePlus imp) { + IJ.run(imp, "Enhance Contrast", "saturated=0.35"); + } + + FHT newFHT(ImageProcessor ip) { + FHT fht; + if (ip instanceof ColorProcessor) { + showStatus("Extracting brightness"); + ImageProcessor ip2 = ((ColorProcessor)ip).getBrightness(); + fht = new FHT(pad(ip2)); + fht.rgb = (ColorProcessor)ip.duplicate(); // save so we can later update the brightness + } else + fht = new FHT(pad(ip)); + if (padded) { + fht.originalWidth = originalWidth; + fht.originalHeight = originalHeight; + } + int bitDepth = imp.getBitDepth(); + fht.originalBitDepth = bitDepth; + if (bitDepth!=24) + fht.originalColorModel = ip.getColorModel(); + return fht; + } + + ImageProcessor pad(ImageProcessor ip) { + originalWidth = ip.getWidth(); + originalHeight = ip.getHeight(); + int maxN = Math.max(originalWidth, originalHeight); + int i = 2; + while(i=65536) { + IJ.error("FFT", "Padded image is too large ("+maxN+"x"+maxN+")"); + return null; + } + ImageStatistics stats = ImageStatistics.getStatistics(ip, MEAN, null); + ImageProcessor ip2 = ip.createProcessor(maxN, maxN); + ip2.setValue(stats.mean); + ip2.fill(); + ip2.insert(ip, 0, 0); + padded = true; + Undo.reset(); + return ip2; + } + + void showStatus(String msg) { + if (stackSize>1) + IJ.showStatus("FFT: " + slice+"/"+stackSize); + else + IJ.showStatus(msg); + } + + void doMasking(FHT ip) { + if (stackSize>1) + return; + float[] fht = (float[])ip.getPixels(); + ImageProcessor mask = imp.getProcessor(); + int bitDepth = mask.getBitDepth(); + mask = mask.convertToByte(false); + if (mask.getWidth()!=ip.getWidth() || mask.getHeight()!=ip.getHeight()) + return; + mask.resetRoi(); + ImageStatistics stats = mask.getStats(); + if (stats.histogram[0]==0 && stats.histogram[255]==0) { + if (bitDepth==8 && ip.powerSpectrumMean!=stats.mean) + IJ.showMessage("Inverse FFT", "No pixels have been set to 0 (black) or\n255 (white) so filtering will not be done."); + return; + } + boolean passMode = stats.histogram[255]!=0; + IJ.showStatus("Masking: "+(passMode?"pass":"filter")); + mask = mask.duplicate(); + if (passMode) + changeValuesAndSymmetrize(mask, (byte)255, (byte)0); //0-254 become 0 + else + changeValuesAndSymmetrize(mask, (byte)0, (byte)255); //1-255 become 255 + for (int i=0; i<3; i++) + smooth(mask); + if (IJ.debugMode || IJ.altKeyDown()) + new ImagePlus("mask", mask.duplicate()).show(); + swapQuadrants(mask); + byte[] maskPixels = (byte[])mask.getPixels(); + for (int i=0; i0) pixels[n*n-i] = v1; + } else if (i=wList2.length) index1 = 0; + if (index2>=wList2.length) index2 = 0; + if (WindowManager.getImage(title)!=null) + title = WindowManager.getUniqueName(title); + GenericDialog gd = new GenericDialog("FFT Math"); + gd.addChoice("Image1: ", titles, titles[index1]); + gd.addChoice("Operation:", ops, ops[operation]); + gd.addChoice("Image2: ", titles, titles[index2]); + gd.addStringField("Result:", title); + gd.addCheckbox("Do inverse transform", doInverse); + gd.addHelp(IJ.URL+"/docs/menus/process.html#fft-math"); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + index1 = gd.getNextChoiceIndex(); + operation = gd.getNextChoiceIndex(); + index2 = gd.getNextChoiceIndex(); + title = gd.getNextString(); + doInverse = gd.getNextBoolean(); + imp1 = WindowManager.getImage(wList2[index1]); + imp2 = WindowManager.getImage(wList2[index2]); + return true; + } + + public void doMath(ImagePlus imp1, ImagePlus imp2) { + FHT h1, h2=null; + ImageProcessor fht1, fht2; + fht1 = (ImageProcessor)imp1.getProperty("FHT"); + if (fht1!=null) + h1 = new FHT(fht1); + else { + IJ.showStatus("Converting to float"); + ImageProcessor ip1 = imp1.getProcessor(); + h1 = new FHT(ip1); + } + fht2 = (ImageProcessor)imp2.getProperty("FHT"); + if (fht2!=null) + h2 = new FHT(fht2); + else { + ImageProcessor ip2 = imp2.getProcessor(); + if (imp2!=imp1) + h2 = new FHT(ip2); + } + if (!h1.powerOf2Size()) { + IJ.error("FFT Math", "Images must be a power of 2 size (256x256, 512x512, etc.)"); + return; + } + if (imp1.getWidth()!=imp2.getWidth()) { + IJ.error("FFT Math", "Images must be the same size"); + return; + } + if (fht1==null) { + IJ.showStatus("Transform image1"); + h1.transform(); + } + if (fht2==null) { + if (h2==null) + h2 = new FHT(h1.duplicate()); + else { + IJ.showStatus("Transform image2"); + h2.transform(); + } + } + FHT result=null; + switch (operation) { + case CONJUGATE_MULTIPLY: + IJ.showStatus("Complex conjugate multiply"); + result = h1.conjugateMultiply(h2); + break; + case MULTIPLY: + IJ.showStatus("Fourier domain multiply"); + result = h1.multiply(h2); + break; + case DIVIDE: + IJ.showStatus("Fourier domain divide"); + result = h1.divide(h2); + break; + } + ImagePlus imp3 = null; + if (doInverse) { + IJ.showStatus("Inverse transform"); + result.inverseTransform(); + IJ.showStatus("Swap quadrants"); + result.swapQuadrants(); + IJ.showStatus("Display image"); + result.resetMinAndMax(); + imp3 = new ImagePlus(title, result); + } else { + IJ.showStatus("Power spectrum"); + ImageProcessor ps = result.getPowerSpectrum(); + imp3 = new ImagePlus(title, ps.convertToFloat()); + result.quadrantSwapNeeded = true; + imp3.setProperty("FHT", result); + } + Calibration cal1 = imp1.getCalibration(); + Calibration cal2 = imp2.getCalibration(); + Calibration cal3 = cal1.scaled() ? cal1 : cal2; + if (cal1.scaled() && cal2.scaled() && !cal1.equals(cal2)) + cal3 = null; //can't decide between different calibrations + imp3.setCalibration(cal3); + cal3 = imp3.getCalibration(); //imp3 has a copy, which we may modify + cal3.disableDensityCalibration(); + imp3.show(); + IJ.showProgress(1.0); + } + +} diff --git a/src/ij/plugin/FITS_Reader.java b/src/ij/plugin/FITS_Reader.java new file mode 100644 index 0000000..0e14df4 --- /dev/null +++ b/src/ij/plugin/FITS_Reader.java @@ -0,0 +1,191 @@ +package ij.plugin; +import java.awt.*; +import java.io.*; +import java.util.zip.GZIPInputStream; +import ij.*; +import ij.io.*; +import ij.process.*; +import ij.measure.*; + +/** Opens and displays FITS images. The FITS format is + * described at "http://fits.gsfc.nasa.gov/fits_standard.html". + * Add setOption("FlipFitsImages",false) to the + * Edit/Options/Startup dialog to have FITS images not + * flipped vertically. +*/ +public class FITS_Reader extends ImagePlus implements PlugIn { + private static boolean flipImages = true; + + public void run(String arg) { + OpenDialog od = new OpenDialog("Open FITS...", arg); + String directory = od.getDirectory(); + String fileName = od.getFileName(); + if (fileName==null) + return; + IJ.showStatus("Opening: " + directory + fileName); + FitsDecoder fd = new FitsDecoder(directory, fileName); + FileInfo fi = null; + try { + fi = fd.getInfo(); + } catch (IOException e) {} + if (fi!=null && fi.width>0 && fi.height>0 && fi.offset>0) { + FileOpener fo = new FileOpener(fi); + ImagePlus imp = fo.openImage(); + if (flipImages) { + if (fi.nImages==1) { + ImageProcessor ip = imp.getProcessor(); + ip.flipVertical(); // origin is at bottom left corner + setProcessor(fileName, ip); + } else { + ImageStack stack = imp.getStack(); // origin is at bottom left corner + for(int i=1; i<=stack.getSize(); i++) + stack.getProcessor(i).flipVertical(); + setStack(fileName, stack); + } + } + setStack(fileName, imp.getStack()); + Calibration cal = imp.getCalibration(); + if (fi.fileType==FileInfo.GRAY16_SIGNED && fd.bscale==1.0 && fd.bzero==32768.0) + cal.setFunction(Calibration.NONE, null, "Gray Value"); + setCalibration(cal); + setProperty("Info", fd.getHeaderInfo()); + setFileInfo(fi); // needed for File->Revert + if (arg.equals("")) show(); + } else + IJ.error("This does not appear to be a FITS file."); + IJ.showStatus(""); + } + + public static void flipImages(boolean flip) { + flipImages = flip; + } + +} + +class FitsDecoder { + private String directory, fileName; + private DataInputStream f; + private StringBuffer info = new StringBuffer(512); + double bscale, bzero; + + public FitsDecoder(String directory, String fileName) { + this.directory = directory; + this.fileName = fileName; + } + + FileInfo getInfo() throws IOException { + FileInfo fi = new FileInfo(); + fi.fileFormat = FileInfo.FITS; + fi.fileName = fileName; + fi.directory = directory; + fi.width = 0; + fi.height = 0; + fi.offset = 0; + + InputStream is = new FileInputStream(directory + fileName); + if (fileName.toLowerCase().endsWith(".gz")) is = new GZIPInputStream(is); + f = new DataInputStream(is); + String line = getString(80); + info.append(line+"\n"); + if (!line.startsWith("SIMPLE")) + {f.close(); return null;} + int count = 1; + while ( true ) { + count++; + line = getString(80); + info.append(line+"\n"); + + // Cut the key/value pair + int index = line.indexOf ( "=" ); + + // Strip out comments + int commentIndex = line.indexOf ( "/", index ); + if ( commentIndex < 0 ) + commentIndex = line.length (); + + // Split that values + String key; + String value; + if ( index >= 0 ) { + key = line.substring ( 0, index ).trim (); + value = line.substring ( index + 1, commentIndex ).trim (); + } else { + key = line.trim (); + value = ""; + } + + // Time to stop ? + if (key.equals ("END") ) break; + + // Look for interesting information + if (key.equals("BITPIX")) { + int bitsPerPixel = Integer.parseInt ( value ); + if (bitsPerPixel==8) + fi.fileType = FileInfo.GRAY8; + else if (bitsPerPixel==16) + fi.fileType = FileInfo.GRAY16_SIGNED; + else if (bitsPerPixel==32) + fi.fileType = FileInfo.GRAY32_INT; + else if (bitsPerPixel==-32) + fi.fileType = FileInfo.GRAY32_FLOAT; + else if (bitsPerPixel==-64) + fi.fileType = FileInfo.GRAY64_FLOAT; + else { + IJ.error("BITPIX must be 8, 16, 32, -32 (float) or -64 (double)."); + f.close(); + return null; + } + } else if (key.equals("NAXIS1")) + fi.width = Integer.parseInt ( value ); + else if (key.equals("NAXIS2")) + fi.height = Integer.parseInt( value ); + else if (key.equals("NAXIS3")) //for multi-frame fits + fi.nImages = Integer.parseInt ( value ); + else if (key.equals("BSCALE")) + bscale = parseDouble ( value ); + else if (key.equals("BZERO")) + bzero = parseDouble ( value ); + else if (key.equals("CDELT1")) + fi.pixelWidth = parseDouble ( value ); + else if (key.equals("CDELT2")) + fi.pixelHeight = parseDouble ( value ); + else if (key.equals("CDELT3")) + fi.pixelDepth = parseDouble ( value ); + else if (key.equals("CTYPE1")) + fi.unit = value; + + if (count>360 && fi.width==0) + {f.close(); return null;} + } + if (fi.pixelWidth==1.0 && fi.pixelDepth==1) + fi.unit = "pixel"; + + f.close(); + fi.offset = 2880+2880*(((count*80)-1)/2880); + return fi; + } + + String getString(int length) throws IOException { + byte[] b = new byte[length]; + f.readFully(b); + if (IJ.debugMode) + IJ.log(new String(b)); + return new String(b); + } + + int getInteger(String s) { + s = s.substring(10, 30); + s = s.trim(); + return Integer.parseInt(s); + } + + double parseDouble(String s) throws NumberFormatException { + Double d = new Double(s); + return d.doubleValue(); + } + + String getHeaderInfo() { + return new String(info); + } + +} diff --git a/src/ij/plugin/FITS_Writer.java b/src/ij/plugin/FITS_Writer.java new file mode 100644 index 0000000..b6b8943 --- /dev/null +++ b/src/ij/plugin/FITS_Writer.java @@ -0,0 +1,353 @@ +package ij.plugin; +import java.io.*; +import java.util.Properties; +import ij.*; +import ij.io.*; +import ij.process.*; +import ij.measure.*; + +/** + * This plugin saves a 16 or 32 bit image in FITS format. It is a stripped-down version of the SaveAs_FITS + * plugin from the collection of astronomical image processing plugins by Jennifer West at + * http://www.umanitoba.ca/faculties/science/astronomy/jwest/plugins.html. + * + *
Version 2010-11-23 : corrects 16-bit writing, adds BZERO & BSCALE updates (K.A. Collins, Univ. Louisville). + *
Version 2008-09-07 : preserves non-minimal FITS header if already present (F.V. Hessman, Univ. Goettingen). + *
Version 2008-12-15 : fixed END card recognition bug (F.V. Hessman, Univ. Goettingen). + *
Version 2019-11-03 : various updates (K.A. Collins, CfA-Harvard and Smithsonian). + */ +public class FITS_Writer implements PlugIn { + + private int numCards = 0; + private Calibration cal; + private boolean unsigned16 = false; + private double bZero = 0.0; + private double bScale = 1.0; + + public void run(String path) { + ImagePlus imp = IJ.getImage(); + ImageProcessor ip = imp.getProcessor(); + int numImages = imp.getImageStackSize(); + int bitDepth = imp.getBitDepth(); + if (bitDepth==24) { + IJ.error("RGB images are not supported"); + return; + } + + // GET PATH + if (path == null || path.trim().length() == 0) { + String title = "image.fits"; + SaveDialog sd = new SaveDialog("Write FITS image",title,".fits"); + path = sd.getDirectory()+sd.getFileName(); + } + + // GET FILE + File f = new File(path); + String directory = f.getParent()+File.separator; + String name = f.getName(); + if (f.exists()) f.delete(); + int numBytes = 0; + + cal = imp.getCalibration(); + unsigned16 = (bitDepth==16 && cal.getFunction()==Calibration.NONE && cal.getCoefficients()==null); + + // GET IMAGE + if (bitDepth==8) { + numBytes = 1; + if (cal.getFunction()!=Calibration.NONE && cal.getCoefficients()!=null) { + bZero = cal.getCoefficients()[0]; + if (cal.getCoefficients()[1] != 0) bScale = cal.getCoefficients()[1]; + } + } else if (ip instanceof ShortProcessor) { + numBytes = 2; + if (unsigned16) { + bZero = 32768.0; + bScale = 1.0; + } else { + if (cal.getCoefficients()[1] != 0) bScale = cal.getCoefficients()[1]; + bZero = cal.getCoefficients()[0] + (32768.0*bScale); + } + } else if (ip instanceof FloatProcessor) { + numBytes = 4; //float processor does not support calibration - data values are shifted and scaled in FITS_Reader + bZero = 0.0; //float values are written back out without shifting + bScale = 1.0; //and without scaling + } + + int fillerLength = 2880 - ( (numBytes * imp.getWidth() * imp.getHeight()) % 2880 ); + + // WRITE FITS HEADER + String[] hdr = getHeader(imp); +// if (hdr == null) +// createHeader(path, ip, numBytes); +// else + createHeader(hdr, path, ip, numBytes); + + // WRITE DATA + writeData(path, ip); + char[] endFiller = new char[fillerLength]; + appendFile(endFiller, path); + } + +// /** +// * Creates a FITS header for an image which doesn't have one already. +// */ +// void createHeader(String path, ImageProcessor ip, int numBytes) { +// +// String bitperpix = ""; +// if (numBytes==2) {bitperpix = " 16";} +// else if (numBytes==4) {bitperpix = " -32";} +// else if (numBytes==1) {bitperpix = " 8";} +// appendFile(writeCard("SIMPLE", " T", "Created by ImageJ FITS_Writer"), path); +// appendFile(writeCard("BITPIX", bitperpix, "number of bits per data pixel"), path); +// appendFile(writeCard("NAXIS", " 2", "number of data axes"), path); +// appendFile(writeCard("NAXIS1", " "+ip.getWidth(), "length of data axis 1"), path); +// appendFile(writeCard("NAXIS2", " "+ip.getHeight(), "length of data axis 2"), path); +// if (bZero != 0 || bScale != 1.0) +// { +// appendFile(writeCard("BZERO", ""+bZero, "data range offset"), path); +// appendFile(writeCard("BSCALE", ""+bScale, "scaling factor"), path); +// } +// +// int fillerSize = 2880 - ((numCards*80+3) % 2880); +// char[] end = new char[3]; +// end[0] = 'E'; end[1] = 'N'; end[2] = 'D'; +// char[] filler = new char[fillerSize]; +// for (int i = 0; i < fillerSize; i++) +// filler[i] = ' '; +// appendFile(end, path); +// appendFile(filler, path); +// } + + /** + * Writes one line of a FITS header + */ + char[] writeCard(String title, String value, String comment) { + char[] card = new char[80]; + for (int i = 0; i < 80; i++) + card[i] = ' '; + s2ch(title, card, 0); + card[8] = '='; + s2ch(value, card, 10); + card[31] = '/'; + card[32] = ' '; + s2ch(comment, card, 33); + numCards++; + return card; + } + + void writeCard(char[] line, String path) { + appendFile(line, path); + numCards++; + } + + /** + * Converts a String to a char[] + */ + void s2ch (String str, char[] ch, int offset) { + int j = 0; + for (int i = offset; i < 80 && i < str.length()+offset; i++) + ch[i] = str.charAt(j++); + } + + + /** + * Appends 'line' to the end of the file specified by 'path'. + */ + void appendFile(char[] line, String path) { + try { + FileWriter output = new FileWriter(path, true); + output.write(line); + output.close(); + } catch (IOException e) { + IJ.showStatus("Error writing file!"); + return; + } + } + + /** + * Appends the data of the current image to the end of the file specified by path. + */ + void writeData(String path, ImageProcessor ip) { + int w = ip.getWidth(); + int h = ip.getHeight(); + if (ip instanceof ByteProcessor) { + byte[] pixels = (byte[])ip.getPixels(); + try { + DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(path,true))); + for (int i = h - 1; i >= 0; i-- ) + for (int j = i*w; j < w*(i+1); j++) + dos.writeByte(pixels[j]); + dos.close(); + } catch (IOException e) { + IJ.showStatus("Error writing file!"); + return; + } + } else if (ip instanceof ShortProcessor) { + short[] pixels = (short[])ip.getPixels(); + try { + DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(path,true))); + for (int i = h - 1; i >= 0; i-- ) + for (int j = i*w; j < w*(i+1); j++) + dos.writeShort(pixels[j]^0x8000); + dos.close(); + } catch (IOException e) { + IJ.showStatus("Error writing file!"); + return; + } + } else if (ip instanceof FloatProcessor) { + float[] pixels = (float[])ip.getPixels(); + try { + DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(path,true))); + for (int i = h - 1; i >= 0; i-- ) + for (int j = i*w; j < w*(i+1); j++) + dos.writeFloat(pixels[j]); + dos.close(); + } catch (IOException e) { + IJ.showStatus("Error writing file!"); + return; + } + } + } + + /** + * Extracts the original FITS header from the Properties object of the + * ImagePlus image (or from the current slice label in the case of an ImageStack) + * and returns it as an array of String objects representing each card. + * + * Taken from the ImageJ astroj package (www.astro.physik.uni-goettingen.de/~hessman/ImageJ/Astronomy) + * + * @param img The ImagePlus image which has the FITS header in it's "Info" property. + */ + public static String[] getHeader (ImagePlus img) { + String content = null; + + int depth = img.getStackSize(); + if (depth == 1) { + Properties props = img.getProperties(); + if (props == null) + return null; + content = (String)props.getProperty ("Info"); + } + else if (depth > 1) { + int slice = img.getCurrentSlice(); + ImageStack stack = img.getStack(); + content = stack.getSliceLabel(slice); + if (content == null) { + Properties props = img.getProperties(); + if (props == null) + return null; + content = (String)props.getProperty ("Info"); + } + } + if (content == null) + return null; + + // PARSE INTO LINES + + String[] lines = content.split("\n"); + + // FIND "SIMPLE" AND "END" KEYWORDS + + int istart = 0; + for (; istart < lines.length; istart++) { + if (lines[istart].startsWith("SIMPLE") ) break; + } + if (istart == lines.length) return null; + + int iend = istart+1; + for (; iend < lines.length; iend++) { + String s = lines[iend].trim(); + if ( s.equals ("END") || s.startsWith ("END ") ) break; + } + if (iend >= lines.length) return null; + + int l = iend-istart+1; + String header = ""; + for (int i=0; i < l; i++) + header += lines[istart+i]+"\n"; + return header.split("\n"); + } + + /** + * Converts a string into an 80-char array. + */ + char[] eighty(String s) { + char[] c = new char[80]; + int l=s.length(); + for (int i=0; i < l && i < 80; i++) + c[i]=s.charAt(i); + if (l < 80) { + for (; l < 80; l++) c[l]=' '; + } + return c; + } + + /** + * Copies the image header contained in the image's Info property. + */ + void createHeader(String[] hdr, String path, ImageProcessor ip, int numBytes) { + String bitperpix = ""; + int imw=ip.getWidth(); + int imh=ip.getHeight(); + String wbuf = " "; + String hbuf = " "; + if (imw < 10000) + wbuf = wbuf + " "; + if (imw < 1000) + wbuf = wbuf + " "; + if (imw < 100) + wbuf = wbuf + " "; + if (imw < 10) + wbuf = wbuf + " "; + if (imh < 10000) + hbuf = hbuf + " "; + if (imh < 1000) + hbuf = hbuf + " "; + if (imh < 100) + hbuf = hbuf + " "; + if (imh < 10) + hbuf = hbuf + " "; + // THESE KEYWORDS NEED TO BE MADE CONFORMAL WITH THE PRESENT IMAGE + if (numBytes==2) {bitperpix = " 16";} + else if (numBytes==4) {bitperpix = " -32";} + else if (numBytes==1) {bitperpix = " 8";} + appendFile(writeCard("SIMPLE", " T", "Created by ImageJ FITS_Writer"), path); + appendFile(writeCard("BITPIX", bitperpix, "number of bits per data pixel"), path); + appendFile(writeCard("NAXIS", " 2", "number of data axes"), path); + appendFile(writeCard("NAXIS1", wbuf + imw, "length of data axis 1"), path); + appendFile(writeCard("NAXIS2", hbuf + imh, "length of data axis 2"), path); + if (bZero != 0 || bScale != 1.0) { + appendFile(writeCard("BZERO", ""+bZero, "data range offset"), path); + appendFile(writeCard("BSCALE", ""+bScale, "scaling factor"), path); + } + + if (hdr != null) { + // APPEND THE REST OF THE HEADER IF ONE EXISTS + char[] card; + for (int i=0; i < hdr.length; i++) { + String s = hdr[i]; + card = eighty(s); + if (!s.startsWith("SIMPLE") && + !s.startsWith("BITPIX") && + !s.startsWith("NAXIS") && + !s.startsWith("BZERO") && + !s.startsWith("BSCALE") && + !s.startsWith("END") && + s.trim().length() > 1) { + writeCard(card, path); + } + } + } + + // FINISH OFF THE HEADER + int fillerSize = 2880 - ((numCards*80+3) % 2880); + char[] end = new char[3]; + end[0] = 'E'; end[1] = 'N'; end[2] = 'D'; + char[] filler = new char[fillerSize]; + for (int i = 0; i < fillerSize; i++) + filler[i] = ' '; + appendFile(end, path); + appendFile(filler, path); + } + + } diff --git a/src/ij/plugin/FileInfoVirtualStack.java b/src/ij/plugin/FileInfoVirtualStack.java new file mode 100644 index 0000000..9836747 --- /dev/null +++ b/src/ij/plugin/FileInfoVirtualStack.java @@ -0,0 +1,288 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.io.*; +import java.awt.*; +import java.io.*; +import java.util.Properties; + +/** This plugin opens a multi-page TIFF file, or a set of raw images, as a + virtual stack. It implements the File/Import/TIFF Virtual Stack command. */ +public class FileInfoVirtualStack extends VirtualStack implements PlugIn { + private FileInfo[] info; + private int nImages; + + /* Default constructor. */ + public FileInfoVirtualStack() {} + + /* Constructs a FileInfoVirtualStack from a FileInfo object. */ + public FileInfoVirtualStack(FileInfo fi) { + info = new FileInfo[1]; + info[0] = fi; + ImagePlus imp = open(); + if (imp!=null) + imp.show(); + } + + /* Constructs a FileInfoVirtualStack from a FileInfo + object and displays it if 'show' is true. */ + public FileInfoVirtualStack(FileInfo fi, boolean show) { + info = new FileInfo[1]; + info[0] = fi; + ImagePlus imp = open(); + if (imp!=null && show) + imp.show(); + } + + /* Constructs a FileInfoVirtualStack from an array of FileInfo objects. */ + public FileInfoVirtualStack(FileInfo[] fi) { + info = fi; + nImages = info.length; + } + + /** Opens the specified tiff file as a virtual stack. */ + public static ImagePlus openVirtual(String path) { + OpenDialog od = new OpenDialog("Open TIFF", path); + String name = od.getFileName(); + String dir = od.getDirectory(); + if (name==null) + return null; + FileInfoVirtualStack stack = new FileInfoVirtualStack(); + stack.init(dir, name); + if (stack.info==null) + return null; + else + return stack.open(); + } + + public void run(String arg) { + OpenDialog od = new OpenDialog("Open TIFF", arg); + String name = od.getFileName(); + String dir = od.getDirectory(); + if (name==null) + return; + init(dir, name); + if (info==null) + return; + ImagePlus imp = open(); + if (imp!=null) + imp.show(); + } + + private void init(String dir, String name) { + if (name.endsWith(".zip")) { + IJ.error("Virtual Stack", "ZIP compressed stacks not supported"); + return; + } + TiffDecoder td = new TiffDecoder(dir, name); + if (IJ.debugMode) td.enableDebugging(); + IJ.showStatus("Decoding TIFF header..."); + try { + info = td.getTiffInfo(); + } catch (IOException e) { + String msg = e.getMessage(); + if (msg==null||msg.equals("")) msg = ""+e; + IJ.error("TiffDecoder", msg); + return; + } + if (info==null || info.length==0) { + IJ.error("Virtual Stack", "This does not appear to be a TIFF stack"); + return; + } + if (IJ.debugMode) + IJ.log(info[0].debugInfo); + } + + private ImagePlus open() { + FileInfo fi = info[0]; + int n = fi.nImages; + if (info.length==1 && n>1) { + n = validateNImages(fi); + info = new FileInfo[n]; + long size = fi.width*fi.height*fi.getBytesPerPixel(); + for (int i=0; i1 && fi.description!=null) { + int mode = IJ.COMPOSITE; + if (fi.description.indexOf("mode=color")!=-1) + mode = IJ.COLOR; + else if (fi.description.indexOf("mode=gray")!=-1) + mode = IJ.GRAYSCALE; + imp2 = new CompositeImage(imp2, mode); + } + } + return imp2; + } + + private int validateNImages(FileInfo fi) { + File f = new File(fi.getFilePath()); + if (!f.exists()) + return fi.nImages; + long fileLength = f.length(); + long bytesPerImage = fi.width*fi.height*fi.getBytesPerPixel(); + for (int i=fi.nImages-1; i>=0; i--) { + long offset = fi.getOffset() + i*(bytesPerImage+fi.getGap()); + if (offset+bytesPerImage<=fileLength) + return i+1; + } + return fi.nImages; + } + + int getInt(Properties props, String key) { + Double n = getNumber(props, key); + return n!=null?(int)n.doubleValue():1; + } + + Double getNumber(Properties props, String key) { + String s = props.getProperty(key); + if (s!=null) { + try { + return Double.valueOf(s); + } catch (NumberFormatException e) {} + } + return null; + } + + boolean getBoolean(Properties props, String key) { + String s = props.getProperty(key); + return s!=null&&s.equals("true")?true:false; + } + + /** Deletes the specified image, were 1<=n<=nImages. */ + public void deleteSlice(int n) { + if (n<1 || n>nImages) + throw new IllegalArgumentException("Argument out of range: "+n); + if (nImages<1) return; + for (int i=n; inImages) + throw new IllegalArgumentException("Argument out of range: "+n); + //if (n>1) IJ.log(" "+(info[n-1].getOffset()-info[n-2].getOffset())); + info[n-1].nImages = 1; // why is this needed? + ImageProcessor ip = null; + if (IJ.debugMode) { + long t0 = System.currentTimeMillis(); + FileOpener fo = new FileOpener(info[n-1]); + ip = fo.openProcessor(); + IJ.log("FileInfoVirtualStack: "+n+", offset="+info[n-1].getOffset()+", "+(System.currentTimeMillis()-t0)+"ms"); + } else { + FileOpener fo = new FileOpener(info[n-1]); + if (info[n-1].fileType==FileInfo.RGB48) { + ImagePlus imp = fo.openImage(); + if (info[n-1].sliceNumber>0) + imp.setSlice(info[n-1].sliceNumber); + ip = imp.getProcessor(); + } else + ip = fo.openProcessor(); + } + if (ip!=null) + return ip; + else { + int w=getWidth(), h=getHeight(); + IJ.log("Read error or file not found ("+n+"): "+info[n-1].directory+info[n-1].fileName); + switch (getBitDepth()) { + case 8: return new ByteProcessor(w, h); + case 16: return new ShortProcessor(w, h); + case 24: return new ColorProcessor(w, h); + case 32: return new FloatProcessor(w, h); + default: return null; + } + } + } + + /** Returns the number of slices in this stack. */ + public int size() { + return getSize(); + } + + public int getSize() { + return nImages; + } + + /** Returns the label of the Nth image. */ + public String getSliceLabel(int n) { + if (n<1 || n>nImages) + throw new IllegalArgumentException("Argument out of range: "+n); + if (info[0].sliceLabels==null || info[0].sliceLabels.length!=nImages) + return null; + else + return info[0].sliceLabels[n-1]; + } + + public int getWidth() { + return info[0].width; + } + + public int getHeight() { + return info[0].height; + } + + /** Adds an image to this stack. */ + public synchronized void addImage(FileInfo fileInfo) { + nImages++; + if (info==null) + info = new FileInfo[250]; + if (nImages==info.length) { + FileInfo[] tmp = new FileInfo[nImages*2]; + System.arraycopy(info, 0, tmp, 0, nImages); + info = tmp; + } + info[nImages-1] = fileInfo; + } + + @Override + public String getDirectory() { + if (info!=null && info.length>0) + return info[0].directory; + else + return null; + } + + @Override + public String getFileName(int n) { + int index = n - 1; + if (index>=0 && info!=null && info.length>index) + return info[index].fileName; + else + return null; + } + + +} diff --git a/src/ij/plugin/Filters3D.java b/src/ij/plugin/Filters3D.java new file mode 100644 index 0000000..83de72d --- /dev/null +++ b/src/ij/plugin/Filters3D.java @@ -0,0 +1,148 @@ +package ij.plugin; + +import ij.*; +import ij.process.*; +import ij.gui.GenericDialog; +import ij.util.ThreadUtil; +import ij.plugin.RGBStackMerge; +import ij.gui.*; +import java.util.concurrent.atomic.AtomicInteger; + +/* + * This plugin implements most of the 3D filters in the Process/Filters submenu. + * @author Thomas Boudier + */ +public class Filters3D implements PlugIn { + public final static int MEAN=10, MEDIAN=11, MIN=12, MAX=13, VAR=14, MAXLOCAL=15; + private static float xradius = 2, yradius = 2, zradius = 2; + + public void run(String arg) { + String name = null; + int filter = 0; + if (arg.equals("mean")) { + name = "3D Mean"; + filter = MEAN; + } else if (arg.equals("median")) { + name = "3D Median"; + filter = MEDIAN; + } else if (arg.equals("min")) { + name = "3D Minimum"; + filter = MIN; + } else if (arg.equals("max")) { + name = "3D Maximum"; + filter = MAX; + } else if (arg.equals("var")) { + name = "3D Variance"; + filter = VAR; + } else + return; + ImagePlus imp = IJ.getImage(); + if (imp.isComposite() && imp.getNChannels()==imp.getStackSize()) { + IJ.error(name, "Composite color images not supported"); + return; + } + if (!showDialog(name)) + return; + imp.startTiming(); + run(imp, filter, xradius, yradius, zradius); + IJ.showTime(imp, imp.getStartTime(), "", imp.getStackSize()); + } + + private boolean showDialog(String name) { + GenericDialog gd = new GenericDialog(name); + gd.addNumericField("X radius:", xradius, 1); + gd.addNumericField("Y radius:", yradius, 1); + gd.addNumericField("Z radius:", zradius, 1); + gd.showDialog(); + if (gd.wasCanceled()) { + return false; + } + xradius = (float) gd.getNextNumber(); + yradius = (float) gd.getNextNumber(); + zradius = (float) gd.getNextNumber(); + return true; + } + + private void run(ImagePlus imp, int filter, float radX, float radY, float radZ) { + if (imp.isHyperStack()) { + filterHyperstack(imp, filter, radX, radY, radZ); + return; + } + ImageStack res = filter(imp.getStack(), filter, radX, radY, radZ); + imp.setStack(res); + } + + public static ImageStack filter(ImageStack stackorig, int filter, float vx, float vy, float vz) { + + if (stackorig.getBitDepth()==24) + return filterRGB(stackorig, filter, vx, vy, vz); + + // get stack info + final ImageStack stack = stackorig; + final float voisx = vx; + final float voisy = vy; + final float voisz = vz; + final int width= stack.getWidth(); + final int height= stack.getHeight(); + final int depth= stack.size(); + ImageStack res = null; + + if ((filter==MEAN) || (filter==MEDIAN) || (filter==MIN) || (filter==MAX) || (filter==VAR)) { + if (filter==VAR) + res = ImageStack.create(width, height, depth, 32); + else + res = ImageStack.create(width, height, depth, stackorig.getBitDepth()); + IJ.showStatus("3D filtering..."); + // PARALLEL + final ImageStack out = res; + final AtomicInteger ai = new AtomicInteger(0); + final int n_cpus = Prefs.getThreads(); + + final int f = filter; + final int dec = (int) Math.ceil((double) stack.size() / (double) n_cpus); + Thread[] threads = ThreadUtil.createThreadArray(n_cpus); + for (int ithread = 0; ithread < threads.length; ithread++) { + threads[ithread] = new Thread() { + public void run() { + StackProcessor processor = new StackProcessor(stack); + for (int k = ai.getAndIncrement(); k < n_cpus; k = ai.getAndIncrement()) { + processor.filter3D(out, voisx, voisy, voisz, dec * k, dec * (k + 1), f); + } + } + }; + } + ThreadUtil.startAndJoin(threads); + } + return res; + } + + private static void filterHyperstack(ImagePlus imp, int filter, float vx, float vy, float vz) { + if (imp.getNDimensions()>4) { + IJ.error("5D hyperstacks are currently not supported"); + return; + } + if (imp.getNChannels()==1) { + ImageStack stack = filter(imp.getStack(), filter, vx, vy, vz); + imp.setStack(stack); + return; + } + ImagePlus[] channels = ChannelSplitter.split(imp); + int n = channels.length; + for (int i=0; idirectory.length()-5)) + directory = f.getParent(); + } + legacyRegex = Macro.getValue(macroOptions, "or", ""); + if (legacyRegex.equals("")) + legacyRegex = null; + } + } + if (arg==null) { + if (!showDialog()) return; + } + if (directory==null || directory.length()==0) { + error("No directory specified. "); + return; + } + File file = new File(directory); + if (!file.exists()) { + error("Directory not found: "+directory); + return; + } + String[] list = file.list(); + if (list==null) { + String parent = file.getParent(); + file = new File(parent); + list = file.list(); + if (list!=null) + directory = parent; + else { + error("Directory not found: "+directory); + return; + } + } + //remove subdirectories from list + ArrayList fileList = new ArrayList(); + for (int i=0; ilist.length) + this.start = 1; + if (this.start+this.nFiles-1>list.length) + this.nFiles = list.length-this.start+1; + int count = 0; + int counter = 0; + ImagePlus imp = null; + boolean firstMessage = true; + boolean fileInfoStack = false; + + // open images as stack + for (int i=this.start-1; i0 && stackHeight>0) { + width = stackWidth; + height = stackHeight; + } + if (bitDepth==0) + bitDepth = imp.getBitDepth(); + fi = imp.getOriginalFileInfo(); + ImageProcessor ip = imp.getProcessor(); + min = ip.getMin(); + max = ip.getMax(); + cal = imp.getCalibration(); + ColorModel cm = imp.getProcessor().getColorModel(); + if (openAsVirtualStack) { + if (stackSize>1) { + stack = new FileInfoVirtualStack(); + fileInfoStack = true; + } else { + if (stackWidth>0 && stackHeight>0) + stack = new VirtualStack(stackWidth, stackHeight, cm, directory); + else + stack = new VirtualStack(width, height, cm, directory); + } + } else if (this.scale<100.0) + stack = new ImageStack((int)(width*this.scale/100.0), (int)(height*this.scale/100.0), cm); + else + stack = new ImageStack(width, height, cm); + if (bitDepth!=0) + stack.setBitDepth(bitDepth); + info1 = (String)imp.getProperty("Info"); + } + if (imp==null) + continue; + if (imp.getWidth()!=width || imp.getHeight()!=height) { + if (stackWidth>0 && stackHeight>0) { + ImagePlus imp2 = imp.createImagePlus(); + ImageProcessor ip = imp.getProcessor(); + ImageProcessor ip2 = ip.createProcessor(width,height); + ip2.insert(ip, 0, 0); + imp2.setProcessor(ip2); + imp = imp2; + } else { + IJ.log(list[i] + ": wrong size; "+width+"x"+height+" expected, "+imp.getWidth()+"x"+imp.getHeight()+" found"); + continue; + } + } + String label = imp.getTitle(); + if (stackSize==1) { + String info = (String)imp.getProperty("Info"); + if (info!=null) { + if (useInfo(info)) + label += "\n" + info; + } else if (imp.getStackSize()>0) { + String sliceLabel = imp.getStack().getSliceLabel(1); + if (useInfo(sliceLabel)) + label = sliceLabel; + } + } + if (Math.abs(imp.getCalibration().pixelWidth-cal.pixelWidth)>0.0000000001) + allSameCalibration = false; + ImageStack inputStack = imp.getStack(); + Overlay overlay2 = imp.getOverlay(); + if (overlay2!=null && !openAsVirtualStack) { + if (overlay==null) + overlay = new Overlay(); + for (int j=0; j1) { + String sliceLabel = inputStack.getSliceLabel(slice); + if (sliceLabel!=null && sliceLabel.length()<=15) + label2 += ":"+sliceLabel; + else if (label2!=null && !label2.equals("")) + label2 += ":"+slice; + } + ip = inputStack.getProcessor(slice); + if (bitDepth2!=bitDepth) { + if (dicomImages && bitDepth==16 && bitDepth2==32 && this.scale==100) { + ip = ip.convertToFloat(); + bitDepth = 32; + ImageStack stack2 = new ImageStack(width, height, stack.getColorModel()); + for (int n=1; n<=stack.size(); n++) { + ImageProcessor ip2 = stack.getProcessor(n); + ip2 = ip2.convertToFloat(); + ip2.subtract(32768); + String sliceLabel = stack.getSliceLabel(n); + stack2.addSlice(sliceLabel, ip2.convertToFloat()); + } + stack = stack2; + } + } + if (this.scale<100.0) + ip = ip.resize((int)(width*this.scale/100.0), (int)(height*this.scale/100.0)); + if (ip.getMin()max) max = ip.getMax(); + stack.addSlice(label2, ip); + } + } + count++; + IJ.showStatus("!"+count+"/"+this.nFiles); + IJ.showProgress(count, this.nFiles); + if (count>=this.nFiles) + break; + if (IJ.escapePressed()) + {IJ.beep(); break;} + } // open images as stack + + } catch(OutOfMemoryError e) { + IJ.outOfMemory("FolderOpener"); + if (stack!=null) stack.trim(); + } + if (stack!=null && stack.size()>0) { + ImagePlus imp2 = new ImagePlus(title, stack); + if (imp2.getType()==ImagePlus.GRAY16 || imp2.getType()==ImagePlus.GRAY32) + imp2.getProcessor().setMinAndMax(min, max); + if (fi==null) + fi = new FileInfo(); + fi.fileFormat = FileInfo.UNKNOWN; + fi.fileName = ""; + fi.directory = directory; + imp2.setFileInfo(fi); // saves FileInfo of the first image + imp2.setOverlay(overlay); + if (stack instanceof VirtualStack) { + Properties props = ((VirtualStack)stack).getProperties(); + if (props!=null) + imp2.setProperty("FHT", props.get("FHT")); + } + if (allSameCalibration) { + // use calibration from first image + if (this.scale!=100.0 && cal.scaled()) { + cal.pixelWidth /= this.scale/100.0; + cal.pixelHeight /= this.scale/100.0; + } + if (cal.pixelWidth!=1.0 && cal.pixelDepth==1.0) + cal.pixelDepth = cal.pixelWidth; + imp2.setCalibration(cal); + } + if (info1!=null && info1.lastIndexOf("7FE0,0010")>0) { //DICOM + if (sortByMetaData) + stack = DicomTools.sort(stack); + imp2.setStack(stack); + double voxelDepth = DicomTools.getVoxelDepth(stack); + if (voxelDepth>0.0) { + if (IJ.debugMode) IJ.log("DICOM voxel depth set to "+voxelDepth+" ("+cal.pixelDepth+")"); + cal.pixelDepth = voxelDepth; + imp2.setCalibration(cal); + } + if (imp2.getType()==ImagePlus.GRAY16 || imp2.getType()==ImagePlus.GRAY32) { + imp2.getProcessor().setMinAndMax(min, max); + imp2.updateAndDraw(); + } + } + if (imp2.getStackSize()==1) { + int idx = this.start-1; + if (idx<0 || idx>=list.length) + idx = 0; + imp2.setProp("Slice_Label", list[idx]); + if (info1!=null) + imp2.setProperty("Info", info1); + } + if (arg==null && !saveImage) { + String time = (System.currentTimeMillis()-t0)/1000.0 + " seconds"; + if (openAsSeparateImages) { + if (imp2.getStackSize()>MAX_SEPARATE && !IJ.isMacro()) { + boolean ok = IJ.showMessageWithCancel("Import>Image Sequence", + "Are you sure you want to open "+imp2.getStackSize() + +" separate windows?\nThis may cause the system to become very slow or stall."); + if (!ok) return; + } + openAsSeparateImages(imp2); + } else + imp2.show(time); + if (stack.isVirtual()) { + overlay = stack.getProcessor(1).getOverlay(); + if (overlay!=null) + imp2.setOverlay(overlay); + } + } + if (saveImage) + image = imp2; + } + IJ.showProgress(1.0); + if (Recorder.record) { + String options = openAsVirtualStack?"virtual":""; + if (bitDepth!=defaultBitDepth) + options = options + " bitdepth=" + bitDepth; + if (filter!=null && filter.length()>0) { + if (filter.contains(" ")) + filter = "["+filter+"]"; + options = options + " filter=" + filter; + } + if (start!=1) + options = options + " start=" + start; + if (step!=1) + options = options + " step=" + step; + if (scale!=100) + options = options + " scale=" + scale; + if (!sortByMetaData) + options = options + " noMetaSort"; + String dir = Recorder.fixPath(directory); + Recorder.recordCall("imp = FolderOpener.open(\""+dir+"\", \""+options+"\");"); + } + } + + private void error(String msg) { + IJ.error("Import>Image Sequence", msg); + } + + private void openAsSeparateImages(ImagePlus imp) { + VirtualStack stack = (VirtualStack)imp.getStack(); + String dir = stack.getDirectory(); + int skip = 0; + for (int n=1; n<=stack.size(); n++) { + ImagePlus imp2 = IJ.openImage(dir+stack.getFileName(n)); + if (skip<=0) { + imp2.show(); + skip = imp2.getStackSize()-1; + } else + skip--; + } + } + + public static boolean useInfo(String info) { + return info!=null && !(info.startsWith("Software")||info.startsWith("ImageDescription")); + } + + private void openAsFileInfoStack(FileInfoVirtualStack stack, String path) { + FileInfo[] info = Opener.getTiffFileInfo(path); + if (info==null || info.length==0) + return; + int n =info[0].nImages; + if (info.length==1 && n>1) { + long size = fi.width*fi.height*fi.getBytesPerPixel(); + for (int i=0; i100.0) this.scale = 100.0; + sortFileNames = gd.getNextBoolean(); + if (!sortFileNames) + sortByMetaData = false; + openAsVirtualStack = gd.getNextBoolean(); + if (openAsVirtualStack) + scale = 100.0; + openAsSeparateImages = gd.getNextBoolean(); + if (openAsSeparateImages) + openAsVirtualStack = true; + if (!IJ.macroRunning()) { + staticSortFileNames = sortFileNames; + if (!openAsSeparateImages) + staticOpenAsVirtualStack = openAsVirtualStack; + } + return true; + } + + public static String[] getFilteredList(String[] list, String filter, String title) { + boolean isRegex = false; + if (filter!=null && (filter.equals("") || filter.equals("*"))) + filter = null; + if (list==null || filter==null) + return list; + if (title==null) { + String[] list2 = new String[list.length]; + for (int i=0; i=2 && filter.startsWith("(")&&filter.endsWith(")")) { + filter = filter.substring(1,filter.length()-1); + isRegex = true; + } + int filteredImages = 0; + for (int i=0; i0) + msg = msg.substring(0,index); + IJ.showStatus("Regex error: "+msg); + contains = true; + } + return contains; + } + + private int typeToBitDepth(String type) { + int depth = 0; + if (type.equals("16-bit")) depth=16; + else if (type.equals("32-bit")) depth=32; + else if (type.equals("RGB")) depth=24; + return depth; + } + + private String bitDepthToType(int bitDepth) { + switch (bitDepth) { + case 0: return types[0]; + case 16: return types[1]; + case 32: return types[2]; + case 24: return types[3]; + } + return types[0]; + } + + /** Removes names that start with "." or end with ".db", ".txt", ".lut", "roi", ".pty", ".hdr", ".py", etc. */ + public String[] trimFileList(String[] rawlist) { + if (rawlist==null) + return null; + int count = 0; + for (int i=0; i< rawlist.length; i++) { + String name = rawlist[i]; + if (name.startsWith(".")||name.equals("Thumbs.db")||excludedFileType(name)) + rawlist[i] = null; + else + count++; + } + if (count==0) return null; + String[] list = rawlist; + if (count1) + count = count - ((int)start-1); + double step = Tools.parseDouble(stepField.getText(), Double.NaN); + if (!Double.isNaN(step) && step>1) + count = count/(int)step; + String countStr = count>0?""+count:"---"; + countField.setText(countStr); + } + +} // FolderOpener + + diff --git a/src/ij/plugin/GIF_Reader.java b/src/ij/plugin/GIF_Reader.java new file mode 100644 index 0000000..14ff7db --- /dev/null +++ b/src/ij/plugin/GIF_Reader.java @@ -0,0 +1,741 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.io.*; +import java.net.*; +import java.io.*; +import java.util.*; +import java.awt.*; +import java.awt.image.*; + +/** This plugin opens GIFs and Animated GIFs. */ +public class GIF_Reader extends ImagePlus implements PlugIn { + + public void run(String arg) { + OpenDialog od = new OpenDialog("Open GIF...", arg); + String name = od.getFileName(); + if (name==null) + return; + String dir = od.getDirectory(); + GifDecoder d = new GifDecoder(); + int status = d.read(dir+name); + int n = d.getFrameCount(); + ImageStack stack = null; + if (n==1) { + Image img = Toolkit.getDefaultToolkit().createImage(dir+name); + setImage(img); + setTitle(name); + } else { + for (int i=0; i < n; i++) { + ImageProcessor frame = d.getFrame(i); + if (i==0) + stack = new ImageStack(frame.getWidth(), frame.getHeight()); + int t = d.getDelay(i); // display duration of frame in milliseconds + stack.addSlice(null, frame); + } + if (stack==null) + return; + setStack(name, stack); + if (getType()==COLOR_RGB) + Opener.convertGrayJpegTo8Bits(this); + } + FileInfo fi = new FileInfo(); + fi.fileFormat = fi.GIF_OR_JPG; + fi.fileName = name; + fi.directory = dir; + setFileInfo(fi); + } + +} + +/** + * Class GifDecoder - Decodes a GIF file into one or more frames. + * No copyright asserted on this code assembly. + * + * @author Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick. + * @version 1.0 January 2001 + * + * June 2001: Updated to work with ImageJ and JDK 1.1 + */ +class GifDecoder { + + /** + * File read status: No errors. + */ + public static final int STATUS_OK = 0; + + /** + * File read status: Error decoding file (may be partially decoded) + */ + public static final int STATUS_FORMAT_ERROR = 1; + + /** + * File read status: Unable to open source. + */ + public static final int STATUS_OPEN_ERROR = 2; + + private BufferedInputStream in; + private int status; + + private int width; // full image width + private int height; // full image height + private boolean gctFlag; // global color table used + private int gctSize; // size of global color table + private int loopCount; // iterations; 0 = repeat forever + + private int[] gct; // global color table + private int[] lct; // local color table + private int[] act; // active color table + + private int bgIndex; // background color index + private int bgColor; // background color + private int lastBgColor; // previous bg color + private int pixelAspect; // pixel aspect ratio + + private boolean lctFlag; // local color table flag + private boolean interlace; // interlace flag + private int lctSize; // local color table size + + private int ix, iy, iw, ih; // current image rectangle + private Rectangle lastRect; // last image rect + private ImageProcessor image; // current frame + private ImageProcessor lastImage; // previous frame + + private byte[] block = new byte[256]; // current data block + private int blockSize = 0; // block size + + // last graphic control extension info + private int dispose = 0; // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev + private int lastDispose = 0; + private boolean transparency = false; // use transparent color + private int delay = 0; // delay in milliseconds + private int transIndex; // transparent color index + + private static final int MaxStackSize = 4096; // max decoder pixel stack size + + // LZW decoder working arrays + private short[] prefix; + private byte[] suffix; + private byte[] pixelStack; + private byte[] pixels; + + private Vector frames; // frames read from current file + private int frameCount; + + /** + * Gets display duration for specified frame. + * + * @param n int index of frame + * @return delay in milliseconds + */ + + public int getDelay(int n) { + delay = -1; + if ((n >= 0) && (n < frameCount)) + delay = ((GifFrame) frames.elementAt(n)).delay; + return delay; + } + + + /** + * Gets the image contents of frame n. + * + * @return ImageProcessor representation of frame, or null if n is invalid. + */ + public ImageProcessor getFrame(int n) { + ImageProcessor im = null; + if ((n >= 0) && (n < frameCount)) + im = ((GifFrame) frames.elementAt(n)).image; + return im; + } + + + /** + * Gets the number of frames read from file. + * @return int frame count + */ + public int getFrameCount() { + return frameCount; + } + + + /** + * Gets the first (or only) image read. + * + * @return ImageProcessor containing first frame, or null if none. + */ + public ImageProcessor getImage() { + return getFrame(0); + } + + + /** + * Gets the "Netscape" iteration count, if any. + * A count of 0 means repeat indefinitiely. + * + * @return int iteration count if one was specified, else 1. + */ + public int getLoopCount() { + return loopCount; + } + + + /** + * Reads GIF image from stream + * + * @param BufferedInputStream containing GIF file. + * @return int read status code + */ + public int read(BufferedInputStream is) { + init(); + if (is != null) { + in = is; + readHeader(); + if (!err()) { + readContents(); + if (frameCount < 0) + status = STATUS_FORMAT_ERROR; + } + } else { + status = STATUS_OPEN_ERROR; + } + try { + is.close(); + } catch (IOException e) {} + return status; + } + + + /** + * Reads GIF file from specified source (file or URL string) + * + * @param name File source string + * @return int read status code + */ + public int read(String name) { + status = STATUS_OK; + try { + name = name.trim(); + if (name.indexOf("://") > 0) { + URL url = new URL(name); + in = new BufferedInputStream(url.openStream()); + } else { + in = new BufferedInputStream(new FileInputStream(name)); + } + status = read(in); + } catch (IOException e) { + status = STATUS_OPEN_ERROR; + } + + return status; + } + + + /** + * Decodes LZW image data into pixel array. + * Adapted from John Cristy's ImageMagick. + */ + + private void decodeImageData() { + int NullCode = -1; + int npix = iw * ih; + int available, clear, code_mask, code_size, end_of_information, in_code, + old_code, bits, code, count, i, datum, data_size, first, top, bi, pi; + + if ((pixels == null) || (pixels.length < npix)) + pixels = new byte[npix]; // allocate new pixel array + + if (prefix == null) + prefix = new short[MaxStackSize]; + if (suffix == null) + suffix = new byte[MaxStackSize]; + if (pixelStack == null) + pixelStack = new byte[MaxStackSize+1]; + + + // Initialize GIF data stream decoder. + + data_size = read(); + clear = 1 << data_size; + end_of_information = clear + 1; + available = clear + 2; + old_code = NullCode; + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + for (code = 0; code < clear; code++) { + prefix[code] = 0; + suffix[code] = (byte) code; + } + + // Decode GIF pixel stream. + + datum = bits = count = first = top = pi = bi = 0; + + for (i = 0; i < npix; ) { + if (top == 0) { + if (bits < code_size) { + // Load bytes until there are enough bits for a code. + if (count == 0) { + // Read a new data block. + count = readBlock(); + if (count <= 0) + break; + bi = 0; + } + datum += (((int) block[bi]) & 0xff) << bits; + bits += 8; + bi++; + count--; + continue; + } + + // Get the next code. + + code = datum & code_mask; + datum >>= code_size; + bits -= code_size; + + // Interpret the code + + if ((code > available) || (code == end_of_information)) + break; + if (code == clear) { + // Reset decoder. + code_size = data_size + 1; + code_mask = (1 << code_size) - 1; + available = clear + 2; + old_code = NullCode; + continue; + } + if (old_code == NullCode) { + pixelStack[top++] = suffix[code]; + old_code = code; + first = code; + continue; + } + in_code = code; + if (code == available) { + pixelStack[top++] = (byte) first; + code = old_code; + } + while (code > clear) { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } + first = ((int) suffix[code]) & 0xff; + + // Add a new string to the string table, + + if (available >= MaxStackSize) + break; + pixelStack[top++] = (byte) first; + prefix[available] = (short) old_code; + suffix[available] = (byte) first; + available++; + if (((available & code_mask) == 0) && (available < MaxStackSize)) { + code_size++; + code_mask += available; + } + old_code = in_code; + } + + // Pop a pixel off the pixel stack. + + top--; + pixels[pi++] = pixelStack[top]; + i++; + } + + for (i = pi; i < npix; i++) + pixels[i] = 0; // clear missing pixels + + } + + + /** + * Returns true if an error was encountered during reading/decoding + */ + private boolean err() { + return status != STATUS_OK; + } + + + /** + * Initializes or re-initializes reader + */ + private void init() { + status = STATUS_OK; + frameCount = 0; + frames = new Vector(); + gct = null; + lct = null; + } + + + /** + * Reads a single byte from the input stream. + */ + private int read() { + int curByte = 0; + try { + curByte = in.read(); + } catch (IOException e) { + status = STATUS_FORMAT_ERROR; + } + return curByte; + } + + + /** + * Reads next variable length block from input. + * + * @return int number of bytes stored in "buffer" + */ + private int readBlock() { + blockSize = read(); + int n = 0; + int count; + if (blockSize > 0) { + try { + while (n> 1; + transparency = (packed & 1) != 0; + delay = readShort() * 10; // delay in milliseconds + transIndex = read(); // transparent color index + read(); // block terminator + } + + + /** + * Reads GIF file header information. + */ + private void readHeader() { + String id = ""; + for (int i = 0; i < 6; i++) + id += (char) read(); + if (!id.startsWith("GIF")) { + status = STATUS_FORMAT_ERROR; + return; + } + + readLSD(); + if (gctFlag && !err()) { + gct = readColorTable(gctSize); + bgColor = gct[bgIndex]; + } + } + + + /** + * Reads next frame image + */ + + private void readImage() { + ix = readShort(); // (sub)image position & size + iy = readShort(); + iw = readShort(); + ih = readShort(); + + int packed = read(); + lctFlag = (packed & 0x80) != 0; // 1 - local color table flag + interlace = (packed & 0x40) != 0; // 2 - interlace flag + // 3 - sort flag + // 4-5 - reserved + lctSize = 2 << (packed & 7); // 6-8 - local color table size + + if (lctFlag) { + lct = readColorTable(lctSize); // read table + act = lct; // make local table active + } else { + act = gct; // make global table active + if (bgIndex == transIndex) + bgColor = 0; + } + int save = 0; + if (transparency) { + save = act[transIndex]; + act[transIndex] = 0; // set transparent color if specified + } + + if (act == null) { + status = STATUS_FORMAT_ERROR; // no color table defined + } + + if (err()) return; + + decodeImageData(); // decode pixel data + skip(); + + if (err()) return; + + frameCount++; + + // create new image to receive frame data + image = new ColorProcessor(width, height); + + setPixels(); // transfer pixel data to image + + frames.addElement(new GifFrame(image, delay)); // add image to frame list + + if (transparency) + act[transIndex] = save; + resetFrame(); + + } + + + /** + * Reads Logical Screen Descriptor + */ + private void readLSD() { + + // logical screen size + width = readShort(); + height = readShort(); + + // packed fields + int packed = read(); + gctFlag = (packed & 0x80) != 0; // 1 : global color table flag + // 2-4 : color resolution + // 5 : gct sort flag + gctSize = 2 << (packed & 7); // 6-8 : gct size + + bgIndex = read(); // background color index + pixelAspect = read(); // pixel aspect ratio + } + + + /** + * Reads Netscape extenstion to obtain iteration count + */ + private void readNetscapeExt() { + do { + readBlock(); + if (block[0] == 0x03) { + // loop count sub-block + int b1 = ((int) block[1]) & 0xff; + int b2 = ((int) block[2]) & 0xff; + loopCount = (b2 << 8) | b1; + } + } while ((blockSize > 0) && !err()); + } + + + /** + * Reads next 16-bit value, LSB first + */ + private int readShort() { + // read 16-bit value, LSB first + return read() | (read() << 8); + } + + + /** + * Resets frame state for reading next image. + */ + private void resetFrame() { + lastDispose = dispose; + lastRect = new Rectangle(ix, iy, iw, ih); + lastImage = image; + lastBgColor = bgColor; + int dispose = 0; + boolean transparency = false; + int delay = 0; + lct = null; + } + + + /** + * Creates new frame image from current data (and previous + * frames as specified by their disposition codes). + */ + private void setPixels() { + + // expose destination image's pixels as int array + int[] dest = (int[]) image.getPixels(); + + // fill in starting image contents based on last image's dispose code (if any) + if (lastDispose > 0) { + if (lastDispose == 3) { // use image before last + int n = frameCount - 2; + if (n > 0) + lastImage = getFrame(n-1); + else + lastImage = null; + } + + if (lastImage != null) { + int[] prev = (int[]) lastImage.getPixels(); + System.arraycopy(prev, 0, dest, 0, width*height); // copy pixels + + if ((lastDispose == 2) && (lastBgColor != 0)) { + // fill last image rect area with background color + image.setColor(new Color(lastBgColor)); + image.setRoi(lastRect); + image.fill(); + } + } + } + + // copy each source line to the appropriate place in the destination + int pass = 1; + int inc = 8; + int iline = 0; + for (int i = 0; i < ih; i++) { + int line = i; + if (interlace) { + if (iline >= ih) { + pass++; + switch (pass) { + case 2: + iline = 4; + break; + case 3: + iline = 2; + inc = 4; + break; + case 4: + iline = 1; + inc = 2; + } + } + line = iline; + iline += inc; + } + line += iy; + if (line < height) { + int k = line * width; + int dx = k + ix; // start of line in dest + int dlim = dx + iw; // end of dest line + if ((k + width) < dlim) + dlim = k + width; // past dest edge + int sx = i * iw; // start of line in source + while (dx < dlim) { + // map color and insert in destination + int index = ((int) pixels[sx++]) & 0xff; + dest[dx++] = act[index]; + } + } + } + } + + /** + * Skips variable length blocks up to and including + * next zero length block. + */ + private void skip() { + do { + readBlock(); + } while ((blockSize > 0) && !err()); + } +} + + class GifFrame { + public GifFrame(ImageProcessor im, int del) { + image = im; + delay = del; + } + public ImageProcessor image; + public int delay; + } + diff --git a/src/ij/plugin/GaussianBlur3D.java b/src/ij/plugin/GaussianBlur3D.java new file mode 100644 index 0000000..bafe5fe --- /dev/null +++ b/src/ij/plugin/GaussianBlur3D.java @@ -0,0 +1,119 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.plugin.filter.*; + +public class GaussianBlur3D implements PlugIn { + private static double xsigma=2, ysigma=2, zsigma=2; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + if (imp.isComposite() && imp.getNChannels()==imp.getStackSize()) { + IJ.error("3D Gaussian Blur", "Composite color images not supported"); + return; + } + if (!showDialog()) + return; + imp.startTiming(); + blur(imp, xsigma, ysigma, zsigma); + IJ.showTime(imp, imp.getStartTime(), "", imp.getStackSize()); + } + + private boolean showDialog() { + GenericDialog gd = new GenericDialog("3D Gaussian Blur"); + gd.addNumericField("X sigma:", xsigma, 1); + gd.addNumericField("Y sigma:", ysigma, 1); + gd.addNumericField("Z sigma:", zsigma, 1); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + xsigma = gd.getNextNumber(); + ysigma = gd.getNextNumber(); + zsigma = gd.getNextNumber(); + return true; + } + + public static void blur(ImagePlus imp, double sigmaX, double sigmaY, double sigmaZ) { + imp.deleteRoi(); + ImageStack stack = imp.getStack(); + if (sigmaX>0.0 || sigmaY>0.0) { + GaussianBlur gb = new GaussianBlur(); + int channels = stack.getProcessor(1).getNChannels(); + gb.setNPasses(channels*imp.getStackSize()); + for (int i=1; i<=imp.getStackSize(); i++) { + ImageProcessor ip = stack.getProcessor(i); + double accuracy = (imp.getBitDepth()==8||imp.getBitDepth()==24)?0.002:0.0002; + gb.blurGaussian(ip, sigmaX, sigmaY, accuracy); + } + } + if (sigmaZ>0.0) { + if (imp.isHyperStack()) + blurHyperStackZ(imp, sigmaZ); + else + blurZ(stack, sigmaZ); + imp.updateAndDraw(); + } + } + + private static void blurZ(ImageStack stack, double sigmaZ) { + GaussianBlur gb = new GaussianBlur(); + double accuracy = (stack.getBitDepth()==8||stack.getBitDepth()==24)?0.002:0.0002; + int w=stack.getWidth(), h=stack.getHeight(), d=stack.size(); + float[] zpixels = null; + FloatProcessor fp =null; + IJ.showStatus("Z blurring"); + gb.showProgress(false); + int channels = stack.getProcessor(1).getNChannels(); + for (int y=0; y=2 || IJ.altKeyDown()) { + if (showLaneDialog) { + String msg = "Are the lanes really horizontal?\n \n"+ + "ImageJ assumes the lanes are\n"+ + "horizontal if the selection is more\n"+ + "than twice as wide as it is tall. Note\n"+ + "that the selection can only be moved\n"+ + "vertically when the lanes are horizontal."; + GenericDialog gd = new GenericDialog("Gel Analyzer"); + gd.addMessage(msg); + gd.setOKLabel("Yes"); + gd.showDialog(); + if (gd.wasCanceled()) return; + showLaneDialog = false; + } + isVertical = false; + } else + isVertical = true; + + /* + if ( (isVertical && (rect.height/rect.width)<2 ) || (!isVertical && (rect.width/rect.height)<2 ) ) { + GenericDialog gd = new GenericDialog("Lane Orientation"); + String[] orientations = {"Vertical","Horizontal"}; + int defaultOrientation = isVertical?0:1; + gd.addChoice("Lane Orientation:", orientations, orientations[defaultOrientation]); + gd.showDialog(); + if (gd.wasCanceled()) + return; + String orientation = gd.getNextChoice(); + if(orientation.equals(orientations[0])) + isVertical=true; + else + isVertical=false; + } + */ + + IJ.showStatus("Lane 1 selected ("+(isVertical?"vertical":"horizontal")+" lanes)"); + firstRect = rect; + nLanes = 1; + saveNLanes = 0; + if(isVertical) + x[1] = rect.x; + else + x[1] = rect.y; + gel = imp; + saveID = imp.getID(); + overlay = null; + updateRoiList(rect); + } + + void selectNextLane(Rectangle rect) { + if (rect.width!=firstRect.width || rect.height!=firstRect.height) { + show("Selections must all be the same size."); + return; + } + if (nLanesmax) + max = pp.getMax(); + if (uncalibratedOD) + profiles[i] = od(profiles[i]); + } + if (uncalibratedOD) { + min = odMin; + max = odMax; + } + + if (isVertical) + plotWidth = firstRect.height; + else + plotWidth = firstRect.width; + if (plotWidth<650) + plotWidth = 650; + if (isVertical) { + if (plotWidth>4*firstRect.height) + plotWidth = 4*firstRect.height; + } else { + if (plotWidth>4*firstRect.width) + plotWidth = 4*firstRect.width; + } + + if (verticalScaleFactor==0.0) verticalScaleFactor=1.0; + if (horizontalScaleFactor==0.0) horizontalScaleFactor=1.0; + Dimension screen = IJ.getScreenSize(); + if (plotWidth>screen.width-screen.width/6) + plotWidth = screen.width - screen.width/6; + plotWidth = (int)(plotWidth*horizontalScaleFactor); + plotHeight = plotWidth/2; + if (plotHeight<250) plotHeight = 250; + // if (plotHeight>500) plotHeight = 500; + plotHeight = (int)(plotHeight*verticalScaleFactor); + ImageProcessor ip = new ByteProcessor(plotWidth, topMargin+nLanes*plotHeight+bottomMargin); + ip.setColor(Color.white); + ip.fill(); + ip.setColor(Color.black); + //draw border + int h= ip.getHeight(); + ip.moveTo(0,0); + ip.lineTo(plotWidth-1,0); + ip.lineTo(plotWidth-1, h-1); + ip.lineTo(0, h-1); + ip.lineTo(0, 0); + ip.moveTo(0, h-2); + ip.lineTo(plotWidth-1, h-2); + String s = imp.getTitle()+"; "; + Calibration cal = imp.getCalibration(); + if (cal.calibrated()) + s += cal.getValueUnit(); + else if (uncalibratedOD) + s += "Uncalibrated OD"; + else + s += "Uncalibrated"; + ip.moveTo(5,topMargin); + ip.drawString(s); + double xScale = (double)plotWidth/profiles[1].length; + double yScale; + if ((max-min)==0.0) + yScale = 1.0; + else + yScale = plotHeight/(max-min); + for (int i=1; i<=nLanes; i++) { + double[] profile = profiles[i]; + int top = (i-1)*plotHeight + topMargin; + int base = top+plotHeight; + ip.moveTo(0, base); + ip.lineTo((int)(profile.length*xScale), base); + ip.moveTo(0, base-(int)((profile[0]-min)*yScale)); + for (int j = 1; jodMax) odMax = v; + profile[i] = v; + } + return profile; + } + + void outlineLanes() { + if (gel==null || overlay==null) { + show("Data needed to outline lanes is no longer available."); + return; + } + int lineWidth = (int)(1.0/gel.getCanvas().getMagnification()); + if (lineWidth<1) + lineWidth = 1; + Font f = new Font("Helvetica", Font.PLAIN, 12*lineWidth); + ImageProcessor ip = gel.getProcessor(); + ImageProcessor ipLanes = ip.duplicate(); + if (!(ipLanes instanceof ByteProcessor)) + ipLanes = ipLanes.convertToByte(true); + ipLanes.setFont(f); + ipLanes.setLineWidth(lineWidth); + setCustomLut(ipLanes); + ImagePlus lanes = new ImagePlus("Lanes of "+gel.getShortTitle(), ipLanes); + lanes.changes = true; + lanes.setRoi(gel.getRoi()); + gel.deleteRoi(); + for (int i=0; iscreen.width) + w = screen.width-loc.x-20; + if (loc.y+h>screen.height) + h = screen.height-loc.y-30; + win.setSize(w, h); + win.validate(); + repaintWindow(); + } + +} + + +class PlotsCanvas extends ImageCanvas { + + public static final int MAX_PEAKS = 200; + + double[] actual = {428566.00,351368.00,233977.00,99413.00,60057.00,31382.00, + 14531.00,7843.00,2146.00,752.00,367.00}; + double[] measured = new double[MAX_PEAKS]; + Rectangle[] rect = new Rectangle[MAX_PEAKS]; + int counter; + ResultsTable rt; + + public PlotsCanvas(ImagePlus imp) { + super(imp); + } + + public void mousePressed(MouseEvent e) { + super.mousePressed(e); + Roi roi = imp.getRoi(); + if (roi==null) + return; + if (roi.getType()==Roi.LINE) + Roi.setColor(Color.blue); + else + Roi.setColor(Color.yellow); + if (Toolbar.getToolId()!=Toolbar.WAND || IJ.spaceBarDown()) + return; + if (IJ.shiftKeyDown()) { + IJ.showMessage("Gel Analyzer", "Unable to measure area because shift key is down."); + imp.deleteRoi(); + counter = 0; + return; + } + ImageStatistics stats = imp.getStatistics(); + if (counter==0) { + rt = ResultsTable.getResultsTable(); + rt.reset(); + } + //IJ.setColumnHeadings(" \tArea"); + double perimeter = roi.getLength(); + String error = ""; + double circularity = 4.0*Math.PI*(stats.pixelCount/(perimeter*perimeter)); + if (circularity<0.025) + error = " (error?)"; + double area = stats.pixelCount+perimeter/2.0; // add perimeter/2 to account area under border + Calibration cal = imp.getCalibration(); + area = area*cal.pixelWidth*cal.pixelHeight; + rect[counter] = roi.getBounds(); + + //area += (rect[counter].width/rect[counter].height)*1.5; + // adjustment for small peaks from NIH Image gel macros + + int places = cal.scaled()?3:0; + rt.incrementCounter(); + rt.addValue("Area", area); + rt.show("Results"); + measured[counter] = area; + if (counter500; + if (r.height>=(GelAnalyzer.plotHeight-11)) + fits = true; + if (!fits) + y = r.y - 2; + ip.drawString(s, x, y); + } + imp.updateAndDraw(); + displayPercentages(); + //Toolbar.getInstance().setTool(Toolbar.RECTANGLE); + reset(); + } + + void displayPercentages() { + ResultsTable rt = ResultsTable.getResultsTable(); + rt.reset(); + //IJ.setColumnHeadings(" \tarea\tpercent"); + double total = 0.0; + for (int i=0; i60.0) fps = 60.0; + ge.setDelay((int)((1.0/fps)*1000.0)); + if (transparentIndex!=-1) { + ge.transparent = true; + ge.transIndex = transparentIndex; + } + ge.start(path); + ImagePlus tmp = new ImagePlus(); + for (int i=1; i<=nSlices; i++) { + IJ.showStatus("writing: "+i+"/"+nSlices); + IJ.showProgress((double)i/nSlices); + tmp.setProcessor(null, stack.getProcessor(i)); + if (overlay!=null) { + Overlay overlay2 = overlay.duplicate(); + overlay2.crop(i, i); + if (overlay2.size()>0) { + tmp.setOverlay(overlay2); + tmp = tmp.flatten(); + if (imp.getBitDepth()==8) + new ImageConverter(tmp).convertRGBtoIndexedColor(256); + } + } + try { + ge.addFrame(tmp); + } catch(Exception e) { + error = ""+e; + if (showErrors) { + IJ.error("Save as Gif: "+e); + showErrors = false; + } + } + } + ge.finish(); + IJ.showStatus(""); + IJ.showProgress(1.0); + } + + private void writeImage(ImagePlus imp, String path, int transparentIndex) throws Exception { + if (transparentIndex>=0 && transparentIndex<=255) + writeImageWithTransparency(imp, path, transparentIndex); + else + ImageIO.write(imp.getBufferedImage(), "gif", new File(path)); + } + + private void writeImageWithTransparency(ImagePlus imp, String path, int transparentIndex) throws Exception { + int width = imp.getWidth(); + int height = imp.getHeight(); + ImageProcessor ip = imp.getProcessor(); + IndexColorModel cm = (IndexColorModel)ip.getColorModel(); + int size = cm.getMapSize(); + //IJ.log("write: "+size+" "+transparentIndex); + byte[] reds = new byte[256]; + byte[] greens = new byte[256]; + byte[] blues = new byte[256]; + cm.getReds(reds); + cm.getGreens(greens); + cm.getBlues(blues); + cm = new IndexColorModel(8, size, reds, greens, blues, transparentIndex); + WritableRaster wr = cm.createCompatibleWritableRaster(width, height); + DataBufferByte db = (DataBufferByte)wr.getDataBuffer(); + byte[] biPixels = db.getData(); + System.arraycopy(ip.getPixels(), 0, biPixels, 0, biPixels.length); + BufferedImage bi = new BufferedImage(cm, wr, false, null); + ImageIO.write(bi, "gif", new File(path)); + } + +} + + +/** + * Class AnimatedGifEncoder2 - Encodes a GIF file consisting of one or + * more frames. + *
+ *
+ *
+ * Extensively Modified for ImagePlus
+ * Extended to handle 8 bit Images with more complex Color lookup tables with transparency index
+ *
+ * Ryan Raz March 2002
+ * raz@rraz.ca
+ * Version 1.01
+ ** Extensively Modified for ImagePlus
+ * Extended to handle 8 bit Images with more complex Color lookup tables with transparency index
+ *
+ * Ryan Raz March 2002
+ * ryan@rraz.ca
+ * Version 1.01 Please report any bugs
+ *
+ * Operation Manual
+ *
+ *
+ * 1) Load stack with 8 bit or RGB images it is possible to use the animated gif reader but because the color
+ *	 table is lost it is best to also load a separate copy of the first image in the series this will allow 
+ *	 extraction of the original image color look up table (see 1below)
+ * 2)Check the option list to bring up the option list.
+ * 3)Experiment with the option list. I usually use a global color table to save space, set to do not dispose if 
+ *		each consecutive image is overlayed on the previous image.
+ * 4)Color table can be imported from another image or extracted from 8bit stack images or loaded as the
+ *	  first 256	 RGB triplets from a RGB images, the last mode takes either a imported image or current 
+ *	  stack and creates the color table from scratch.
+ *	
+ *
+ *	  To do list 
+ *
+ *	   1) Modify existing Animated Gif reader plug in to import in 8 bit mode (currently only works in 
+ *		   RGB	mode.  Right now the best way to alter an animated gif is to save the first image separately
+ *		   and then read the single gif and use the plugin animated reader to read the animated gif to the 
+ *		   stack. Let this plugin encode the stack using the single gif's color table.
+ *		2) Add support for background colors easy but I have no use for them
+ *		3) RGB to 8 bit converter is a linear search. Needs to be replaced with sorted list and fast search. But 
+ *			this update could cause problems with some types of gifs. Easy fix get a faster computer.
+ *		4) Try updating NN color converter seems to be heavily weighted towards quantity of pixels.
+ *		  example:
+ *			 if there is 90% of the image covered in shades of one color or grey the 10% of other colors tend 
+ *			 to be poorly represented it  over fits the shades and under fits the others. Works well if the
+ *			distribution  is balanced.
+ *		 5) Add support for all sizes of Color Look Up tables.
+ *		 6) Re-code to be cleaner. This is my second Java program and I started with some code with too 
+ *			 many  global variables and I added more switches so its a bit hard to follow.
+ *
+ * Credits for the base conversion codes
+ * No copyright asserted on the source code of this class.	May be used
+ * for any purpose, however, refer to the Unisys LZW patent for restrictions
+ * on use of the associated LZWEncoder class.  Please forward any corrections
+ * to kweiner@fmsware.com.
+ *
+ * @author Kevin Weiner, FM Software
+ * @version 1.0 December 2000
+ *
+ *
+ * Example:
+ *	  AnimatedGifEncoder2 e = new AnimatedGifEncoder2();
+ *	  e.start(outputFileName);
+ *	  e.addFrame(image1);
+ *	  e.addFrame(image2);
+ *		"			"			  "
+ *	  e.finish();
+ * 
+ * + * + */ + +class AnimatedGifEncoder2 { + + protected int width; // image size + protected int height; + protected boolean transparent = false; // transparent color if given + protected int transIndex; // transparent index in color table + protected int repeat = -1; // no repeat + protected int delay = 50; // frame delay (hundredths) + protected boolean started = false; // ready to output frames + protected OutputStream out; + protected ImagePlus image; // current frame + protected byte[] pixels; // BGR byte array from frame + protected byte[] indexedPixels; // converted frame indexed to palette + protected int colorDepth; // number of bit planes + protected byte[] colorTab; // RGB palette + protected int lctSize = 7; // local color table size (bits-1) + protected int dispose = 0; // disposal code (-1 = use default) + protected boolean closeStream = false; // close stream when finished + protected boolean firstFrame = true; + protected boolean sizeSet = false; // if false, get size from first frame + protected int sample = 2; // default sample interval for quantizer distance should be small for small icons + protected byte[] gct = null; //Global color table + protected boolean gctused = false; // Set to true to use Global color table + protected boolean autotransparent = false; // Set True if transparency index coming from image 8 bit only + protected boolean GCTextracted = false; // Set if global color table extracted from rgb image + protected boolean GCTloadedExternal = false; // Set if global color table loaded directly from external image + protected int GCTred = 0; //Transparent Color + protected int GCTgrn = 0; // green + protected int GCTbl = 0; // blue + protected int GCTcindex = 0; //index into color table + protected boolean GCTsetTransparent = false; //If true then Color table transparency index is set + protected boolean GCToverideIndex = false; //If true Transparent index is set to index with closest colors + protected boolean GCToverideColor = false; //if true Color at Transparent index is set to GCTred, GCTgrn GCTbl + + /** + * Adds next GIF frame. The frame is not written immediately, but is + * actually deferred until the next frame is received so that timing + * data can be inserted. Invoking finish() flushes all + * frames. If setSize was not invoked, the size of the + * first image is used for all subsequent frames. + * + * @param im containing frame to write. + * @return true if successful. + */ + public boolean addFrame(ImagePlus image) { + if ((image == null) || !started) return false; + boolean ok = true; + try { + if (firstFrame) { + if (!sizeSet) { + // use first frame's size + setSize(image.getWidth(), image.getHeight()); + } + if(gctused) + writeLSDgct(); // logical screen descriptior + if (GCTloadedExternal){ //Using external image as color table + colorTab = gct; + TransparentIndex(colorTab); //check transparency color + writePalette(); // write global color table + if (repeat >= 0) + writeNetscapeExt(); // use NS app extension to indicate reps + } + if (!gctused) { + writeLSD(); + if (repeat >= 0) + writeNetscapeExt(); // use NS app extension to indicate reps + } + firstFrame = false; + } + + int type = image.getType(); + // If indexed byte image then format does not need changing + int k; + if ((type == 0) ||( type == 3)) //8 bit images + Process8bitCLT(image); + else if (type==4) { //4 for RGB + packrgb(image); + OverRideQuality(image.getWidth()*image.getHeight()); + if (gctused && (gct == null)) { //quality should not depend on image size + analyzePixels(); // build global color table & map pixels + colorTab = gct; + TransparentIndex(colorTab); //check transparency color + writePalette(); // write global color table + if (repeat >= 0) + writeNetscapeExt(); // use NS app extension to indicate reps + } else + analyzePixels(); // build color table & map pixels + } + else throw new IllegalArgumentException("Image must be 8-bit or RGB"); + TransparentIndex(colorTab); //check transparency color + writeGraphicCtrlExt(); // write graphic control extension + writeImageDesc(); // image descriptor + if(!gctused) writePalette(); // local color table + writePixels(); // encode and write pixel data + } catch (IOException e) { ok = false; } + + return ok; + } + + /* + Handles transparency color Index + Assumes colors and index are already checked for validity + */ + void TransparentIndex(byte[] colorTab){ + if(autotransparent|| !GCTsetTransparent) return; + if(colorTab==null)throw new IllegalArgumentException("Color Table not loaded."); + int len = colorTab.length; + setTransparent(true); //Sets color tranparency flag + if (!(GCToverideColor||GCToverideIndex)){ + transIndex = GCTcindex; //sets color index + return; + } + if(GCToverideIndex) + GCTcindex= findClosest(colorTab, GCTred, GCTgrn, GCTbl); + //finds index in color Table + transIndex = GCTcindex; + int pindex = 3*GCTcindex; + if (pindex>(len-3)) + throw new IllegalArgumentException("Index ("+transIndex+") too large for Color Lookup table."); + colorTab[pindex++] = (byte)GCTred; //Set Color Table[transparent index] with specified color + colorTab[pindex++] = (byte)GCTgrn; + colorTab[pindex] = (byte)GCTbl; + } + +String name; + +public boolean setoptions() { + String[] GCTtype = {"Do not use","Load from Current Image", "Load from another Image RGB or 8 Bit", + "Use another RGB to create a new color table " }; + String[] DisposalType = { "No Disposal","Do not Dispose", "Restore to Background", "Restore to previous" }; + String[] TransparencyType ={"No Transparency", "Automatically Set if Available (8 bit only)", "Set to Index", + "Set to index with specified color", "Set to the index that is closest to specified color"}; + int setdelay=delay*10; + int gctType=0; + int setTrans; + if (GCTloadedExternal) gctType = 2; + if (GCTextracted&&GCTloadedExternal) gctType =3; + if (gctused&&!(GCTextracted||GCTloadedExternal))gctType=1; + setTrans=1; + if (!(autotransparent||GCTsetTransparent||GCToverideIndex||GCToverideColor)) setTrans=0; + if (GCTsetTransparent&& !(GCToverideIndex||GCToverideColor)) setTrans = 2; + if (GCTsetTransparent&& GCToverideIndex && !GCToverideColor) setTrans = 4; + if (GCTsetTransparent&& !GCToverideIndex && GCToverideColor) setTrans = 3; + int red = GCTred; + int grn = GCTgrn; + int bl = GCTbl; + int cindex =GCTcindex; + setRepeat(0); + autotransparent=false; //no transparent index + GCTsetTransparent=false; + GCToverideIndex=false; + GCToverideColor=false; + setTransparent(false); + switch (setTrans) { + case 0: break; + case 1: autotransparent=true; //Set if available from image byte images only + break; + case 2: if(cindex>-1) { + GCTsetTransparent=true; //set specified index as transparent color + GCTcindex=cindex; + } else + IJ.error("Incorrect color index must have value between 0 and 255"); + break; + case 3: if((cindex>-1)&&(red>-1)) { //Set transparent index with specified color + GCTsetTransparent=true; + GCToverideColor=true; + GCTcindex=cindex; + GCTred=red; + GCTgrn=grn; + GCTbl=bl; + } else + IJ.error("Incorrect colors or color index, they must have values between 0 and 255."); + break; + case 4: if(red>-1){ + GCTsetTransparent=true; //Set transparent index to + GCToverideIndex=true; //index which is closest to the specified color + GCTred=red; // and replace the color at the index with + GCTgrn=grn; + GCTbl=bl; + } else + IJ.error("Incorrect colors, they must have values between 0 and 255."); + break; + default: break; + } + + gctused = false; // Set to true to use Global color table + GCTextracted = false; // Set if global color table extracted from rgb image + GCTloadedExternal = false; // Set if global color table loaded directly from external image + return true; + } + +/******************************************************** +* Gets Color lookup Table from 8 bit image plus pointer to image +*/ +void Process8bitCLT(ImagePlus image) { + colorDepth = 8; + setTransparent(false); + ByteProcessor pg = new ByteProcessor(image.getImage()); + ColorModel cm = pg.getColorModel(); + if (cm instanceof IndexColorModel) + indexedPixels = (byte[])(pg.getPixels()); + else + throw new IllegalArgumentException("Image must be 8-bit"); + IndexColorModel m = (IndexColorModel)cm; + if (autotransparent) { + transIndex = m.getTransparentPixel(); + if ((transIndex > -1) && (transIndex < 256)) setTransparent(true); //Sets color flag + else transIndex =0; + } + int mapSize = m.getMapSize(); + int k; + if (gctused && (gct == null)) { + gct = new byte[mapSize*3]; //Global color table needs to be intialized + for (int i = 0; i < mapSize; i++) { + k=i*3; + colorTab[k] = (byte)m.getRed(i); + colorTab[k+1] = (byte)m.getGreen(i); + colorTab[k+2] = (byte)m.getBlue(i); + } + try { + if (! GCTloadedExternal) { + colorTab = gct; + writePalette(); // write global color table + if (repeat >= 0) + writeNetscapeExt(); // use NS app extension to indicate reps + } + } catch (IOException e) { + System.err.println("Caught IOException: " + e.getMessage()); + } + } + if (gctused) + colorTab = gct; + else { + colorTab = new byte[mapSize*3]; + for (int i = 0; i < mapSize; i++) { + k=i*3; + colorTab[k] = (byte)m.getRed(i); + colorTab[k+1] = (byte)m.getGreen(i); + colorTab[k+2] = (byte)m.getBlue(i); + } + } + m.finalize(); + } + + /** + * Flushes any pending data and closes output file. + * If writing to an OutputStream, the stream is not + * closed. + */ + public boolean finish() { + if (!started) return false; + boolean ok = true; + started = false; + try { + out.write(0x3b); // gif trailer + out.flush(); + if (closeStream) + out.close(); + } catch (IOException e) { ok = false; } + + // reset for subsequent use + GCTextracted = false; // Set if global color table extracted from rgb image + GCTloadedExternal = false; // Set if global color table loaded directly from external image + transIndex = 0; + transparent = false; + gct = null; //Global color table + out = null; + image = null; + pixels = null; + indexedPixels = null; + colorTab = null; + closeStream = false; + firstFrame = true; + + return ok; + } + +/* + * Function to load Global Color Table from 8 bit ImagePlus + * This function has to be called before addFrame + */ + public void loadGCT8bit(ImagePlus image){ + int type = image.getType(); + if (!(((type == 0) ||( type == 3))&&(image!=null))) + throw new IllegalArgumentException("Color Table Image must be 8 bit"); + gctused = true; + GCTloadedExternal = true; + gct = null; + Process8bitCLT(image); + } +/* + * Function to extract Global Color Table from RGB ImagePlus + * This function has to be called before addFrame + */ + public void extractGCTrgb(ImagePlus image){ + if((image== null)||(4!=image.getType())) + throw new IllegalArgumentException("Color Table Image must be RGB"); + packrgb(image); + gctused = true; + GCTextracted = true; + GCTloadedExternal =true; + gct = null; + OverRideQuality(image.getWidth()*image.getHeight()); + analyzePixels(); // build color table + pixels = null; + } + +void packrgb(ImagePlus image){ + int len = image.getWidth()*image.getHeight(); + ImageProcessor imp = image.getProcessor(); + int[] pix = (int[]) imp.getPixels(); + pixels = new byte[len*3]; + //pack pixels + for(int i=0; i>16); //red + pixels[k+1] = (byte)((pix[i] & 0x00ff00)>>8); //green + pixels[k] = (byte)(pix[i] & 0x0000ff); //blue + } +} + +/* + * Function to use the first up to 255 elements of a RGB ImagePlus to construct + * a global color table + * This function has to be called before addFrame + */ +public void loadGCTrgb(ImagePlus image){ + if((image == null)||(4!=image.getType())) + throw new IllegalArgumentException("Color Table Image must be RGB"); + int len = image.getWidth()*image.getHeight(); + if(len>255)len=255; + ImageProcessor imp = image.getProcessor(); + int[] pix = (int[]) imp.getPixels(); + gct = new byte[len*3]; + //pack pixels into color Table + for(int i=0; i>16); //red + gct[k+1] = (byte)((pix[i] & 0x00ff00)>>8); //green + gct[k+2] = (byte)(pix[i] & 0x0000ff); //blue + } + gctused = true; + GCTloadedExternal = true; +} + + /* + * If gct = true then a global color table is use + * + */ + public void setGCT(boolean flag){ + gctused = flag; + } + + /** + * Sets the delay time between each frame, or changes it + * for subsequent frames (applies to last frame added). + * + * @param ms int delay time in milliseconds + */ + public void setDelay(int ms) { + delay = Math.round(ms / 10.0f); + } + + + /** + * Sets the GIF frame disposal code for the last added frame + * and any subsequent frames. Default is 0 if no transparent + * color has been set, otherwise 2. + * @param code int disposal code. + */ + public void setDispose(int code) { + if (code >= 0) + dispose = code; + } + + + /** + * Sets frame rate in frames per second. Equivalent to + * setDelay(1000/fps). + * + * @param fps float frame rate (frames per second) + */ + public void setFrameRate(float fps) { + if (fps != 0f) { + delay = Math.round(100f/fps); + } + } + + + /** + * Sets quality of color quantization (conversion of images + * to the maximum 256 colors allowed by the GIF specification). + * Lower values (minimum = 1) produce better colors, but slow + * processing significantly. 10 is the default, and produces + * good color mapping at reasonable speeds. Values greater + * than 20 do not yield significant improvements in speed. + * + * @param quality int greater than 0. + * @return + */ + public void setQuality(int quality) { + if (quality < 1) quality = 1; + sample = quality; + } +/** + * Set True for Global Color Table use + * This saves space in the output file but colors may not be so goodif the stack uses + * True color images + */ + public void GlobalColorTableused(boolean gtu){ + gctused = gtu; + } + + /** + * Sets the number of times the set of GIF frames + * should be played. Default is 1; 0 means play + * indefinitely. Must be invoked before the first + * image is added. + * + * @param iter int number of iterations. + * @return + */ + public void setRepeat(int iter) { + if (iter >= 0) + repeat = iter; + } + + /** + * Sets the GIF frame size. The default size is the + * size of the first frame added if this method is + * not invoked. + * + * @param w int frame width. + * @param h int frame width. + */ + public void setSize(int w, int h) { + if (started && !firstFrame) return; + width = w; + height = h; + if (width < 1) width = 320; + if (height < 1) height = 240; + sizeSet = true; + } + + + /** + * Sets the transparent color for the last added frame + * and any subsequent frames. + * Since all colors are subject to modification + * in the quantization process, the color in the final + * palette for each frame closest to the given color + * becomes the transparent color for that frame. + * May be set to null to indicate no transparent color. + * + * @param c Color to be treated as transparent on display. + */ + public void setTransparent(boolean c) { + transparent = c; + } + + + /** + * Initiates GIF file creation on the given stream. The stream + * is not closed automatically. + * + * @param os OutputStream on which GIF images are written. + * @return false if initial write failed. + */ + public boolean start(OutputStream os) { + if (os == null) return false; + boolean ok = true; + closeStream = false; + out = os; + try { + writeString("GIF89a"); // header + } catch (IOException e) { ok = false; } + return started = ok; + } + + + /** + * Initiates writing of a GIF file with the specified name. + * + * @param file String containing output file name. + * @return false if open or initial write failed. + */ + public boolean start(String file) { + boolean ok = true; + try { + out = new BufferedOutputStream(new FileOutputStream(file)); + ok = start(out); + closeStream = true; + } catch (IOException e) { ok = false; } + return started = ok; + } +/** + Sets Net sample size depending on image size + +**/ + public void OverRideQuality(int npixs){ + if(npixs>100000) sample = 10; + else sample = npixs/10000; + if(sample < 1) sample = 1; + + } + /** + * Analyzes image colors and creates color map. + */ + protected void analyzePixels() { + int len = pixels.length; + int nPix = len / 3; + indexedPixels = new byte[nPix]; + if (gctused && (gct == null)) { + NeuQuant nq = new NeuQuant(pixels, len, sample); // initialize quantizer + colorTab = nq.process(); // create reduced palette + gct = new byte[colorTab.length]; + // convert map from BGR to RGB + for (int i = 0; i < colorTab.length; i+=3) { + byte temp = colorTab[i]; + colorTab[i] = colorTab[i+2]; + colorTab[i+2] = temp; + gct[i] = colorTab[i]; + gct[i+1] = colorTab[i+1]; + gct[i+2] =colorTab[i+2]; + } + if(GCTextracted){ + indexedPixels= null; + return; + } + } + if (!gctused){ + NeuQuant nq = new NeuQuant(pixels, len, sample); // initialize quantizer + colorTab = nq.process(); // create reduced palette + // convert map from BGR to RGB + for (int i = 0; i < colorTab.length; i+=3) { + byte temp = colorTab[i]; + colorTab[i] = colorTab[i+2]; + colorTab[i+2] = temp; + } + // map image pixels to new palette + int k = 0; + for (int i = 0; i < nPix; i++) + indexedPixels[i] = + (byte) nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff, pixels[k++] & 0xff); + pixels = null; + colorDepth = 8; + lctSize = 7; + } + if(gctused){ + // find closest match for all pixels This routine is not optimized real slow linear search. + colorTab = gct; + int k = 0; + int minpos; + for (int j = 0; j < nPix; j++){ + int b = pixels[k++] & 0xff; + int g = pixels[k++] & 0xff; + int r = pixels[k++] & 0xff; + minpos = 0; + int dmin = 256*256*256; + int lenct = colorTab.length; + for (int i = 0; i < lenct; ) { + int dr = r - (colorTab[i++] & 0xff); + int dg = g - (colorTab[i++] & 0xff); + int db = b - (colorTab[i] & 0xff); + int d = dr*dr + dg*dg + db*db; + if (d < dmin) { + dmin = d; + minpos = i/3; + } + i++; + }//end inside for + indexedPixels[j]=(byte)minpos; + }//end for + pixels = null; + colorDepth = 8; + lctSize = 7; + } //end if +} + + + + /** + * Returns index of palette color closest to c + * + */ + protected int findClosest(byte[] colorTab, int r, int g, int b) { + if (colorTab == null) return -1; + int minpos = 0; + int dmin = 256*256*256; + int len = colorTab.length; + for (int i = 0; i < len; ) { + int dr = r - (colorTab[i++] & 0xff); + int dg = g - (colorTab[i++] & 0xff); + int db = b - (colorTab[i] & 0xff); + int d = dr*dr + dg*dg + db*db; + if (d < dmin) { + dmin = d; + minpos = i/3; + } + i++; + } + return minpos; + } + + /** + * Writes Graphic Control Extension + */ + protected void writeGraphicCtrlExt() throws IOException { + out.write(0x21); // extension introducer + out.write(0xf9); // GCE label + out.write(4); // data block size + int transp, disp; + if (!transparent) { + transp = 0; + disp = 0; // dispose = no action + } else { + transp = 1; + disp = 2; // force clear if using transparent color + } + if (dispose >= 0) + disp = dispose & 7; // user override + disp <<= 2; + + // packed fields + out.write( 0 | // 1:3 reserved + disp | // 4:6 disposal + 0 | // 7 user input - 0 = none + transp); // 8 transparency flag + + writeShort(delay); // delay x 1/100 sec + out.write(transIndex); // transparent color index + out.write(0); // block terminator + } + + + /** + * Writes Image Descriptor + */ + protected void writeImageDesc() throws IOException { + out.write(0x2c); // image separator + writeShort(0); // image position x,y = 0,0 + writeShort(0); + writeShort(width); // image size + writeShort(height); + // packed fields + if(gctused) + out.write(0x00); //global color table + else + out.write(0x80 | // 1 local color table 1=yes + 0 | // 2 interlace - 0=no + 0 | // 3 sorted - 0=no + 0 | // 4-5 reserved + lctSize); // size of local color table + + } + + /** + * Writes Logical Screen Descriptor with global color table + */ + protected void writeLSDgct() throws IOException { + // logical screen size + writeShort(width); + writeShort(height); + // packed fields + out.write((0x80 | // 1 : global color table flag = 0 (nn + 0x70 | // 2-4 : color resolution = 7 + 0x00 | // 5 : gct sort flag = 0 + lctSize)); // 6-8 : gct size = 0 + + out.write(0); // background color index + out.write(0); // pixel aspect ratio - assume 1:1 + } + + /** + * Writes Logical Screen Descriptor without global color table + */ + protected void writeLSD() throws IOException { + // logical screen size + writeShort(width); + writeShort(height); + // packed fields + out.write((0x00 | // 1 : global color table flag = 0 (none) + 0x70 | // 2-4 : color resolution = 7 + 0x00 | // 5 : gct sort flag = 0 + 0x00)); // 6-8 : gct size = 0 + + out.write(0); // background color index + out.write(0); // pixel aspect ratio - assume 1:1 + } + + /** + * Writes Netscape application extension to define + * repeat count. + */ + protected void writeNetscapeExt() throws IOException { + out.write(0x21); // extension introducer + out.write(0xff); // app extension label + out.write(11); // block size + writeString("NETSCAPE"+"2.0"); // app id + auth code + out.write(3); // sub-block size + out.write(1); // loop sub-block id + writeShort(repeat); // loop count (extra iterations, 0=repeat forever) + out.write(0); // block terminator + } + + /** + * Writes color table + */ + protected void writePalette() throws IOException { + out.write(colorTab, 0, colorTab.length); + int n = (3 * 256) - colorTab.length; + for (int i = 0; i < n; i++) + out.write(0); + } + + /** + * Encodes and writes pixel data + */ + protected void writePixels() throws IOException { + LZWEncoder2 encoder = + new LZWEncoder2(width, height, indexedPixels, colorDepth); + encoder.encode(out); + } + + /** + * Write 16-bit value to output stream, LSB first + */ + protected void writeShort(int value) throws IOException { + out.write(value & 0xff); + out.write((value >> 8) & 0xff); + } + + /** + * Writes string to output stream + */ + protected void writeString(String s) throws IOException { + for (int i = 0; i < s.length(); i++) + out.write((byte) s.charAt(i)); + } +} + +//============================================================================== +// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. +// K Weiner 12/00 +class LZWEncoder2 { + + private static final int EOF = -1; + + private int imgW, imgH; + private byte[] pixAry; + private int initCodeSize; + private int remaining; + private int curPixel; + + + // GIFCOMPR.C - GIF Image compression routines + // + // Lempel-Ziv compression based on 'compress'. GIF modifications by + // David Rowley (mgardi@watdcsu.waterloo.edu) + + // General DEFINEs + + static final int BITS = 12; + + static final int HSIZE = 5003; // 80% occupancy + + // GIF Image compression - modified 'compress' + // + // Based on: compress.c - File compression ala IEEE Computer, June 1984. + // + // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) + // Jim McKie (decvax!mcvax!jim) + // Steve Davies (decvax!vax135!petsd!peora!srd) + // Ken Turkowski (decvax!decwrl!turtlevax!ken) + // James A. Woods (decvax!ihnp4!ames!jaw) + // Joe Orost (decvax!vax135!petsd!joe) + + int n_bits; // number of bits/code + int maxbits = BITS; // user settable max # bits/code + int maxcode; // maximum code, given n_bits + int maxmaxcode = 1 << BITS; // should NEVER generate this code + + int[] htab = new int[HSIZE]; + int[] codetab = new int[HSIZE]; + + int hsize = HSIZE; // for dynamic table sizing + + int free_ent = 0; // first unused entry + + // block compression parameters -- after all codes are used up, + // and compression rate changes, start over. + boolean clear_flg = false; + + // Algorithm: use open addressing double hashing (no chaining) on the + // prefix code / next character combination. We do a variant of Knuth's + // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime + // secondary probe. Here, the modular division first probe is gives way + // to a faster exclusive-or manipulation. Also do block compression with + // an adaptive reset, whereby the code table is cleared when the compression + // ratio decreases, but after the table fills. The variable-length output + // codes are re-sized at this point, and a special CLEAR code is generated + // for the decompressor. Late addition: construct the table according to + // file size for noticeable speed improvement on small files. Please direct + // questions about this implementation to ames!jaw. + + int g_init_bits; + + int ClearCode; + int EOFCode; + + // output + // + // Output the given code. + // Inputs: + // code: A n_bits-bit integer. If == -1, then EOF. This assumes + // that n_bits =< wordsize - 1. + // Outputs: + // Outputs code to the file. + // Assumptions: + // Chars are 8 bits long. + // Algorithm: + // Maintain a BITS character long buffer (so that 8 codes will + // fit in it exactly). Use the VAX insv instruction to insert each + // code in turn. When the buffer fills up empty it and start over. + + int cur_accum = 0; + int cur_bits = 0; + + int masks[] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, + 0x001F, 0x003F, 0x007F, 0x00FF, + 0x01FF, 0x03FF, 0x07FF, 0x0FFF, + 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF }; + + // Number of characters so far in this 'packet' + int a_count; + + // Define the storage for the packet accumulator + byte[] accum = new byte[256]; + + + //---------------------------------------------------------------------------- + LZWEncoder2(int width, int height, byte[] pixels, int color_depth) + { + imgW = width; + imgH = height; + pixAry = pixels; + initCodeSize = Math.max(2, color_depth); + } + + + // Add a character to the end of the current packet, and if it is 254 + // characters, flush the packet to disk. + void char_out( byte c, OutputStream outs ) throws IOException + { + accum[a_count++] = c; + if ( a_count >= 254 ) + flush_char( outs ); + } + + + // Clear out the hash table + + // table clear for block compress + void cl_block( OutputStream outs ) throws IOException + { + cl_hash( hsize ); + free_ent = ClearCode + 2; + clear_flg = true; + + output( ClearCode, outs ); + } + + + // reset code table + void cl_hash( int hsize ) + { + for ( int i = 0; i < hsize; ++i ) + htab[i] = -1; + } + + + void compress( int init_bits, OutputStream outs ) throws IOException + { + int fcode; + int i /* = 0 */; + int c; + int ent; + int disp; + int hsize_reg; + int hshift; + + // Set up the globals: g_init_bits - initial number of bits + g_init_bits = init_bits; + + // Set up the necessary values + clear_flg = false; + n_bits = g_init_bits; + maxcode = MAXCODE( n_bits ); + + ClearCode = 1 << ( init_bits - 1 ); + EOFCode = ClearCode + 1; + free_ent = ClearCode + 2; + + a_count = 0; // clear packet + + ent = nextPixel(); + + hshift = 0; + for ( fcode = hsize; fcode < 65536; fcode *= 2 ) + ++hshift; + hshift = 8 - hshift; // set hash code range bound + + hsize_reg = hsize; + cl_hash( hsize_reg ); // clear hash table + + output( ClearCode, outs ); + + outer_loop: + while ( (c = nextPixel()) != EOF ) + { + fcode = ( c << maxbits ) + ent; + i = ( c << hshift ) ^ ent; // xor hashing + + if ( htab[i] == fcode ) + { + ent = codetab[i]; + continue; + } + else if ( htab[i] >= 0 ) // non-empty slot + { + disp = hsize_reg - i; // secondary hash (after G. Knott) + if ( i == 0 ) + disp = 1; + do + { + if ( (i -= disp) < 0 ) + i += hsize_reg; + + if ( htab[i] == fcode ) + { + ent = codetab[i]; + continue outer_loop; + } + } + while ( htab[i] >= 0 ); + } + output( ent, outs ); + ent = c; + if ( free_ent < maxmaxcode ) + { + codetab[i] = free_ent++; // code -> hashtable + htab[i] = fcode; + } + else + cl_block( outs ); + } + // Put out the final code. + output( ent, outs ); + output( EOFCode, outs ); + } + + + //---------------------------------------------------------------------------- + void encode(OutputStream os) throws IOException + { + os.write(initCodeSize); // write "initial code size" byte + + remaining = imgW * imgH; // reset navigation variables + curPixel = 0; + + compress(initCodeSize + 1, os); // compress and write the pixel data + + os.write(0); // write block terminator + } + + + // Flush the packet to disk, and reset the accumulator + void flush_char( OutputStream outs ) throws IOException + { + if ( a_count > 0 ) + { + outs.write( a_count ); + outs.write( accum, 0, a_count ); + a_count = 0; + } + } + + + final int MAXCODE( int n_bits ) + { + return ( 1 << n_bits ) - 1; + } + + + //---------------------------------------------------------------------------- + // Return the next pixel from the image + //---------------------------------------------------------------------------- + private int nextPixel() + { + if (remaining == 0) + return EOF; + + --remaining; + + byte pix = pixAry[curPixel++]; + + return pix & 0xff; + } + + + void output( int code, OutputStream outs ) throws IOException + { + cur_accum &= masks[cur_bits]; + + if ( cur_bits > 0 ) + cur_accum |= ( code << cur_bits ); + else + cur_accum = code; + + cur_bits += n_bits; + + while ( cur_bits >= 8 ) + { + char_out( (byte) ( cur_accum & 0xff ), outs ); + cur_accum >>= 8; + cur_bits -= 8; + } + + // If the next entry is going to be too big for the code size, + // then increase it, if possible. + if ( free_ent > maxcode || clear_flg ) + { + if ( clear_flg ) + { + maxcode = MAXCODE(n_bits = g_init_bits); + clear_flg = false; + } + else + { + ++n_bits; + if ( n_bits == maxbits ) + maxcode = maxmaxcode; + else + maxcode = MAXCODE(n_bits); + } + } + + if ( code == EOFCode ) + { + // At EOF, write the rest of the buffer. + while ( cur_bits > 0 ) + { + char_out( (byte) ( cur_accum & 0xff ), outs ); + cur_accum >>= 8; + cur_bits -= 8; + } + + flush_char( outs ); + } + } +} + + +/* NeuQuant Neural-Net Quantization Algorithm + * ------------------------------------------ + * + * Copyright (c) 1994 Anthony Dekker + * + * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. + * See "Kohonen neural networks for optimal colour quantization" + * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. + * for a discussion of the algorithm. + * + * Any party obtaining a copy of these files from the author, directly or + * indirectly, is granted, free of charge, a full and unrestricted irrevocable, + * world-wide, paid up, royalty-free, nonexclusive right and license to deal + * in this software and documentation files (the "Software"), including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons who receive + * copies from any such party to do so, with the only requirement being + * that this copyright notice remain intact. + */ + +// Ported to Java 12/00 K Weiner + +class NeuQuant { + + protected static final int netsize = 256; /* number of colours used */ + + /* four primes near 500 - assume no image has a length so large */ + /* that it is divisible by all four primes */ + protected static final int prime1 = 499; + protected static final int prime2 = 491; + protected static final int prime3 = 487; + protected static final int prime4 = 503; + + protected static final int minpicturebytes = (3 * prime4); + /* minimum size for input image */ + + /* Program Skeleton + ---------------- + [select samplefac in range 1..30] + [read image from input file] + pic = (unsigned char*) malloc(3*width*height); + initnet(pic,3*width*height,samplefac); + learn(); + unbiasnet(); + [write output image header, using writecolourmap(f)] + inxbuild(); + write output image using inxsearch(b,g,r) */ + + /* Network Definitions + ------------------- */ + + protected static final int maxnetpos = (netsize - 1); + protected static final int netbiasshift = 4; /* bias for colour values */ + protected static final int ncycles = 100; /* no. of learning cycles */ + + /* defs for freq and bias */ + protected static final int intbiasshift = 16; /* bias for fractions */ + protected static final int intbias = (((int) 1) << intbiasshift); + protected static final int gammashift = 10; /* gamma = 1024 */ + protected static final int gamma = (((int) 1) << gammashift); + protected static final int betashift = 10; + protected static final int beta = (intbias >> betashift); /* beta = 1/1024 */ + protected static final int betagamma = (intbias << (gammashift - betashift)); + + /* defs for decreasing radius factor */ + protected static final int initrad = (netsize >> 3); /* for 256 cols, radius starts */ + protected static final int radiusbiasshift = 6; /* at 32.0 biased by 6 bits */ + protected static final int radiusbias = (((int) 1) << radiusbiasshift); + protected static final int initradius = (initrad * radiusbias); /* and decreases by a */ + protected static final int radiusdec = 30; /* factor of 1/30 each cycle */ + + /* defs for decreasing alpha factor */ + protected static final int alphabiasshift = 10; /* alpha starts at 1.0 */ + protected static final int initalpha = (((int) 1) << alphabiasshift); + + protected int alphadec; /* biased by 10 bits */ + + /* radbias and alpharadbias used for radpower calculation */ + protected static final int radbiasshift = 8; + protected static final int radbias = (((int) 1) << radbiasshift); + protected static final int alpharadbshift = (alphabiasshift + radbiasshift); + protected static final int alpharadbias = (((int) 1) << alpharadbshift); + + /* Types and Global Variables + -------------------------- */ + + protected byte[] thepicture; /* the input image itself */ + protected int lengthcount; /* lengthcount = H*W*3 */ + + protected int samplefac; /* sampling factor 1..30 */ + + // typedef int pixel[4]; /* BGRc */ + protected int[][] network; /* the network itself - [netsize][4] */ + + protected int[] netindex = new int[256]; /* for network lookup - really 256 */ + + protected int[] bias = new int[netsize]; /* bias and freq arrays for learning */ + protected int[] freq = new int[netsize]; + protected int[] radpower = new int[initrad]; /* radpower for precomputation */ + + + /* Initialise network in range (0,0,0) to (255,255,255) and set parameters + ----------------------------------------------------------------------- */ + + public NeuQuant(byte[] thepic, int len, int sample) { + + int i; + int[] p; + + thepicture = thepic; + lengthcount = len; + samplefac = sample; + + network = new int[netsize][]; + for (i = 0; i < netsize; i++) { + network[i] = new int[4]; + p = network[i]; + p[0] = p[1] = p[2] = (i << (netbiasshift + 8)) / netsize; + freq[i] = intbias / netsize; /* 1/netsize */ + bias[i] = 0; + } + } + + + public byte[] colorMap() { + byte[] map = new byte[3*netsize]; + int[] index = new int[netsize]; + for (int i = 0; i < netsize; i++) + index[network[i][3]] = i; + int k = 0; + for (int i = 0; i < netsize; i++) { + int j = index[i]; + map[k++] = (byte) (network[j][0]); + map[k++] = (byte) (network[j][1]); + map[k++] = (byte) (network[j][2]); + } + return map; + } + + + /* Insertion sort of network and building of netindex[0..255] (to do after unbias) + ------------------------------------------------------------------------------- */ + + public void inxbuild() { + + int i, j, smallpos, smallval; + int[] p; + int[] q; + int previouscol, startpos; + + previouscol = 0; + startpos = 0; + for (i = 0; i < netsize; i++) { + p = network[i]; + smallpos = i; + smallval = p[1]; /* index on g */ + /* find smallest in i..netsize-1 */ + for (j = i + 1; j < netsize; j++) { + q = network[j]; + if (q[1] < smallval) { /* index on g */ + smallpos = j; + smallval = q[1]; /* index on g */ + } + } + q = network[smallpos]; + /* swap p (i) and q (smallpos) entries */ + if (i != smallpos) { + j = q[0]; q[0] = p[0]; p[0] = j; + j = q[1]; q[1] = p[1]; p[1] = j; + j = q[2]; q[2] = p[2]; p[2] = j; + j = q[3]; q[3] = p[3]; p[3] = j; + } + /* smallval entry is now in position i */ + if (smallval != previouscol) { + netindex[previouscol] = (startpos + i) >> 1; + for (j = previouscol + 1; j < smallval; j++) + netindex[j] = i; + previouscol = smallval; + startpos = i; + } + } + netindex[previouscol] = (startpos + maxnetpos) >> 1; + for (j = previouscol + 1; j < 256; j++) + netindex[j] = maxnetpos; /* really 256 */ + } + + + /* Main Learning Loop + ------------------ */ + + public void learn() { + + int i, j, b, g, r; + int radius, rad, alpha, step, delta, samplepixels; + byte[] p; + int pix, lim; + + if (lengthcount < minpicturebytes) + samplefac = 1; + alphadec = 30 + ((samplefac - 1) / 3); + p = thepicture; + pix = 0; + lim = lengthcount; + samplepixels = lengthcount / (3 * samplefac); + delta = samplepixels / ncycles; + alpha = initalpha; + radius = initradius; + + rad = radius >> radiusbiasshift; + if (rad <= 1) + rad = 0; + for (i = 0; i < rad; i++) + radpower[i] = alpha * (((rad * rad - i * i) * radbias) / (rad * rad)); + + //fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad); + + if (lengthcount < minpicturebytes) + step = 3; + else if ((lengthcount % prime1) != 0) + step = 3 * prime1; + else { + if ((lengthcount % prime2) != 0) + step = 3 * prime2; + else { + if ((lengthcount % prime3) != 0) + step = 3 * prime3; + else + step = 3 * prime4; + } + } + + i = 0; + while (i < samplepixels) { + b = (p[pix + 0] & 0xff) << netbiasshift; + g = (p[pix + 1] & 0xff) << netbiasshift; + r = (p[pix + 2] & 0xff) << netbiasshift; + j = contest(b, g, r); + + altersingle(alpha, j, b, g, r); + if (rad != 0) + alterneigh(rad, j, b, g, r); /* alter neighbours */ + + pix += step; + if (pix >= lim) + pix -= lengthcount; + + i++; + if (i % delta == 0) { + alpha -= alpha / alphadec; + radius -= radius / radiusdec; + rad = radius >> radiusbiasshift; + if (rad <= 1) + rad = 0; + for (j = 0; j < rad; j++) + radpower[j] = alpha * (((rad * rad - j * j) * radbias) / (rad * rad)); + } + } + //fprintf(stderr,"finished 1D learning: final alpha=%f !\n",((float)alpha)/initalpha); + } + + + /* Search for BGR values 0..255 (after net is unbiased) and return colour index + ---------------------------------------------------------------------------- */ + + public int map(int b, int g, int r) { + + int i, j, dist, a, bestd; + int[] p; + int best; + + bestd = 1000; /* biggest possible dist is 256*3 */ + best = -1; + i = netindex[g]; /* index on g */ + j = i - 1; /* start at netindex[g] and work outwards */ + + while ((i < netsize) || (j >= 0)) { + if (i < netsize) { + p = network[i]; + dist = p[1] - g; /* inx key */ + if (dist >= bestd) + i = netsize; /* stop iter */ + else { + i++; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + if (j >= 0) { + p = network[j]; + dist = g - p[1]; /* inx key - reverse dif */ + if (dist >= bestd) + j = -1; /* stop iter */ + else { + j--; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + } + return (best); + } + + + public byte[] process() { + learn(); + unbiasnet(); + inxbuild(); + return colorMap(); + } + + + /* Unbias network to give byte values 0..255 and record position i to prepare for sort + ----------------------------------------------------------------------------------- */ + + public void unbiasnet() { + + int i, j; + + for (i = 0; i < netsize; i++) { + network[i][0] >>= netbiasshift; + network[i][1] >>= netbiasshift; + network[i][2] >>= netbiasshift; + network[i][3] = i; /* record colour no */ + } + } + + + /* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in radpower[|i-j|] + --------------------------------------------------------------------------------- */ + + protected void alterneigh(int rad, int i, int b, int g, int r) { + + int j, k, lo, hi, a, m; + int[] p; + + lo = i - rad; + if (lo < -1) + lo = -1; + hi = i + rad; + if (hi > netsize) + hi = netsize; + + j = i + 1; + k = i - 1; + m = 1; + while ((j < hi) || (k > lo)) { + a = radpower[m++]; + if (j < hi) { + p = network[j++]; + try { + p[0] -= (a * (p[0] - b)) / alpharadbias; + p[1] -= (a * (p[1] - g)) / alpharadbias; + p[2] -= (a * (p[2] - r)) / alpharadbias; + } catch (Exception e) {} // prevents 1.3 miscompilation + } + if (k > lo) { + p = network[k--]; + try { + p[0] -= (a * (p[0] - b)) / alpharadbias; + p[1] -= (a * (p[1] - g)) / alpharadbias; + p[2] -= (a * (p[2] - r)) / alpharadbias; + } catch (Exception e) {} + } + } + } + + + /* Move neuron i towards biased (b,g,r) by factor alpha + ---------------------------------------------------- */ + + protected void altersingle(int alpha, int i, int b, int g, int r) { + + /* alter hit neuron */ + int[] n = network[i]; + n[0] -= (alpha * (n[0] - b)) / initalpha; + n[1] -= (alpha * (n[1] - g)) / initalpha; + n[2] -= (alpha * (n[2] - r)) / initalpha; + } + + + /* Search for biased BGR values + ---------------------------- */ + + protected int contest(int b, int g, int r) { + + /* finds closest neuron (min dist) and updates freq */ + /* finds best neuron (min dist-bias) and returns position */ + /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */ + /* bias[i] = gamma*((1/netsize)-freq[i]) */ + + int i, dist, a, biasdist, betafreq; + int bestpos, bestbiaspos, bestd, bestbiasd; + int[] n; + + bestd = ~(((int) 1) << 31); + bestbiasd = bestd; + bestpos = -1; + bestbiaspos = bestpos; + + for (i = 0; i < netsize; i++) { + n = network[i]; + dist = n[0] - b; + if (dist < 0) + dist = -dist; + a = n[1] - g; + if (a < 0) + a = -a; + dist += a; + a = n[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + bestpos = i; + } + biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); + if (biasdist < bestbiasd) { + bestbiasd = biasdist; + bestbiaspos = i; + } + betafreq = (freq[i] >> betashift); + freq[i] -= betafreq; + bias[i] += (betafreq << gammashift); + } + freq[bestpos] += beta; + bias[bestpos] -= betagamma; + return (bestbiaspos); + } +} + + +//============================================================================== +// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. +// K Weiner 12/00 + + +class LZWEncoder { + + private static final int EOF = -1; + + private int imgW, imgH; + private byte[] pixAry; + private int initCodeSize; + private int remaining; + private int curPixel; + + + // GIFCOMPR.C - GIF Image compression routines + // + // Lempel-Ziv compression based on 'compress'. GIF modifications by + // David Rowley (mgardi@watdcsu.waterloo.edu) + + // General DEFINEs + + static final int BITS = 12; + + static final int HSIZE = 5003; // 80% occupancy + + // GIF Image compression - modified 'compress' + // + // Based on: compress.c - File compression ala IEEE Computer, June 1984. + // + // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) + // Jim McKie (decvax!mcvax!jim) + // Steve Davies (decvax!vax135!petsd!peora!srd) + // Ken Turkowski (decvax!decwrl!turtlevax!ken) + // James A. Woods (decvax!ihnp4!ames!jaw) + // Joe Orost (decvax!vax135!petsd!joe) + + int n_bits; // number of bits/code + int maxbits = BITS; // user settable max # bits/code + int maxcode; // maximum code, given n_bits + int maxmaxcode = 1 << BITS; // should NEVER generate this code + + int[] htab = new int[HSIZE]; + int[] codetab = new int[HSIZE]; + + int hsize = HSIZE; // for dynamic table sizing + + int free_ent = 0; // first unused entry + + // block compression parameters -- after all codes are used up, + // and compression rate changes, start over. + boolean clear_flg = false; + + // Algorithm: use open addressing double hashing (no chaining) on the + // prefix code / next character combination. We do a variant of Knuth's + // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime + // secondary probe. Here, the modular division first probe is gives way + // to a faster exclusive-or manipulation. Also do block compression with + // an adaptive reset, whereby the code table is cleared when the compression + // ratio decreases, but after the table fills. The variable-length output + // codes are re-sized at this point, and a special CLEAR code is generated + // for the decompressor. Late addition: construct the table according to + // file size for noticeable speed improvement on small files. Please direct + // questions about this implementation to ames!jaw. + + int g_init_bits; + + int ClearCode; + int EOFCode; + + // output + // + // Output the given code. + // Inputs: + // code: A n_bits-bit integer. If == -1, then EOF. This assumes + // that n_bits =< wordsize - 1. + // Outputs: + // Outputs code to the file. + // Assumptions: + // Chars are 8 bits long. + // Algorithm: + // Maintain a BITS character long buffer (so that 8 codes will + // fit in it exactly). Use the VAX insv instruction to insert each + // code in turn. When the buffer fills up empty it and start over. + + int cur_accum = 0; + int cur_bits = 0; + + int masks[] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, + 0x001F, 0x003F, 0x007F, 0x00FF, + 0x01FF, 0x03FF, 0x07FF, 0x0FFF, + 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF }; + + // Number of characters so far in this 'packet' + int a_count; + + // Define the storage for the packet accumulator + byte[] accum = new byte[256]; + + + //---------------------------------------------------------------------------- + LZWEncoder(int width, int height, byte[] pixels, int color_depth) { + imgW = width; + imgH = height; + pixAry = pixels; + initCodeSize = Math.max(2, color_depth); + } + + + // Add a character to the end of the current packet, and if it is 254 + // characters, flush the packet to disk. + void char_out( byte c, OutputStream outs ) throws IOException + { + accum[a_count++] = c; + if ( a_count >= 254 ) + flush_char( outs ); + } + + + // Clear out the hash table + + // table clear for block compress + void cl_block( OutputStream outs ) throws IOException + { + cl_hash( hsize ); + free_ent = ClearCode + 2; + clear_flg = true; + + output( ClearCode, outs ); + } + + + // reset code table + void cl_hash( int hsize ) + { + for ( int i = 0; i < hsize; ++i ) + htab[i] = -1; + } + + + void compress( int init_bits, OutputStream outs ) throws IOException + { + int fcode; + int i /* = 0 */; + int c; + int ent; + int disp; + int hsize_reg; + int hshift; + + // Set up the globals: g_init_bits - initial number of bits + g_init_bits = init_bits; + + // Set up the necessary values + clear_flg = false; + n_bits = g_init_bits; + maxcode = MAXCODE( n_bits ); + + ClearCode = 1 << ( init_bits - 1 ); + EOFCode = ClearCode + 1; + free_ent = ClearCode + 2; + + a_count = 0; // clear packet + + ent = nextPixel(); + + hshift = 0; + for ( fcode = hsize; fcode < 65536; fcode *= 2 ) + ++hshift; + hshift = 8 - hshift; // set hash code range bound + + hsize_reg = hsize; + cl_hash( hsize_reg ); // clear hash table + + output( ClearCode, outs ); + + outer_loop: + while ( (c = nextPixel()) != EOF ) + { + fcode = ( c << maxbits ) + ent; + i = ( c << hshift ) ^ ent; // xor hashing + + if ( htab[i] == fcode ) + { + ent = codetab[i]; + continue; + } + else if ( htab[i] >= 0 ) // non-empty slot + { + disp = hsize_reg - i; // secondary hash (after G. Knott) + if ( i == 0 ) + disp = 1; + do + { + if ( (i -= disp) < 0 ) + i += hsize_reg; + + if ( htab[i] == fcode ) + { + ent = codetab[i]; + continue outer_loop; + } + } + while ( htab[i] >= 0 ); + } + output( ent, outs ); + ent = c; + if ( free_ent < maxmaxcode ) + { + codetab[i] = free_ent++; // code -> hashtable + htab[i] = fcode; + } + else + cl_block( outs ); + } + // Put out the final code. + output( ent, outs ); + output( EOFCode, outs ); + } + + + //---------------------------------------------------------------------------- + void encode(OutputStream os) throws IOException + { + os.write(initCodeSize); // write "initial code size" byte + + remaining = imgW * imgH; // reset navigation variables + curPixel = 0; + + compress(initCodeSize + 1, os); // compress and write the pixel data + + os.write(0); // write block terminator + } + + + // Flush the packet to disk, and reset the accumulator + void flush_char( OutputStream outs ) throws IOException + { + if ( a_count > 0 ) + { + outs.write( a_count ); + outs.write( accum, 0, a_count ); + a_count = 0; + } + } + + + final int MAXCODE( int n_bits ) + { + return ( 1 << n_bits ) - 1; + } + + + //---------------------------------------------------------------------------- + // Return the next pixel from the image + //---------------------------------------------------------------------------- + private int nextPixel() + { + if (remaining == 0) + return EOF; + + --remaining; + + byte pix = pixAry[curPixel++]; + + return pix & 0xff; + } + + + void output( int code, OutputStream outs ) throws IOException + { + cur_accum &= masks[cur_bits]; + + if ( cur_bits > 0 ) + cur_accum |= ( code << cur_bits ); + else + cur_accum = code; + + cur_bits += n_bits; + + while ( cur_bits >= 8 ) + { + char_out( (byte) ( cur_accum & 0xff ), outs ); + cur_accum >>= 8; + cur_bits -= 8; + } + + // If the next entry is going to be too big for the code size, + // then increase it, if possible. + if ( free_ent > maxcode || clear_flg ) + { + if ( clear_flg ) + { + maxcode = MAXCODE(n_bits = g_init_bits); + clear_flg = false; + } + else + { + ++n_bits; + if ( n_bits == maxbits ) + maxcode = maxmaxcode; + else + maxcode = MAXCODE(n_bits); + } + } + + if ( code == EOFCode ) + { + // At EOF, write the rest of the buffer. + while ( cur_bits > 0 ) + { + char_out( (byte) ( cur_accum & 0xff ), outs ); + cur_accum >>= 8; + cur_bits -= 8; + } + + flush_char( outs ); + } + } +} + diff --git a/src/ij/plugin/Grid.java b/src/ij/plugin/Grid.java new file mode 100644 index 0000000..348647e --- /dev/null +++ b/src/ij/plugin/Grid.java @@ -0,0 +1,288 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.*; +import ij.util.Tools; +import java.awt.*; +import java.awt.geom.*; +import java.util.*; + +/** This class implements the Analyze/Tools/Grid command. */ +public class Grid implements PlugIn, DialogListener { + private static final String OPTIONS = "grid.options"; + private static final String GRID = "|GRID|"; + private static double crossSize = 0.1; + private static String[] colors = {"Red","Green","Blue","Magenta","Cyan","Yellow","Orange","Black","White"}; + private final static int LINES=0, HLINES=1, CROSSES=2, POINTS=3, CIRCLES=4, NONE=4; + private static String[] types = {"Lines","Horizontal Lines", "Crosses", "Points", "Circles", "None"}; + private Random random = new Random(System.currentTimeMillis()); + private ImagePlus imp; + private double tileWidth, tileHeight; + private int xstart, ystart; + private int linesV, linesH; + private double pixelWidth=1.0, pixelHeight=1.0; + private String units = "pixels"; + private boolean isMacro; + private Roi gridOnEntry; + + private String type = types[LINES]; + private double areaPerPoint; + private static double saveAreaPerPoint; + private String color = "Cyan"; + private boolean bold; + private boolean randomOffset; + private boolean centered; + private Checkbox centerCheckbox, randomCheckbox; + + public void run(String arg) { + imp = IJ.getImage(); + Overlay overlay = imp.getOverlay(); + int index = overlay!=null?overlay.getIndex(GRID):-1; + if (index>=0) + gridOnEntry = overlay.get(index); + if (showDialog() && !isMacro) + saveSettings(); + } + + // http://stackoverflow.com/questions/30654203/how-to-create-a-circle-using-generalpath-and-apache-poi + private void drawCircles(double size) { + double R = size*tileWidth; + if (R<1) R =1; + if (bold && type.equals(types[POINTS])) R*=1.5; + double kappa = 0.5522847498f; + GeneralPath path = new GeneralPath(); + for(int h=0; h1) { + overlay.remove(GRID); + imp.draw(); + } else + imp.setOverlay(null); + } + } else { + Roi roi = new ShapeRoi(shape); + roi.setStrokeColor(Colors.getColor(color,Color.cyan)); + if (bold && linesV*linesH<5000) { + ImageCanvas ic = imp.getCanvas(); + double mag = ic!=null?ic.getMagnification():1.0; + double width = 2.0; + if (mag<1.0) + width = width/mag; + roi.setStrokeWidth(width); + } + IJ.showStatus(linesV*linesH+" nodes"); + Overlay overlay = imp.getOverlay(); + if (overlay!=null) + overlay.remove(GRID); + else + overlay = new Overlay(); + overlay.add(roi, GRID); + imp.setOverlay(overlay); + } + } + + private boolean showDialog() { + isMacro = Macro.getOptions()!=null; + if (!isMacro) + getSettings(); + int width = imp.getWidth(); + int height = imp.getHeight(); + Calibration cal = imp.getCalibration(); + int places; + if (cal.scaled()) { + pixelWidth = cal.pixelWidth; + pixelHeight = cal.pixelHeight; + units = cal.getUnits(); + places = 2; + } else { + pixelWidth = 1.0; + pixelHeight = 1.0; + units = "pixels"; + places = 0; + } + if (areaPerPoint==0.0) + areaPerPoint = (width*cal.pixelWidth*height*cal.pixelHeight)/81.0; // default to 9x9 grid + GenericDialog gd = new GenericDialog("Grid..."); + gd.addChoice("Grid type:", types, type); + gd.addNumericField("Area per point:", areaPerPoint, places, 6, units+"^2"); + gd.addChoice("Color:", colors, color); + gd.addCheckbox("Bold", bold); + gd.addCheckbox("Center grid on image", centered); + gd.addCheckbox("Random offset", randomOffset); + gd.addDialogListener(this); + if (!isMacro) { + Vector v = gd.getCheckboxes(); + centerCheckbox = (Checkbox)v.elementAt(1); + randomCheckbox = (Checkbox)v.elementAt(2); + } + dialogItemChanged(gd, null); + gd.showDialog(); + if (gd.wasCanceled()) { + Overlay overlay = imp.getOverlay(); + if (overlay!=null && gridOnEntry!=null) { + overlay.remove(GRID); + overlay.add(gridOnEntry); + imp.draw(); + } else + drawGrid(null); + return false; + } else + return true; + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + int width = imp.getWidth(); + int height = imp.getHeight(); + type = gd.getNextChoice(); + areaPerPoint = gd.getNextNumber(); + color = gd.getNextChoice(); + bold = gd.getNextBoolean(); + centered = gd.getNextBoolean(); + randomOffset = gd.getNextBoolean(); + if (randomOffset) { + centered = false; + if (centerCheckbox!=null) + centerCheckbox.setState(false); + } + double minArea= (width*height)/50000.0; + if (type.equals(types[CROSSES])&&minArea<50.0) + minArea = 50.0; + else if (minArea<16) + minArea = 16.0; + if (areaPerPoint/(pixelWidth*pixelHeight)=3) { + type = options[0]; + if ("None".equals(type)) + type = types[LINES]; + areaPerPoint = saveAreaPerPoint; + color = options[1]; + bold = options[2].contains("bold"); + centered = options[2].contains("centered"); + randomOffset = options[2].contains("random"); + if (centered) + randomOffset = false; + } + } + + private void saveSettings() { + String options = type+","+color+","; + String options2 = (bold?"bold ":"")+(centered?"centered ":"")+(randomOffset?"random ":""); + if (options2.length()==0) + options2 = "-"; + Prefs.set(OPTIONS, options+options2); + saveAreaPerPoint = areaPerPoint; + } + +} diff --git a/src/ij/plugin/GroupedZProjector.java b/src/ij/plugin/GroupedZProjector.java new file mode 100644 index 0000000..ea6f72c --- /dev/null +++ b/src/ij/plugin/GroupedZProjector.java @@ -0,0 +1,87 @@ +package ij.plugin; +import ij.*; +import ij.gui.GenericDialog; +import ij.process.*; +import ij.measure.Calibration; + +/** This plugin implements the Image/Stacks/Tools/Grouped Z Project command. */ + +public class GroupedZProjector implements PlugIn { + private static int method = ZProjector.AVG_METHOD; + private int groupSize; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + int size = imp.getStackSize(); + if (size==1) { + IJ.error("Z Project", "This command requires a stack"); + return; + } + if (imp.isHyperStack()) { + new ZProjector().run(""); + return; + } + if (!showDialog(imp)) + return; + ImagePlus imp2 = groupZProject(imp, method, groupSize); + imp2.setCalibration(imp.getCalibration()); + Calibration cal = imp2.getCalibration(); + cal.pixelDepth *= groupSize; + if (imp!=null) + imp2.show(); + } + + public ImagePlus groupZProject(ImagePlus imp, int method, int groupSize) { + if (method<0 || method>=ZProjector.METHODS.length) + return null; + int[] dim = imp.getDimensions(); + int projectedStackSize = imp.getStackSize()/groupSize; + imp.setDimensions(1, groupSize, projectedStackSize); + ZProjector zp = new ZProjector(imp); + zp.setMethod(method); + zp.setStartSlice(1); + zp.setStopSlice(groupSize); + zp.doHyperStackProjection(true); + imp.setDimensions(dim[2], dim[3], dim[4]); + + ImagePlus zProjectorOutput = zp.getProjection(); + int[] zProjectDim = zProjectorOutput.getDimensions(); + for (int i=2; isize || (size%groupSize)!=0) { + IJ.error("ZProject", "Group size must divide evenly into the stack size."); + return false; + } + return true; + } + +} \ No newline at end of file diff --git a/src/ij/plugin/Histogram.java b/src/ij/plugin/Histogram.java new file mode 100644 index 0000000..4753db6 --- /dev/null +++ b/src/ij/plugin/Histogram.java @@ -0,0 +1,186 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.util.Tools; +import ij.plugin.filter.PlugInFilter; +import ij.plugin.frame.Recorder; +import ij.measure.Calibration; +import java.awt.*; +import java.awt.event.*; +import java.util.Vector; + + +/** This plugin implements the Analyze/Histogram command. */ +public class Histogram implements PlugIn, TextListener { + + private static boolean staticUseImageMinAndMax = true; + private static double staticXMin, staticXMax; + private static String staticYMax = "Auto"; + private static boolean staticStackHistogram; + private static int imageID; + private int nBins = 256; + private boolean useImageMinAndMax = true; + private double xMin, xMax; + private String yMax = "Auto"; + private boolean stackHistogram; + private Checkbox checkbox; + private TextField minField, maxField; + private String defaultMin, defaultMax; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + int bitDepth = imp.getBitDepth(); + if (bitDepth==32 || IJ.altKeyDown() || (IJ.isMacro()&&Macro.getOptions()!=null)) { + IJ.setKeyUp(KeyEvent.VK_ALT); + if (!showDialog(imp)) + return; + } else { + int stackSize = imp.getStackSize(); + boolean noDialog = stackSize==1; + if (stackSize==3) { + ImageStack stack = imp.getStack(); + String label1 = stack.getSliceLabel(1); + if ("Hue".equals(label1)) + noDialog = true; + } + int flags = noDialog?0:setupDialog(imp, 0); + if (flags==PlugInFilter.DONE) return; + stackHistogram = flags==PlugInFilter.DOES_STACKS; + Calibration cal = imp.getCalibration(); + if (bitDepth==16 && ImagePlus.getDefault16bitRange()!=0) { + xMin = 0.0; + xMax = Math.pow(2,ImagePlus.getDefault16bitRange())-1; + useImageMinAndMax = false; + } else if (stackHistogram && ((bitDepth==8&&!cal.calibrated())||imp.isRGB())) { + xMin = 0.0; + xMax = 256.0; + useImageMinAndMax = false; + } else + useImageMinAndMax = true; + yMax = "Auto"; + } + ImageStatistics stats = null; + if (useImageMinAndMax) { + xMin = 0.0; + xMax = 0.0; + } + int iyMax = (int)Tools.parseDouble(yMax, 0.0); + boolean customHistogram = (bitDepth==8||imp.isRGB()) && (!(xMin==0.0&&xMax==0.0)||nBins!=256||iyMax>0); + HistogramPlot plot = new HistogramPlot(); + if (stackHistogram || customHistogram) { + ImagePlus imp2 = imp; + if (customHistogram && !stackHistogram && imp.getStackSize()>1) + imp2 = new ImagePlus("Temp", imp.getProcessor()); + stats = new StackStatistics(imp2, nBins, xMin, xMax); + stats.histYMax = iyMax; + plot.draw(imp, stats); + } else + plot.draw(imp, nBins, xMin, xMax, iyMax); + plot.show(); + } + + boolean showDialog(ImagePlus imp) { + if (!IJ.isMacro()) { + nBins = HistogramWindow.nBins; + useImageMinAndMax = staticUseImageMinAndMax; + xMin=staticXMin; xMax=staticXMax; + yMax = staticYMax; + stackHistogram = staticStackHistogram; + } + ImageProcessor ip = imp.getProcessor(); + double min = ip.getMin(); + double max = ip.getMax(); + if (imp.getID()!=imageID || (min==xMin&&min==xMax)) + useImageMinAndMax = true; + if (imp.getID()!=imageID || useImageMinAndMax) { + xMin = min; + xMax = max; + Calibration cal = imp.getCalibration(); + xMin = cal.getCValue(xMin); + xMax = cal.getCValue(xMax); + } + defaultMin = IJ.d2s(xMin,2); + defaultMax = IJ.d2s(xMax,2); + imageID = imp.getID(); + int stackSize = imp.getStackSize(); + GenericDialog gd = new GenericDialog("Histogram"); + gd.addNumericField("Bins:", nBins, 0); + gd.addCheckbox("Use pixel value range", useImageMinAndMax); + gd.setInsets(5, 40, 10); + gd.addMessage("or use:"); + int fwidth = 6; + int nwidth = Math.max(IJ.d2s(xMin,2).length(), IJ.d2s(xMax,2).length()); + if (nwidth>fwidth) fwidth = nwidth; + int digits = 2; + if (xMin==(int)xMin && xMax==(int)xMax) + digits = 0; + gd.addNumericField("X_min:", xMin, digits, fwidth, null); + gd.addNumericField("X_max:", xMax, digits, fwidth, null); + gd.setInsets(15, 0, 10); + gd.addStringField("Y_max:", yMax, 6); + if (stackSize>1) + gd.addCheckbox("Stack histogram", stackHistogram); + + Vector numbers = gd.getNumericFields(); + if (numbers!=null) { + minField = (TextField)numbers.elementAt(1); + minField.addTextListener(this); + maxField = (TextField)numbers.elementAt(2); + maxField.addTextListener(this); + } + checkbox = (Checkbox)(gd.getCheckboxes().elementAt(0)); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + nBins = (int)gd.getNextNumber(); + useImageMinAndMax = gd.getNextBoolean(); + xMin = gd.getNextNumber(); + xMax = gd.getNextNumber(); + yMax = gd.getNextString(); + stackHistogram = (stackSize>1)?gd.getNextBoolean():false; + if (!IJ.isMacro()) { + if (nBins>=2 && nBins<=1000) + HistogramWindow.nBins = nBins; + staticUseImageMinAndMax = useImageMinAndMax; + staticXMin=xMin; staticXMax=xMax; + staticYMax = yMax; + staticStackHistogram = stackHistogram; + } + IJ.register(Histogram.class); + return true; + } + + public void textValueChanged(TextEvent e) { + boolean rangeChanged = !defaultMin.equals(minField.getText()) + || !defaultMax.equals(maxField.getText()); + if (rangeChanged) + checkbox.setState(false); + } + + int setupDialog(ImagePlus imp, int flags) { + int stackSize = imp.getStackSize(); + if (stackSize>1) { + String macroOptions = Macro.getOptions(); + if (macroOptions!=null) { + if (macroOptions.indexOf("stack ")>=0) + return flags+PlugInFilter.DOES_STACKS; + else + return flags; + } + YesNoCancelDialog d = new YesNoCancelDialog(IJ.getInstance(), + "Histogram", "Include all "+stackSize+" images?"); + if (d.cancelPressed()) + return PlugInFilter.DONE; + else if (d.yesPressed()) { + if (Recorder.record) + Recorder.recordOption("stack"); + return flags+PlugInFilter.DOES_STACKS; + } + if (Recorder.record) + Recorder.recordOption("slice"); + } + return flags; + } + +} diff --git a/src/ij/plugin/Hotkeys.java b/src/ij/plugin/Hotkeys.java new file mode 100644 index 0000000..f0c3824 --- /dev/null +++ b/src/ij/plugin/Hotkeys.java @@ -0,0 +1,198 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.util.*; +import ij.measure.ResultsTable; +import java.awt.*; +import java.io.*; +import java.util.*; + +/** Implements the Plugins/Hotkeys/Create Shortcut and Remove commands. */ +public class Hotkeys implements PlugIn { + + private static final String TITLE = "Hotkeys"; + private static String command = ""; + private static String shortcut = ""; + + public void run(String arg) { + if (arg.equals("install") || arg.equals("install2")) + installHotkey(arg); + else if (arg.equals("remove")) + removeHotkey(); + else if (arg.equals("list")) + listCommands(); + else { + Executer e = new Executer(arg); + e.run(); + } + IJ.register(Hotkeys.class); + } + + void installHotkey(String arg) { + boolean byName = arg.equals("install2"); + String[] commands = byName?null:getAllCommands(); + String[] shortcuts = getAvailableShortcuts(); + String nCommands = commands!=null?" ("+commands.length+")":""; + GenericDialog gd = new GenericDialog("Add Shortcut"+nCommands); + gd.addChoice("Shortcut:", shortcuts, shortcuts[0]); + if (byName) + gd.addStringField("Command:", "", 20); + else + gd.addChoice("Command:", commands, command); + gd.showDialog(); + if (gd.wasCanceled()) + return; + shortcut = gd.getNextChoice(); + if (byName) { + command = gd.getNextString(); + Hashtable cmds = Menus.getCommands(); + if (cmds==null || cmds.get(command)==null) { + String command2 = command; + if (cmds.get(command)==null) + command = command+" "; + if (cmds.get(command)==null) { + command = command2 + "..."; + if (cmds.get(command)==null) { + command = command2; + IJ.error("Command not found:\n \n "+ "\""+command+"\""); + return; + } + } + } + } else { + command = gd.getNextChoice(); + Hashtable cmds = Menus.getCommands(); + if (command.contains("[") && cmds!=null && cmds.get(command)==null) { + if (cmds.get(command+"]")!=null) + command += "]"; + } + } + String plugin = "ij.plugin.Hotkeys("+"\""+command+"\")"; + int err = Menus.installPlugin(plugin,Menus.SHORTCUTS_MENU,"*"+command,shortcut,IJ.getInstance()); + switch (err) { + case Menus.COMMAND_IN_USE: + IJ.showMessage(TITLE, "The command \"" + command + "\" is already installed."); + break; + case Menus.INVALID_SHORTCUT: + IJ.showMessage(TITLE, "The shortcut must be a single character or F1-F24."); + break; + case Menus.SHORTCUT_IN_USE: + IJ.showMessage("The \""+shortcut+"\" shortcut is in use."); + break; + default: + shortcut = ""; + break; + } + } + + void removeHotkey() { + String[] shortcuts = getShortcuts(); + if (shortcuts==null) { + IJ.showMessage("Remove...", "No shortcuts found."); + return; + } + GenericDialog gd = new GenericDialog("Remove"); + gd.addChoice("Shortcut:", shortcuts, ""); + if (shortcuts.length>1) + gd.addCheckbox("Remove all "+shortcuts.length+" shortcuts", false); + gd.addMessage("Shortcuts are not removed\nuntil ImageJ is restarted."); + gd.showDialog(); + if (gd.wasCanceled()) + return; + command = gd.getNextChoice(); + boolean removeAll = false; + if (shortcuts.length>1) + removeAll = gd.getNextBoolean(); + if (removeAll) { + boolean ok = IJ.showMessageWithCancel("Remove", "Remove all "+shortcuts.length+" shortcuts?"); + if (!ok) + return; + command = ""; + } else { + shortcuts = new String[1]; + shortcuts[0] = command; + } + int count = 0; + for (int i=0; i1?"s":"")+" removed; ImageJ restart required"); + } + + private void listCommands() { + String[] commands = getAllCommands(); + Hashtable classes = Menus.getCommands(); + ResultsTable rt = new ResultsTable(); + for (int i=0; i1) { + LUT[] luts = imp.getLuts(); + if (luts!=null && luts.length1) { + IJ.error("HyperStack Converter", "RGB stacks are limited to one channel"); + return; + } + if (nChannels*nSlices*nFrames!=stackSize) { + IJ.error("HyperStack Converter", "channels x slices x frames <> stack size"); + return; + } + imp.setDimensions(nChannels, nSlices, nFrames); + if (ordering!=CZT && imp.getStack().isVirtual()) + reorderVirtualStack(imp, ordering); + else + shuffle(imp, ordering); + ImagePlus imp2 = imp; + if (nChannels>1 && imp.getBitDepth()!=24) { + LUT[] luts = imp.getLuts(); + if (luts!=null && luts.length0"); + return false; + } else + return true; + } + + public static void labelHyperstack(ImagePlus imp) { + int width = imp.getWidth(); + int height = imp.getHeight(); + int c = imp.getNChannels(); + int z = imp.getNSlices(); + int t = imp.getNFrames(); + ImageStack stack = imp.getStack(); + Overlay overlay = new Overlay(); + int n = stack.size(); + int channel=1, slice=1, frame=1; + boolean hyperstack = imp.isHyperStack(); + for (int i=1; i<=n; i++) { + int yloc = 30; + IJ.showProgress(i, n); + ImageProcessor ip = stack.getProcessor(i); + ip.setAntialiasedText(true); + ip.setColor(Color.black); + ip.setRoi(0, 0, width, yloc); + ip.fill(); + ip.setRoi(0, yloc+25, width, height-(yloc+25)); + ip.fill(); + + ip.setColor(Color.white); + Font font = new Font("SansSerif",Font.PLAIN,24); + ip.setFont(font); + String text = "c="+IJ.pad(channel,3)+", z="+IJ.pad(slice,3)+", t="+IJ.pad(frame,3)+", i="+IJ.pad(i,4); + if (!hyperstack) + text = IJ.pad(i,4); + TextRoi roi = new TextRoi(5, yloc-28, text, font); + roi.setStrokeColor(Color.white); + if (hyperstack || c>1) + roi.setPosition(channel, slice, frame); + else + roi.setPosition(i); + overlay.add(roi); + ip.drawString(text, 5, yloc+27); + + // embed channel, slice, frame and stack index into pixel data + yloc += 30;; + int size = 20; + ip.setValue(channel); ip.setRoi(size,yloc,size,size); ip.fill(); + ip.setColor(Color.white); ip.drawRect(size,yloc,size,size); + ip.setValue(slice); ip.setRoi(size*3,yloc,size,size); ip.fill(); + ip.setColor(Color.white); ip.drawRect(size*3,yloc,size,size); + ip.setValue(frame); ip.setRoi(size*5,yloc,size,size); ip.fill(); + ip.setColor(Color.white); ip.drawRect(size*5,yloc,size,size); + ip.setValue(i); ip.setRoi(size*7,yloc,size,size); ip.fill(); + ip.setColor(Color.white); ip.drawRect(size*7,yloc,size,size); + + yloc = 90; + if (i==1 && hyperstack) { + String msg = "Press shift-z (Image>Color>Channels Tool)\n"+ + "to open the \"Channels\" window, which will\n"+ + "allow you switch to composite color mode\n"+ + "and to enable/disable channels.\n"; + font = new Font("SansSerif", Font.PLAIN, imp.getWidth()>399?14:12); + roi = new TextRoi(25, yloc, msg, font); + roi.setStrokeColor(Color.white); + roi.setPosition(0, 1, 1); + overlay.add(roi); + } + channel++; + if (channel>c) { + channel = 1; + slice++; + if (slice>z) { + slice = 1; + frame++; + if (frame>t) frame = 1; + } + } + } + imp.setOverlay(overlay); + } + +} + diff --git a/src/ij/plugin/HyperStackReducer.java b/src/ij/plugin/HyperStackReducer.java new file mode 100644 index 0000000..ea12156 --- /dev/null +++ b/src/ij/plugin/HyperStackReducer.java @@ -0,0 +1,192 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.Calibration; +import java.awt.*; +import java.util.Vector; + +/** Implements the Image/HyperStacks/Reduce Dimensionality command. */ +public class HyperStackReducer implements PlugIn, DialogListener { + ImagePlus imp; + int channels1, slices1, frames1; + int channels2, slices2, frames2; + double imageSize; + static boolean keep = true; + + /** Default constructor */ + public HyperStackReducer() { + } + + /** Constructs a HyperStackReducer using the specified source image. */ + public HyperStackReducer(ImagePlus imp) { + this.imp = imp; + } + + public void run(String arg) { + //IJ.log("HyperStackReducer-1"); + imp = IJ.getImage(); + if (!imp.isHyperStack() && imp.getNChannels()==1) { + IJ.error("Reducer", "HyperStack required"); + return; + } + int width = imp.getWidth(); + int height = imp.getHeight(); + imageSize = width*height*imp.getBytesPerPixel()/(1024.0*1024.0); + channels1 = channels2 = imp.getNChannels(); + slices1 = slices2 = imp.getNSlices(); + frames1 = frames2 = imp.getNFrames(); + int z0 = imp.getSlice(); + int t0 = imp.getFrame(); + if (!showDialog()) + return; + //IJ.log("HyperStackReducer-2: "+keep+" "+channels2+" "+slices2+" "+frames2); + String title2 = keep?WindowManager.getUniqueName(imp.getTitle()):imp.getTitle(); + ImagePlus imp2 = null; + if (keep) { + imp2 = IJ.createImage(title2, imp.getBitDepth()+"-bit", width, height, channels2*slices2*frames2); + if (imp2==null) return; + imp2.setDimensions(channels2, slices2, frames2); + imp2.setCalibration(imp.getCalibration()); + imp2.setOpenAsHyperStack(true); + } else + imp2 = imp.createHyperStack(title2, channels2, slices2, frames2, imp.getBitDepth()); + imp2.setProperty("Info", (String)imp.getProperty("Info")); + imp2.setProperties(imp.getPropertiesAsArray()); + reduce(imp2); + if (channels2>1 && channels2==imp.getNChannels() && imp.isComposite()) { + int mode = ((CompositeImage)imp).getMode(); + imp2 = new CompositeImage(imp2, mode); + ((CompositeImage)imp2).copyLuts(imp); + } else { + imp2.setDisplayRange(imp.getDisplayRangeMin(), imp.getDisplayRangeMax()); + if (imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.GRAYSCALE) + IJ.run(imp2, "Grays", ""); + } + if (imp.getWindow()==null && !keep) { + imp.setImage(imp2); + return; + } + imp2.show(); + if (z0>1 || t0>1) + imp2.setPosition(1, z0, t0); + //IJ.log("HyperStackReducer-4"); + if (!keep) { + imp.changes = false; + imp.close(); + } + } + + public void reduce(ImagePlus imp2) { + int channels = imp2.getNChannels(); + int slices = imp2.getNSlices(); + int frames = imp2.getNFrames(); + int c1 = imp.getChannel(); + int z1 = imp.getSlice(); + int t1 = imp.getFrame(); + int i = 1; + int n = channels*slices*frames; + ImageStack stack = imp.getStack(); + ImageStack stack2 = imp2.getStack(); + for (int c=1; c<=channels; c++) { + if (channels==1) c = c1; + LUT lut = imp.isComposite()?((CompositeImage)imp).getChannelLut():imp.getProcessor().getLut(); + imp.setPositionWithoutUpdate(c, 1, 1); + ImageProcessor ip = imp.getProcessor(); + double min = ip.getMin(); + double max = ip.getMax(); + for (int z=1; z<=slices; z++) { + if (slices==1) z = z1; + for (int t=1; t<=frames; t++) { + if (frames==1) t = t1; + int n1 = imp.getStackIndex(c, z, t); + ip = stack.getProcessor(n1); + String label = stack.getSliceLabel(n1); + int n2 = imp2.getStackIndex(c, z, t); + if (stack2.getPixels(n2)!=null) + stack2.getProcessor(n2).insert(ip, 0, 0); + else + stack2.setPixels(ip.getPixels(), n2); + stack2.setSliceLabel(label, n2); + } + } + if (lut!=null) { + if (imp2.isComposite()) + ((CompositeImage)imp2).setChannelLut(lut); + else + imp2.getProcessor().setColorModel(lut); + } + imp2.getProcessor().setMinAndMax(min, max); + } + imp.setPosition(c1, z1, t1); + imp2.resetStack(); + imp2.setPosition(1, 1, 1); + Overlay overlay = imp.getOverlay(); + if (overlay!=null && !imp.getHideOverlay()) + imp2.setOverlay(reduce(overlay)); + } + + //Added by Marcel Boeglin 2013.11.29 + /** Returns a copy of 'overlay', limited to the dimensions of the reduced image. */ + private Overlay reduce(Overlay overlay) { + int c1 = imp.getChannel(); + int z1 = imp.getSlice(); + int t1 = imp.getFrame(); + Overlay overlay2 = overlay.duplicate(); + if (channels2==1 && slices2==slices1 && frames2==frames1) + overlay2.crop(c1, c1, 1, slices1, 1, frames1); + else if (channels2==channels1 && slices2==1 && frames2==frames1) + overlay2.crop(1, channels1, z1, z1, 1, frames1); + else if (channels2==channels1 && slices2==slices1 && frames2==1) + overlay2.crop(1, channels1, 1, slices1, t1, t1); + else if (channels2==channels1 && slices2==1 && frames2==1) + overlay2.crop(1, channels1, z1, z1, t1, t1); + else if (channels2==1 && slices2==slices1 && frames2==1) + overlay2.crop(c1, c1, 1, slices1, t1, t1); + else if (channels2==1 && slices2==1 && frames2==frames1) + overlay2.crop(c1, c1, z1, z1, 1, frames1); + else if (channels2==1 && slices2==1 && frames2==1) + overlay2.crop(c1, c1, z1, z1, t1, t1); + return overlay2; + } + + boolean showDialog() { + GenericDialog gd = new GenericDialog("Reduce"); + gd.setInsets(10, 20, 5); + gd.addMessage("Create image with:"); + gd.setInsets(0, 35, 0); + if (channels1!=1) gd.addCheckbox("Channels ("+channels1+")", true); + gd.setInsets(0, 35, 0); + if (slices1!=1) gd.addCheckbox("Slices ("+slices1+")", true); + gd.setInsets(0, 35, 0); + if (frames1!=1) gd.addCheckbox("Frames ("+frames1+")", true); + gd.setInsets(5, 20, 0); + gd.addMessage(getNewDimensions()+" "); + gd.setInsets(15, 20, 0); + gd.addCheckbox("Keep source", keep); + gd.addDialogListener(this); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + else + return true; + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (IJ.isMacOSX()) IJ.wait(100); + if (channels1!=1) channels2 = gd.getNextBoolean()?channels1:1; + if (slices1!=1) slices2 = gd.getNextBoolean()?slices1:1; + if (frames1!=1) frames2 = gd.getNextBoolean()?frames1:1; + keep = gd.getNextBoolean(); + if (imp!=null && imp.getWindow()!=null) + ((Label)gd.getMessage()).setText(getNewDimensions()); + return true; + } + + String getNewDimensions() { + String s = channels2+"x"+slices2+"x"+frames2; + s += " ("+(int)Math.round(imageSize*channels2*slices2*frames2)+"MB)"; + return(s); + } + +} diff --git a/src/ij/plugin/ImageCalculator.java b/src/ij/plugin/ImageCalculator.java new file mode 100644 index 0000000..255a094 --- /dev/null +++ b/src/ij/plugin/ImageCalculator.java @@ -0,0 +1,349 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.plugin.filter.*; +import ij.measure.Calibration; +import ij.plugin.frame.Recorder; +import ij.macro.Interpreter; + +/** This plugin implements the Process/Image Calculator command. +
+   // test script
+   imp1 = IJ.openImage("http://imagej.nih.gov/ij/images/boats.gif")
+   imp2 = IJ.openImage("http://imagej.nih.gov/ij/images/bridge.gif")
+   imp3 = ImageCalculator.run(imp1, imp2, "add create 32-bit");
+   imp3.show();
+
+*/ +public class ImageCalculator implements PlugIn { + + private static String[] operators = {"Add","Subtract","Multiply","Divide", "AND", "OR", "XOR", "Min", "Max", "Average", "Difference", "Copy", "Transparent-zero"}; + private static String[] lcOperators = {"add","sub","mul","div", "and", "or", "xor", "min", "max", "ave", "diff", "copy", "zero"}; + private static int operator; + private static String title1 = ""; + private static String title2 = ""; + private static boolean createWindow = true; + private static boolean floatResult; + private boolean processStack; + private boolean macroCall; + + public void run(String arg) { + int[] wList = WindowManager.getIDList(); + if (wList==null) { + IJ.noImage(); + return; + } + IJ.register(ImageCalculator.class); + String[] titles = new String[wList.length]; + for (int i=0; i + imp3 = ImageCalculator.run(imp1, imp2, "divide create 32-bit"); +
+ divides 'imp1' by 'imp2' and returns the result as a new 32-bit image. + */ + public static ImagePlus run(ImagePlus img1, ImagePlus img2, String operation) { + ImageCalculator ic = new ImageCalculator(); + return ic.run(operation, img1, img2); + } + + public ImagePlus run(String operation, ImagePlus img1, ImagePlus img2) { + if (img1==null || img2==null || operation==null) return null; + operator = getOperator(operation); + if (operator==-1) + throw new IllegalArgumentException("No valid operator"); + createWindow = operation.indexOf("create")!=-1; + floatResult= operation.indexOf("32")!=-1 || operation.indexOf("float")!=-1; + processStack = operation.indexOf("stack")!=-1; + return calculate(img1, img2, true); + } + + /** + * @deprecated + * replaced by run(String,ImagePlus,ImagePlus) + */ + public void calculate(String operation, ImagePlus img1, ImagePlus img2) { + if (img1==null || img2==null || operation==null) return; + operator = getOperator(operation); + if (operator==-1) + {IJ.error("Image Calculator", "No valid operator"); return;} + createWindow = operation.indexOf("create")!=-1; + floatResult = operation.indexOf("32")!=-1 || operation.indexOf("float")!=-1; + processStack = operation.indexOf("stack")!=-1; + macroCall = true; + ImagePlus img3 = calculate(img1, img2, true); + if (img3!=null) img3.show(); + } + + int getOperator(String options) { + options = options.toLowerCase(); + int op= -1; + if (options.indexOf("xor")!=-1) + op = 6; + if (op==-1) { + for (int i=0; i1||size2>1)) + img3 = doStackOperation(img1, img2); + else + img3 = doOperation(img1, img2); + if (img3==null && !macroCall && (img1.getWindow()==null)) + img3 = img1; + return img3; + } + boolean stackOp = false; + if (size1>1) { + int result = IJ.setupDialog(img1, PlugInFilter.NO_CHANGES); + if (result==PlugInFilter.DONE) + return null; + processStack = (result&PlugInFilter.DOES_STACKS)!=0; + if (processStack) { + if (img1.getStack().isVirtual()) + createWindow = true; + img3 = doStackOperation(img1, img2); + stackOp = true; + } else + img3 = doOperation(img1, img2); + } else + img3 = doOperation(img1, img2); + if (Recorder.record) { + String options = operators[operator]; + if (createWindow) options += " create"; + if (floatResult) options += " 32-bit"; + if (stackOp) options += " stack"; + if (Recorder.scriptMode()) { + Recorder.recordCall("ImagePlus", "imp1 = WindowManager.getImage(\""+img1.getTitle()+"\");"); + Recorder.recordCall("ImagePlus", "imp2 = WindowManager.getImage(\""+img2.getTitle()+"\");"); + Recorder.recordCall("ImagePlus", "imp3 = ImageCalculator.run(imp1, imp2, \""+options+"\");"); + Recorder.recordCall("imp3.show();"); + } else + Recorder.record("imageCalculator", options, img1.getTitle(), img2.getTitle()); + Recorder.setCommand(null); // don't record run(...) + } + return img3; + } + + /** img1 = img2 op img2 (e.g. img1 = img2/img1) */ + ImagePlus doStackOperation(ImagePlus img1, ImagePlus img2) { + ImagePlus img3 = null; + int size1 = img1.getStackSize(); + int size2 = img2.getStackSize(); + if (size1>1 && size2>1 && size1!=size2) { + IJ.error("Image Calculator", "'Image1' and 'image2' must be stacks with the same\nnumber of slices, or 'image2' must be a single image."); + return null; + } + if (createWindow) { + img1 = duplicateStack(img1); + if (img1==null) { + IJ.error("Calculator", "Out of memory"); + return null; + } + img3 = img1; + } + int mode = getBlitterMode(); + ImageWindow win = img1.getWindow(); + if (win!=null) + WindowManager.setCurrentWindow(win); + else if (Interpreter.isBatchMode() && !createWindow && WindowManager.getImage(img1.getID())!=null) + IJ.selectWindow(img1.getID()); + Undo.reset(); + ImageStack stack1 = img1.getStack(); + StackProcessor sp = new StackProcessor(stack1, img1.getProcessor()); + try { + if (size2==1) + sp.copyBits(img2.getProcessor(), 0, 0, mode); + else + sp.copyBits(img2.getStack(), 0, 0, mode); + } + catch (IllegalArgumentException e) { + IJ.error("\""+img1.getTitle()+"\": "+e.getMessage()); + return null; + } + img1.setStack(null, stack1); + if (img1.getType()!=ImagePlus.GRAY8) { + img1.getProcessor().resetMinAndMax(); + } + if (img3==null) + img1.updateAndDraw(); + return img3; + } + + ImagePlus doOperation(ImagePlus img1, ImagePlus img2) { + ImagePlus img3 = null; + int mode = getBlitterMode(); + ImageProcessor ip1 = img1.getProcessor(); + ImageProcessor ip2 = img2.getProcessor(); + Calibration cal1 = img1.getCalibration(); + Calibration cal2 = img2.getCalibration(); + if (createWindow) + ip1 = createNewImage(ip1, ip2); + else { + ImageWindow win = img1.getWindow(); + if (win!=null) + WindowManager.setCurrentWindow(win); + else if (Interpreter.isBatchMode() && !createWindow && WindowManager.getImage(img1.getID())!=null) + IJ.selectWindow(img1.getID()); + ip1.snapshot(); + Undo.setup(Undo.FILTER, img1); + } + boolean rgb = ip2 instanceof ColorProcessor; + if (floatResult && !rgb) + ip2 = ip2.convertToFloat(); + try { + ip1.copyBits(ip2, 0, 0, mode); + } + catch (IllegalArgumentException e) { + IJ.error("\""+img1.getTitle()+"\": "+e.getMessage()); + return null; + } + if (floatResult && rgb) + ip1 = ip1.convertToFloat(); + if (!(ip1 instanceof ByteProcessor)) + ip1.resetMinAndMax(); + if (createWindow) { + img3 = new ImagePlus("Result of "+img1.getTitle(), ip1); + img3.setCalibration(cal1); + } else + img1.updateAndDraw(); + return img3; + } + + ImageProcessor createNewImage(ImageProcessor ip1, ImageProcessor ip2) { + int width = Math.min(ip1.getWidth(), ip2.getWidth()); + int height = Math.min(ip1.getHeight(), ip2.getHeight()); + ImageProcessor ip3 = ip1.createProcessor(width, height); + if (floatResult && !(ip1 instanceof ColorProcessor)) { + ip1 = ip1.convertToFloat(); + ip3 = ip3.convertToFloat(); + } + ip3.insert(ip1, 0, 0); + return ip3; + } + + private int getBlitterMode() { + int mode=0; + switch (operator) { + case 0: mode = Blitter.ADD; break; + case 1: mode = Blitter.SUBTRACT; break; + case 2: mode = Blitter.MULTIPLY; break; + case 3: mode = Blitter.DIVIDE; break; + case 4: mode = Blitter.AND; break; + case 5: mode = Blitter.OR; break; + case 6: mode = Blitter.XOR; break; + case 7: mode = Blitter.MIN; break; + case 8: mode = Blitter.MAX; break; + case 9: mode = Blitter.AVERAGE; break; + case 10: mode = Blitter.DIFFERENCE; break; + case 11: mode = Blitter.COPY; break; + case 12: mode = Blitter.COPY_ZERO_TRANSPARENT; break; + } + return mode; + } + + ImagePlus duplicateStack(ImagePlus img1) { + Calibration cal = img1.getCalibration(); + ImageStack stack1 = img1.getStack(); + int width = stack1.getWidth(); + int height = stack1.getHeight(); + int n = stack1.getSize(); + ImageStack stack2 = img1.createEmptyStack(); + try { + for (int i=1; i<=n; i++) { + ImageProcessor ip1 = stack1.getProcessor(i); + ip1.resetRoi(); + ImageProcessor ip2 = ip1.crop(); + if (floatResult) { + ip2.setCalibrationTable(cal.getCTable()); + ip2 = ip2.convertToFloat(); + } + stack2.addSlice(stack1.getSliceLabel(i), ip2); + } + } + catch(OutOfMemoryError e) { + stack2.trim(); + stack2 = null; + return null; + } + ImagePlus img3 = new ImagePlus("Result of "+img1.getTitle(), stack2); + img3.setCalibration(cal); + if (img3.getStackSize()==n) { + int[] dim = img1.getDimensions(); + img3.setDimensions(dim[2], dim[3], dim[4]); + if (img1.isComposite()) { + img3 = new CompositeImage(img3, 0); + ((CompositeImage)img3).copyLuts(img1); + } + if (img1.isHyperStack()) + img3.setOpenAsHyperStack(true); + } + return img3; + } + +} diff --git a/src/ij/plugin/ImageInfo.java b/src/ij/plugin/ImageInfo.java new file mode 100644 index 0000000..09d3c72 --- /dev/null +++ b/src/ij/plugin/ImageInfo.java @@ -0,0 +1,526 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import ij.io.*; +import ij.util.Tools; +import ij.plugin.frame.Editor; +import ij.plugin.filter.Analyzer; +import ij.text.TextWindow; +import ij.macro.Interpreter; +import java.awt.*; +import java.util.*; +import java.lang.reflect.*; +import java.awt.geom.Rectangle2D; + +/** This plugin implements the Image/Show Info command. */ +public class ImageInfo implements PlugIn { + + public void run(String arg) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) + showInfo(); + else { + String info = getImageInfo(imp); + if (info.contains("----")) + showInfo(imp, info, 450, 600); + else { + int inc = info.contains("No selection")?0:130; + showInfo(imp, info, 400, 500+inc); + } + } + } + + private void showInfo() { + String s = new String(""); + if (IJ.getInstance()!=null) + s += IJ.getInstance().getInfo()+"\n \n"; + s += "No images are open\n"; + Dimension screen = IJ.getScreenSize(); + s += "ImageJ home: "+IJ.getDir("imagej")+"\n"; + s += "Java home: "+System.getProperty("java.home")+"\n"; + s += "Java version: "+IJ.javaVersion()+"\n"; + s += "Screen size: "+screen.width+"x"+screen.height+"\n"; + s += "GUI scale: "+IJ.d2s(Prefs.getGuiScale(),2)+"\n"; + //s += "Active window: "+WindowManager.getActiveWindow()+"\n"; + String path = Prefs.getCustomPropsPath(); + if (path!=null) + s += "*Custom properties*: "+ path +"\n"; + path = Prefs.getCustomPrefsPath(); + if (path!=null) + s += "*Custom preferences*: "+ path +"\n"; + //if (IJ.isMacOSX()) { + // String time = " ("+ImageWindow.setMenuBarTime+"ms)"; + // s += "SetMenuBarCount: "+Menus.setMenuBarCount+time+"\n"; + //} + new TextWindow("Info", s, 600, 300); + } + + public String getImageInfo(ImagePlus imp) { + ImageProcessor ip = imp.getProcessor(); + String infoProperty = null; + if (imp.getStackSize()>1) { + ImageStack stack = imp.getStack(); + String label = stack.getSliceLabel(imp.getCurrentSlice()); + if (label!=null && label.indexOf('\n')>0) + infoProperty = label; + } + if (infoProperty==null) { + infoProperty = (String)imp.getProperty("Info"); + if (infoProperty==null) + infoProperty = getExifData(imp); + } + if (imp.getProp("HideInfo")==null) { + String properties = getImageProperties(imp); + if (properties!=null) { + if (infoProperty!=null) + infoProperty = properties + "\n" + infoProperty; + else + infoProperty = properties; + } + } + String info = getInfo(imp, ip); + if (infoProperty!=null) + return infoProperty + "--------------------------------------------\n" + info; + else + return info; + } + + public String getExifData(ImagePlus imp) { + FileInfo fi = imp.getOriginalFileInfo(); + if (fi==null) + return null; + String directory = fi.directory; + String name = fi.fileName; + if (directory==null) + return null; + if ((name==null||name.equals("")) && imp.getStack().isVirtual()) + name = imp.getStack().getSliceLabel(imp.getCurrentSlice()); + if (name==null || !(name.endsWith("jpg")||name.endsWith("JPG"))) + return null; + String path = directory + name; + String metadata = null; + try { + Class c = IJ.getClassLoader().loadClass("Exif_Reader"); + if (c==null) return null; + String methodName = "getMetadata"; + Class[] argClasses = new Class[1]; + argClasses[0] = methodName.getClass(); + Method m = c.getMethod("getMetadata", argClasses); + Object[] args = new Object[1]; + args[0] = path; + Object obj = m.invoke(null, args); + metadata = obj!=null?obj.toString():null; + } catch(Exception e) { + return null; + } + if (metadata!=null && !metadata.startsWith("Error:")) + return metadata; + else + return null; + } + + private String getInfo(ImagePlus imp, ImageProcessor ip) { + String s = new String(""); + if (IJ.getInstance()!=null) + s += IJ.getInstance().getInfo()+"\n \n"; + s += "Title: " + imp.getTitle() + "\n"; + Calibration cal = imp.getCalibration(); + int stackSize = imp.getStackSize(); + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + int digits = imp.getBitDepth()==32?4:0; + int dp, dp2; + boolean nonUniformUnits = !cal.getXUnit().equals(cal.getYUnit()); + String xunit = cal.getXUnit(); + String yunit = cal.getYUnit(); + String zunit = cal.getZUnit(); + if (cal.scaled()) { + String xunits = cal.getUnits(); + String yunits = xunits; + String zunits = xunits; + if (nonUniformUnits) { + xunits = xunit; + yunits = yunit; + zunits = zunit; + } + double pw = imp.getWidth()*cal.pixelWidth; + double ph = imp.getHeight()*cal.pixelHeight; + s += "Width: "+d2s(pw)+" " + xunits+" ("+imp.getWidth()+")\n"; + s += "Height: "+d2s(ph)+" " + yunits+" ("+imp.getHeight()+")\n"; + if (slices>1) { + double pd = slices*cal.pixelDepth; + s += "Depth: "+d2s(pd)+" " + zunits+" ("+slices+")\n"; + } + s += "Size: "+ImageWindow.getImageSize(imp)+"\n"; + double xResolution = 1.0/cal.pixelWidth; + double yResolution = 1.0/cal.pixelHeight; + if (xResolution==yResolution) + s += "Resolution: "+d2s(xResolution) + " pixels per "+xunit+"\n"; + else { + s += "X Resolution: "+d2s(xResolution) + " pixels per "+xunit+"\n"; + s += "Y Resolution: "+d2s(yResolution) + " pixels per "+yunit+"\n"; + } + } else { + s += "Width: " + imp.getWidth() + " pixels\n"; + s += "Height: " + imp.getHeight() + " pixels\n"; + if (stackSize>1) + s += "Depth: " + slices + " pixels\n"; + s += "Size: "+ImageWindow.getImageSize(imp)+"\n"; + } + if (stackSize>1) { + String vunit = cal.getUnit()+"^3"; + if (nonUniformUnits) + vunit = "("+xunit+" x "+yunit+" x "+zunit+")"; + s += "Voxel size: "+d2s(cal.pixelWidth)+"x"+d2s(cal.pixelHeight)+"x"+d2s(cal.pixelDepth)+" "+vunit+"\n"; + } else { + String punit = cal.getUnit()+"^2"; + if (nonUniformUnits) + punit = "("+xunit+" x "+yunit+")"; + dp = Tools.getDecimalPlaces(cal.pixelWidth, cal.pixelHeight); + s += "Pixel size: "+d2s(cal.pixelWidth)+"x"+d2s(cal.pixelHeight)+" "+punit+"\n"; + } + + s += "ID: "+imp.getID()+"\n"; + int type = imp.getType(); + switch (type) { + case ImagePlus.GRAY8: + s += "Bits per pixel: 8 "; + String lut = "LUT"; + if (imp.getProcessor().isColorLut()) + lut = "color " + lut; + else + lut = "grayscale " + lut; + if (imp.isInvertedLut()) + lut = "inverting " + lut; + s += "(" + lut + ")\n"; + if (imp.getNChannels()>1) + s += displayRanges(imp); + else + s += "Display range: "+(int)ip.getMin()+"-"+(int)ip.getMax()+"\n"; + break; + case ImagePlus.GRAY16: case ImagePlus.GRAY32: + if (type==ImagePlus.GRAY16) { + String sign = cal.isSigned16Bit()?"signed":"unsigned"; + s += "Bits per pixel: 16 ("+sign+")\n"; + } else + s += "Bits per pixel: 32 (float)\n"; + if (imp.getNChannels()>1) + s += displayRanges(imp); + else { + s += "Display range: "; + double min = ip.getMin(); + double max = ip.getMax(); + if (cal.calibrated()) { + min = cal.getCValue((int)min); + max = cal.getCValue((int)max); + } + s += d2s(min) + " - " + d2s(max) + "\n"; + } + break; + case ImagePlus.COLOR_256: + s += "Bits per pixel: 8 (color LUT)\n"; + break; + case ImagePlus.COLOR_RGB: + s += "Bits per pixel: 32 (RGB)\n"; + break; + } + String lutName = imp.getProp(LUT.nameKey); + if (lutName!=null) + s += "LUT name: "+lutName+"\n"; + double interval = cal.frameInterval; + double fps = cal.fps; + if (stackSize>1) { + ImageStack stack = imp.getStack(); + int slice = imp.getCurrentSlice(); + String number = slice + "/" + stackSize; + String label = stack.getSliceLabel(slice); + if (label!=null && label.contains("\n")) + label = stack.getShortSliceLabel(slice); + if (label!=null && label.length()>0) + label = " (" + label + ")"; + else + label = ""; + if (interval>0.0 || fps!=0.0) { + s += "Frame: " + number + label + "\n"; + if (fps!=0.0) { + String sRate = Math.abs(fps-Math.round(fps))<0.00001?IJ.d2s(fps,0):IJ.d2s(fps,5); + s += "Frame rate: " + sRate + " fps\n"; + } + if (interval!=0.0) + s += "Frame interval: " + ((int)interval==interval?IJ.d2s(interval,0):IJ.d2s(interval,5)) + " " + cal.getTimeUnit() + "\n"; + } else + s += "Image: " + number + label + "\n"; + if (imp.isHyperStack()) { + if (channels>1) + s += " Channel: " + imp.getChannel() + "/" + channels + "\n"; + if (slices>1) + s += " Slice: " + imp.getSlice() + "/" + slices + "\n"; + if (frames>1) + s += " Frame: " + imp.getFrame() + "/" + frames + "\n"; + } + if (imp.isComposite()) { + if (!imp.isHyperStack() && channels>1) + s += " Channels: " + channels + "\n"; + String mode = ((CompositeImage)imp).getModeAsString(); + s += " Composite mode: \"" + mode + "\"\n"; + } + if (stack.isVirtual()) { + String stackType = "virtual"; + if (stack instanceof AVI_Reader) + stackType += " (AVI Reader)"; + if (stack instanceof FileInfoVirtualStack) + stackType += " (FileInfoVirtualStack)"; + if (stack instanceof ListVirtualStack) + stackType += " (ListVirtualStack)"; + s += "Stack type: " + stackType+ "\n"; + } + } else if (imp.isStack()) { // one image stack + String label = imp.getStack().getShortSliceLabel(1); + if (label!=null && label.length()>0) + s += "Image: 1/1 (" + label + ")\n"; + } + + if (imp.isLocked()) + s += "**Locked**\n"; + if (ip.getMinThreshold()==ImageProcessor.NO_THRESHOLD) + s += "No threshold\n"; + else { + double lower = ip.getMinThreshold(); + double upper = ip.getMaxThreshold(); + String uncalibrated = ""; + if (cal.calibrated()) { + uncalibrated = " ("+(int)lower+"-"+(int)upper+")"; + lower = cal.getCValue((int)lower); + upper = cal.getCValue((int)upper); + } + int lutMode = ip.getLutUpdateMode(); + String mode = "red"; + switch (lutMode) { + case ImageProcessor.BLACK_AND_WHITE_LUT: mode="B&W"; break; + case ImageProcessor.NO_LUT_UPDATE: mode="invisible"; break; + case ImageProcessor.OVER_UNDER_LUT: mode="over/under"; break; + } + s += "Threshold: "+d2s(lower)+"-"+d2s(upper)+uncalibrated+" ("+mode+")\n"; + } + ImageCanvas ic = imp.getCanvas(); + double mag = ic!=null?ic.getMagnification():1.0; + if (mag!=1.0) + s += "Magnification: " + IJ.d2s(mag,2) + "\n"; + if (ic!=null) + s += "ScaleToFit: " + ic.getScaleToFit() + "\n"; + + + String valueUnit = cal.getValueUnit(); + if (cal.calibrated()) { + s += " \n"; + int curveFit = cal.getFunction(); + s += "Calibration function: "; + if (curveFit==Calibration.UNCALIBRATED_OD) + s += "Uncalibrated OD\n"; + else if (curveFit==Calibration.CUSTOM) + s += "Custom lookup table\n"; + else + s += CurveFitter.fList[curveFit]+"\n"; + double[] c = cal.getCoefficients(); + if (c!=null) { + s += " a: "+IJ.d2s(c[0],6)+"\n"; + s += " b: "+IJ.d2s(c[1],6)+"\n"; + if (c.length>=3) + s += " c: "+IJ.d2s(c[2],6)+"\n"; + if (c.length>=4) + s += " c: "+IJ.d2s(c[3],6)+"\n"; + if (c.length>=5) + s += " c: "+IJ.d2s(c[4],6)+"\n"; + } + s += " Unit: \""+valueUnit+"\"\n"; + } else if (valueUnit!=null && !valueUnit.equals("Gray Value")) { + s += "Calibration function: None\n"; + s += " Unit: \""+valueUnit+"\"\n"; + } else + s += "Uncalibrated\n"; + + FileInfo fi = imp.getOriginalFileInfo(); + if (fi!=null) { + if (fi.url!=null && !fi.url.equals("")) + s += "URL: " + fi.url + "\n"; + else { + String defaultDir = (fi.directory==null || fi.directory.length()==0)?System.getProperty("user.dir"):""; + if (defaultDir.length()>0) { + defaultDir = defaultDir.replaceAll("\\\\", "/"); + defaultDir += "/"; + } + s += "Path: " + defaultDir + fi.getFilePath() + "\n"; + } + } + + ImageWindow win = imp.getWindow(); + if (win!=null) { + Point loc = win.getLocation(); + Rectangle bounds = GUI.getScreenBounds(win); + s += "Screen location: "+(loc.x-bounds.x)+","+(loc.y-bounds.y)+" ("+bounds.width+"x"+bounds.height+")\n"; + } + if (IJ.isMacOSX()) { + String time = " ("+ImageWindow.setMenuBarTime+"ms)"; + s += "SetMenuBarCount: "+Menus.setMenuBarCount+time+"\n"; + } + + String zOrigin = stackSize>1||cal.zOrigin!=0.0?","+d2s(cal.zOrigin):""; + String origin = d2s(cal.xOrigin)+","+d2s(cal.yOrigin)+zOrigin; + if (!origin.equals("0,0") || cal.getInvertY()) + s += "Coordinate origin: "+origin+"\n"; + if (cal.getInvertY()) + s += "Inverted y coordinates\n"; + + String pinfo = imp.getPropsInfo(); + if (!pinfo.equals("0")) + s += "Properties: " + pinfo + "\n"; + else + s += "No properties\n"; + + Overlay overlay = imp.getOverlay(); + if (overlay!=null) { + int n = overlay.size(); + String elements = n==1?" element":" elements"; + String selectable = overlay.isSelectable()?" selectable ":" non-selectable "; + String hidden = imp.getHideOverlay()?" (hidden)":""; + s += "Overlay: " + n + selectable + elements + hidden + "\n"; + } else + s += "No overlay\n"; + + Interpreter interp = Interpreter.getInstance(); + if (interp!=null) + s += "Macro is running"+(Interpreter.isBatchMode()?" in batch mode":"")+"\n"; + + Roi roi = imp.getRoi(); + if (roi == null) { + if (cal.calibrated()) + s += " \n"; + s += "No selection\n"; + } else if (roi instanceof RotatedRectRoi) { + s += "\nRotated rectangle selection\n"; + double[] p = ((RotatedRectRoi)roi).getParams(); + double dx = p[2] - p[0]; + double dy = p[3] - p[1]; + double major = Math.sqrt(dx*dx+dy*dy); + s += " Length: " + IJ.d2s(major,2) + "\n"; + s += " Width: " + IJ.d2s(p[4],2) + "\n"; + s += " X1: " + IJ.d2s(p[0],2) + "\n"; + s += " Y1: " + IJ.d2s(p[1],2) + "\n"; + s += " X2: " + IJ.d2s(p[2],2) + "\n"; + s += " Y2: " + IJ.d2s(p[3],2) + "\n"; + } else if (roi instanceof EllipseRoi) { + s += "\nElliptical selection\n"; + double[] p = ((EllipseRoi)roi).getParams(); + double dx = p[2] - p[0]; + double dy = p[3] - p[1]; + double major = Math.sqrt(dx*dx+dy*dy); + s += " Major: " + IJ.d2s(major,2) + "\n"; + s += " Minor: " + IJ.d2s(major*p[4],2) + "\n"; + s += " X1: " + IJ.d2s(p[0],2) + "\n"; + s += " Y1: " + IJ.d2s(p[1],2) + "\n"; + s += " X2: " + IJ.d2s(p[2],2) + "\n"; + s += " Y2: " + IJ.d2s(p[3],2) + "\n"; + s += " Aspect ratio: " + IJ.d2s(p[4],2) + "\n"; + } else { + s += " \n"; + s += roi.getTypeAsString()+" Selection"; + String points = null; + if (roi instanceof PointRoi) { + int npoints = ((PolygonRoi)roi).getNCoordinates(); + String suffix = npoints>1?"s)":")"; + points = " (" + npoints + " point" + suffix; + } + String name = roi.getName(); + if (name!=null) { + s += " (\"" + name + "\")"; + if (points!=null) s += "\n " + points; + } else if (points!=null) + s += points; + s += "\n"; + if (roi instanceof Line) { + Line line = (Line)roi; + s += " X1: " + IJ.d2s(cal.getX(line.x1d)) + "\n"; + s += " Y1: " + IJ.d2s(cal.getY(line.y1d, imp.getHeight())) + "\n"; + s += " X2: " + IJ.d2s(cal.getX(line.x2d)) + "\n"; + s += " Y2: " + IJ.d2s(cal.getY(line.y2d, imp.getHeight())) + "\n"; + } else { + Rectangle2D.Double r = roi.getFloatBounds(); + int decimals = r.x==(int)r.x && r.y==(int)r.y && r.width==(int)r.width && r.height==(int)r.height ? + 0 : 2; + if (cal.scaled()) { + s += " X: " + IJ.d2s(cal.getX(r.x)) + " (" + IJ.d2s(r.x, decimals) + ")\n"; + s += " Y: " + IJ.d2s(cal.getY(r.y,imp.getHeight())) + " (" + IJ.d2s(yy(r.y, imp), decimals) + ")\n"; + s += " Width: " + IJ.d2s(r.width*cal.pixelWidth) + " (" + IJ.d2s(r.width, decimals) + ")\n"; + s += " Height: " + IJ.d2s(r.height*cal.pixelHeight) + " (" + IJ.d2s(r.height, decimals) + ")\n"; + } else { + s += " X: " + IJ.d2s(r.x, decimals) + "\n"; + s += " Y: " + IJ.d2s(yy(r.y, imp), decimals) + "\n"; + s += " Width: " + IJ.d2s(r.width, decimals) + "\n"; + s += " Height: " + IJ.d2s(r.height, decimals) + "\n"; + } + } + } + + return s; + } + + private String displayRanges(ImagePlus imp) { + LUT[] luts = imp.getLuts(); + if (luts==null) + return ""; + String s = "Display ranges\n"; + int n = luts.length; + if (n>7) n=7; + for (int i=0; i\n"; + } + } + return (s.length()>0)?s:null; + } + +} diff --git a/src/ij/plugin/ImageJ_Updater.java b/src/ij/plugin/ImageJ_Updater.java new file mode 100644 index 0000000..0f7280f --- /dev/null +++ b/src/ij/plugin/ImageJ_Updater.java @@ -0,0 +1,192 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.util.Tools; +import java.io.*; +import java.net.*; +import java.util.*; +import java.lang.reflect.*; + + +/** This plugin implements the Help/Update ImageJ command. */ +public class ImageJ_Updater implements PlugIn { + private static final String URL = "http://wsr.imagej.net"; + private String notes; + + public void run(String arg) { + if (arg.equals("menus")) { + updateMenus(); + return; + } + if (IJ.getApplet()!=null) + return; + URL url = getClass().getResource("/ij/IJ.class"); + String ij_jar = url == null ? null : url.toString().replaceAll("%20", " "); + if (ij_jar==null || !ij_jar.startsWith("jar:file:")) { + error("Could not determine location of ij.jar"); + return; + } + int exclamation = ij_jar.indexOf('!'); + ij_jar = ij_jar.substring(9, exclamation); + if (IJ.debugMode) IJ.log("Updater (jar loc): "+ij_jar); + File file = new File(ij_jar); + if (!file.exists()) { + error("File not found: "+file.getPath()); + return; + } + if (!file.canWrite()) { + String path = file.getPath(); + String msg = "No write access: "+path; + if (IJ.isMacOSX() && path!=null && path.startsWith("/private/var/folders/")) { + msg = "ImageJ is in a read-only folder due to Path Randomization.\n" + + "To work around this problem, drag ImageJ.app to another\n" + + "folder and then (optionally) drag it back."; + } + error(msg); + return; + } + String[] list = openUrlAsList(URL+"/jars/list.txt"); + if (list==null || list.length==0) { + error("Error opening "+URL+"/jars/list.txt"); + return; + } + int count = list.length + 2; + String[] versions = new String[count]; + String[] urls = new String[count]; + versions[0] = list[0]; + urls[0] = URL+"/jars/ij.jar"; + for (int i=1; i1) + msg = "\n \nUse the Image>Stacks>Tools>Concatenate\ncommand to combine stacks."; + IJ.error("Images to Stack", "There must be at least two open 2D images."+msg); + return; + } + + filter = null; + count = findMinMaxSize(images, count); + boolean sizesDiffer = width!=minWidth||height!=minHeight; + boolean showDialog = true; + String macroOptions = Macro.getOptions(); + if (IJ.macroRunning() && macroOptions==null) { + if (sizesDiffer) { + IJ.error("Images are not all the same size"); + return; + } + showDialog = false; + } + if (showDialog) { + GenericDialog gd = new GenericDialog("Images to Stack"); + if (sizesDiffer) { + String msg = "The "+count+" images differ in size (smallest="+minWidth+"x"+minHeight + +",\nlargest="+maxWidth+"x"+maxHeight+"). They will be converted\nto a stack using the specified method."; + gd.setInsets(0,0,5); + gd.addMessage(msg); + gd.addChoice("Method:", methods, methods[staticMethod]); + } + gd.setSmartRecording(true); + gd.addStringField("Name:", name, 12); + gd.addStringField("Title contains:", "", 12); + gd.addStringField("Fill color:", "", 12); + if (sizesDiffer) + gd.addCheckbox("Bicubic interpolation", staticBicubic); + gd.addCheckbox("Use titles as labels", staticTitlesAsLabels); + gd.addCheckbox("Keep source images", staticKeep); + gd.showDialog(); + if (gd.wasCanceled()) return; + if (sizesDiffer) + method = gd.getNextChoiceIndex(); + name = gd.getNextString(); + filter = gd.getNextString(); + String fillc = gd.getNextString(); + fillColor = Colors.decode(fillc, null); + if (sizesDiffer) + bicubic = gd.getNextBoolean(); + titlesAsLabels = gd.getNextBoolean(); + keep = gd.getNextBoolean(); + if (filter!=null && (filter.equals("") || filter.equals("*"))) + filter = null; + if (filter!=null) { + count = findMinMaxSize(images, count); + if (count==0) { + IJ.error("Images to Stack", "None of the images have a title containing \""+filter+"\""); + } + } + if (!IJ.isMacro()) { + staticMethod = method; + staticBicubic = bicubic; + staticKeep = keep; + staticTitlesAsLabels = titlesAsLabels; + } + if (Recorder.record) + Recorder.recordCall("imp = ImagesToStack.run(arrayOfImages);"); + } else + keep = false; + if (method==SCALE_SMALL) { + width = minWidth; + height = minHeight; + } else if (method==SCALE_LARGE) { + width = maxWidth; + height = maxHeight; + } + ImagePlus stack = convert(images, count); + if (stack!=null) + stack.show(); + } + + private ImagePlus convert(ImagePlus[] images, int count) { + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + ImageStack stack = new ImageStack(width, height); + FileInfo fi = images[0].getOriginalFileInfo(); + if (fi!=null && fi.directory==null) fi = null; + Overlay overlay = new Overlay(); + for (int i=0; imax) max = ip.getMax(); + String label = titlesAsLabels?images[i].getTitle():null; + if (label==null) + label = images[i].getProp("Slice_Label"); + if (label!=null) { + String info = (String)images[i].getProperty("Info"); + if (info!=null) label += "\n" + info; + } + if (fi!=null) { + FileInfo fi2 = images[i].getOriginalFileInfo(); + if (fi2!=null && !fi.directory.equals(fi2.directory)) + fi = null; + } + switch (stackType) { + case 16: ip = ip.convertToShort(false); break; + case 32: ip = ip.convertToFloat(); break; + case rgb: ip = ip.convertToRGB(); break; + default: break; + } + if (invertedLut && !allInvertedLuts) { + if (keep) + ip = ip.duplicate(); + ip.invert(); + } + if (ip.getWidth()!=width||ip.getHeight()!=height) { + switch (method) { + case COPY_TOP_LEFT: case COPY_CENTER: + ImageProcessor ip2 = null; + switch (stackType) { + case 8: ip2 = new ByteProcessor(width, height); break; + case 16: ip2 = new ShortProcessor(width, height); break; + case 32: ip2 = new FloatProcessor(width, height); break; + case rgb: ip2 = new ColorProcessor(width, height); break; + } + if (fillColor!=null) { + ip2.setColor(fillColor); + ip2.fill(); + } + int xoff=0, yoff=0; + if (method==COPY_CENTER) { + xoff = (width-ip.getWidth())/2; + yoff = (height-ip.getHeight())/2; + } + ip2.insert(ip, xoff, yoff); + ip = ip2; + break; + case SCALE_SMALL: case SCALE_LARGE: + ip.setInterpolationMethod((bicubic?ImageProcessor.BICUBIC:ImageProcessor.BILINEAR)); + ip.resetRoi(); + ip = ip.resize(width, height); + break; + } + } else { + if (keep) + ip = ip.duplicate(); + Overlay overlay2 = images[i].getOverlay(); + if (overlay2!=null) { + for (int j=0; j0) + imp.setOverlay(overlay); + return imp; + } + + private int findMinMaxSize(ImagePlus[] images, int count) { + int index = 0; + stackType = 8; + width = 0; + height = 0; + cal2 = images[0].getCalibration(); + maxWidth = 0; + maxHeight = 0; + minWidth = Integer.MAX_VALUE; + minHeight = Integer.MAX_VALUE; + minSize = Integer.MAX_VALUE; + allInvertedLuts = true; + maxSize = 0; + for (int i=0; istackType) stackType = type; + int w=images[i].getWidth(), h=images[i].getHeight(); + if (w>width) width = w; + if (h>height) height = h; + int size = w*h; + if (sizemaxSize) { + maxSize = size; + maxWidth = w; + maxHeight = h; + } + Calibration cal = images[i].getCalibration(); + if (!images[i].getCalibration().equals(cal2)) + cal2 = null; + images[index++] = images[i]; + } + return index; + } + + final boolean exclude(String title) { + return filter!=null && title!=null && title.indexOf(filter)==-1; + } + +} + diff --git a/src/ij/plugin/JavaProperties.java b/src/ij/plugin/JavaProperties.java new file mode 100644 index 0000000..f627dd0 --- /dev/null +++ b/src/ij/plugin/JavaProperties.java @@ -0,0 +1,190 @@ +package ij.plugin; +import ij.*; +import ij.text.*; +import ij.io.OpenDialog; +import ij.gui.GUI; +import java.awt.*; +import java.util.*; +import java.applet.Applet; + +/** Displays the Java system properties in a text window. */ +public class JavaProperties implements PlugIn { + + ArrayList list = new ArrayList(); + + public void run(String arg) { + show("java.version"); + show("java.vendor"); + if (IJ.isMacintosh()) show("mrj.version"); + show("os.name"); + show("os.version"); + show("os.arch"); + show("file.separator"); + show("path.separator"); + + String s = System.getProperty("line.separator"); + char ch1, ch2; + String str1, str2=""; + ch1 = s.charAt(0); + if (ch1=='\r') + str1 = ""; + else + str1 = ""; + if (s.length()==2) { + ch2 = s.charAt(1); + if (ch2=='\r') + str2 = ""; + else + str2 = ""; + } + list.add(" line.separator: " + str1 + str2); + + Applet applet = IJ.getApplet(); + if (applet!=null) { + list.add(""); + list.add(" code base: "+applet.getCodeBase()); + list.add(" document base: "+applet.getDocumentBase()); + list.add(" sample images dir: "+Prefs.getImagesURL()); + TextWindow tw = new TextWindow("Properties", "", list, 400, 400); + return; + } + list.add(""); + show("user.name"); + show("user.home"); + show("user.dir"); + show("user.country"); + show("file.encoding"); + show("java.home"); + show("java.compiler"); + show("java.class.path"); + show("java.ext.dirs"); + show("java.io.tmpdir"); + + list.add(""); + String userDir = System.getProperty("user.dir"); + String userHome = System.getProperty("user.home"); + String osName = System.getProperty("os.name"); + String path = Prefs.getCustomPropsPath(); + if (path!=null) + list.add(" *Custom properties*: "+path); + path = Prefs.getCustomPrefsPath(); + if (path!=null) + list.add(" *Custom preferences*: "+path); + list.add(" IJ.getVersion: "+IJ.getVersion()); + list.add(" IJ.getFullVersion: "+IJ.getFullVersion()); + list.add(" IJ.javaVersion: "+IJ.javaVersion()); + list.add(" IJ.isJava18(): "+IJ.isJava18()); + list.add(" IJ.isLinux: "+IJ.isLinux()); + list.add(" IJ.isMacintosh: "+IJ.isMacintosh()); + list.add(" IJ.isMacOSX: "+IJ.isMacOSX()); + list.add(" IJ.isWindows: "+IJ.isWindows()); + list.add(" IJ.is64Bit: "+IJ.is64Bit()); + list.add(""); + list.add(" IJ.getDir(\"imagej\"): "+ IJ.getDir("imagej")); + list.add(" IJ.getDir(\"home\"): "+ IJ.getDir("home")); + list.add(" IJ.getDir(\"plugins\"): "+ IJ.getDir("plugins")); + list.add(" IJ.getDir(\"macros\"): "+ IJ.getDir("macros")); + list.add(" IJ.getDir(\"luts\"): "+ IJ.getDir("luts")); + list.add(" IJ.getDir(\"current\"): "+ IJ.getDir("current")); + list.add(" IJ.getDir(\"cwd\"): "+ IJ.getDir("cwd")); + list.add(" IJ.getDir(\"temp\"): "+ IJ.getDir("temp")); + list.add(" IJ.getDir(\"default\"): "+ IJ.getDir("default")); + list.add(" IJ.getDir(\"image\"): "+ IJ.getDir("image")); + list.add(""); + + list.add(" Menus.getPlugInsPath: "+Menus.getPlugInsPath()); + list.add(" Menus.getMacrosPath: "+Menus.getMacrosPath()); + list.add(" Prefs.getImageJDir: "+Prefs.getImageJDir()); + list.add(" Prefs.getThreads: "+Prefs.getThreads()+cores()); + list.add(" Prefs.open100Percent: "+Prefs.open100Percent); + list.add(" Prefs.blackBackground: "+Prefs.blackBackground); + list.add(" Prefs.useJFileChooser: "+Prefs.useJFileChooser); + list.add(" Prefs.weightedColor: "+Prefs.weightedColor); + list.add(" Prefs.blackCanvas: "+Prefs.blackCanvas); + list.add(" Prefs.pointAutoMeasure: "+Prefs.pointAutoMeasure); + list.add(" Prefs.pointAutoNextSlice: "+Prefs.pointAutoNextSlice); + list.add(" Prefs.requireControlKey: "+Prefs.requireControlKey); + list.add(" Prefs.useInvertingLut: "+Prefs.useInvertingLut); + list.add(" Prefs.antialiasedTools: "+Prefs.antialiasedTools); + list.add(" Prefs.useInvertingLut: "+Prefs.useInvertingLut); + list.add(" Prefs.intelByteOrder: "+Prefs.intelByteOrder); + list.add(" Prefs.noPointLabels: "+Prefs.noPointLabels); + list.add(" Prefs.disableUndo: "+Prefs.disableUndo); + list.add(" Prefs dir: "+Prefs.getPrefsDir()); + list.add(" Current dir: "+OpenDialog.getDefaultDirectory()); + list.add(" Sample images dir: "+Prefs.getImagesURL()); + list.add(" Memory in use: "+IJ.freeMemory()); + Rectangle s1 = GUI.getScreenBounds(); // primary screen + Rectangle s2 = GUI.getScreenBounds(IJ.getInstance()); // screen with "ImageJ" window + if (s1.equals(s2)) + list.add(" Screen size: " + s1.width + "x" + s1.height); + else { + list.add(" Size of primary screen: " + s1.width + "x" + s1.height); + list.add(" Size of \"ImageJ\" screen: " + s2.width + "x" + s2.height); + } + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + list.add(" Max window bounds: " + toString(GUI.getMaxWindowBounds(IJ.getInstance()))); + listMonitors(ge, list); + System.gc(); + doFullDump(); + if (IJ.getInstance()==null) { + for (int i=0; i1) { + for (int i=0; i0 && !msg.contains("Macro canceled")) { + if (evaluating) + error = msg; + else + IJ.log(msg); + } + } + } + + public String toString() { + return result!=null?""+result:""; + } + +} diff --git a/src/ij/plugin/JpegWriter.java b/src/ij/plugin/JpegWriter.java new file mode 100644 index 0000000..3478524 --- /dev/null +++ b/src/ij/plugin/JpegWriter.java @@ -0,0 +1,162 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.io.FileSaver; +import ij.io.SaveDialog; +import java.awt.image.*; +import java.awt.*; +import java.io.*; +import java.util.Iterator; +import javax.imageio.*; +import javax.imageio.stream.*; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import javax.imageio.metadata.IIOMetadata; + + +/** The File/Save As/Jpeg command (FileSaver.saveAsJpeg() method) + uses this plugin to save images in JPEG format. */ +public class JpegWriter implements PlugIn { + public static final int DEFAULT_QUALITY = 75; + private static boolean disableChromaSubsampling; + private static boolean chromaSubsamplingSet; + + public void run(String arg) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) return; + imp.startTiming(); + saveAsJpeg(imp,arg,FileSaver.getJpegQuality()); + IJ.showTime(imp, imp.getStartTime(), "JpegWriter: "); + } + + /** Thread-safe method. */ + public static String save(ImagePlus imp, String path, int quality) { + if (imp==null) + imp = IJ.getImage(); + if (path==null || path.length()==0) + path = SaveDialog.getPath(imp, ".jpg"); + if (path==null) + return null; + String error = (new JpegWriter()).saveAsJpeg(imp, path, quality); + return error; + } + + String saveAsJpeg(ImagePlus imp, String path, int quality) { + int width = imp.getWidth(); + int height = imp.getHeight(); + int biType = BufferedImage.TYPE_INT_RGB; + boolean overlay = imp.getOverlay()!=null && !imp.getHideOverlay(); + ImageProcessor ip = imp.getProcessor(); + if (ip.isDefaultLut() && !imp.isComposite() && !overlay && ip.getMinThreshold()==ImageProcessor.NO_THRESHOLD) + biType = BufferedImage.TYPE_BYTE_GRAY; + BufferedImage bi = new BufferedImage(width, height, biType); + String error = null; + try { + Graphics g = bi.createGraphics(); + Image img = imp.getImage(); + if (overlay && !imp.tempOverlay()) + img = imp.flatten().getImage(); + g.drawImage(img, 0, 0, null); + g.dispose(); + Iterator iter = ImageIO.getImageWritersByFormatName("jpeg"); + ImageWriter writer = (ImageWriter)iter.next(); + File f = new File(path); + String originalPath = null; + boolean replacing = f.exists(); + if (replacing) { + originalPath = path; + path += ".temp"; + f = new File(path); + } + ImageOutputStream ios = ImageIO.createImageOutputStream(f); + writer.setOutput(ios); + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(param.MODE_EXPLICIT); + param.setCompressionQuality(quality/100f); + if (quality == 100) + param.setSourceSubsampling(1, 1, 0, 0); + IIOImage iioImage = null; + boolean disableSubsampling = quality>=90; + if (chromaSubsamplingSet) + disableSubsampling = disableChromaSubsampling; + if (!disableSubsampling) // Use chroma subsampling YUV420 + iioImage = new IIOImage(bi, null, null); + else { + // Disable JPEG chroma subsampling + // http://svn.apache.org/repos/asf/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizer.java + // http://svn.apache.org/repos/asf/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtils.java + // Peter Haub, Okt. 2019 + IIOMetadata metadata = writer.getDefaultImageMetadata(new ImageTypeSpecifier(bi.getColorModel(), bi.getSampleModel()), param); + Node rootNode = metadata!=null ? metadata.getAsTree("javax_imageio_jpeg_image_1.0") : null; + boolean metadataUpdated = false; + // The top level root node has two children, out of which the second one will + // contain all the information related to image markers. + if (rootNode!=null && rootNode.getLastChild() != null) { + Node markerNode = rootNode.getLastChild(); + NodeList markers = markerNode.getChildNodes(); + // Search for 'SOF' marker where subsampling information is stored. + for (int i = 0; i < markers.getLength(); i++) { + Node node = markers.item(i); + // 'SOF' marker can have + // 1 child node if the color representation is greyscale, + // 3 child nodes if the color representation is YCbCr, and + // 4 child nodes if the color representation is YCMK. + // This subsampling applies only to YCbCr. + if (node.getNodeName().equalsIgnoreCase("sof") && node.hasChildNodes() && node.getChildNodes().getLength() == 3) { + // In 'SOF' marker, first child corresponds to the luminance channel, and setting + // the HsamplingFactor and VsamplingFactor to 1, will imply 4:4:4 chroma subsampling. + NamedNodeMap attrMap = node.getFirstChild().getAttributes(); + // SamplingModes: UNKNOWN(-2), DEFAULT(-1), YUV444(17), YUV422(33), YUV420(34), YUV411(65) + int samplingmode = 17; // YUV444 + attrMap.getNamedItem("HsamplingFactor").setNodeValue((samplingmode & 0xf) + ""); + attrMap.getNamedItem("VsamplingFactor").setNodeValue(((samplingmode >> 4) & 0xf) + ""); + metadataUpdated = true; + break; + } + } + } + // Read the updated metadata from the metadata node tree. + if (metadataUpdated) + metadata.setFromTree("javax_imageio_jpeg_image_1.0", rootNode); + iioImage = new IIOImage(bi, null, metadata); + } // end of code adaption (Disable JPEG chroma subsampling) + writer.write(null, iioImage, param); + ios.close(); + writer.dispose(); + if (replacing) { + File f2 = new File(originalPath); + boolean ok = f2.delete(); + if (ok) f.renameTo(f2); + } + } catch (Exception e) { + error = ""+e; + if (error.contains("Output has not been set!")) + error = "Incorrect file path: \""+path+"\""; + IJ.error("JPEG Writer", error); + } + return error; + } + + public static void setQuality(int jpegQuality) { + FileSaver.setJpegQuality(jpegQuality); + } + + public static int getQuality() { + return FileSaver.getJpegQuality(); + } + + /** Enhance quality of JPEGs by disabing chroma subsampling. + By default, enhanced quality is automatically used + when the Quality setting is 90 or greater. */ + public static void enhanceQuality(boolean enhance) { + disableChromaSubsampling = enhance; + chromaSubsamplingSet = true; + } + + public static void disableChromaSubsampling(boolean disable) { + enhanceQuality(disable); + } + + +} diff --git a/src/ij/plugin/LUT_Editor.java b/src/ij/plugin/LUT_Editor.java new file mode 100644 index 0000000..e05ca35 --- /dev/null +++ b/src/ij/plugin/LUT_Editor.java @@ -0,0 +1,481 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.plugin.frame.Recorder; +import java.awt.*; +import java.awt.image.*; +import ij.util.*; +import ij.measure.*; +import java.util.Vector; +import java.awt.event.*; + +public class LUT_Editor implements PlugIn, ActionListener{ + private ImagePlus imp; + Button openButton, saveButton, resizeButton, invertButton; + ColorPanel colorPanel; + int bitDepth; + + public void run(String args) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) { + IJ.showMessage("LUT Editor", "No images are open"); + return; + } + bitDepth = imp.getBitDepth(); + if (bitDepth==24) { + IJ.showMessage("LUT Editor", "RGB images do not use LUTs"); + return; + } + if (bitDepth!=8) { + imp.getProcessor().resetMinAndMax(); + imp.updateAndDraw(); + } + + colorPanel = new ColorPanel(imp); + if (colorPanel.getMapSize()!=256) { + IJ.showMessage("LUT Editor", "LUT must have 256 entries"); + return; + } + boolean recording = Recorder.record; + Recorder.record = false; + int red=0, green=0, blue=0; + GenericDialog gd = new GenericDialog("LUT Editor"); + Panel buttonPanel = new Panel(new GridLayout(4, 1, 0, 5)); + openButton = new Button("Open..."); + openButton.addActionListener(this); + buttonPanel.add(openButton); + saveButton = new Button("Save..."); + saveButton.addActionListener(this); + buttonPanel.add(saveButton); + resizeButton = new Button("Set..."); + resizeButton.addActionListener(this); + buttonPanel.add(resizeButton); + invertButton = new Button("Invert..."); + invertButton.addActionListener(this); + buttonPanel.add(invertButton); + Panel panel = new Panel(); + panel.add(colorPanel); + panel.add(buttonPanel); + gd.addPanel(panel, GridBagConstraints.CENTER, new Insets(10, 0, 0, 0)); + gd.showDialog(); + Recorder.record = recording; + if (gd.wasCanceled()){ + colorPanel.cancelLUT(); + return; + } else { + colorPanel.applyLUT(); + String lutName = imp.getProp(LUT.nameKey); + if (lutName!=null && !lutName.endsWith(" (edited)")) + imp.setProp(LUT.nameKey, lutName+" (edited)"); + } + } + + void save() { + try {IJ.run("LUT...");} // File>Save As>Lut... + catch(RuntimeException e) {} + } + + public void actionPerformed(ActionEvent e) { + Object source = e.getSource(); + if (source==openButton) + colorPanel.open(); + else if (source==saveButton) + save(); + else if (source==resizeButton) + colorPanel.resize(); + else if (source==invertButton) + colorPanel.invert(); + } +} + + +class ColorPanel extends Panel implements MouseListener, MouseMotionListener{ + static final int entryWidth=12, entryHeight=12; + int rows = 16; + int columns = 16; + Color c[] = new Color[256]; + Color b; + ColorProcessor cp; + IndexColorModel origin; + private ImagePlus imp; + private int[] xSize = new int[256], redY, greenY, blueY; + private int mapSize, x, y, initialC = -1, finalC = -1; + private byte[] reds, greens, blues; + private boolean updateLut; + private static String[] choices = {"Replication","Interpolation", "Spline Fitting"}; + private static String scaleMethod = choices[1]; + private int bitDepth; + + ColorPanel(ImagePlus imp) { + setup(imp); + } + + public void setup(ImagePlus imp) { + if (imp==null) { + IJ.noImage(); + return; + } + this.imp = imp; + bitDepth = imp.getBitDepth(); + ImageProcessor ip = imp.getChannelProcessor(); + IndexColorModel cm = (IndexColorModel)ip.getColorModel(); + origin = cm; + mapSize = cm.getMapSize(); + reds = new byte[256]; + greens = new byte[256]; + blues = new byte[256]; + cm.getReds(reds); + cm.getGreens(greens); + cm.getBlues(blues); + addMouseListener(this); + addMouseMotionListener(this); + for(int index = 0; index < mapSize; index++) + c[index] = new Color(reds[index]&255, greens[index]&255, blues[index]&255); + } + + public Dimension getPreferredSize() { + return new Dimension(columns*entryWidth, rows*entryHeight); + } + + public Dimension getMinimumSize() { + return new Dimension(columns*entryWidth, rows*entryHeight); + } + + int getMouseZone(int x, int y){ + int horizontal = (int)x/entryWidth; + int vertical = (int)y/entryHeight; + int index = (columns*vertical + horizontal); + return index; + } + + public void colorRamp() { + if (initialC>finalC) { + int tmp = initialC; + initialC = finalC; + finalC = tmp; + } + float difference = finalC - initialC+1; + int start = (byte)c[initialC].getRed()&255; + int end = (byte)c[finalC].getRed()&255; + float rstep = (end-start)/difference; + for(int index = initialC; index <= finalC; index++) + reds[index] = (byte)(start+ (index-initialC)*rstep); + + start = (byte)c[initialC].getGreen()&255; + end = (byte)c[finalC].getGreen()&255; + float gstep = (end-start)/difference; + for(int index = initialC; index <= finalC; index++) + greens[index] = (byte)(start + (index-initialC)*gstep); + + start = (byte)c[initialC].getBlue()&255; + end = (byte)c[finalC].getBlue()&255; + float bstep = (end-start)/difference; + for(int index = initialC; index <= finalC; index++) + blues[index] = (byte)(start + (index-initialC)*bstep); + for (int index = initialC; index <= finalC; index++) + c[index] = new Color(reds[index]&255, greens[index]&255, blues[index]&255); + repaint(); + } + + public void mousePressed(MouseEvent e){ + x = (e.getX()); + y = (e.getY()); + initialC = getMouseZone(x,y); + } + + public void mouseReleased(MouseEvent e){ + x = (e.getX()); + y = (e.getY()); + finalC = getMouseZone(x,y); + if(initialC>=mapSize&&finalC>=mapSize) { + initialC = finalC = -1; + return; + } + if(initialC>=mapSize) + initialC = mapSize-1; + if(finalC>=mapSize) + finalC = mapSize-1; + if(finalC<0) + finalC = 0; + if (initialC == finalC) { + b = c[finalC]; + ColorChooser cc = new ColorChooser("Color at Entry " + (finalC) , c[finalC] , false); + c[finalC] = cc.getColor(); + if (c[finalC]==null){ + c[finalC] = b; + } + colorRamp(); + } else { + b = c[initialC]; + ColorChooser icc = new ColorChooser("Initial Entry (" + (initialC)+")" , c[initialC] , false); + c[initialC] = icc.getColor(); + if (c[initialC]==null){ + c[initialC] = b; + initialC = finalC = -1; + return; + } + b = c[finalC]; + ColorChooser fcc = new ColorChooser("Final Entry (" + (finalC)+")" , c[finalC] , false); + c[finalC] = fcc.getColor(); + if (c[finalC]==null){ + c[finalC] = b; + initialC = finalC = -1; + return; + } + colorRamp(); + } + initialC = finalC = -1; + applyLUT(); + } + + public void mouseClicked(MouseEvent e){} + public void mouseEntered(MouseEvent e){} + public void mouseExited(MouseEvent e){} + + public void mouseDragged(MouseEvent e){ + x = (e.getX()); + y = (e.getY()); + finalC = getMouseZone(x,y); + IJ.showStatus("index=" + getIndex(finalC)); + repaint(); + } + + public void mouseMoved(MouseEvent e) { + x = (e.getX()); + y = (e.getY()); + int entry = getMouseZone(x,y); + if (entryImport>Lut... + catch(RuntimeException e) {} + updateLut = true; + repaint(); + } + + void updateLut() { + IndexColorModel cm = (IndexColorModel)imp.getChannelProcessor().getColorModel(); + if (mapSize == 0) + return; + cm.getReds(reds); + cm.getGreens(greens); + cm.getBlues(blues); + for(int i=0; i256) newSize =256; + scaleMethod = sgd.getNextChoice(); + scale(reds, greens, blues, newSize); + mapSize = newSize; + for(int i=0; i255.0) v=255.0; reds[i] = (byte)v; + v = Math.round(sfGreens.evalSpline(xValues, greens2, mapSize, i)); + if (v<0.0) v=0.0; if (v>255.0) v=255.0; greens[i] = (byte)v; + v = Math.round(sfBlues.evalSpline(xValues, blues2, mapSize, i)); + if (v<0.0) v=0.0; if (v>255.0) v=255.0; blues[i] = (byte)v; + } + } + + public void cancelLUT() { + if (mapSize == 0) + return; + origin.getReds(reds); + origin.getGreens(greens); + origin.getBlues(blues); + mapSize = 256; + applyLUT(); + } + + public void applyLUT() { + byte[] reds2=reds, greens2=greens, blues2=blues; + if (mapSize<256) { + reds2 = new byte[256]; + greens2 = new byte[256]; + blues2 = new byte[256]; + for(int i = 0; i < mapSize; i++) { + reds2[i] = reds[i]; + greens2[i] = greens[i]; + blues2[i] = blues[i]; + } + scale(reds2, greens2, blues2, 256); + } + IndexColorModel cm = new IndexColorModel(8, 256, reds2, greens2, blues2); + ImageProcessor ip = imp.getChannelProcessor(); + ip.setColorModel(cm); + if (imp.isComposite()) + ((CompositeImage)imp).setChannelColorModel(cm); + if (imp.getStackSize()>1 && !imp.isComposite()) + imp.getStack().setColorModel(cm); + imp.updateAndDraw(); + } + + public void update(Graphics g) { + paint(g); + } + + public void paint(Graphics g) { + if (updateLut) { + updateLut(); + updateLut = false; + } + int index = 0; + for (int y=0; y=mapSize) { + g.setColor(Color.lightGray); + g.fillRect(x*entryWidth, y*entryHeight, entryWidth, entryHeight); + } else if (((index <= finalC) && (index >= initialC)) || ((index >= finalC) && (index <= initialC))){ + g.setColor(c[index].brighter()); + g.fillRect(x*entryWidth, y*entryHeight, entryWidth, entryHeight); + g.setColor(Color.white); + g.drawRect((x*entryWidth), (y*entryHeight), entryWidth, entryHeight); + g.setColor(Color.black); + g.drawLine((x*entryWidth)+entryWidth-1, (y*entryHeight), (x*entryWidth)+entryWidth-1, (y*entryWidth)+entryHeight); + g.drawLine((x*entryWidth), (y*entryHeight)+entryHeight-1, (x*entryWidth)+entryWidth-1, (y*entryHeight)+entryHeight-1); + g.setColor(Color.white); + } else { + g.setColor(c[index]); + g.fillRect(x*entryWidth, y*entryHeight, entryWidth, entryHeight); + g.setColor(Color.white); + g.drawRect((x*entryWidth), (y*entryHeight), entryWidth-1, entryHeight-1); + g.setColor(Color.black); + g.drawLine((x*entryWidth), (y*entryHeight), (x*entryWidth)+entryWidth-1, (y*entryWidth)); + g.drawLine((x*entryWidth), (y*entryHeight), (x*entryWidth), (y*entryHeight)+entryHeight-1); + } + index++; + } + } + } + + int getMapSize() { + return mapSize; + } + +} diff --git a/src/ij/plugin/ListVirtualStack.java b/src/ij/plugin/ListVirtualStack.java new file mode 100644 index 0000000..bc0b93a --- /dev/null +++ b/src/ij/plugin/ListVirtualStack.java @@ -0,0 +1,201 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.io.*; +import ij.util.Tools; +import java.awt.*; +import java.io.*; +import java.util.*; + +/** This plugin opens images specified by list of file paths as a virtual stack. + It implements the File/Import/Stack From List command. */ +public class ListVirtualStack extends VirtualStack implements PlugIn { + private static boolean virtual; + private String[] list; + private String[] labels; + private int nImages; + private int imageWidth, imageHeight; + + public void run(String arg) { + OpenDialog od = new OpenDialog("Open Image List", arg); + String name = od.getFileName(); + if (name==null) return; + String dir = od.getDirectory(); + //IJ.log("ListVirtualStack: "+dir+" "+name); + list = open(dir+name); + if (list==null) return; + nImages = list.length; + labels = new String[nImages]; + //for (int i=0; inImages) + throw new IllegalArgumentException("Argument out of range: "+n); + if (nImages<1) return; + for (int i=n; inImages) + throw new IllegalArgumentException("Argument out of range: "+n); + IJ.redirectErrorMessages(true); + String url = list[n-1]; + ImagePlus imp = null; + if (url.length()>0) + imp = IJ.openImage(url); + if (imp!=null) { + labels[n-1] = (new File(list[n-1])).getName()+"\n"+(String)imp.getProperty("Info"); + ImageProcessor ip = imp.getProcessor(); + int bitDepth = getBitDepth(); + if (imp.getBitDepth()!=bitDepth) { + switch (bitDepth) { + case 8: ip=ip.convertToByte(true); break; + case 16: ip=ip.convertToShort(true); break; + case 24: ip=ip.convertToRGB(); break; + case 32: ip=ip.convertToFloat(); break; + } + } + if (ip.getWidth()!=imageWidth || ip.getHeight()!=imageHeight) + ip = ip.resize(imageWidth, imageHeight); + IJ.redirectErrorMessages(false); + return ip; + } else { + ImageProcessor ip = null; + switch (getBitDepth()) { + case 8: ip=new ByteProcessor(imageWidth,imageHeight); break; + case 16: ip=new ShortProcessor(imageWidth,imageHeight); break; + case 24: ip=new ColorProcessor(imageWidth,imageHeight); break; + case 32: ip=new FloatProcessor(imageWidth,imageHeight); break; + } + IJ.redirectErrorMessages(false); + return ip; + } + } + + /** Returns the number of images in this stack. */ + public int getSize() { + return nImages; + } + + /** Returns the name of the specified image. */ + public String getSliceLabel(int n) { + if (n<1 || n>nImages) + throw new IllegalArgumentException("Argument out of range: "+n); + if (labels[n-1]!=null) + return labels[n-1]; + else + return (new File(list[n-1])).getName(); + } + + public int getWidth() { + return imageWidth; + } + + public int getHeight() { + return imageHeight; + } + + +} diff --git a/src/ij/plugin/LutLoader.java b/src/ij/plugin/LutLoader.java new file mode 100644 index 0000000..77568f4 --- /dev/null +++ b/src/ij/plugin/LutLoader.java @@ -0,0 +1,446 @@ +package ij.plugin; +import ij.*; +import ij.io.*; +import ij.process.*; +import ij.gui.ImageWindow; +import java.awt.*; +import java.io.*; +import java.awt.image.*; +import java.net.*; + +/** Opens NIH Image look-up tables (LUTs), 768 byte binary LUTs + (256 reds, 256 greens and 256 blues), LUTs in text format, + or generates the LUT specified by the string argument + passed to the run() method. */ +public class LutLoader extends ImagePlus implements PlugIn { + + private static String defaultDirectory = null; + private boolean suppressErrors; + + /** Returns the LUT 'name' as an IndexColorModel, where + * 'name' can be any entry in the Image/Lookup Tables menu. + * @see ij.IJ#getLuts + * See: Help>Examples>JavaScript/Show all LUTs + */ + public static IndexColorModel getLut(String name) { + if (name==null) return null; + LutLoader ll = new LutLoader(); + FileInfo fi = ll.getBuiltInLut(name.toLowerCase()); + if (fi.fileName!=null) + return new IndexColorModel(8, 256, fi.reds, fi.greens, fi.blues); + String path = IJ.getDir("luts")+name+".lut"; + IndexColorModel lut = LutLoader.openLut("noerror:"+path); + if (lut==null) { + path = IJ.getDir("luts")+name.replaceAll(" ","_")+".lut"; + lut = LutLoader.openLut("noerror:"+path); + } + return lut; + } + + /** If 'arg'="", displays a file open dialog and opens the specified + LUT. If 'arg' is a path, opens the LUT specified by the path. If + 'arg'="fire", "ice", etc., uses a method to generate the LUT. */ + public void run(String arg) { + if (arg.equals("invert")) { + invertLut(); + return; + } + + // Built in LUT + FileInfo fi = getBuiltInLut(arg); + if (fi.fileName!=null) { + showLut(fi, true); + Menus.updateMenus(); + return; + } + + // LUT in luts folder + OpenDialog od = new OpenDialog("Open LUT...", arg); + fi.directory = od.getDirectory(); + fi.fileName = od.getFileName(); + if (fi.fileName==null) + return; + if (openLut(fi)) + showLut(fi, arg.equals("")); + IJ.showStatus(""); + } + + private FileInfo getBuiltInLut(String name) { + FileInfo fi = new FileInfo(); + fi.reds = new byte[256]; + fi.greens = new byte[256]; + fi.blues = new byte[256]; + fi.lutSize = 256; + fi.fileName = null; + if (name==null) + return fi; + if (name.equals("3-3-2 rgb")) name="3-3-2 RGB"; + if (name.equals("red/green")) name="redgreen"; + int nColors = 0; + if (name.equals("fire")) + nColors = fire(fi.reds, fi.greens, fi.blues); + else if (name.equals("grays")) + nColors = grays(fi.reds, fi.greens, fi.blues); + else if (name.equals("ice")) + nColors = ice(fi.reds, fi.greens, fi.blues); + else if (name.equals("spectrum")) + nColors = spectrum(fi.reds, fi.greens, fi.blues); + else if (name.equals("3-3-2 RGB")) + nColors = rgb332(fi.reds, fi.greens, fi.blues); + else if (name.equals("red")) + nColors = primaryColor(4, fi.reds, fi.greens, fi.blues); + else if (name.equals("green")) + nColors = primaryColor(2, fi.reds, fi.greens, fi.blues); + else if (name.equals("blue")) + nColors = primaryColor(1, fi.reds, fi.greens, fi.blues); + else if (name.equals("cyan")) + nColors = primaryColor(3, fi.reds, fi.greens, fi.blues); + else if (name.equals("magenta")) + nColors = primaryColor(5, fi.reds, fi.greens, fi.blues); + else if (name.equals("yellow")) + nColors = primaryColor(6, fi.reds, fi.greens, fi.blues); + else if (name.equals("redgreen")) + nColors = redGreen(fi.reds, fi.greens, fi.blues); + if (nColors>0) { + if (nColors<256) + interpolate(fi.reds, fi.greens, fi.blues, nColors); + fi.fileName = name; + } + return fi; + } + + private void showLut(FileInfo fi, boolean showImage) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + if (imp.getProcessor().getNChannels()!=1) + IJ.error("LUTs cannot be assiged to RGB Images."); + else if (imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.GRAYSCALE) { + CompositeImage cimp = (CompositeImage)imp; + cimp.setMode(IJ.COLOR); + int saveC = cimp.getChannel(); + IndexColorModel cm = new IndexColorModel(8, 256, fi.reds, fi.greens, fi.blues); + for (int c=1; c<=cimp.getNChannels(); c++) { + cimp.setC(c); + cimp.setChannelColorModel(cm); + } + imp.setC(saveC); + imp.updateAndRepaintWindow(); + } else { + ImageProcessor ip = imp.getChannelProcessor(); + IndexColorModel cm = new IndexColorModel(8, 256, fi.reds, fi.greens, fi.blues); + if (imp.isComposite()) + ((CompositeImage)imp).setChannelColorModel(cm); + else { + ip.setColorModel(cm); + //ip.setMinAndMax(ip.getMin(), ip.getMax()); + if (imp.getWindow()==null) + imp.setProcessor(ip); + } + if (imp.getStackSize()>1) + imp.getStack().setColorModel(cm); + imp.updateAndRepaintWindow(); + if (IJ.isMacro() && imp.getWindow()!=null) + IJ.wait(25); + } + saveLUTName(imp, fi); + } else + createImage(fi, showImage); + } + + private void saveLUTName(ImagePlus imp, FileInfo fi) { + if (imp!=null && fi!=null && fi.fileName!=null) { + String name = fi.fileName; + if (name.endsWith(".lut")) + name = name.substring(0,name.length()-4); + if (name.equals("grays")) + imp.setProp(LUT.nameKey, null); + else + imp.setProp(LUT.nameKey, name); + } + } + + void invertLut() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) + {IJ.noImage(); return;} + if (imp.getProcessor().getNChannels()==3) { + IJ.error("RGB images do not use LUTs"); + return; + } + if (imp.isComposite()) { + CompositeImage ci = (CompositeImage)imp; + LUT lut = ci.getChannelLut(); + if (lut!=null) + ci.setChannelLut(lut.createInvertedLut()); + } else { + ImageProcessor ip = imp.getProcessor(); + ip.invertLut(); + if (imp.getStackSize()>1) + imp.getStack().setColorModel(ip.getColorModel()); + } + imp.updateAndRepaintWindow(); + } + + int fire(byte[] reds, byte[] greens, byte[] blues) { + int[] r = {0,0,1,25,49,73,98,122,146,162,173,184,195,207,217,229,240,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255}; + int[] g = {0,0,0,0,0,0,0,0,0,0,0,0,0,14,35,57,79,101,117,133,147,161,175,190,205,219,234,248,255,255,255,255}; + int[] b = {0,61,96,130,165,192,220,227,210,181,151,122,93,64,35,5,0,0,0,0,0,0,0,0,0,0,0,35,98,160,223,255}; + for (int i=0; i10000) { + error(path); + return false; + } + } + int size = 0; + try { + if (length>768) + size = openBinaryLut(fi, isURL, false); // attempt to read NIH Image LUT + if (size==0 && (length==0||length==768||length==970)) + size = openBinaryLut(fi, isURL, true); // otherwise read raw LUT + if (size==0 && length>768) + size = openTextLut(fi); + if (size==0) + error(path); + } catch (IOException e) { + if (!suppressErrors) + IJ.error("LUT Loader", ""+e); + } + return size==256; + } + + private void error(String path) { + IJ.error("LUT Reader", "This is not an ImageJ or NIH Image LUT, a 768 byte \nraw LUT, or a LUT in text format.\n \n"+path); + } + + /** Opens an NIH Image LUT or a 768 byte binary LUT. */ + int openBinaryLut(FileInfo fi, boolean isURL, boolean raw) throws IOException { + InputStream is; + if (isURL) + is = new URL(fi.url+fi.fileName).openStream(); + else + is = new FileInputStream(fi.getFilePath()); + DataInputStream f = new DataInputStream(is); + int nColors = 256; + if (!raw) { + // attempt to read 32 byte NIH Image LUT header + int id = f.readInt(); + if (id!=1229147980) { // 'ICOL' + f.close(); + return 0; + } + int version = f.readShort(); + nColors = f.readShort(); + int start = f.readShort(); + int end = f.readShort(); + long fill1 = f.readLong(); + long fill2 = f.readLong(); + int filler = f.readInt(); + } + f.read(fi.reds, 0, nColors); + f.read(fi.greens, 0, nColors); + f.read(fi.blues, 0, nColors); + if (nColors<256) + interpolate(fi.reds, fi.greens, fi.blues, nColors); + f.close(); + return 256; + } + + int openTextLut(FileInfo fi) throws IOException { + TextReader tr = new TextReader(); + tr.hideErrorMessages(); + ImageProcessor ip = tr.open(fi.getFilePath()); + if (ip==null) + return 0; + int width = ip.getWidth(); + int height = ip.getHeight(); + if (width<3||width>4||height<256||height>258) + return 0; + int x = width==4?1:0; + int y = height>256?1:0; + ip.setRoi(x, y, 3, 256); + ip = ip.crop(); + for (int i=0; i<256; i++) { + fi.reds[i] = (byte)ip.getPixelValue(0,i); + fi.greens[i] = (byte)ip.getPixelValue(1,i); + fi.blues[i] = (byte)ip.getPixelValue(2,i); + } + return 256; + } + + private void createImage(FileInfo fi, boolean show) { + IndexColorModel cm = new IndexColorModel(8, 256, fi.reds, fi.greens, fi.blues); + ByteProcessor bp = createImage(cm); + setProcessor(fi.fileName, bp); + saveLUTName(this, fi); + if (show) show(); + } + + /** Opens the specified ImageJ LUT and returns + it as an IndexColorModel. Since 1.43t. */ + public static IndexColorModel open(String path) throws IOException { + return open(new FileInputStream(path)); + } + + /** Opens an ImageJ LUT using an InputStream + and returns it as an IndexColorModel. Since 1.43t. */ + public static IndexColorModel open(InputStream stream) throws IOException { + DataInputStream f = new DataInputStream(stream); + byte[] reds = new byte[256]; + byte[] greens = new byte[256]; + byte[] blues = new byte[256]; + f.read(reds, 0, 256); + f.read(greens, 0, 256); + f.read(blues, 0, 256); + f.close(); + return new IndexColorModel(8, 256, reds, greens, blues); + } + + /** Creates a 256x32 image from an IndexColorModel. Since 1.43t. */ + public static ByteProcessor createImage(IndexColorModel cm) { + int width = 256; + int height = 32; + byte[] pixels = new byte[width*height]; + ByteProcessor bp = new ByteProcessor(width, height, pixels, cm); + int[] ramp = new int[width]; + for (int i=0; iMacros submenu + + private int[] macroStarts; + private String[] macroNames; + private MenuBar mb = new MenuBar(); + private int nMacros; + private Program pgm; + private boolean firstEvent = true; + private String shortcutsInUse; + private int inUseCount; + private int nShortcuts; + private int toolCount; + private String text; + private String anonymousName; + private Menu macrosMenu; + private int autoRunCount, autoRunAndHideCount; + private boolean openingStartupMacrosInEditor; + private boolean installTools = true; + + private static String defaultDir, fileName; + private static MacroInstaller instance, listener; + private Thread macroToolThread; + private ArrayList
subMenus = new ArrayList(); + + private static Program autoRunPgm; + private static int autoRunAddress; + private static String autoRunName; + private boolean autoRunOnCurrentThread; + + public void run(String path) { + if (path==null || path.equals("")) + path = showDialog(); + if (path==null) return; + openingStartupMacrosInEditor = path.indexOf("StartupMacros")!=-1; + String text = open(path); + if (text!=null) { + String functions = Interpreter.getAdditionalFunctions(); + if (functions!=null) { + if (!(text.endsWith("\n") || functions.startsWith("\n"))) + text = text + "\n" + functions; + else + text = text + functions; + } + install(text); + } + } + + void install() { + subMenus.clear(); + if (text!=null) { + Tokenizer tok = new Tokenizer(); + pgm = tok.tokenize(text); + } + if (macrosMenu!=null) + IJ.showStatus(""); + int[] code = pgm.getCode(); + Symbol[] symbolTable = pgm.getSymbolTable(); + int count=0, token, nextToken, address; + String name; + Symbol symbol; + shortcutsInUse = null; + inUseCount = 0; + nShortcuts = 0; + toolCount = 0; + macroStarts = new int[MAX_MACROS]; + macroNames = new String[MAX_MACROS]; + boolean isPluginsMacrosMenu = false; + if (macrosMenu!=null) { + int itemCount = macrosMenu.getItemCount(); + isPluginsMacrosMenu = macrosMenu==Menus.getMacrosMenu(); + int baseCount =isPluginsMacrosMenu?MACROS_MENU_COMMANDS:Editor.MACROS_MENU_ITEMS; + if (itemCount>baseCount) { + for (int i=itemCount-1; i>=baseCount; i--) + macrosMenu.remove(i); + } + } + if (pgm.hasVars() && pgm.macroCount()>0 && pgm.getGlobals()==null) + new Interpreter().saveGlobals(pgm); + ArrayList tools = new ArrayList(); + for (int i=0; i>TOK_SHIFT; + symbol = symbolTable[address]; + name = symbol.str; + macroStarts[count] = i + 2; + macroNames[count] = name; + if (name.indexOf('-')!=-1 && (name.indexOf("Tool")!=-1||name.indexOf("tool")!=-1)) { + tools.add(name); + toolCount++; + } else if (name.startsWith("AutoRun")) { + if (autoRunCount==0 && !openingStartupMacrosInEditor && !IJ.isMacro()) { + if (autoRunOnCurrentThread) { //autoRun() method will run later + autoRunPgm = pgm; + autoRunAddress = macroStarts[count]; + autoRunName = name; + } else + new MacroRunner(pgm, macroStarts[count], name, (String)null); // run on separate thread + if (name.equals("AutoRunAndHide")) + autoRunAndHideCount++; + } + autoRunCount++; + count--; + } else if (name.equals("Popup Menu")) + installPopupMenu(name, pgm); + else if (!name.endsWith("Tool Selected")) { + if (macrosMenu!=null) { + addShortcut(name); + int pos = name.indexOf(">"); + boolean inSubMenu = name.startsWith("<") && (pos>1); + if (inSubMenu) { + Menu parent = macrosMenu; + Menu subMenu = null; + String parentStr = name.substring(1, pos).trim(); + String childStr = name.substring(pos + 1).trim(); + MenuItem mnuItem = new MenuItem(); + mnuItem.setActionCommand(name); + mnuItem.setLabel(childStr); + for (int jj = 0; jj < subMenus.size(); jj++) { + String aName = subMenus.get(jj).getName(); + if (aName.equals(parentStr)) + subMenu = subMenus.get(jj); + } + if (subMenu==null) { + subMenu = new Menu(parentStr); + subMenu.setName(parentStr); + subMenu.addActionListener(this); + subMenus.add(subMenu); + parent.add(subMenu); + } + subMenu.add(mnuItem); + } else + macrosMenu.add(new MenuItem(name)); + } + } + count++; + } + } else if (token==EOF) + break; + } + nMacros = count; + if (toolCount>0 && installTools) { + Toolbar tb = Toolbar.getInstance(); + if (toolCount==1) + tb.addMacroTool((String)tools.get(0), this); + else { + for (int i=0; i6) + toolName = "Unused "+toolName; + tb.addMacroTool(toolName, this, i); + } + } + if (toolCount>1 && Toolbar.getToolId()>=Toolbar.CUSTOM1) + tb.setTool(Toolbar.RECTANGLE); + tb.repaint(); + installTools = false; + } + if (macrosMenu!=null) + this.instance = this; + if (shortcutsInUse!=null && text!=null) + IJ.showMessage("Install Macros", (inUseCount==1?"This keyboard shortcut is":"These keyboard shortcuts are") + + " already in use:"+shortcutsInUse); + if (nMacros==0 && fileName!=null) { + if (text==null||text.length()==0) + return; + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex>0) + anonymousName = fileName.substring(0, dotIndex); + else + anonymousName =fileName; + if (macrosMenu!=null) + macrosMenu.add(new MenuItem(anonymousName)); + macroNames[0] = anonymousName; + nMacros = 1; + } + String word = nMacros==1?" macro":" macros"; + if (isPluginsMacrosMenu) + IJ.showStatus(nMacros + word + " installed"); + } + + public int install(String text) { + if (text==null && pgm==null) + return 0; + this.text = text; + macrosMenu = Menus.getMacrosMenu(); + if (listener!=null) + macrosMenu.removeActionListener(listener); + macrosMenu.addActionListener(this); + listener = this; + install(); + return nShortcuts; + } + + public int install(String text, Menu menu) { + this.text = text; + macrosMenu = menu; + install(); + return nShortcuts; + } + + public void installFile(String path) { + String text = open(path); + if (text==null) return; + boolean isStartupMacros = path.contains("StartupMacros"); + if (isStartupMacros && !Toolbar.installStartupMacrosTools()) + installTools = false; + install(text); + installTools = true; + if (isStartupMacros) { + Toolbar tb = Toolbar.getInstance(); + if (tb!=null) + tb.installStartupTools(); + } + } + + public void installTool(String path) { + String text = open(path); + if (text!=null) + installSingleTool(text); + } + + public void installLibrary(String path) { + String text = open(path); + if (text!=null) + Interpreter.setAdditionalFunctions(text); + } + + /** Installs a macro set contained in ij.jar. */ + public static void installFromJar(String path) { + try { + (new MacroInstaller()).installFromIJJar(path); + } catch (Exception e) {} + } + + /** Installs a macro set contained in ij.jar. */ + public void installFromIJJar(String path) { + boolean installMacros = false; + if (path.endsWith("MenuTool.txt+")) { + path = path.substring(0,path.length()-1); + installMacros = true; + } + String text = openFromIJJar(path); + if (text==null) return; + if (path.endsWith("StartupMacros.txt")) { + if (Toolbar.installStartupMacrosTools()) + install(text); + Toolbar tb = Toolbar.getInstance(); + if (tb!=null) + tb.installStartupTools(); + } else if (path.contains("Tools") || installMacros) { + install(text); + } else + installSingleTool(text); + } + + public void installSingleTool(String text) { + this.text = text; + macrosMenu = null; + install(); + } + + void installPopupMenu(String name, Program pgm) { + Hashtable h = pgm.getMenus(); + if (h==null) return; + String[] commands = (String[])h.get(name); + if (commands==null) return; + PopupMenu popup = Menus.getPopupMenu(); + if (popup==null) return; + popup.removeAll(); + for (int i=0; i1) + shortcut = shortcut.toUpperCase(Locale.US);; + if (len>3 || (len>1 && shortcut.charAt(0)!='F' && shortcut.charAt(0)!='N' && shortcut.charAt(0)!='&')) + return; + boolean bothNumKeys = shortcut.startsWith("&"); + if (bothNumKeys){ //first handle num key of keyboard + shortcut = shortcut.replace("&", ""); + len = shortcut.length(); + } + int code = Menus.convertShortcutToCode(shortcut); + if (code==0) + return; + if (nShortcuts==0) + removeShortcuts(); + // One character shortcuts go in a separate hash table to + // avoid conflicts with ImageJ menu shortcuts. + if (len==1 || shortcut.equals("N+") || shortcut.equals("N-") ) { + Hashtable macroShortcuts = Menus.getMacroShortcuts(); + macroShortcuts.put(new Integer(code), commandPrefix+name); + nShortcuts++; + if(!bothNumKeys) + return; + } + if(bothNumKeys){//now handle numerical keypad + shortcut = "N" + shortcut; + code = Menus.convertShortcutToCode(shortcut); + } + Hashtable shortcuts = Menus.getShortcuts(); + if (shortcuts.get(new Integer(code))!=null) { + if (shortcutsInUse==null) + shortcutsInUse = "\n \n"; + shortcutsInUse += " " + name + "\n"; + inUseCount++; + return; + } + shortcuts.put(new Integer(code), commandPrefix+name); + nShortcuts++; + } + + String showDialog() { + if (defaultDir==null) defaultDir = Menus.getMacrosPath(); + OpenDialog od = new OpenDialog("Install Macros", defaultDir, fileName); + String name = od.getFileName(); + if (name==null) return null; + String dir = od.getDirectory(); + if (!(name.endsWith(".txt")||name.endsWith(".ijm"))) { + IJ.showMessage("Macro Installer", "File name must end with \".txt\" or \".ijm\" ."); + return null; + } + fileName = name; + defaultDir = dir; + return dir+name; + } + + String open(String path) { + if (path==null) return null; + try { + StringBuffer sb = new StringBuffer(5000); + BufferedReader r = new BufferedReader(new FileReader(path)); + while (true) { + String s=r.readLine(); + if (s==null) + break; + else + sb.append(s+"\n"); + } + r.close(); + return new String(sb); + } + catch (Exception e) { + IJ.error(e.getMessage()); + return null; + } + } + + /** Returns a text file contained in ij.jar. */ + public String openFromIJJar(String path) { + String text = null; + try { + InputStream is = this.getClass().getResourceAsStream(path); + if (is==null) return null; + InputStreamReader isr = new InputStreamReader(is); + StringBuffer sb = new StringBuffer(); + char [] b = new char [8192]; + int n; + while ((n = isr.read(b)) > 0) + sb.append(b,0, n); + text = sb.toString(); + } + catch (IOException e) {} + return text; + } + + public boolean runMacroTool(String name) { + for (int i=0; i0; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + openingStartupMacrosInEditor = fileName.startsWith("StartupMacros"); + } + + public static String getFileName() { + return fileName; + } + + public void actionPerformed(ActionEvent evt) { + String cmd = evt.getActionCommand(); + ImageJ.setCommandName(cmd); + MenuItem item = (MenuItem)evt.getSource(); + MenuContainer parent = item.getParent(); + if (parent instanceof PopupMenu) { + for (int i=0; iname + is an empty string. The macro or script is assumed to be in the ImageJ + plugins folder if name is not a full path. */ + public void run(String name) { + if (IJ.debugMode) + IJ.log("Macro_Runner.run(): "+name); + Thread thread = Thread.currentThread(); + String threadName = thread.getName(); + if (!threadName.endsWith("Macro$")) { + if (name.endsWith(".js")||name.endsWith(".bsh")||name.endsWith(".py")) + thread.setName(threadName+"_Script_Macro$"); + else + thread.setName(threadName+"_Macro$"); + } + String path = null; + if (name.equals("")) { + OpenDialog od = new OpenDialog("Run Macro or Script...", path); + String directory = od.getDirectory(); + name = od.getFileName(); + if (name!=null) { + path = directory+name; + runMacroFile(path, null); + if (Recorder.record) { + path = Recorder.fixPath(path); + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.runMacroFile(\""+path+"\");"); + else + Recorder.record("runMacro", path); + } + } + } else if (name.startsWith("JAR:")) + runMacroFromJar(name.substring(4), null); + else if (name.startsWith("ij.jar:")) + runMacroFromIJJar(name, null); + else if (name.endsWith("Tool.ijm") || name.endsWith("Tool.txt") + || name.endsWith("Menu.ijm") || name.endsWith("Menu.txt")) + (new MacroInstaller()).installTool(Menus.getPlugInsPath()+name); + else { + boolean fullPath = name.startsWith("/") || name.startsWith("\\") || name.indexOf(":\\")==1 || name.indexOf(":/")==1; + if (fullPath) + path = name; + else + path = Menus.getPlugInsPath() + name; + runMacroFile(path, null); + } + } + + /** Opens and runs the specified macro or script on the current + thread. The file is assumed to be in the ImageJ/macros folder + unless 'name' is a full path. The macro or script can use the + getArgument() function to retrieve the string argument. + */ + public String runMacroFile(String name, String arg) { + if (arg==null) arg = ""; + if (name.startsWith("ij.jar:")) + return runMacroFromIJJar(name, arg); + boolean fullPath = name.startsWith("/") || name.startsWith("\\") || name.indexOf(":\\")==1 || name.indexOf(":/")==1; + String path = name; + boolean exists = false; + if (!fullPath) { + String macrosDir = Menus.getMacrosPath(); + if (macrosDir!=null) + path = Menus.getMacrosPath() + name; + } + File f = new File(path); + boolean hasExtension = f.getName().contains("."); + if (hasExtension) + exists = f.exists(); + if (!exists && !fullPath && !hasExtension) { + String path2 = path+".txt"; + f = new File(path2); + exists = f.exists(); + if (exists) + path = path2; + else { + path2 = path+".ijm"; + f = new File(path2); + exists = f.exists(); + if (exists) path=path2; + } + } + if (!exists) { + f = new File(path); + exists = f.exists(); + } + if (!exists && !fullPath) { + String path2 = System.getProperty("user.dir") + File.separator + name; + if (!hasExtension) { + String path3 = path2 +".txt"; + f = new File(path3); + exists = f.exists(); + if (exists) + path = path3; + else { + path3 = path2+".ijm"; + f = new File(path3); + exists = f.exists(); + if (exists) path=path3; + } + } + if (!exists) { + f = new File(path2); + exists = f.exists(); + if (exists) path=path2; + } + } + if (IJ.debugMode) IJ.log("runMacro: "+path+" ("+name+")"); + if (!exists || f==null) { + IJ.error("RunMacro", "Macro or script not found:\n \n"+path); + return null; + } + filePath = path; + try { + int size = (int)f.length(); + byte[] buffer = new byte[size]; + FileInputStream in = new FileInputStream(f); + in.read(buffer, 0, size); + String macro = new String(buffer, 0, size, "ISO8859_1"); + in.close(); + OpenDialog.setLastDirectory(f.getParent()+File.separator); + OpenDialog.setLastName(f.getName()); + if (name.endsWith(".js")) + return runJavaScript(macro, arg); + else if (name.endsWith(".bsh")) + return runBeanShell(macro, arg); + else if (name.endsWith(".py")) + return runPython(macro, arg); + else + return runMacro(macro, arg); + } + catch (Exception e) { + if (!Macro.MACRO_CANCELED.equals(e.getMessage())) + IJ.error(e.getMessage()); + return null; + } + } + + /** Runs the specified macro on the current thread. Macros can retrieve + the optional string argument by calling the getArgument() macro function. + Returns the string value returned by the macro, null if the macro does not + return a value, or "[aborted]" if the macro was aborted due to an error. */ + public String runMacro(String macro, String arg) { + Interpreter interp = new Interpreter(); + try { + return interp.run(macro, arg); + } catch(Throwable e) { + interp.abortMacro(); + IJ.showStatus(""); + IJ.showProgress(1.0); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) imp.unlock(); + String msg = e.getMessage(); + if (e instanceof RuntimeException && msg!=null && e.getMessage().equals(Macro.MACRO_CANCELED)) + return "[aborted]"; + IJ.handleException(e); + } + return "[aborted]"; + } + + /** Runs the specified macro from a JAR file in the plugins folder, + passing it the specified argument. Returns the String value returned + by the macro, null if the macro does not return a value, or "[aborted]" + if the macro was aborted due to an error. The macro can reside anywhere + in the plugins folder, in or out of a JAR file, so name conflicts are possible. + To avoid name conflicts, it is a good idea to incorporate the plugin + or JAR file name in the macro name (e.g., "Image_5D_Macro1.ijm"). */ + public static String runMacroFromJar(String name, String arg) { + String macro = null; + try { + ClassLoader pcl = IJ.getClassLoader(); + InputStream is = pcl.getResourceAsStream(name); + if (is==null) { + IJ.error("Macro Runner", "Unable to load \""+name+"\" from jar file"); + return null; + } + InputStreamReader isr = new InputStreamReader(is); + StringBuffer sb = new StringBuffer(); + char [] b = new char [8192]; + int n; + while ((n = isr.read(b)) > 0) + sb.append(b,0, n); + macro = sb.toString(); + is.close(); + } catch (IOException e) { + IJ.error("Macro Runner", ""+e); + } + if (macro!=null) + return (new Macro_Runner()).runMacro(macro, arg); + else + return null; + } + + public String runMacroFromIJJar(String name, String arg) { + ImageJ ij = IJ.getInstance(); + //if (ij==null) return null; + Class c = ij!=null?ij.getClass():(new ImageStack()).getClass(); + name = name.substring(7); + String macro = null; + try { + InputStream is = c .getResourceAsStream("/macros/"+name+".txt"); + //IJ.log(is+" "+("/macros/"+name+".txt")); + if (is==null) + return runMacroFile(name, arg); + InputStreamReader isr = new InputStreamReader(is); + StringBuffer sb = new StringBuffer(); + char [] b = new char [8192]; + int n; + while ((n = isr.read(b)) > 0) + sb.append(b,0, n); + macro = sb.toString(); + } + catch (IOException e) { + String msg = e.getMessage(); + if (msg==null || msg.equals("")) + msg = "" + e; + IJ.showMessage("Macro Runner", msg); + } + if (macro!=null) + return runMacro(macro, arg); + else + return null; + } + + /** Runs a JavaScript script on the current thread, passing a string argument, + which the script can retrieve using the getArgument() function. Returns, + as a string, the last expression evaluated by the script. */ + public String runJavaScript(String script, String arg) { + Object js = null; + if (!(IJ.isMacOSX()&&!IJ.is64Bit())) { + // Use JavaScript engine built into Java 6 and later. + js = IJ.runPlugIn("ij.plugin.JavaScriptEvaluator", ""); + } else { + js = IJ.runPlugIn("JavaScript", ""); + if (js==null) { + boolean ok = downloadJar("/download/tools/JavaScript.jar"); + if (ok) + js = IJ.runPlugIn("JavaScript", ""); + } + } + script = Editor.getJSPrefix(arg)+script; + if (IJ.isJava18()) + script = "load(\"nashorn:mozilla_compat.js\");" + script; + if (js!=null) + return runScript(js, script, arg); + else + return null; + } + + private static String runScript(Object plugin, String script, String arg) { + if (plugin instanceof PlugInInterpreter) { + PlugInInterpreter interp = (PlugInInterpreter)plugin; + if (IJ.debugMode) + IJ.log("Running "+interp.getName()+" script; arg=\""+arg+"\""); + interp.run(script, arg); + return interp.getReturnValue(); + } else { // call run(script,arg) method using reflection + try { + Class c = plugin.getClass(); + Method m = c.getMethod("run", new Class[] {script.getClass(), arg.getClass()}); + String s = (String)m.invoke(plugin, new Object[] {script, arg}); + } catch(Exception e) { + if ("Jython".equals(plugin.getClass().getName())) + IJ.runPlugIn("Jython", script); + } + return ""+plugin; + } + } + + /** Runs a BeanShell script on the current thread, passing a string argument, + which the script can retrieve using the getArgument() function. Returns, + as a string, the last expression evaluated by the script. + Uses the plugin at http://imagej.nih.gov/ij/plugins/bsh/ + to run the script. + */ + public static String runBeanShell(String script, String arg) { + if (arg==null) + arg = ""; + Object bsh = IJ.runPlugIn("bsh", ""); + if (bsh==null) { + boolean ok = downloadJar("/plugins/bsh/BeanShell.jar"); + if (ok) + bsh = IJ.runPlugIn("bsh", ""); + } + if (bsh!=null) + return runScript(bsh, script, arg); + else + return null; + } + + /** Runs a Python script on the current thread, passing a string argument, + which the script can retrieve using the getArgument() function. Returns, + as a string, the value of the variable 'result'. For example, a Python script + containing the line "result=123" will return the string "123". + Uses the plugin at http://imagej.nih.gov/ij/plugins/jython/ + to run the script. + */ + public static String runPython(String script, String arg) { + if (arg==null) + arg = ""; + Object jython = IJ.runPlugIn("Jython", ""); + if (jython==null) { + boolean ok = downloadJar("/plugins/jython/Jython.jar"); + if (ok) + jython = IJ.runPlugIn("Jython", ""); + } + if (jython!=null) + return runScript(jython, script, arg); + else + return null; + } + + public static boolean downloadJar(String url) { + String name = url.substring(url.lastIndexOf("/")+1); + boolean ok = false; + String msg = name+" was not found in the plugins\nfolder or it is outdated. Click \"OK\" to download\nit from the ImageJ website."; + GenericDialog gd = new GenericDialog("Download "+name+"?"); + gd.addMessage(msg); + gd.showDialog(); + if (!gd.wasCanceled()) { + ok = (new PluginInstaller()).install(IJ.URL+url); + if (!ok) + IJ.error("Unable to download "+name+" from "+IJ.URL+url); + } + return ok; + } + + /** Returns the file path of the most recently loaded macro or script. */ + public static String getFilePath() { + return filePath; + } + + public static void setFilePath(String path) { + filePath = path; + } + +} diff --git a/src/ij/plugin/MeasurementsWriter.java b/src/ij/plugin/MeasurementsWriter.java new file mode 100644 index 0000000..edefc40 --- /dev/null +++ b/src/ij/plugin/MeasurementsWriter.java @@ -0,0 +1,50 @@ +package ij.plugin; +import ij.*; +import ij.text.*; +import ij.measure.ResultsTable; +import ij.io.*; +import java.io.*; +import java.awt.Frame; + +/** Saves a table as a csv or tab-delimited text file. */ +public class MeasurementsWriter implements PlugIn { + + public void run(String path) { + save(path); + } + + public boolean save(String path) { + Frame frame = WindowManager.getFrontWindow(); + if (frame!=null && (frame instanceof TextWindow) && !"Log".equals(frame.getTitle())) { + TextWindow tw = (TextWindow)frame; + return tw.getTextPanel().saveAs(path); + } else if (IJ.isResultsWindow()) { + TextPanel tp = IJ.getTextPanel(); + if (tp!=null) { + if (!tp.saveAs(path)) + return false; + } + } else { + ResultsTable rt = ResultsTable.getResultsTable(); + if (rt==null || rt.size()==0) { + frame = WindowManager.getFrame("Results"); + if (frame==null || !(frame instanceof TextWindow)) + return false; + else { + TextWindow tw = (TextWindow)frame; + return tw.getTextPanel().saveAs(path); + } + } + if (path.equals("")) { + SaveDialog sd = new SaveDialog("Save as Text", "Results", Prefs.defaultResultsExtension()); + String file = sd.getFileName(); + if (file == null) return false; + path = sd.getDirectory() + file; + } + return rt.save(path); + } + return true; + } + +} + diff --git a/src/ij/plugin/Memory.java b/src/ij/plugin/Memory.java new file mode 100644 index 0000000..bce11de --- /dev/null +++ b/src/ij/plugin/Memory.java @@ -0,0 +1,164 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import java.io.*; +import ij.util.Tools; + + +/** This plugin implements the Edit/Options/Memory command. */ +public class Memory implements PlugIn { + String s; + int index1, index2; + File f; + boolean fileMissing; + boolean sixtyFourBit; + + public void run(String arg) { + changeMemoryAllocation(); + //IJ.log("setting="+getMemorySetting()/(1024*1024)+"MB"); + //IJ.log("maxMemory="+maxMemory()/(1024*1024)+"MB"); + } + + void changeMemoryAllocation() { + IJ.maxMemory(); // forces IJ to cache old limit + int max = (int)(getMemorySetting()/1048576L); + boolean unableToSet = max==0; + if (max==0) max = (int)(maxMemory()/1048576L); + String title = "Memory "+(IJ.is64Bit()?"(64-bit)":"(32-bit)"); + GenericDialog gd = new GenericDialog(title); + gd.addNumericField("Maximum memory:", max, 0, 5, "MB"); + gd.addNumericField("Parallel threads:", Prefs.getThreads(), 0, 5, ""); + gd.setInsets(12, 0, 0); + gd.addCheckbox("Keep multiple undo buffers", Prefs.keepUndoBuffers); + gd.setInsets(12, 0, 0); + gd.addCheckbox("Run garbage collector on status bar click", !Prefs.noClickToGC); + gd.addHelp(IJ.URL+"/docs/menus/edit.html#memory"); + gd.showDialog(); + if (gd.wasCanceled()) return; + int max2 = (int)gd.getNextNumber(); + Prefs.setThreads((int)gd.getNextNumber()); + Prefs.keepUndoBuffers = gd.getNextBoolean(); + Prefs.noClickToGC = !gd.getNextBoolean(); + if (gd.invalidNumber()) { + IJ.showMessage("Memory", "The number entered was invalid."); + return; + } + if (unableToSet && max2!=max) + {showError(); return;} + if (IJ.isMacOSX() && max2<256) + max2 = 256; + else if (max2<32) + max2 = 32; + if (max2==max) return; + int limit = IJ.isWindows()?1600:1700; + String OSXInfo = ""; + if (max2>=limit && !IJ.is64Bit()) { + if (!IJ.showMessageWithCancel(title, + "Note: setting the memory limit to a value\n" + +"greater than "+limit+"MB on a 32-bit system\n" + +"may cause ImageJ to fail to start. The title of\n" + +"the Edit>Options>Memory & Threads dialog\n" + +"box changes to \"Memory (64-bit)\" when ImageJ\n" + +"is running on a 64-bit version of Java.")); + return; + } + try { + String s2 = s.substring(index2); + if (s2.startsWith("g")) + s2 = "m"+s2.substring(1); + String s3 = s.substring(0, index1) + max2 + s2; + FileOutputStream fos = new FileOutputStream(f); + PrintWriter pw = new PrintWriter(fos); + pw.print(s3); + pw.close(); + } catch (IOException e) { + String error = e.getMessage(); + if (error==null || error.equals("")) error = ""+e; + String name = IJ.isMacOSX()?"Info.plist":"ImageJ.cfg"; + String msg = + "Unable to update the file \"" + name + "\".\n" + + " \n" + + "\"" + error + "\""; + IJ.showMessage("Memory", msg); + return; + } + String hint = ""; + if (IJ.isWindows() && max2>640 && max2>max) + hint = "\nDelete the \"ImageJ.cfg\" file, located in the ImageJ folder,\nif ImageJ fails to start."; + IJ.showMessage("Memory", "The new " + max2 +"MB limit will take effect after ImageJ is restarted."+hint); + } + + public long getMemorySetting() { + if (IJ.getApplet()!=null) return 0L; + long max = 0L; + if (IJ.isMacOSX()) { + String appPath = System.getProperty("java.class.path"); + if (appPath==null) return 0L; + int index = appPath.indexOf(".app/"); + if (index==-1) return 0L; + appPath = appPath.substring(0,index+5); + max = getMemorySetting(appPath+"Contents/Info.plist"); + } else + max = getMemorySetting("ImageJ.cfg"); + return max; + } + + void showError() { + int max = (int)(maxMemory()/1048576L); + String msg = + "ImageJ is unable to change the memory limit. For \n" + + "more information, refer to the installation notes at\n \n" + + " "+IJ.URL+"/docs/install/\n" + + " \n"; + if (fileMissing) { + if (IJ.isMacOSX()) + msg += "The ImageJ application (ImageJ.app) was not found.\n \n"; + else if (IJ.isWindows()) + msg += "ImageJ.cfg not found.\n \n"; + fileMissing = false; + } + if (max>0) + msg += "Current limit: " + max + "MB"; + IJ.showMessage("Memory", msg); + } + + long getMemorySetting(String file) { + String path = file.startsWith("/")?file:Prefs.getImageJDir()+file; + if (IJ.debugMode) IJ.log("getMemorySetting: "+path); + f = new File(path); + if (!f.exists()) { + fileMissing = true; + return 0L; + } + long max = 0L; + try { + int size = (int)f.length(); + byte[] buffer = new byte[size]; + FileInputStream in = new FileInputStream(f); + in.read(buffer, 0, size); + s = new String(buffer, 0, size, "ISO8859_1"); + in.close(); + index1 = s.indexOf("-mx"); + if (index1==-1) index1 = s.indexOf("-Xmx"); + if (index1==-1) return 0L; + if (s.charAt(index1+1)=='X') index1+=4; else index1+=3; + index2 = index1; + while (index21 && imp.getNFrames()>1) { + error("5D hyperstacks are not supported"); + return; + } + int channels = imp.getNChannels(); + if (!hyperstack && imp.isComposite() && channels>1) { + int channel = imp.getChannel(); + CompositeImage ci = (CompositeImage)imp; + int mode = ci.getMode(); + if (mode==IJ.COMPOSITE) + ci.setMode(IJ.COLOR); + ImageStack stack = new ImageStack(imp.getWidth(), imp.getHeight()); + for (int c=1; c<=channels; c++) { + imp.setPosition(c, imp.getSlice(), imp.getFrame()); + Image img = imp.getImage(); + stack.addSlice(null, new ColorProcessor(img)); + } + if (ci.getMode()!=mode) + ci.setMode(mode); + imp.setPosition(channel, imp.getSlice(), imp.getFrame()); + Calibration cal = imp.getCalibration(); + imp = new ImagePlus(imp.getTitle(), stack); + imp.setCalibration(cal); + } + makeMontage(imp); + imp.updateImage(); + saveID = imp.getID(); + IJ.register(MontageMaker.class); + } + + public void makeMontage(ImagePlus imp) { + int nSlices = imp.getStackSize(); + if (hyperstack) { + nSlices = imp.getNSlices(); + if (nSlices==1) + nSlices = imp.getNFrames(); + } + boolean macro = Macro.getOptions()!=null; + if (macro || columns==0 || !(imp.getID()==saveID || nSlices==saveStackSize)) { + columns = (int)Math.sqrt(nSlices); + rows = columns; + int n = nSlices - columns*rows; + if (n>0) columns += (int)Math.ceil((double)n/rows); + scale = 1.0; + if (imp.getWidth()*columns>800) + scale = 0.5; + if (imp.getWidth()*columns>1600) + scale = 0.25; + inc = 1; + first = 1; + last = nSlices; + } + if (macro) { + fontSize = defaultFontSize; + borderWidth = 0; + label = false; + useForegroundColor = false; + } + saveStackSize = nSlices; + + GenericDialog gd = new GenericDialog("Make Montage"); + gd.addNumericField("Columns:", columns, 0); + gd.addNumericField("Rows:", rows, 0); + gd.addNumericField("Scale factor:", scale, 2); + if (!hyperstack) { + gd.addNumericField("First slice:", first, 0); + gd.addNumericField("Last slice:", last, 0); + } + gd.addNumericField("Increment:", inc, 0); + gd.addNumericField("Border width:", borderWidth, 0); + gd.addNumericField("Font size:", fontSize, 0); + gd.addCheckbox("Label slices", label); + gd.addCheckbox("Use foreground color", useForegroundColor); + gd.showDialog(); + if (gd.wasCanceled()) + return; + columns = (int)gd.getNextNumber(); + rows = (int)gd.getNextNumber(); + scale = gd.getNextNumber(); + gd.setSmartRecording(true); + if (!hyperstack) { + first = (int)gd.getNextNumber(); + last = (int)gd.getNextNumber(); + } + inc = (int)gd.getNextNumber(); + borderWidth = (int)gd.getNextNumber(); + fontSize = (int)gd.getNextNumber(); + if (borderWidth<0) borderWidth = 0; + if (first<1) first = 1; + if (last>nSlices) last = nSlices; + if (first>last) + {first=1; last=nSlices;} + if (inc<1) inc = 1; + if (gd.invalidNumber()) { + error("Invalid number"); + return; + } + label = gd.getNextBoolean(); + useForegroundColor = gd.getNextBoolean(); + ImagePlus imp2 = null; + if (hyperstack) + imp2 = makeHyperstackMontage(imp, columns, rows, scale, inc, borderWidth, label); + else + imp2 = makeMontage2(imp, columns, rows, scale, first, last, inc, borderWidth, label); + if (imp2!=null) + imp2.show(); + if (macro) { + fontSize = defaultFontSize; + borderWidth = 0; + label = false; + useForegroundColor = false; + columns = 0; + } + } + + /** Creates a montage and displays it. */ + public void makeMontage(ImagePlus imp, int columns, int rows, double scale, int first, int last, int inc, int borderWidth, boolean labels) { + ImagePlus imp2 = makeMontage2(imp, columns, rows, scale, first, last, inc, borderWidth, labels); + imp2.show(); + } + + /** Creates a montage and returns it as an ImagePlus. */ + public ImagePlus makeMontage2(ImagePlus imp, int columns, int rows, double scale, int first, int last, int inc, int borderWidth, boolean labels) { + int stackWidth = imp.getWidth(); + int stackHeight = imp.getHeight(); + int nSlices = imp.getStackSize(); + int width = (int)(stackWidth*scale); + int height = (int)(stackHeight*scale); + int montageWidth = width*columns + borderWidth*(columns-1); + int montageHeight = height*rows + borderWidth*(rows-1); + ImageProcessor ip = imp.getProcessor(); + ImageProcessor montage = ip.createProcessor(montageWidth, montageHeight); + ImagePlus imp2 = new ImagePlus("Montage", montage); + imp2.setCalibration(imp.getCalibration()); + montage = imp2.getProcessor(); + Color fgColor=Color.white; + Color bgColor = Color.black; + if (useForegroundColor) { + fgColor = Toolbar.getForegroundColor(); + bgColor = Toolbar.getBackgroundColor(); + } else { + boolean whiteBackground = false; + if ((ip instanceof ByteProcessor) || (ip instanceof ColorProcessor)) { + ip.setRoi(0, stackHeight-12, stackWidth, 12); + ImageStatistics stats = ImageStatistics.getStatistics(ip, Measurements.MODE, null); + ip.resetRoi(); + whiteBackground = stats.mode>=200; + if (imp.isInvertedLut()) + whiteBackground = !whiteBackground; + } + if (whiteBackground) { + fgColor=Color.black; + bgColor = Color.white; + } + } + if (Double.isNaN(Toolbar.getBackgroundValue())) + montage.setColor(bgColor); + else + montage.setGlobalBackgroundColor(); + montage.fill(); + if (Double.isNaN(Toolbar.getForegroundValue())) + montage.setColor(fgColor); + else + montage.setGlobalForegroundColor(); + montage.setFont(new Font("SansSerif", Font.PLAIN, fontSize)); + montage.setAntialiasedText(true); + ImageStack stack = imp.getStack(); + int x = 0; + int y = 0; + ImageProcessor aSlice; + int slice = first; + while (slice<=last) { + aSlice = stack.getProcessor(slice); + if (scale!=1.0) { + aSlice.setInterpolationMethod(ImageProcessor.BILINEAR); + boolean averageWhenDownSizing = width<200; + aSlice = aSlice.resize(width, height, averageWhenDownSizing); + } + montage.insert(aSlice, x, y); + String label = stack.getShortSliceLabel(slice); + if (labels) + drawLabel(montage, slice, label, x, y, width, height, borderWidth); + x += width + borderWidth; + if (x>=montageWidth) { + x = 0; + y += height + borderWidth;; + if (y>=montageHeight) + break; + } + IJ.showProgress((double)(slice-first)/(last-first)); + slice += inc; + } + if (borderWidth>0) { + for (x=width; x=width) { + do { + label = label.substring(0, label.length()-1); + } while (label.length()>1 && montage.getStringWidth(label)>=width); + } + if (label==null || label.equals("")) + label = ""+slice; + int swidth = montage.getStringWidth(label); + x += width/2 - swidth/2; + y -= borderWidth/2; + y += height; + montage.drawString(label, x, y); + } + + public void setFontSize(int fontSize) { + this.fontSize = fontSize; + } + +} + + diff --git a/src/ij/plugin/NewPlugin.java b/src/ij/plugin/NewPlugin.java new file mode 100644 index 0000000..3adccc3 --- /dev/null +++ b/src/ij/plugin/NewPlugin.java @@ -0,0 +1,187 @@ +package ij.plugin; +import java.awt.*; +import ij.*; +import ij.gui.*; +import ij.plugin.frame.Editor; +import ij.text.TextWindow; +import ij.io.SaveDialog; +import ij.util.Tools; + +/** This class creates a new macro or the Java source for a new plugin. */ +public class NewPlugin implements PlugIn { + + public static final int MACRO=0, JAVASCRIPT=1, PLUGIN=2, PLUGIN_FILTER=3, PLUGIN_FRAME=4, + TEXT_FILE=5, TABLE=6, MACRO_TOOL=7, PLUGIN_TOOL=8, TEMPLATE=9; + private static int rows = 24; + private static int columns = 80; + private static int tableWidth = 350; + private static int tableHeight = 250; + private int type = MACRO; + private String name = "Macro.ijm"; + private boolean monospaced; + private boolean menuBar = true; + private Editor ed; + + public void run(String arg) { + type = -1; + if (arg.startsWith("text")||arg.equals("")) { + type = TEXT_FILE; + name = "Untitled.txt"; + } else if (arg.equals("macro")) { + type = MACRO; + name = "Macro.ijm"; + } else if (arg.equals("macro-tool")) { + type = TEMPLATE; + name = "Circle_Tool.txt"; + } else if (arg.equals("javascript")) { + type = JAVASCRIPT; + name = "Script.js"; + } else if (arg.equals("plugin")) { + type = TEMPLATE; + name = "My_Plugin.src"; + } else if (arg.equals("frame")) { + type = TEMPLATE; + name = "Plugin_Frame.src"; + } else if (arg.equals("plugin-tool")) { + type = TEMPLATE; + name = "Prototype_Tool.src"; + } else if (arg.equals("filter")) { + type = TEMPLATE; + name = "Filter_Plugin.src"; + } else if (arg.equals("table")) { + String options = Macro.getOptions(); + if (IJ.isMacro() && options!=null && options.indexOf("[Text File]")!=-1) { + type = TEXT_FILE; + name = "Untitled.txt"; + arg = "text+dialog"; + } else { + type = TABLE; + name = "Table"; + } + } + menuBar = true; + if (arg.equals("text+dialog") || type==TABLE) { + if (!showDialog()) return; + } + if (type==-1) + createPlugin("Converted_Macro.java", PLUGIN, arg); + else if (type==TEMPLATE || type==MACRO || type==TEXT_FILE || type==JAVASCRIPT) { + if (type==TEXT_FILE && name.equals("Macro")) + name = "Untitled.txt"; + createMacro(name); + } else if (type==TABLE) + createTable(); + else + createPlugin(name, type, arg); + } + + public void createMacro(String name) { + int options = (monospaced?Editor.MONOSPACED:0)+(menuBar?Editor.MENU_BAR:0); + if (name.endsWith(".ijm") || name.endsWith(".js")) + options |= Editor.RUN_BAR; + if (name.endsWith(".ijm")) + options |= Editor.INSTALL_BUTTON; + String text = ""; + ed = new Editor(rows, columns, 0, options); + if (type==TEMPLATE) + text = Tools.openFromIJJarAsString("/macros/"+name); + if (name.endsWith(".src")) + name = name.substring(0,name.length()-4) + ".java"; + if (type==MACRO && !name.endsWith(".ijm")) + name = SaveDialog.setExtension(name, ".ijm"); + else if (type==JAVASCRIPT && !name.endsWith(".js")) { + if (name.equals("Macro")) name = "script"; + name = SaveDialog.setExtension(name, ".js"); + } + if (text!=null) + ed.create(name, text); + } + + void createTable() { + new TextWindow(name, "", tableWidth, tableHeight); + } + + public void createPlugin(String name, int type, String methods) { + ed = (Editor)IJ.runPlugIn("ij.plugin.frame.Editor", ""); + if (ed==null) return; + String pluginName = name; + if (!(name.endsWith(".java") || name.endsWith(".JAVA"))) + name = SaveDialog.setExtension(name, ".java"); + String className = pluginName.substring(0, pluginName.length()-5); + String text = ""; + text += "import ij.*;\n"; + text += "import ij.process.*;\n"; + text += "import ij.gui.*;\n"; + text += "import java.awt.*;\n"; + text += "import ij.plugin.*;\n"; + text += "\n"; + text += "public class "+className+" implements PlugIn {\n"; + text += "\n"; + text += "\tpublic void run(String arg) {\n"; + text += methods; + text += "\t}\n"; + text += "\n"; + text += "}\n"; + ed.create(pluginName, text); + } + + public boolean showDialog() { + String title; + String widthUnit, heightUnit; + int width, height; + if (type==TABLE) { + title = "New Table"; + name = "Table"; + width = tableWidth; + height = tableHeight; + widthUnit = "pixels"; + heightUnit = "pixels"; + } else { + title = "New Text Window"; + name = "Untitled"; + width = columns; + height = rows; + widthUnit = "characters"; + heightUnit = "lines"; + } + GenericDialog gd = new GenericDialog(title); + gd.addStringField("Name:", name, 16); + gd.addMessage(""); + gd.addNumericField("Width:", width, 0, 3, widthUnit); + gd.addNumericField("Height:", height, 0, 3, heightUnit); + if (type!=TABLE) { + gd.setInsets(5, 30, 0); + gd.addCheckbox("Menu Bar", true); + gd.setInsets(0, 30, 0); + gd.addCheckbox("Monospaced Font", monospaced); + } + gd.showDialog(); + if (gd.wasCanceled()) + return false; + name = gd.getNextString(); + width = (int)gd.getNextNumber(); + height = (int)gd.getNextNumber(); + if (width<1) width = 1; + if (height<1) height = 1; + if (type!=TABLE) { + menuBar = gd.getNextBoolean(); + monospaced = gd.getNextBoolean(); + columns = width; + rows = height; + if (rows>100) rows = 100; + if (columns>200) columns = 200; + } else { + tableWidth = width; + tableHeight = height; + if (tableWidth<128) tableWidth = 128; + if (tableHeight<75) tableHeight = 75; + } + return true; + } + + /** Returns the Editor the newly created macro or plugin was opened in. */ + public Editor getEditor() { + return ed; + } + +} diff --git a/src/ij/plugin/NextImageOpener.java b/src/ij/plugin/NextImageOpener.java new file mode 100644 index 0000000..fa128e2 --- /dev/null +++ b/src/ij/plugin/NextImageOpener.java @@ -0,0 +1,164 @@ +/** +This plugin, written by Jon Harmon, implements the File/Open Next command. +It opens the "next" image in a directory, where "next" can be the +succeeding or preceeding image in the directory list. +Press shift-o to open the succeeding image or +alt-shift-o to open the preceeding image. +It can leave the previous file open, or close it. +You may contact the author at Jonathan_Harman at yahoo.com +This code was modified from Image_Browser by Albert Cardona +*/ + +package ij.plugin; +import ij.*; +import ij.io.*; +import ij.gui.*; +import java.io.File; + +public class NextImageOpener implements PlugIn { + + boolean forward = true; // default browse direction is forward + boolean closeCurrent = true; //default behavior is to close current window + ImagePlus imp0; + + public void run(String arg) { + /* get changes to defaults */ + if (arg.equals("backward") || IJ.altKeyDown()) forward = false; + if (arg.equals("backwardsc")) { + forward = false; + closeCurrent = false; + } + if (arg.equals("forwardsc")) { + forward = true; + closeCurrent = false; + } + + // get current image; displays error and aborts if no image is open + imp0 = IJ.getImage(); + // get current image directory + String currentPath = getDirectory(imp0); + if (IJ.debugMode) IJ.log("OpenNext.currentPath:" + currentPath); + if (currentPath==null) { + IJ.error("Next Image", "Directory information for \""+imp0.getTitle()+"\" not found."); + return; + } + String nextPath = getNext(currentPath, getName(imp0), forward); + if (IJ.debugMode) IJ.log("OpenNext.nextPath:" + nextPath); + // open + if (nextPath != null) { + String rtn = open(nextPath); + if (rtn==null) + open(getNext(currentPath, (new File(nextPath)).getName(), forward)); + } + } + + String getDirectory(ImagePlus imp) { + FileInfo fi = imp.getOriginalFileInfo(); + if (fi==null) return null; + String dir = fi.openNextDir; + if (dir==null) dir = fi.directory; + return dir; + } + + String getName(ImagePlus imp) { + String name = imp.getTitle(); + FileInfo fi = imp.getOriginalFileInfo(); + if (fi!=null) { + if (fi.openNextName!=null) + name = fi.openNextName; + else if (fi.fileName!=null) + name = fi.fileName; + } + return name; + } + + String open(String nextPath) { + int nImages = WindowManager.getImageCount(); + ImagePlus imp2 = IJ.openImage(nextPath); + if (imp2==null) { + if (WindowManager.getImageCount()>nImages) + return "ok"; + else + return null; + } + String newTitle = imp2.getTitle(); + if (imp0.changes) { + String msg; + String name = imp0.getTitle(); + if (name.length()>22) + msg = "Save changes to\n" + "\"" + name + "\"?"; + else + msg = "Save changes to \"" + name + "\"?"; + YesNoCancelDialog d = new YesNoCancelDialog(imp0.getWindow(), "ImageJ", msg); + if (d.cancelPressed()) + return "Canceled"; + else if (d.yesPressed()) { + FileSaver fs = new FileSaver(imp0); + if (!fs.save()) + return "Canceled"; + } + imp0.changes = false; + } + if (!(imp0 instanceof CompositeImage) && (imp2.isComposite() || imp2.isHyperStack())) { + // imp0.setImage(imp2) does not work if 'imp2' is composite and 'imp0' is not + imp2.show(); + imp0.close(); + imp0 = imp2; + } else + imp0.setImage(imp2); + return "ok"; + } + + /** gets the next image name in a directory list */ + String getNext(String path, String imageName, boolean forward) { + File dir = new File(path); + if (!dir.isDirectory()) return null; + String[] names = dir.list(); + ij.util.StringSorter.sort(names); + int thisfile = -1; + for (int i=0; i\" and \"<\"", Prefs.reverseNextPreviousOrder); + if (IJ.isMacOSX()) + gd.addCheckbox("Don't set Mac menu bar", !Prefs.setIJMenuBar); + if (IJ.isLinux()) + gd.addCheckbox("Save window locations", !Prefs.doNotSaveWindowLocations); + gd.addCheckbox("Non-blocking filter dialogs", Prefs.nonBlockingFilterDialogs); + gd.addCheckbox("Debug mode", IJ.debugMode); + //gd.addCheckbox("Modern mode", Prefs.modernMode); + gd.addHelp(IJ.URL+"/docs/menus/edit.html#misc"); + gd.showDialog(); + if (gd.wasCanceled()) + return; + + String divValue = gd.getNextString(); + if (divValue.equalsIgnoreCase("infinity") || divValue.equalsIgnoreCase("infinite")) + FloatBlitter.divideByZeroValue = Float.POSITIVE_INFINITY; + else if (divValue.equalsIgnoreCase("NaN")) + FloatBlitter.divideByZeroValue = Float.NaN; + else if (divValue.equalsIgnoreCase("max")) + FloatBlitter.divideByZeroValue = Float.MAX_VALUE; + else { + Float f; + try {f = new Float(divValue);} + catch (NumberFormatException e) {f = null;} + if (f!=null) + FloatBlitter.divideByZeroValue = f.floatValue(); + } + IJ.register(FloatBlitter.class); + + Prefs.usePointerCursor = gd.getNextBoolean(); + IJ.hideProcessStackDialog = gd.getNextBoolean(); + Prefs.requireControlKey = gd.getNextBoolean(); + Prefs.moveToMisc = gd.getNextBoolean(); + if (!IJ.isMacOSX()) + Prefs.runSocketListener = gd.getNextBoolean(); + Prefs.enhancedLineTool = gd.getNextBoolean(); + Prefs.reverseNextPreviousOrder = gd.getNextBoolean(); + if (IJ.isMacOSX()) + Prefs.setIJMenuBar = !gd.getNextBoolean(); + if (IJ.isLinux()) + Prefs.doNotSaveWindowLocations = !gd.getNextBoolean(); + Prefs.nonBlockingFilterDialogs = gd.getNextBoolean(); + IJ.setDebugMode(gd.getNextBoolean()); + //Prefs.modernMode = gd.getNextBoolean(); + } + + void lineWidth() { + int width = (int)IJ.getNumber("Line Width:", Line.getWidth()); + if (width==IJ.CANCELED) return; + Line.setWidth(width); + LineWidthAdjuster.update(); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null && imp.isProcessor()) { + ImageProcessor ip = imp.getProcessor(); + ip.setLineWidth(Line.getWidth()); + Roi roi = imp.getRoi(); + if (roi!=null && roi.isLine()) imp.draw(); + } + } + + // Input/Output options + void io() { + GenericDialog gd = new GenericDialog("I/O Options"); + gd.addNumericField("JPEG quality (0-100):", FileSaver.getJpegQuality(), 0, 3, ""); + gd.addNumericField("GIF and PNG transparent index:", Prefs.getTransparentIndex(), 0, 3, ""); + gd.addStringField("File extension for tables (.csv, .tsv or .txt):", Prefs.defaultResultsExtension(), 4); + gd.addCheckbox("Use JFileChooser to open/save", Prefs.useJFileChooser); + if (!IJ.isMacOSX()) + gd.addCheckbox("Use_file chooser to import sequences", Prefs.useFileChooser); + gd.addCheckbox("Save TIFF and raw in Intel byte order", Prefs.intelByteOrder); + gd.addCheckbox("Skip dialog when opening .raw files", Prefs.skipRawDialog); + + gd.setInsets(15, 20, 0); + gd.addMessage("Results Table Options"); + gd.setInsets(3, 40, 0); + gd.addCheckbox("Copy_column headers", Prefs.copyColumnHeaders); + gd.setInsets(0, 40, 0); + gd.addCheckbox("Copy_row numbers", !Prefs.noRowNumbers); + gd.setInsets(0, 40, 0); + gd.addCheckbox("Save_column headers", !Prefs.dontSaveHeaders); + gd.setInsets(0, 40, 0); + gd.addCheckbox("Save_row numbers", !Prefs.dontSaveRowNumbers); + + gd.showDialog(); + if (gd.wasCanceled()) + return; + int quality = (int)gd.getNextNumber(); + if (quality<0) quality = 0; + if (quality>100) quality = 100; + FileSaver.setJpegQuality(quality); + int transparentIndex = (int)gd.getNextNumber(); + Prefs.setTransparentIndex(transparentIndex); + String extension = gd.getNextString(); + if (!extension.startsWith(".")) + extension = "." + extension; + Prefs.set("options.ext", extension); + boolean useJFileChooser2 = Prefs.useJFileChooser; + Prefs.useJFileChooser = gd.getNextBoolean(); + if (Prefs.useJFileChooser!=useJFileChooser2) + Prefs.jFileChooserSettingChanged = true; + if (!IJ.isMacOSX()) + Prefs.useFileChooser = gd.getNextBoolean(); + Prefs.intelByteOrder = gd.getNextBoolean(); + Prefs.skipRawDialog = gd.getNextBoolean(); + Prefs.copyColumnHeaders = gd.getNextBoolean(); + Prefs.noRowNumbers = !gd.getNextBoolean(); + Prefs.dontSaveHeaders = !gd.getNextBoolean(); + ResultsTable.getResultsTable().saveColumnHeaders(!Prefs.dontSaveHeaders); + Prefs.dontSaveRowNumbers = !gd.getNextBoolean(); + return; + } + + // Conversion Options + void conversions() { + double[] weights = ColorProcessor.getWeightingFactors(); + boolean weighted = !(weights[0]==1d/3d && weights[1]==1d/3d && weights[2]==1d/3d); + //boolean weighted = !(Math.abs(weights[0]-1d/3d)<0.0001 && Math.abs(weights[1]-1d/3d)<0.0001 && Math.abs(weights[2]-1d/3d)<0.0001); + GenericDialog gd = new GenericDialog("Conversion Options"); + gd.addCheckbox("Scale when converting", ImageConverter.getDoScaling()); + String prompt = "Weighted RGB conversions"; + if (weighted) + prompt += " (" + IJ.d2s(weights[0]) + "," + IJ.d2s(weights[1]) + ","+ IJ.d2s(weights[2]) + ")"; + gd.addCheckbox(prompt, weighted); + gd.showDialog(); + if (gd.wasCanceled()) + return; + ImageConverter.setDoScaling(gd.getNextBoolean()); + Prefs.weightedColor = gd.getNextBoolean(); + if (!Prefs.weightedColor) + ColorProcessor.setWeightingFactors(1d/3d, 1d/3d, 1d/3d); + else if (Prefs.weightedColor && !weighted) + ColorProcessor.setWeightingFactors(0.299, 0.587, 0.114); + return; + } + + // replaced by AppearanceOptions class + void appearance() { + } + + // DICOM options + void dicom() { + GenericDialog gd = new GenericDialog("DICOM Options"); + gd.addCheckbox("Open as 32-bit float", Prefs.openDicomsAsFloat); + gd.addCheckbox("Ignore rescale slope", Prefs.ignoreRescaleSlope); + gd.addCheckbox("Fixed Z slope and intercept", Prefs.fixedDicomScaling); + gd.addMessage("Orthogonal views"); + gd.setInsets(5, 40, 0); + gd.addCheckbox("Rotate YZ", Prefs.rotateYZ); + gd.setInsets(0, 40, 0); + gd.addCheckbox("Flip XZ", Prefs.flipXZ); + gd.showDialog(); + if (gd.wasCanceled()) + return; + Prefs.openDicomsAsFloat = gd.getNextBoolean(); + Prefs.ignoreRescaleSlope = gd.getNextBoolean(); + Prefs.fixedDicomScaling = gd.getNextBoolean(); + Prefs.rotateYZ = gd.getNextBoolean(); + Prefs.flipXZ = gd.getNextBoolean(); + } + + /** Close all images, empty ROI Manager, clear the + Results table, clears the Log window and sets + "Black background" 'true'. + */ + private void freshStart() { + String options = Macro.getOptions(); + boolean keepImages = false; + boolean keepResults = false; + boolean keepRois = false; + if (options!=null) { + options = options.toLowerCase(); + keepImages = options.contains("images"); + keepResults = options.contains("results"); + keepRois = options.contains("rois"); + } + if (!keepImages) { + if (!Commands.closeAll()) + return; + } + if (!keepResults) { + if (!Analyzer.resetCounter()) + return; + } + if (!keepRois) { + RoiManager rm = RoiManager.getInstance(); + if (rm!=null) + rm.reset(); + } + if (WindowManager.getWindow("Log")!=null) + IJ.log("\\Clear"); + Prefs.blackBackground = true; + } + + // Delete preferences file when ImageJ quits + private void reset() { + if (IJ.showMessageWithCancel("Reset Preferences", "Preferences will be reset when ImageJ restarts.")) + Prefs.resetPreferences(); + } + +} // class Options diff --git a/src/ij/plugin/Orthogonal_Views.java b/src/ij/plugin/Orthogonal_Views.java new file mode 100644 index 0000000..00a2e9e --- /dev/null +++ b/src/ij/plugin/Orthogonal_Views.java @@ -0,0 +1,1002 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.measure.*; +import ij.process.*; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; +import java.awt.geom.*; +import java.util.*; + +/** + * This plugin projects dynamically orthogonal XZ and YZ views of a stack. + * The output images are calibrated, which allows measurements to be performed more easily. + * + * Many thanks to Jerome Mutterer for the code contributions and testing. + * Thanks to Wayne Rasband for the code that properly handles the image magnification. + * + * @author Dimiter Prodanov + */ +public class Orthogonal_Views implements PlugIn, MouseListener, MouseMotionListener, KeyListener, ActionListener, + ImageListener, WindowListener, AdjustmentListener, MouseWheelListener, FocusListener, CommandListener, Runnable { + + private ImageWindow win; + private ImagePlus imp; + private boolean rgb; + private ImageStack imageStack; + private boolean hyperstack; + private int currentChannel, currentFrame, currentMode; + private ImageCanvas canvas; + private static final int H_ROI=0, H_ZOOM=1; + private static boolean sticky=true; + private static int xzID, yzID; + private static Orthogonal_Views instance; + private ImagePlus xz_image, yz_image; + /** ImageProcessors for the xz and yz images */ + private ImageProcessor fp1, fp2; + private double ax, ay, az; + private boolean rotateYZ = Prefs.rotateYZ; + private boolean flipXZ = Prefs.flipXZ; + + private int xyX, xyY; + private Calibration cal, cal_xz, cal_yz; + private double magnification=1.0; + private Color color = Roi.getColor(); + private double min, max; + private boolean syncZoom = true; + private Point crossLoc; + private boolean firstTime = true; + private static int previousID, previousX, previousY; + private Rectangle startingSrcRect; + private boolean done; + private boolean initialized; + private boolean sliceSet; + private Thread thread; + + + public void run(String arg) { + imp = IJ.getImage(); + boolean isStack = imp.getStackSize()>1; + hyperstack = imp.isHyperStack(); + if ((hyperstack||imp.isComposite()) && imp.getNSlices()<=1) + isStack = false; + if (instance!=null) { + if (imp==instance.imp) { + instance.dispose(); + return; + } else if (isStack) { + instance.dispose(); + if (IJ.isMacro()) IJ.wait(1000); + } else { + ImageWindow win = instance.imp!=null?instance.imp.getWindow():null; + if (win!=null) win.toFront(); + return; + } + } + if (!isStack) { + IJ.error("Othogonal Views", "This command requires a stack, or a hypertack with Z>1."); + return; + } + yz_image = WindowManager.getImage(yzID); + rgb = imp.getBitDepth()==24 || hyperstack; + int yzBitDepth = hyperstack?24:imp.getBitDepth(); + if (yz_image==null || yz_image.getHeight()!=imp.getHeight() || yz_image.getBitDepth()!=yzBitDepth) + yz_image = imp.createImagePlus(); + xz_image = WindowManager.getImage(xzID); + if (xz_image==null || xz_image.getWidth()!=imp.getWidth() || xz_image.getBitDepth()!=yzBitDepth) + xz_image = imp.createImagePlus(); + instance = this; + int mode = imp.getCompositeMode(); + ImageProcessor ip = mode==IJ.COMPOSITE?new ColorProcessor(imp.getImage()):imp.getProcessor(); + min = ip.getMin(); + max = ip.getMax(); + cal=this.imp.getCalibration(); + cal_xz = cal.copy(); + cal_yz = cal.copy(); + double calx=cal.pixelWidth; + double caly=cal.pixelHeight; + double calz=cal.pixelDepth; + ax = 1.0; + ay = caly/calx; + az = calz/calx; + if (az>100) { + IJ.error("Z spacing ("+(int)az+") is too large."); + return; + } + win = imp.getWindow(); + canvas = win.getCanvas(); + addListeners(canvas); + magnification= canvas.getMagnification(); + imp.deleteRoi(); + Rectangle r = canvas.getSrcRect(); + if (imp.getID()==previousID) + crossLoc = new Point(previousX, previousY); + else + crossLoc = new Point(r.x+r.width/2, r.y+r.height/2); + imageStack = getStack(); + calibrate(); + if (createProcessors(imageStack)) { + if (ip.isColorLut() || ip.isInvertedLut()) { + ColorModel cm = ip.getColorModel(); + fp1.setColorModel(cm); + fp2.setColorModel(cm); + } + thread = new Thread(this, "Orthogonal Views"); + thread.start(); + IJ.wait(100); + update(); + } else + dispose(); + } + + private ImageStack getStack() { + if (imp.isHyperStack()) { + int slices = imp.getNSlices(); + int c=imp.getChannel(); + int z=imp.getSlice(); + int t=imp.getFrame(); + int mode = imp.getCompositeMode(); + rgb = mode==IJ.COMPOSITE; + ColorModel cm = rgb?null:imp.getProcessor().getColorModel(); + if (cm!=null && fp1!=null && fp1.getBitDepth()!=24) { + fp1.setColorModel(cm); + fp2.setColorModel(cm); + } + ImageStack stack = imp.getStack(); + ImageStack stack2 = new ImageStack(imp.getWidth(), imp.getHeight()); + for (int i=1; i<=slices; i++) { + if (rgb) { + imp.setPositionWithoutUpdate(c, i, t); + stack2.addSlice(null, new ColorProcessor(imp.getImage())); + } else { + int index = imp.getStackIndex(c, i, t); + stack2.addSlice(null, stack.getProcessor(index)); + } + } + if (rgb) + imp.setPosition(c, z, t); + currentChannel = c; + currentFrame = t; + currentMode = mode; + return stack2; + } else + return imp.getStack(); + } + + private void addListeners(ImageCanvas canvas) { + canvas.addMouseListener(this); + canvas.addMouseMotionListener(this); + canvas.addKeyListener(this); + win.addWindowListener (this); + win.addMouseWheelListener(this); + win.addFocusListener(this); + ImagePlus.addImageListener(this); + Executer.addCommandListener(this); + } + + private void calibrate() { + String xunit = cal.getXUnit(); + String yunit = cal.getYUnit(); + String zunit = cal.getZUnit(); + double o_depth=cal.pixelDepth; + double o_height=cal.pixelHeight; + double o_width=cal.pixelWidth; + cal_yz.setXUnit(zunit); + cal_yz.setYUnit(yunit); + cal_yz.setZUnit(xunit); + if (rotateYZ) { + cal_yz.pixelHeight=o_depth/az; + cal_yz.pixelWidth=o_height; + cal_yz.setXUnit(yunit); + cal_yz.setYUnit(zunit); + } else { + cal_yz.pixelWidth=o_depth/az; + cal_yz.pixelHeight=o_height; + } + if (flipXZ) + cal_yz.setInvertY(true); + yz_image.setCalibration(cal_yz); + yz_image.setIJMenuBar(false); + cal_xz.setXUnit(xunit); + cal_xz.setYUnit(zunit); + cal_xz.setZUnit(yunit); + cal_xz.pixelWidth=o_width; + cal_xz.pixelHeight=o_depth/az; + if (flipXZ) + cal_xz.setInvertY(true); + xz_image.setCalibration(cal_xz); + xz_image.setIJMenuBar(false); + } + + private void updateMagnification(int x, int y) { + double magnification= win.getCanvas().getMagnification(); + int z = imp.getSlice()-1; + ImageWindow xz_win = xz_image.getWindow(); + if (xz_win==null) return; + ImageCanvas xz_ic = xz_win.getCanvas(); + double xz_mag = xz_ic.getMagnification(); + double arat = az/ax; + int zcoord=(int)(arat*z); + if (flipXZ) zcoord=(int)(arat*(imp.getNSlices()-z)); + while (xz_magmagnification) { + xz_ic.zoomOut(xz_ic.screenX(x), xz_ic.screenY(zcoord)); + xz_mag = xz_ic.getMagnification(); + } + ImageWindow yz_win = yz_image.getWindow(); + if (yz_win==null) return; + ImageCanvas yz_ic = yz_win.getCanvas(); + double yz_mag = yz_ic.getMagnification(); + zcoord = (int)(arat*z); + while (yz_magmagnification) { + yz_ic.zoomOut(yz_ic.screenX(zcoord), yz_ic.screenY(y)); + yz_mag = yz_ic.getMagnification(); + } + } + + void updateViews(Point p, ImageStack is) { + if (fp1==null) return; + updateXZView(p,is); + + double arat=az/ax; + int width2 = fp1.getWidth(); + int height2 = (int)Math.round(fp1.getHeight()*az); + if (height2<1) height2=1; + if (width2!=fp1.getWidth()||height2!=fp1.getHeight()) { + fp1.setInterpolate(true); + ImageProcessor sfp1=fp1.resize(width2, height2); + if (!rgb) sfp1.setMinAndMax(min, max); + xz_image.setProcessor("XZ "+p.y, sfp1); + } else { + if (!rgb) fp1.setMinAndMax(min, max); + xz_image.setProcessor("XZ "+p.y, fp1); + } + + if (rotateYZ) + updateYZView(p, is); + else + updateZYView(p, is); + + width2 = (int)Math.round(fp2.getWidth()*az); + if (width2<1) width2=1; + height2 = fp2.getHeight(); + String title = "YZ "; + if (rotateYZ) { + width2 = fp2.getWidth(); + height2 = (int)Math.round(fp2.getHeight()*az); + if (height2<1) height2=1; + title = "ZY "; + } + if (width2!=fp2.getWidth()||height2!=fp2.getHeight()) { + fp2.setInterpolate(true); + ImageProcessor sfp2=fp2.resize(width2, height2); + if (!rgb) sfp2.setMinAndMax(min, max); + yz_image.setProcessor(title+p.x, sfp2); + } else { + if (!rgb) fp2.setMinAndMax(min, max); + yz_image.setProcessor(title+p.x, fp2); + } + + calibrate(); + if (yz_image.getWindow()==null) { + yz_image.show(); + ImageCanvas ic = yz_image.getCanvas(); + ic.addKeyListener(this); + ic.addMouseListener(this); + ic.addMouseMotionListener(this); + ic.setCustomRoi(true); + yzID = yz_image.getID(); + } else { + ImageCanvas ic = yz_image.getWindow().getCanvas(); + ic.setCustomRoi(true); + } + if (xz_image.getWindow()==null) { + xz_image.show(); + ImageCanvas ic = xz_image.getCanvas(); + ic.addKeyListener(this); + ic.addMouseListener(this); + ic.addMouseMotionListener(this); + ic.setCustomRoi(true); + xzID = xz_image.getID(); + } else { + ImageCanvas ic = xz_image.getWindow().getCanvas(); + ic.setCustomRoi(true); + } + + } + + void arrangeWindows(boolean sticky) { + ImageWindow xyWin = imp.getWindow(); + if (xyWin==null) return; + Point loc = xyWin.getLocation(); + if ((xyX!=loc.x)||(xyY!=loc.y)) { + xyX = loc.x; + xyY = loc.y; + ImageWindow yzWin =null; + long start = System.currentTimeMillis(); + while (yzWin==null && (System.currentTimeMillis()-start)<=2500L) { + yzWin = yz_image.getWindow(); + if (yzWin==null) IJ.wait(50); + } + if (yzWin!=null) + yzWin.setLocation(xyX+xyWin.getWidth(), xyY); + ImageWindow xzWin =null; + start = System.currentTimeMillis(); + while (xzWin==null && (System.currentTimeMillis()-start)<=2500L) { + xzWin = xz_image.getWindow(); + if (xzWin==null) IJ.wait(50); + } + if (xzWin!=null) + xzWin.setLocation(xyX,xyY+xyWin.getHeight()); + if (firstTime) { + imp.getWindow().toFront(); + if (!sliceSet && imp.getSlice()==1) { + if (hyperstack) + imp.setPosition(imp.getChannel(), imp.getNSlices()/2, imp.getFrame()); + else + imp.setSlice(imp.getNSlices()/2); + } + firstTime = false; + } + } + } + + /** + * @param is - used to get the dimensions of the new ImageProcessors + * @return + */ + boolean createProcessors(ImageStack is) { + ImageProcessor ip=is.getProcessor(1); + int width= is.getWidth(); + int height=is.getHeight(); + int ds=is.getSize(); + double arat=1.0;//az/ax; + double brat=1.0;//az/ay; + int za=(int)(ds*arat); + int zb=(int)(ds*brat); + + if (ip instanceof FloatProcessor) { + fp1=new FloatProcessor(width,za); + if (rotateYZ) + fp2=new FloatProcessor(height,zb); + else + fp2=new FloatProcessor(zb,height); + return true; + } + + if (ip instanceof ByteProcessor) { + fp1=new ByteProcessor(width,za); + if (rotateYZ) + fp2=new ByteProcessor(height,zb); + else + fp2=new ByteProcessor(zb,height); + return true; + } + + if (ip instanceof ShortProcessor) { + fp1=new ShortProcessor(width,za); + if (rotateYZ) + fp2=new ShortProcessor(height,zb); + else + fp2=new ShortProcessor(zb,height); + return true; + } + + if (ip instanceof ColorProcessor) { + fp1=new ColorProcessor(width,za); + if (rotateYZ) + fp2=new ColorProcessor(height,zb); + else + fp2=new ColorProcessor(zb,height); + return true; + } + + return false; + } + + void updateXZView(Point p, ImageStack is) { + int width= is.getWidth(); + int size=is.getSize(); + ImageProcessor ip=is.getProcessor(1); + + int y=p.y; + // XZ + if (ip instanceof ShortProcessor) { + short[] newpix=new short[width*size]; + for (int i=0; iyz_image.getWidth()-yzSrcRect.width) + yzSrcRect.y = yz_image.getWidth()-yzSrcRect.width; + } else { + yzSrcRect.y += dy; + if (yzSrcRect.y<0) + yzSrcRect.y = 0; + if (yzSrcRect.y>yz_image.getHeight()-yzSrcRect.height) + yzSrcRect.y = yz_image.getHeight()-yzSrcRect.height; + } + yzic.repaint(); + int dx = srcRect.x - startingSrcRect.x; + ImageCanvas xzic = xz_image.getCanvas(); + Rectangle xzSrcRect =xzic.getSrcRect(); + xzSrcRect.x += dx; + if (xzSrcRect.x<0) + xzSrcRect.x = 0; + if (xzSrcRect.x>xz_image.getWidth()-xzSrcRect.width) + xzSrcRect.x = xz_image.getWidth()-xzSrcRect.width; + xzic.repaint(); + } + } + + /** Refresh the output windows. */ + synchronized void update() { + notify(); + } + + private void exec() { + if (canvas==null) + return; + int width=imp.getWidth(); + int height=imp.getHeight(); + if (hyperstack) { + int mode = IJ.COMPOSITE; + if (imp.isComposite()) { + mode = ((CompositeImage)imp).getMode(); + if (mode!=currentMode) + imageStack = null; + } + if (imageStack!=null) { + int c = imp.getChannel(); + int t = imp.getFrame(); + if ((mode!=IJ.COMPOSITE&&c!=currentChannel) || t!=currentFrame) + imageStack = null; + } + } + ImageStack is = imageStack; + if (is==null) + is = imageStack = getStack(); + double arat=az/ax; + double brat=az/ay; + Point p=crossLoc; + if (p.y>=height) p.y=height-1; + if (p.x>=width) p.x=width-1; + if (p.x<0) p.x=0; + if (p.y<0) p.y=0; + updateViews(p, is); + GeneralPath path = new GeneralPath(); + drawCross(imp, p, path); + if (!done) + imp.setOverlay(path, color, new BasicStroke(1)); + canvas.setCustomRoi(true); + updateCrosses(p.x, p.y, arat, brat); + if (syncZoom) updateMagnification(p.x, p.y); + arrangeWindows(sticky); + initialized = true; + } + + private void updateCrosses(int x, int y, double arat, double brat) { + Point p; + int z=imp.getNSlices(); + int zlice=imp.getSlice()-1; + int zcoord=(int)Math.round(arat*zlice); + if (flipXZ) + zcoord = (int)Math.round(arat*(z-zlice)); + ImageCanvas xzCanvas = xz_image.getCanvas(); + p=new Point (x, zcoord); + GeneralPath path = new GeneralPath(); + drawCross(xz_image, p, path); + if (!done) + xz_image.setOverlay(path, color, new BasicStroke(1)); + if (rotateYZ) { + if (flipXZ) + zcoord=(int)Math.round(brat*(z-zlice)); + else + zcoord=(int)Math.round(brat*(zlice)); + p=new Point (y, zcoord); + } else { + zcoord=(int)Math.round(arat*zlice); + p=new Point (zcoord, y); + } + path = new GeneralPath(); + drawCross(yz_image, p, path); + if (!done) + yz_image.setOverlay(path, color, new BasicStroke(1)); + IJ.showStatus(imp.getLocationAsString(crossLoc.x, crossLoc.y)); + } + + public void mouseMoved(MouseEvent e) { + } + + public void keyPressed(KeyEvent e) { + int key = e.getKeyCode(); + if (key==KeyEvent.VK_ESCAPE) { + IJ.beep(); + dispose(); + } else if (IJ.shiftKeyDown()) { + int width=imp.getWidth(), height=imp.getHeight(); + switch (key) { + case KeyEvent.VK_LEFT: crossLoc.x--; if (crossLoc.x<0) crossLoc.x=0; break; + case KeyEvent.VK_RIGHT: crossLoc.x++; if (crossLoc.x>=width) crossLoc.x=width-1; break; + case KeyEvent.VK_UP: crossLoc.y--; if (crossLoc.y<0) crossLoc.y=0; break; + case KeyEvent.VK_DOWN: crossLoc.y++; if (crossLoc.y>=height) crossLoc.y=height-1; break; + default: return; + } + update(); + } + } + + public void keyReleased(KeyEvent e) { + } + + public void keyTyped(KeyEvent e) { + } + + public void actionPerformed(ActionEvent ev) { + } + + public void imageClosed(ImagePlus imp) { + if (!done) + dispose(); + } + + public void imageOpened(ImagePlus imp) { + } + + public void imageUpdated(ImagePlus imp) { + if (imp==this.imp) { + ImageProcessor ip = imp.getProcessor(); + min = ip.getMin(); + max = ip.getMax(); + update(); + } + } + + public String commandExecuting(String command) { + if (command.equals("In")||command.equals("Out")) { + ImagePlus cimp = WindowManager.getCurrentImage(); + if (cimp==null) return command; + if (cimp==imp) { + ImageCanvas ic = imp.getCanvas(); + if (ic==null) return null; + int x = ic.screenX(crossLoc.x); + int y = ic.screenY(crossLoc.y); + if (command.equals("In")) { + ic.zoomIn(x, y); + if (ic.getMagnification()<=1.0) imp.repaintWindow(); + } else { + ic.zoomOut(x, y); + if (ic.getMagnification()<1.0) imp.repaintWindow(); + } + xyX=crossLoc.x; xyY=crossLoc.y; + update(); + return null; + } else if (cimp==xz_image || cimp==yz_image) { + syncZoom = false; + return command; + } else + return command; + } else if (command.equals("Flip Vertically")&& xz_image!=null) { + if (xz_image==WindowManager.getCurrentImage()) { + flipXZ = !flipXZ; + update(); + return null; + } else + return command; + } else + return command; + } + + public void windowActivated(WindowEvent e) { + arrangeWindows(sticky); + } + + public void windowClosed(WindowEvent e) { + } + + public void windowClosing(WindowEvent e) { + if (!done) + dispose(); + } + + public void windowDeactivated(WindowEvent e) { + } + + public void windowDeiconified(WindowEvent e) { + arrangeWindows(sticky); + } + + public void windowIconified(WindowEvent e) { + } + + public void windowOpened(WindowEvent e) { + } + + public void adjustmentValueChanged(AdjustmentEvent e) { + update(); + } + + public void mouseWheelMoved(MouseWheelEvent e) { + if (e.getSource().equals(xz_image.getWindow())) { + crossLoc.y += e.getWheelRotation(); + } else if (e.getSource().equals(yz_image.getWindow())) { + crossLoc.x += e.getWheelRotation(); + } + update(); + } + + public void focusGained(FocusEvent e) { + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) canvas.requestFocus(); + arrangeWindows(sticky); + } + + public void focusLost(FocusEvent e) { + arrangeWindows(sticky); + } + + public static ImagePlus getImage() { + if (instance!=null) + return instance.imp; + else + return null; + } + + public static int getImageID() { + ImagePlus img = getImage(); + return img!=null?img.getID():0; + } + + /** Returns the IDs of the XY, YZ and XZ images as an int array.*/ + public static int[] getImageIDs() { + int[] ids = new int[3]; + Orthogonal_Views instance2 = getInstance(); + if (instance2==null) + return ids; + ids[0] = instance2.imp.getID(); + ids[1] = instance2.yz_image.getID(); + ids[2] = instance2.xz_image.getID(); + return ids; + } + + public static void stop() { + if (instance!=null) + instance.dispose(); + } + + public static synchronized boolean isOrthoViewsImage(ImagePlus imp) { + if (imp==null || instance==null) + return false; + else + return imp==instance.imp || imp==instance.xz_image || imp==instance.yz_image; + } + + public static Orthogonal_Views getInstance() { + return instance; + } + + public int[] getCrossLoc() { + int[] loc = new int[3]; + loc[0] = crossLoc.x; + loc[1] = crossLoc.y; + loc[2] = imp.getSlice()-1; + return loc; + } + + public void setCrossLoc(int x, int y, int z) { + crossLoc.setLocation(x, y); + int slice = z+1; + if (slice!=imp.getSlice()) { + if (hyperstack) + imp.setPosition(imp.getChannel(), slice, imp.getFrame()); + else + imp.setSlice(slice); + sliceSet = true; + } + while (!initialized) { + IJ.wait(10); + } + update(); + } + + public ImagePlus getXZImage(){ + return xz_image; + } + + public ImagePlus getYZImage(){ + return yz_image; + } + + public void run() { + while (!done) { + synchronized(this) { + try {wait();} + catch(InterruptedException e) {} + } + if (!done) + exec(); + } + } + +} diff --git a/src/ij/plugin/OverlayCommands.java b/src/ij/plugin/OverlayCommands.java new file mode 100644 index 0000000..3c97fc4 --- /dev/null +++ b/src/ij/plugin/OverlayCommands.java @@ -0,0 +1,465 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.plugin.frame.RoiManager; +import ij.plugin.frame.Recorder; +import ij.macro.Interpreter; +import ij.io.RoiDecoder; +import ij.plugin.filter.PlugInFilter; +import ij.measure.*; +import java.awt.*; +import java.util.ArrayList; +import java.awt.geom.Rectangle2D; + +/** This plugin implements the commands in the Image/Overlay menu. */ +public class OverlayCommands implements PlugIn { + private static int opacity = 100; + private static Roi defaultRoi; + private static boolean zeroTransparent; + + static { + defaultRoi = new Roi(0, 0, 1, 1); + defaultRoi.setPosition(1); // set stacks positions by default + } + + public void run(String arg) { + if (arg.equals("add")) + addSelection(); + else if (arg.equals("image")) + addImage(false); + else if (arg.equals("image-roi")) + addImage(true); + else if (arg.equals("flatten")) + flatten(); + else if (arg.equals("hide")) + hide(); + else if (arg.equals("show")) + show(); + else if (arg.equals("remove")) + remove(); + else if (arg.equals("from")) + fromRoiManager(); + else if (arg.equals("to")) + toRoiManager(); + else if (arg.equals("list")) + list(); + else if (arg.equals("measure")) + measure(); + else if (arg.equals("options")) + options(); + } + + private void measure() { + ImagePlus imp = IJ.getImage(); + if (imp==null) + return; + Overlay overlay = imp.getOverlay(); + if (overlay==null) { + IJ.error("Overlay required"); + return; + } + Roi roi0 = imp.getRoi(); + Roi roi1 = roi0; + if (roi0!=null && !roi0.isArea()) + roi0 = null; + boolean isPoints = false; + for (int i=0; iAdd\" requires a selection."); + gd.setInsets(15, 40, 0); + gd.addCheckbox("Remove existing overlay", false); + gd.showDialog(); + if (gd.wasCanceled()) return; + if (gd.getNextBoolean()) + imp.setOverlay(null); + return; + } + if (roi==null) { + IJ.error("This command requires a selection."); + return; + } + roi = (Roi)roi.clone(); + Overlay overlay = imp.getOverlay(); + if (!roi.isDrawingTool()) { + if (roi.getStroke()==null) + roi.setStrokeWidth(defaultRoi.getStrokeWidth()); + if (roi.getStrokeColor()==null || Line.getWidth()>1&&defaultRoi.getStrokeColor()!=null) + roi.setStrokeColor(defaultRoi.getStrokeColor()); + if (roi.getFillColor()==null) + roi.setFillColor(defaultRoi.getFillColor()); + } + setPosition(imp, roi); + boolean points = roi instanceof PointRoi && ((PolygonRoi)roi).getNCoordinates()>1; + if (IJ.altKeyDown() || (IJ.macroRunning() && Macro.getOptions()!=null)) { + RoiProperties rp = new RoiProperties("Add to Overlay", roi); + if (!rp.showDialog()) return; + defaultRoi.setStrokeColor(roi.getStrokeColor()); + defaultRoi.setStrokeWidth(roi.getStrokeWidth()); + defaultRoi.setFillColor(roi.getFillColor()); + } + String name = roi.getName(); + boolean newOverlay = name!=null && name.equals("new-overlay"); + Roi roiClone = (Roi)roi.clone(); + if (roi.getStrokeColor()==null) + roi.setStrokeColor(Roi.getColor()); + if (overlay==null || newOverlay) + overlay = OverlayLabels.createOverlay(); + overlay.add(roi); + imp.setOverlay(overlay); + boolean brushRoi = roi.getType()==Roi.COMPOSITE && Toolbar.getToolId()==Toolbar.OVAL && Toolbar.getBrushSize()>0; + if (points || (roi instanceof ImageRoi) || (roi instanceof Arrow&&!Prefs.keepArrowSelections) || brushRoi) + imp.deleteRoi(); + Undo.setup(Undo.OVERLAY_ADDITION, imp); + } + + void addImage(boolean createImageRoi) { + ImagePlus imp = IJ.getImage(); + int[] wList = WindowManager.getIDList(); + if (wList==null || wList.length<2) { + IJ.error("Add Image...", "The command requires at least two open images."); + return; + } + String[] titles = new String[wList.length]; + for (int i=0; iimp.getWidth() && overlay.getHeight()>imp.getHeight()) { + IJ.error("Add Image...", "Image to be added cannnot be larger than\n\""+imp.getTitle()+"\"."); + return; + } + if (createImageRoi && x==0 && y==0) { + x = imp.getWidth()/2-overlay.getWidth()/2; + y = imp.getHeight()/2-overlay.getHeight()/2; + } + roi = new ImageRoi(x, y, overlay.getProcessor()); + roi.setName(overlay.getShortTitle()); + if (opacity!=100) + ((ImageRoi)roi).setOpacity(opacity/100.0); + ((ImageRoi)roi).setZeroTransparent(zeroTransparent); + if (createImageRoi) + imp.setRoi(roi); + else { + setPosition(imp, roi); + Overlay overlayList = imp.getOverlay(); + if (overlayList==null) + overlayList = new Overlay(); + overlayList.add(roi); + imp.setOverlay(overlayList); + Undo.setup(Undo.OVERLAY_ADDITION, imp); + } + } + + private void setPosition(ImagePlus imp, Roi roi) { + int stackSize = imp.getStackSize(); + if (roi.hasHyperStackPosition() && imp.isHyperStack()) + return; + if (roi.getPosition()>0 && stackSize>1) + return; + boolean setPos = defaultRoi.getPosition()!=0; + if (setPos && stackSize>1) { + if (imp.isHyperStack()||imp.isComposite()) { + boolean compositeMode = imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.COMPOSITE; + int channel = !compositeMode||imp.getNChannels()==stackSize?imp.getChannel():0; + if (imp.getNSlices()>1) + roi.setPosition(channel, imp.getSlice(), 0); + else if (imp.getNFrames()>1) + roi.setPosition(channel, 0, imp.getFrame()); + } else + roi.setPosition(imp.getCurrentSlice()); + } + } + + void hide() { + ImagePlus imp = IJ.getImage(); + imp.setHideOverlay(true); + RoiManager rm = RoiManager.getInstance(); + if (rm!=null) rm.runCommand("show none"); + } + + void show() { + ImagePlus imp = IJ.getImage(); + imp.setHideOverlay(false); + if (imp.getOverlay()==null) { + RoiManager rm = RoiManager.getInstance(); + if (rm!=null && rm.getCount()>1) { + if (!IJ.isMacro()) rm.toFront(); + rm.runCommand("show all with labels"); + } + } + } + + void remove() { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) { + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) + ic.setShowAllList(null); + imp.setOverlay(null); + } + } + + void flatten() { + ImagePlus imp = IJ.getImage(); + Roi roi = imp.getRoi(); + if (imp.getStackSize()>1 && roi!=null && (roi instanceof PointRoi)) { + ImagePlus imp2 = imp.flatten(); + imp2.setTitle(WindowManager.getUniqueName(imp.getTitle())); + imp2.show(); + return; + } + Overlay overlay = imp.getOverlay(); + Overlay roiManagerOverlay = null; + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) + roiManagerOverlay = ic.getShowAllList(); + if (imp.getBitDepth()==24 && overlay==null && imp.getRoi()==null && roiManagerOverlay==null && !imp.isComposite() && !IJ.macroRunning()) { + IJ.error("Flatten", "Overlay or selection required to flatten RGB image"); + return; + } + int flags = IJ.setupDialog(imp, 0); + if (flags==PlugInFilter.DONE) + return; + else if (flags==PlugInFilter.DOES_STACKS && !(imp.isComposite()&&overlay==null)) { + //Added by Marcel Boeglin 2014.01.24 + if (overlay==null && roiManagerOverlay==null && !imp.isComposite()) { + IJ.error("Flatten", "Overlay or multi-channel image required"); + return; + } + flattenStack(imp); + if (Recorder.record) + Recorder.recordCall("imp.flattenStack();"); + } else { + ImagePlus imp2 = imp.flatten(); + imp2.setTitle(WindowManager.getUniqueName(imp.getTitle())); + imp2.show(); + if (Recorder.record) // Added by Marcel Boeglin 2014.01.12 + Recorder.recordCall("imp = imp.flatten();"); + } + } + + + //Marcel Boeglin 2014.01.25 + void flattenStack(ImagePlus imp) { + imp.flattenStack(); + } + + void fromRoiManager() { + ImagePlus imp = IJ.getImage(); + RoiManager rm = RoiManager.getInstance2(); + if (rm==null) { + IJ.error("ROI Manager is not open"); + return; + } + Roi[] rois = rm.getRoisAsArray(); + if (rois.length==0) { + IJ.error("ROI Manager is empty"); + return; + } + rm.moveRoisToOverlay(imp); + imp.deleteRoi(); + } + + void toRoiManager() { + ImagePlus imp = IJ.getImage(); + Overlay overlay = imp.getOverlay(); + if (overlay==null) { + IJ.error("Overlay required"); + return; + } + RoiManager rm = RoiManager.getInstance2(); + if (rm==null) + rm = new RoiManager(); + if (overlay.size()>=4 && overlay.get(3).getPosition()!=0) + Prefs.showAllSliceOnly = true; + rm.setOverlay(overlay); + imp.setOverlay(null); + } + + void options() { + ImagePlus imp = WindowManager.getCurrentImage(); + Overlay overlay = null; + Roi roi = null; + if (imp!=null) { + overlay = imp.getOverlay(); + roi = imp.getRoi(); + if (roi!=null) + roi = (Roi)roi.clone(); + } + if (roi==null) + roi = defaultRoi; + if (roi==null) { + int size = imp!=null?imp.getWidth():512; + roi = new Roi(0, 0, size/4, size/4); + } + if (!roi.isDrawingTool()) { + if (roi.getStroke()==null) + roi.setStrokeWidth(defaultRoi.getStrokeWidth()); + if (roi.getStrokeColor()==null || Line.getWidth()>1&&defaultRoi.getStrokeColor()!=null) + roi.setStrokeColor(defaultRoi.getStrokeColor()); + if (roi.getFillColor()==null) + roi.setFillColor(defaultRoi.getFillColor()); + } + boolean points = roi instanceof PointRoi && ((PolygonRoi)roi).getNCoordinates()>1; + if (points) roi.setStrokeColor(Color.red); + roi.setPosition(defaultRoi.getPosition()); + RoiProperties rp = new RoiProperties("Overlay Options", roi); + if (!rp.showDialog()) return; + defaultRoi = roi; + } + + void list() { + ImagePlus imp = IJ.getImage(); + Overlay overlay = imp.getOverlay(); + if (overlay!=null) + listRois(overlay.toArray()); + } + + public static void listRois(Roi[] rois) { + ImagePlus imp = WindowManager.getCurrentImage(); + ResultsTable rt = new ResultsTable(); + rt.showRowNumbers(true); + for (int i=0; i1) { + t = z; + z = 0; + } + rt.setValue("Index", i, i); + rt.setValue("Name", i, rois[i].getName()); + rt.setValue("Type", i, rois[i].getTypeAsString()); + rt.setValue("Group", i, group); + if (rois[i] instanceof PointRoi) { + Rectangle2D.Double bounds = rois[i].getFloatBounds(); + rt.setValue("X", i, (int)Math.round(bounds.x)); + rt.setValue("Y", i, (int)Math.round(bounds.y)); + } else if (rois[i] instanceof Arrow) { + Polygon p = ((Arrow)rois[i]).getPoints(); + rt.setValue("X", i, p.xpoints[1]); + rt.setValue("Y", i, p.ypoints[1]); + } else { + rt.setValue("X", i, r.x); + rt.setValue("Y", i, r.y); + } + rt.setValue("Width", i, r.width); + rt.setValue("Height", i, r.height); + rt.setValue("Points", i, rois[i].size()); + rt.setValue("Color", i, color); + rt.setValue("Fill", i, fill); + rt.setValue("LWidth", i, sWidth); + rt.setValue("Pos", i, position); + rt.setValue("C", i, c); + rt.setValue("Z", i, z); + rt.setValue("T", i, t); + } + String title = imp!=null?" of "+imp.getTitle():""; + rt.show("Overlay Elements"+title); + } + +} diff --git a/src/ij/plugin/OverlayLabels.java b/src/ij/plugin/OverlayLabels.java new file mode 100644 index 0000000..cec0e82 --- /dev/null +++ b/src/ij/plugin/OverlayLabels.java @@ -0,0 +1,120 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.util.Tools; +import ij.plugin.filter.Analyzer; +import java.awt.*; +import java.util.Vector; + +/** This plugin implements the Image/Overlay/Labels command. */ +public class OverlayLabels implements PlugIn, DialogListener { + private static final String[] fontSizes = {"7", "8", "9", "10", "12", "14", "18", "24", "28", "36", "48", "72"}; + private static Overlay defaultOverlay = new Overlay(); + private ImagePlus imp; + private Overlay overlay; + private GenericDialog gd; + private boolean showLabels; + private boolean showNames; + private boolean drawBackgrounds; + private String colorName; + private int fontSize; + private boolean bold; + + public void run(String arg) { + imp = WindowManager.getCurrentImage(); + overlay = null; + if (imp!=null) { + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) + overlay = ic.getShowAllList(); + if (overlay==null) + overlay = imp.getOverlay(); + } + if (overlay==null) + overlay = defaultOverlay; + showDialog(); + if (!gd.wasCanceled()) { + defaultOverlay.drawLabels(overlay.getDrawLabels()); + defaultOverlay.drawNames(overlay.getDrawNames()); + defaultOverlay.drawBackgrounds(overlay.getDrawBackgrounds()); + defaultOverlay.setLabelColor(overlay.getLabelColor()); + defaultOverlay.setLabelFont(overlay.getLabelFont()); + } + } + + public void showDialog() { + showLabels = overlay.getDrawLabels(); + showNames = overlay.getDrawNames(); + drawBackgrounds = overlay.getDrawBackgrounds(); + colorName = Colors.getColorName(overlay.getLabelColor(), "white"); + fontSize = 12; + Font font = overlay.getLabelFont(); + if (font!=null) { + fontSize = font.getSize(); + bold = font.getStyle()==Font.BOLD; + } + gd = new GenericDialog("Labels"); + gd.addChoice("Color:", Colors.colors, colorName); + gd.addChoice("Font size:", fontSizes, ""+fontSize); + gd.addCheckbox("Show labels", showLabels); + gd.addCheckbox("Use names as labels", showNames); + gd.addCheckbox("Draw backgrounds", drawBackgrounds); + gd.addCheckbox("Bold", bold); + gd.addDialogListener(this); + gd.showDialog(); + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (gd.wasCanceled()) return false; + String colorName2 = colorName; + boolean showLabels2 = showLabels; + boolean showNames2 = showNames; + boolean drawBackgrounds2 = drawBackgrounds; + boolean bold2 = bold; + int fontSize2 = fontSize; + colorName = gd.getNextChoice(); + fontSize = (int)Tools.parseDouble(gd.getNextChoice(), 12); + showLabels = gd.getNextBoolean(); + showNames = gd.getNextBoolean(); + drawBackgrounds = gd.getNextBoolean(); + bold = gd.getNextBoolean(); + boolean colorChanged = !colorName.equals(colorName2); + boolean sizeChanged = fontSize!=fontSize2; + boolean changes = showLabels!=showLabels2 || showNames!=showNames2 + || drawBackgrounds!=drawBackgrounds2 || colorChanged || sizeChanged + || bold!=bold2; + if (changes) { + if ((showNames&&!showNames2) || colorChanged || sizeChanged) { + showLabels = true; + Vector checkboxes = gd.getCheckboxes(); + ((Checkbox)checkboxes.elementAt(0)).setState(true); + } + overlay.drawLabels(showLabels); + Analyzer.drawLabels(showLabels); + overlay.drawNames(showNames); + overlay.drawBackgrounds(drawBackgrounds); + Color color = Colors.getColor(colorName, Color.white); + overlay.setLabelColor(color); + if (sizeChanged || bold || bold!=bold2) + overlay.setLabelFont(new Font("SansSerif", bold?Font.BOLD:Font.PLAIN, fontSize)); + if (imp!=null) { + Overlay o = imp.getOverlay(); + if (o==null) { + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) + o = ic.getShowAllList(); + } + if (o!=null) + imp.draw(); + } + } + return true; + } + + /** Creates an empty Overlay that has the current label settings. */ + public static Overlay createOverlay() { + return defaultOverlay.duplicate(); + } + +} diff --git a/src/ij/plugin/PGM_Reader.java b/src/ij/plugin/PGM_Reader.java new file mode 100644 index 0000000..9d914eb --- /dev/null +++ b/src/ij/plugin/PGM_Reader.java @@ -0,0 +1,320 @@ +package ij.plugin; + +import ij.*; +import ij.io.*; +import ij.process.*; +import java.io.*; + +/** + * This plugin opens PxM format images. + *

+ * The portable graymap format is a lowest common denominator + * grayscale file format. The definition is as follows: + *

+ * - A "magic number" for identifying the file type. A pgm + * file's magic number is the two characters "P2". + * - Whitespace (blanks, TABs, CRs, LFs). + * - A width, formatted as ASCII characters in decimal. + * - Whitespace. + * - A height, again in ASCII decimal. + * - Whitespace. + * - The maximum gray value, again in ASCII decimal. + * - Whitespace. + * - Width * height gray values, each in ASCII decimal, between + * 0 and the specified maximum value, separated by whi- + * tespace, starting at the top-left corner of the graymap, + * proceeding in normal English reading order. A value of 0 + * means black, and the maximum value means white. + * - Characters from a "#" to the next end-of-line are ignored (comments). + * - No line should be longer than 70 characters. + *

+ * Here is an example of a small graymap in this format: + * P2 + * # feep.pgm + * 24 7 + * 15 + * 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + * 0 3 3 3 3 0 0 7 7 7 7 0 0 11 11 11 11 0 0 15 15 15 15 0 + * 0 3 0 0 0 0 0 7 0 0 0 0 0 11 0 0 0 0 0 15 0 0 15 0 + * 0 3 3 3 0 0 0 7 7 7 0 0 0 11 11 11 0 0 0 15 15 15 15 0 + * 0 3 0 0 0 0 0 7 0 0 0 0 0 11 0 0 0 0 0 15 0 0 0 0 + * 0 3 0 0 0 0 0 7 7 7 7 0 0 11 11 11 11 0 0 15 0 0 0 0 + * 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + *

+ * There is a PGM variant that stores the pixel data as raw bytes: + *

+ * -The "magic number" is "P5" instead of "P2". + * -The gray values are stored as plain bytes, instead of ASCII decimal. + * -No whitespace is allowed in the grays section, and only a single + * character of whitespace (typically a newline) is allowed after the maxval. + * -The files are smaller and many times faster to read and write. + *

+ * Kai Barthel Nov 16 2004: + * Extended to support PPM (portable pixmap) format images (24 bits only). + * -The "magic numbers" are "P6" (raw) "P3" (ASCII). + *

+ * Ulf Dittmer April 2005: + * Extended to support PBM (bitmap) images (P1 and P4) + *

+ * Jarek Sacha (jarek.at.ieee.org) December 2005: + * Extended PPM support to 48 bit color images. + */ + +public class PGM_Reader extends ImagePlus implements PlugIn { + + private int width, height; + private boolean rawBits; + private boolean sixteenBits; + private boolean isColor; + private boolean isBlackWhite; + private int maxValue; + + public void run(String arg) { + OpenDialog od = new OpenDialog("PBM/PGM/PPM Reader...", arg); + String directory = od.getDirectory(); + String name = od.getFileName(); + if (name == null) + return; + String path = directory + name; + + IJ.showStatus("Opening: " + path); + ImageStack stack; + try { + stack = openFile(path); + } + catch (IOException e) { + String msg = e.getMessage(); + IJ.showMessage("PBM/PGM/PPM Reader", msg.equals("") ? "" + e : msg); + return; + } + setStack(name, stack); + FileInfo fi = new FileInfo(); + fi.fileFormat = FileInfo.PGM; + fi.directory = directory; + fi.fileName = name; + setFileInfo(fi); + if (arg.equals("")) show(); + } + + public ImageStack openFile(String path) throws IOException { + InputStream is = new BufferedInputStream(new FileInputStream(path)); + try { + StreamTokenizer tok = new StreamTokenizer(is); //deprecated, but it works + //Reader r = new BufferedReader(new InputStreamReader(is)); + //StreamTokenizer tok = new StreamTokenizer(r); // doesn't work + tok.resetSyntax(); + tok.wordChars(33, 255); + tok.whitespaceChars(0, ' '); + tok.parseNumbers(); + tok.eolIsSignificant(true); + tok.commentChar('#'); + openHeader(tok); + //IJ.log("PGM_Reader: w="+width+",h="+height+",raw="+rawBits+",16bits="+sixteenBits+",color="+isColor+",b&w="+isBlackWhite+",max="+maxValue); + + if (!isColor && sixteenBits) { // 16-bit grayscale + if (rawBits) { + ImageProcessor ip = open16bitRawImage(is, width, height); + ImageStack stack = new ImageStack(width, height); + stack.addSlice("", ip); + return stack; + } else { + ImageProcessor ip = open16bitAsciiImage(tok, width, height); + ImageStack stack = new ImageStack(width, height); + stack.addSlice("", ip); + return stack; + } + } + + if (!isColor) { // 8-bit grayscale + byte[] pixels = new byte[width * height]; + ImageProcessor ip = new ByteProcessor(width, height, pixels, null); + if (rawBits) + openRawImage(is, width * height, pixels); + else + openAsciiImage(tok, width * height, pixels); + for (int i = pixels.length - 1; i >= 0; i--) { + if (isBlackWhite) { + if (rawBits) { + if (i < (pixels.length / 8)) { + for (int bit = 7; bit >= 0; bit--) { + pixels[8 * i + 7 - bit] = (byte) ((pixels[i] & ((int) Math.pow(2, bit))) == 0 ? 255 : 0); + } + } + } else + pixels[i] = (byte) (pixels[i] == 0 ? 255 : 0); + } else + pixels[i] = (byte) (0xff & (255 * (int) (0xff & pixels[i]) / maxValue)); + } + ImageStack stack = new ImageStack(width, height); + stack.addSlice("", ip); + return stack; + } + + if (!sixteenBits) { // 8-bit color + int[] pixels = new int[width * height]; + byte[] bytePixels = new byte[3 * width * height]; + ImageProcessor ip = new ColorProcessor(width, height, pixels); + if (rawBits) + openRawImage(is, 3 * width * height, bytePixels); + else + openAsciiImage(tok, 3 * width * height, bytePixels); + for (int i = 0; i < width * height; i++) { + int r = (int) (0xff & bytePixels[i * 3]); + int g = (int) (0xff & bytePixels[i * 3 + 1]); + int b = (int) (0xff & bytePixels[i * 3 + 2]); + r = (r * 255 / maxValue) << 16; + g = (g * 255 / maxValue) << 8; + b = (b * 255 / maxValue); + pixels[i] = 0xFF000000 | r | g | b; + } + ImageStack stack = new ImageStack(width, height); + stack.addSlice("", ip); + return stack; + } + + // 16-bit raw color + short[] red = new short[width*height]; + short[] green = new short[width*height]; + short[] blue = new short[width*height]; + if (rawBits) { + byte[] bytePixels = new byte[6*width*height]; + openRawImage(is, 6*width*height, bytePixels); + for (int i=0; i 255; + } else + maxValue = 255; + if (sixteenBits && maxValue > 65535) + throw new IOException("The maximum gray value is larger than 65535."); + } + + public void openAsciiImage(StreamTokenizer tok, int size, byte[] pixels) throws IOException { + int i = 0; + int inc = size/20; + if (inc==0) inc = 1; + while (tok.nextToken() != tok.TT_EOF) { + if (tok.ttype == tok.TT_NUMBER) { + pixels[i++] = (byte) (((int) tok.nval) & 255); + if (i%inc==0) + IJ.showProgress(0.5 + ((double) i / size) / 2.0); + } + } + IJ.showProgress(1.0); + } + + public void openRawImage(InputStream is, int size, byte[] pixels) throws IOException { + int count = 0; + while (count < size && count >= 0) + count = is.read(pixels, count, size - count); + } + + public ImageProcessor open16bitRawImage(InputStream is, int width, int height) throws IOException { + int size = width * height * 2; + byte[] bytes = new byte[size]; + int count = 0; + while (count < size && count >= 0) + count = is.read(bytes, count, size - count); + short[] pixels = new short[size / 2]; + for (int i = 0, j = 0; i < size / 2; i++, j += 2) + pixels[i] = (short) (((bytes[j] & 0xff) << 8) | (bytes[j + 1] & 0xff)); //big endian + return new ShortProcessor(width, height, pixels, null); + } + + public ImageProcessor open16bitAsciiImage(StreamTokenizer tok, int width, int height) throws IOException { + int i = 0; + int size = width * height; + int inc = size/20; // Progress update interval + if (inc==0) inc = 1; + short[] pixels = new short[size]; + while (tok.nextToken() != tok.TT_EOF) { + if (tok.ttype == tok.TT_NUMBER) { + pixels[i++] = (short) (((int) tok.nval) & 65535); + if (i%inc==0) + IJ.showProgress(0.5 + ((double) i / size) / 2.0); + } + } + IJ.showProgress(1.0); + return new ShortProcessor(width, height, pixels, null); + } + + String getWord(StreamTokenizer tok) throws IOException { + while (tok.nextToken() != tok.TT_EOF) { + if (tok.ttype == tok.TT_WORD) + return tok.sval; + } + return null; + } + + int getInt(StreamTokenizer tok) throws IOException { + while (tok.nextToken() != tok.TT_EOF) { + if (tok.ttype == tok.TT_NUMBER) + return (int) tok.nval; + } + return -1; + } + +} diff --git a/src/ij/plugin/PNG_Writer.java b/src/ij/plugin/PNG_Writer.java new file mode 100644 index 0000000..7960014 --- /dev/null +++ b/src/ij/plugin/PNG_Writer.java @@ -0,0 +1,104 @@ +package ij.plugin; +import ij.*; +import ij.io.*; +import ij.process.*; +import java.awt.*; +import java.io.*; +import java.awt.image.*; +import javax.imageio.ImageIO; + + +/** Saves in PNG format using the ImageIO classes. RGB images are saved + as RGB PNGs. All other image types are saved as 8-bit PNGs. With 8-bit images, + the value of the transparent index can be set in the Edit/Options/Input-Output dialog, + or by calling Prefs.setTransparentIndex(index), where 0<=index<=255. */ +public class PNG_Writer implements PlugIn { + ImagePlus imp; + + public void run(String path) { + imp = WindowManager.getCurrentImage(); + if (imp==null) { + IJ.noImage(); + return; + } + if (path.equals("")) { + SaveDialog sd = new SaveDialog("Save as PNG...", imp.getTitle(), ".png"); + String name = sd.getFileName(); + if (name==null) + return; + String dir = sd.getDirectory(); + path = dir + name; + } + try { + writeImage(imp, path, Prefs.getTransparentIndex()); + } catch (Exception e) { + String msg = e.getMessage(); + if (msg==null || msg.equals("")) + msg = ""+e; + msg = "An error occured writing the file.\n \n" + msg; + if (msg.contains("NullPointerException")) + msg = "Incorrect file path:"; + msg += "\n \n"+path; + IJ.error("PNG Writer", msg); + } + IJ.showStatus(""); + } + + public void writeImage(ImagePlus imp, String path, int transparentIndex) throws Exception { + if (imp.getType()==ImagePlus.COLOR_256) { + imp = imp.duplicate(); + new ImageConverter(imp).convertToRGB(); + } + if (imp.getStackSize()==4 && imp.getBitDepth()==8 && "alpha".equalsIgnoreCase(imp.getStack().getSliceLabel(4))) + writeFourChannelsWithAlpha(imp, path); + else if (transparentIndex>=0 && transparentIndex<=255 && imp.getBitDepth()==8) + writeImageWithTransparency(imp, path, transparentIndex); + else if (imp.getOverlay()!=null && !imp.getHideOverlay() && !imp.tempOverlay()) + ImageIO.write(imp.flatten().getBufferedImage(), "png", new File(path)); + else if (imp.getBitDepth()==16 && !imp.isComposite() && imp.getProcessor().isDefaultLut()) + write16gs(imp, path); + else + ImageIO.write(imp.getBufferedImage(), "png", new File(path)); + } + + private void writeFourChannelsWithAlpha(ImagePlus imp, String path) throws Exception { + ImageStack stack = imp.getStack(); + int w=imp.getWidth(), h=imp.getHeight(); + ImagePlus imp2 = new ImagePlus("", new ColorProcessor(w,h)); + ColorProcessor cp = (ColorProcessor)imp2.getProcessor(); + for (int channel=1; channel<=4; channel++) + cp.setChannel(channel, (ByteProcessor)stack.getProcessor(channel)); + BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + WritableRaster raster = bi.getRaster(); + raster.setDataElements(0, 0, w, h, cp.getPixels()); + ImageIO.write(bi, "png", new File(path)); + } + + void writeImageWithTransparency(ImagePlus imp, String path, int transparentIndex) throws Exception { + int width = imp.getWidth(); + int height = imp.getHeight(); + ImageProcessor ip = imp.getProcessor(); + IndexColorModel cm = (IndexColorModel)ip.getColorModel(); + int size = cm.getMapSize(); + byte[] reds = new byte[256]; + byte[] greens = new byte[256]; + byte[] blues = new byte[256]; + cm.getReds(reds); + cm.getGreens(greens); + cm.getBlues(blues); + cm = new IndexColorModel(8, 256, reds, greens, blues, transparentIndex); + WritableRaster wr = cm.createCompatibleWritableRaster(width, height); + DataBufferByte db = (DataBufferByte)wr.getDataBuffer(); + byte[] biPixels = db.getData(); + System.arraycopy(ip.getPixels(), 0, biPixels, 0, biPixels.length); + BufferedImage bi = new BufferedImage(cm, wr, false, null); + ImageIO.write(bi, "png", new File(path)); + } + + void write16gs(ImagePlus imp, String path) throws Exception { + ShortProcessor sp = (ShortProcessor)imp.getProcessor(); + BufferedImage bi = sp.get16BitBufferedImage(); + File f = new File(path); + ImageIO.write(bi, "png", f); + } +} diff --git a/src/ij/plugin/PNM_Writer.java b/src/ij/plugin/PNM_Writer.java new file mode 100644 index 0000000..e3a3f13 --- /dev/null +++ b/src/ij/plugin/PNM_Writer.java @@ -0,0 +1,107 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.io.SaveDialog; +import java.io.*; +import java.util.*; +import java.awt.image.*; + +/* + This plugin saves grayscale images in PGM (portable graymap) format + and RGB images in PPM (portable pixmap) format. These formats, + along with PBM (portable bitmap), are collectively known as the + PNM format. More information can be found at + "http://en.wikipedia.org/wiki/Portable_Pixmap_file_format". + + @author Johannes Schindelin + */ +public class PNM_Writer implements PlugIn { + + public void run(String path) { + ImagePlus img=IJ.getImage(); + boolean isGray = false; + String extension = null; + ImageProcessor ip = img.getProcessor(); + if (img.getBitDepth()==24) + extension = ".pnm"; + else { + if (img.getBitDepth()==8&& ip.isInvertedLut()) { + ip = ip.duplicate(); + ip.invert(); + } + if (img.getBitDepth()!=16) + ip = ip.convertToByte(true); + isGray = true; + extension = ".pgm"; + } + String title=img.getTitle(); + int length=title.length(); + for (int i=2;i<5;i++) + if (length>i+1 && title.charAt(length-i)=='.') { + title=title.substring(0,length-i); + break; + } + if (path==null || path.equals("")) { + SaveDialog od = new SaveDialog("PNM Writer", title, extension); + String dir=od.getDirectory(); + String name=od.getFileName(); + if (name==null) + return; + path = dir + name; + } + IJ.showStatus("Writing PNM "+path+"..."); + if (img.getBitDepth()==16) { + save16BitImage(ip, path); + return; + } + try { + OutputStream fileOutput = new FileOutputStream(path); + DataOutputStream output = new DataOutputStream(fileOutput); + int w = img.getWidth(), h = img.getHeight(); + output.writeBytes((isGray ? "P5" : "P6") + + "\n# Written by ImageJ PNM Writer\n" + + w + " " + h + "\n255\n"); + if (isGray) + output.write((byte[])ip.getPixels(), 0, w*h); + else { + byte[] pixels = new byte[w * h * 3]; + ColorProcessor proc = + (ColorProcessor)ip; + for (int j = 0; j < h; j++) + for (int i = 0; i < w; i++) { + int c = proc.getPixel(i, j); + pixels[3 * (i + w * j) + 0] = + (byte)((c & 0xff0000) >> 16); + pixels[3 * (i + w * j) + 1] = + (byte)((c & 0xff00) >> 8); + pixels[3 * (i + w * j) + 2] = + (byte)(c & 0xff); + } + output.write(pixels, 0, pixels.length); + } + output.close(); + } catch(IOException e) { + IJ.handleException(e); + } + IJ.showStatus(""); + } + + private void save16BitImage(ImageProcessor ip, String path) { + ip.resetMinAndMax(); + int max = (int)ip.getMax(); + if (max<256) max=256; + try { + DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(path))); + output.writeBytes("P5\n# Written by ImageJ PNM Writer\n" + ip.getWidth() + " " + ip.getHeight() + "\n"+max+"\n"); + for (int i=0; i>MacroConstants.TOK_SHIFT; + Symbol symbol = symbolTable[address]; + name = symbol.str; + break; + } + } + } + if (name==null) + return null; + int index = name.indexOf("Tool"); + if (index==-1) + return null; + name = name.substring(0, index+4); + name = name.replaceAll(" ","_"); + name = name + ".ijm"; + return name; + } + + boolean savePlugin(File f, byte[] data) { + try { + FileOutputStream out = new FileOutputStream(f); + out.write(data, 0, data.length); + out.close(); + } catch (IOException e) { + IJ.error("Plugin Installer", ""+e); + return false; + } + return true; + } + + public static byte[] download(String urlString, String name) { + int maxLength = 52428800; //50MB + URL url = null; + boolean unknownLength = false; + byte[] data = null;; + int n = 0; + try { + url = new URL(urlString); + if (IJ.debugMode) IJ.log("PluginInstaller: "+urlString+" " +url); + if (url==null) + return null; + URLConnection uc = url.openConnection(); + int len = uc.getContentLength(); + unknownLength = len<0; + if (unknownLength) len = maxLength; + if (name!=null) + IJ.showStatus("Downloading "+url.getFile()); + InputStream in = uc.getInputStream(); + data = new byte[len]; + int lenk = len/1024; + while (nPoint Tool" + +"" + +"

    " + +"
  • Click on a point and drag to move it.
    " + +"
  • Alt-click, or control-click, on a point to delete it.
    " + +"
  • To delete multiple points, create an area
    selection while holding down the alt key.
    " + +"
  • Press 'alt+y' (Edit>Selection>Properties plus
    alt key) to display the counts in a results table.
    " + +"
  • Press 'm' (Analyze>Measure) to list the counter
    and stack position associated with each point.
    " + +"
  • To measure a subset of the points, move them
    to an overlay, create a selection and then use
    Image>Overlay>Measure Overlay.
    " + +"
  • Use File>Save As>Tiff or File>Save As>Selection
    to save the points and counts.
    " + +"
  • Press 'F' (Image>Overlay>Flatten) to create an
    RGB image with embedded markers for export.
    " + +"
  • Hold the shift key down and points will be
    constrained to a horizontal or vertical line.
    " + +"
  • Use Edit>Selection>Select None to delete a
    multi-point selection.
    " + +"
  • Switch to the multi-point tool and use
    Edit>Selection>Restore Selection to restore
    a deleted multi-point selection.
    " + +"
" + +"
" + +"
"; + + public void run(String arg) { + if (gd!=null && gd.isShowing() && !IJ.isMacro()) { + gd.toFront(); + update(); + } else + showDialog(); + } + + void showDialog() { + String options = IJ.isMacro()?Macro.getOptions():null; + isMacro = options!=null; + boolean legacyMacro = false; + if (isMacro) { + options = options.replace("selection=", "color="); + options = options.replace("marker=", "size="); + options = options.replace("type=Crosshair", "type=Cross"); + Macro.setOptions(options); + legacyMacro = options.contains("auto-") || options.contains("add"); + } + multipointTool = Toolbar.getMultiPointMode() && !legacyMacro; + if (isMacro && !legacyMacro) + multipointTool = true; + Color sc =Roi.getColor(); + String sname = Colors.getColorName(sc, "Yellow"); + Color cc =PointRoi.getDefaultCrossColor(); + String cname = Colors.getColorName(cc, "None"); + String type = PointRoi.types[PointRoi.getDefaultType()]; + String size = PointRoi.sizes[PointRoi.getDefaultSize()]; + if (multipointTool) + gd = GUI.newNonBlockingDialog("Point Tool"); + else + gd = new GenericDialog("Point Tool"); + gd.setInsets(5,0,2); + gd.addChoice("Type:", PointRoi.types, type); + gd.addChoice("Color:", Colors.getColors(), sname); + gd.addChoice("Size:", PointRoi.sizes, size); + if (!multipointTool) { + gd.addCheckbox("Auto-measure", Prefs.pointAutoMeasure); + gd.addCheckbox("Auto-next slice", Prefs.pointAutoNextSlice); + gd.addCheckbox("Add_to overlay", Prefs.pointAddToOverlay); + gd.addCheckbox("Add to ROI Manager", Prefs.pointAddToManager); + } + gd.setInsets(5, 20, 0); + gd.addCheckbox("Label points", !Prefs.noPointLabels); + gd.addCheckbox("Show on all slices", Prefs.showAllPoints); + if (multipointTool) { + gd.setInsets(15,0,5); + String[] choices = PointRoi.getCounterChoices(); + gd.addChoice("Counter:", choices, choices[getCounter()]); + gd.setInsets(2, 75, 0); + gd.addMessage(getCount(getCounter())+" "); + } + gd.addHelp(help); + gd.addDialogListener(this); + gd.showDialog(); + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + boolean redraw = false; + // type + int typeIndex = gd.getNextChoiceIndex(); + if (typeIndex!=PointRoi.getDefaultType()) { + PointRoi.setDefaultType(typeIndex); + redraw = true; + } + // color + String selectionColor = gd.getNextChoice(); + Color sc = Colors.getColor(selectionColor, Color.yellow); + if (sc!=Roi.getColor()) { + Roi.setColor(sc); + redraw = true; + Toolbar tb = Toolbar.getInstance(); + if (tb!=null) tb.repaint(); + } + // size + int sizeIndex = gd.getNextChoiceIndex(); + if (sizeIndex!=PointRoi.getDefaultSize()) { + PointRoi.setDefaultSize(sizeIndex); + redraw = true; + } + if (!multipointTool) { + Prefs.pointAutoMeasure = gd.getNextBoolean(); + Prefs.pointAutoNextSlice = gd.getNextBoolean(); + Prefs.pointAddToOverlay = gd.getNextBoolean(); + Prefs.pointAddToManager = gd.getNextBoolean(); + if (Prefs.pointAddToOverlay) + Prefs.pointAddToManager = false; + if (Prefs.pointAutoNextSlice&&!Prefs.pointAddToManager) + Prefs.pointAutoMeasure = true; + } + boolean updateLabels = false; + boolean noPointLabels = !gd.getNextBoolean(); + if (noPointLabels!=Prefs.noPointLabels) { + redraw = true; + updateLabels = true; + } + Prefs.noPointLabels = noPointLabels; + boolean showAllPoints = gd.getNextBoolean(); + if (showAllPoints!=Prefs.showAllPoints) + redraw = true; + Prefs.showAllPoints = showAllPoints; + if (multipointTool) { + int counter = gd.getNextChoiceIndex(); + if (counter==0 || counter!=getCounter()) { + setCounter(counter); + redraw = true; + } + } + if (isMacro) { + PointRoi roi = getPointRoi(); + if (roi!=null) { + roi.setPointType(typeIndex); + roi.setStrokeColor(sc); + roi.setSize(sizeIndex); + redraw = true; + } + } + if (redraw) { + ImagePlus imp = null; + boolean impHasPointRoi = false; + PointRoi roi = getPointRoi(); + if (roi!=null) { + roi.setShowLabels(!Prefs.noPointLabels); + imp = roi.getImage(); + impHasPointRoi = true; + } + if (updateLabels) { + imp = WindowManager.getCurrentImage(); + Overlay overlay = imp!=null?imp.getOverlay():null; + if (overlay!=null) { + for (int i=0; iymax) { + double tmp = ymin; + ymin = ymax; + ymax = tmp; + } + ProfilePlot.setMinAndMax(ymin, ymax); + if (!Recorder.scriptMode()) + Recorder.recordString("setOption(\"InterpolateLines\", "+PlotWindow.interpolate+");\n"); + } + +} diff --git a/src/ij/plugin/Projector.java b/src/ij/plugin/Projector.java new file mode 100644 index 0000000..9e4bef1 --- /dev/null +++ b/src/ij/plugin/Projector.java @@ -0,0 +1,871 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.Calibration; +import ij.macro.Interpreter; +import java.awt.*; +import java.awt.image.*; + +/** +This plugin creates a sequence of projections of a rotating volume (stack of slices) onto a plane using +nearest-point (surface), brightest-point, or mean-value projection or a weighted combination of nearest- +point projection with either of the other two methods (partial opacity). The user may choose to rotate the +volume about any of the three orthogonal axes (x, y, or z), make portions of the volume transparent (using +thresholding), or add a greater degree of visual realism by employing depth cues. Based on Pascal code +contributed by Michael Castle of the University of Michigan Mental Health Research Institute. +*/ + +public class Projector implements PlugIn { + + private static final int xAxis=0, yAxis=1, zAxis=2; + private static final int nearestPoint=0, brightestPoint=1, meanValue=2; + private static final int BIGPOWEROF2 = 8192; + private static final String[] axisList = {"X-Axis", "Y-Axis", "Z-Axis"}; + private static final String[] methodList = {"Nearest Point", "Brightest Point", "Mean Value"}; + + private static int axisOfRotationS = yAxis; + private static int projectionMethodS = brightestPoint; + private static int initAngleS = 0; + private static int totalAngleS = 360; + private static int angleIncS = 10; + private static int opacityS = 0; + private static int depthCueSurfS = 0; + private static int depthCueIntS = 50; + private static boolean interpolateS; + private static boolean allTimePointsS; + + private int axisOfRotation = axisOfRotationS; + private int projectionMethod = projectionMethodS; + private int initAngle = initAngleS; + private int totalAngle = totalAngleS; + private int angleInc = angleIncS; + private int opacity = opacityS; + private int depthCueSurf = depthCueSurfS; + private int depthCueInt = depthCueIntS; + private boolean interpolate = interpolateS; + private boolean allTimePoints = allTimePointsS; + + private boolean debugMode; + private double sliceInterval = 1.0; // pixels + private int transparencyLower = 1; + private int transparencyUpper = 255; + private ImagePlus imp; + private ImageStack stack; + private ImageStack stack2; + private int width, height, imageWidth; + private int left, right, top, bottom; + private byte[] projArray, opaArray, brightCueArray; + private short[] zBuffer, cueZBuffer, countBuffer; + private int[] sumBuffer; + private boolean isRGB; + private String label = ""; + private boolean done; + private boolean batchMode = Interpreter.isBatchMode(); + private double progressBase=0.0, progressScale=1.0; + private boolean showMicroProgress = true; + + public void run(String arg) { + imp = IJ.getImage(); + ImageProcessor ip = imp.getProcessor(); + if (ip.isInvertedLut() && !IJ.isMacro()) { + if (!IJ.showMessageWithCancel("3D Project", ZProjector.lutMessage)) + return; + } + if (!showDialog()) + return; + if (sliceInterval>100) { + IJ.error("Z spacing ("+(int)sliceInterval+") is too large."); + return; + } + imp.startTiming(); + isRGB = imp.getType()==ImagePlus.COLOR_RGB; + if (imp.isHyperStack()) { + if (imp.getNSlices()>1) + doHyperstackProjections(imp); + else + IJ.error("Hyperstack Z dimension must be greater than 1"); + return; + } + if (interpolate && sliceInterval>1.0) { + imp = zScale(imp, true); + if (imp==null) return; + sliceInterval = 1.0; + } + if (isRGB) + doRGBProjections(imp); + else { + ImagePlus imp2 = doProjections(imp); + if (imp2!=null) + imp2.show(); + } + } + + private boolean showDialog() { + ImageProcessor ip = imp.getProcessor(); + double lower = ip.getMinThreshold(); + if (lower!=ImageProcessor.NO_THRESHOLD) { + transparencyLower = (int)lower; + transparencyUpper = (int)ip.getMaxThreshold(); + } + Calibration cal = imp.getCalibration(); + boolean hyperstack = imp.isHyperStack() && imp.getNFrames()>1; + GenericDialog gd = new GenericDialog("3D Projection"); + gd.addChoice("Projection method:", methodList, methodList[projectionMethod]); + gd.addChoice("Axis of rotation:", axisList, axisList[axisOfRotation]); + //gd.addMessage(""); + gd.addNumericField("Slice spacing ("+cal.getUnits()+"):",cal.pixelDepth,2); + + gd.addNumericField("Initial angle (0-359 degrees):", initAngle, 0); + gd.addNumericField("Total rotation (0-359 degrees):", totalAngle, 0); + gd.addNumericField("Rotation angle increment:", angleInc, 0); + gd.addNumericField("Lower transparency bound:", transparencyLower, 0); + gd.addNumericField("Upper transparency bound:", transparencyUpper, 0); + gd.addNumericField("Opacity (0-100%):", opacity, 0); + gd.addNumericField("Surface depth-cueing (0-100%):", 100-depthCueSurf, 0); + gd.addNumericField("Interior depth-cueing (0-100%):", 100-depthCueInt, 0); + gd.addCheckbox("Interpolate", interpolate); + if (hyperstack) + gd.addCheckbox("All time points", allTimePoints); + //gd.addCheckbox("Debug Mode:", debugMode); + + gd.addHelp(IJ.URL+"/docs/menus/image.html#project"); + gd.showDialog(); + if (gd.wasCanceled()) + return false;; + projectionMethod = gd.getNextChoiceIndex(); + axisOfRotation = gd.getNextChoiceIndex(); + cal.pixelDepth = gd.getNextNumber(); + if (cal.pixelWidth==0.0) cal.pixelWidth = 1.0; + sliceInterval = cal.pixelDepth/cal.pixelWidth; + initAngle = (int)gd.getNextNumber(); + totalAngle = (int)gd.getNextNumber(); + angleInc = (int)gd.getNextNumber(); + transparencyLower = (int)gd.getNextNumber(); + transparencyUpper = (int)gd.getNextNumber(); + opacity = (int)gd.getNextNumber(); + depthCueSurf = 100-(int)gd.getNextNumber(); + depthCueInt = 100-(int)gd.getNextNumber(); + interpolate = gd.getNextBoolean(); + if (hyperstack) + allTimePoints = gd.getNextBoolean(); + //debugMode = gd.getNextBoolean(); + axisOfRotationS = axisOfRotation; + projectionMethodS = projectionMethod; + initAngleS = initAngle; + totalAngleS = totalAngle; + angleIncS = angleInc; + opacityS = opacity; + depthCueSurfS = depthCueSurf; + depthCueIntS = depthCueInt; + interpolateS = interpolate; + allTimePointsS = allTimePoints; + return true; + } + + private void doHyperstackProjections(ImagePlus imp) { + double originalSliceInterval = sliceInterval; + ImagePlus buildImp = null; + ImagePlus projImpD = null; + int finalChannels = imp.getNChannels(); + int finalSlices = imp.getNSlices(); + int finalFrames = imp.getNFrames(); + int f1 = 0; + int f2 = imp.getNFrames()-1; + if (imp.getBitDepth()==24) + allTimePoints = false; + if (!allTimePoints) + f1 = f2 = imp.getFrame(); + + int channels = imp.getNChannels(); + progressScale = 1.0/channels; + if (allTimePoints) + showMicroProgress = false; + int count = 1; + for (int c=0; c1.0) { + impD = zScale(impD, false); + if (impD==null) return; + sliceInterval = 1.0; + } + if (isRGB) + doRGBProjections(impD); + else { + progressBase = (double)c/channels; + projImpD = doProjections(impD); + if (projImpD==null) return; + finalSlices = projImpD.getNSlices(); + impD.close(); + if ((f==0||!allTimePoints)&& c==0) { + buildImp = projImpD; + buildImp.setTitle("BuildStack"); + } else { + Concatenator concat = new Concatenator(); + buildImp = concat.concatenate(buildImp, projImpD, false); + } + } + if (done) return; + } + } + if (imp.getNFrames()==1 || !allTimePoints) { + finalFrames = finalSlices; + finalSlices = 1; + } + if (imp.getNChannels()>1) + buildImp = HyperStackConverter.toHyperStack(buildImp, finalChannels, finalSlices, finalFrames, "xyztc", "composite"); + if (imp.isComposite()) { + CompositeImage buildImp2 = new CompositeImage(buildImp, 0); + ((CompositeImage)buildImp2).copyLuts(imp); + ((CompositeImage)buildImp2).resetDisplayRanges(); + buildImp = buildImp2; + } + buildImp.setTitle("Projections of "+imp.getShortTitle()); + buildImp.show(); + } + + private void doRGBProjections(ImagePlus imp) { + boolean saveUseInvertingLut = Prefs.useInvertingLut; + Prefs.useInvertingLut = false; + ImageStack[] channels = ChannelSplitter.splitRGB(imp.getStack(), true); + ImagePlus red = new ImagePlus("Red", channels[0]); + ImagePlus green = new ImagePlus("Green", channels[1]); + ImagePlus blue = new ImagePlus("Blue", channels[2]); + Calibration cal = imp.getCalibration(); + Roi roi = imp.getRoi(); + if (roi!=null) + {red.setRoi(roi); green.setRoi(roi); blue.setRoi(roi);} + red.setCalibration(cal); green.setCalibration(cal); blue.setCalibration(cal); + label = "Red: "; + progressBase = 0.0; + progressScale = 1.0/3.0; + red = doProjections(red); + if (red==null || done) return; + label = "Green: "; + progressBase = 1.0/3.0; + green = doProjections(green); + if (green==null || done) return; + label = "Blue: "; + progressBase = 2.0/3.0; + blue = doProjections(blue); + if (blue==null || done) return; + int w = red.getWidth(), h = red.getHeight(), d = red.getStackSize(); + RGBStackMerge merge = new RGBStackMerge(); + ImageStack stack = merge.mergeStacks(w, h, d, red.getStack(), green.getStack(), blue.getStack(), true); + new ImagePlus("Projection of "+imp.getShortTitle(), stack).show(); + Prefs.useInvertingLut = saveUseInvertingLut; + } + + private ImagePlus doProjections(ImagePlus imp) { + int nSlices; // number of slices in volume + int projwidth, projheight; //dimensions of projection image + int xcenter, ycenter, zcenter; //coordinates of center of volume of rotation + int theta; //current angle of rotation in degrees + double thetarad; //current angle of rotation in radians + int sintheta, costheta; //sine and cosine of current angle + int offset; + int curval, prevval, nextval, aboveval, belowval; + int n, nProjections, angle; + boolean minProjSize = true; + + stack = imp.getStack(); + if (imp.getBitDepth()==16 || imp.getBitDepth()==32) { + ImageStack stack2 = new ImageStack(imp.getWidth(),imp.getHeight()); + for (int i=1; i<=stack.size(); i++) + stack2.addSlice(stack.getProcessor(i).convertToByte(true)); + stack = stack2; + } + if ((angleInc==0) && (totalAngle!=0)) + angleInc = 5; + boolean negInc = angleInc<0; + if (negInc) angleInc = -angleInc; + angle = 0; + nProjections = 0; + if (angleInc==0) + nProjections = 1; + else { + while (angle<=totalAngle) { + nProjections++; + angle += angleInc; + } + } + if (angle>360) + nProjections--; + if (nProjections<=0) + nProjections = 1; + if (negInc) angleInc = -angleInc; + + ImageProcessor ip = imp.getProcessor(); + Rectangle r = ip.getRoi(); + left = r.x; + top = r.y; + right = r.x + r.width; + bottom = r.y + r.height; + nSlices = imp.getStackSize(); + imageWidth = imp.getWidth(); + width = right - left; + height = bottom - top; + xcenter = (left + right)/2; //find center of volume of rotation + ycenter = (top + bottom)/2; + zcenter = (int)(nSlices*sliceInterval/2.0+0.5); + + projwidth = 0; + projheight = 0; + if (minProjSize && axisOfRotation!=zAxis) { + switch (axisOfRotation) { + case xAxis: + projheight = (int)(Math.sqrt(nSlices*sliceInterval*nSlices*sliceInterval+height*height) + 0.5); + projwidth = width; + break; + case yAxis: + projwidth = (int)(Math.sqrt(nSlices*sliceInterval*nSlices*sliceInterval+width*width) + 0.5); + projheight = height; + break; + } + } else { + projwidth = (int) (Math.sqrt (nSlices*sliceInterval*nSlices*sliceInterval+width*width) + 0.5); + projheight = (int) (Math.sqrt (nSlices*sliceInterval*nSlices*sliceInterval+height*height) + 0.5); + } + if ((projwidth%2)==1) + projwidth++; + int projsize = projwidth * projheight; + if (projwidth<=0 || projheight<=0) { + IJ.error("'projwidth' or 'projheight' <= 0"); + return null; + } + try { + allocateArrays(nProjections, projwidth, projheight); + } catch(OutOfMemoryError e) { + Object[] images = stack2.getImageArray(); + if (images!=null) + for (int i=0; i0)) { + for (int i=0; i0) && (projectionMethod!=nearestPoint)) { + for (int i=0; i0) && (projectionMethod!=nearestPoint)) { + for (int i=0; i 0)) + zBuffer = new short[projsize]; + if ((opacity>0) && (projectionMethod!=nearestPoint)) + opaArray = new byte[projsize]; + if ((projectionMethod==brightestPoint) && (depthCueInt<100)) { + brightCueArray = new byte[projsize]; + cueZBuffer = new short[projsize]; + } + if (projectionMethod==meanValue) { + sumBuffer = new int[projsize]; + countBuffer = new short[projsize]; + } + } + + + /** + This method projects each pixel of a volume (stack of slices) onto a plane as the volume rotates about the x-axis. Integer + arithmetic, precomputation of values, and iterative addition rather than multiplication inside a loop are used extensively + to make the code run efficiently. Projection parameters stored in global variables determine how the projection will be performed. + This procedure returns various buffers which are actually used by DoProjections() to find the final projected image for the volume + of slices at the current angle. + */ + private void doOneProjectionX (int nSlices, int ycenter, int zcenter, int projwidth, int projheight, int costheta, int sintheta) { + int thispixel; //current pixel to be projected + int offset, offsetinit; //precomputed offsets into an image buffer + int z; //z-coordinate of points in current slice before rotation + int ynew, znew; //y- and z-coordinates of current point after rotation + int zmax, zmin; //z-coordinates of first and last slices before rotation + int zmaxminuszmintimes100; //precomputed values to save time in loops + int c100minusDepthCueInt, c100minusDepthCueSurf; + boolean DepthCueIntLessThan100, DepthCueSurfLessThan100; + boolean OpacityOrNearestPt, OpacityAndNotNearestPt; + boolean MeanVal, BrightestPt; + int ysintheta, ycostheta; + int zsintheta, zcostheta, ysinthetainit, ycosthetainit; + byte[] pixels; + int projsize = projwidth * projheight; + + //find z-coordinates of first and last slices + zmax = zcenter + projheight/2; + zmin = zcenter - projheight/2; + zmaxminuszmintimes100 = 100 * (zmax-zmin); + c100minusDepthCueInt = 100 - depthCueInt; + c100minusDepthCueSurf = 100 - depthCueSurf; + DepthCueIntLessThan100 = (depthCueInt < 100); + DepthCueSurfLessThan100 = (depthCueSurf < 100); + OpacityOrNearestPt = ((projectionMethod==nearestPoint) || (opacity>0)); + OpacityAndNotNearestPt = ((opacity>0) && (projectionMethod!=nearestPoint)); + MeanVal = (projectionMethod==meanValue); + BrightestPt = (projectionMethod==brightestPoint); + ycosthetainit = (top - ycenter - 1) * costheta; + ysinthetainit = (top - ycenter - 1) * sintheta; + offsetinit = ((projheight-bottom+top)/2) * projwidth + (projwidth - right + left)/2 - 1; + + for (int k=1; k<=nSlices; k++) { + pixels = (byte[])stack.getPixels(k); + z = (int)((k-1)*sliceInterval+0.5) - zcenter; + zcostheta = z * costheta; + zsintheta = z * sintheta; + ycostheta = ycosthetainit; + ysintheta = ysinthetainit; + for (int j=top; jPicBaseAddr); + //read each pixel in current row and project it + int lineIndex = j*imageWidth; + for (int i=left; i=projsize) || (offset<0)) + offset = 0; + if ((thispixel <= transparencyUpper) && (thispixel >= transparencyLower)) { + if (OpacityOrNearestPt) { + if (znew(brightCueArray[offset]&0xff)) || (thispixel==(brightCueArray[offset]&0xff)) && (znew>cueZBuffer[offset])) { + brightCueArray[offset] = (byte)thispixel; //use z-buffer to ensure that if depth-cueing is on, + cueZBuffer[offset] = (short)znew; //the closer of two equally-bright points is displayed. + projArray[offset] = (byte)((depthCueInt*thispixel/100 + + c100minusDepthCueInt*thispixel*(zmax-znew)/zmaxminuszmintimes100)); + } + } else { + if (thispixel>(projArray[offset]&0xff)) + projArray[offset] = (byte)thispixel; + } + } // else BrightestPt + } // if thispixel in range + } //for i (all pixels in row) + } // for j (all rows of BoundRect) + } // for k (all slices) + } // doOneProjectionX() + + + /** Projects each pixel of a volume (stack of slices) onto a plane as the volume rotates about the y-axis. */ + private void doOneProjectionY (int nSlices, int xcenter, int zcenter, int projwidth, int projheight, int costheta, int sintheta) { + int thispixel; //current pixel to be projected + int offset, offsetinit; //precomputed offsets into an image buffer + int z; //z-coordinate of points in current slice before rotation + int xnew, znew; //y- and z-coordinates of current point after rotation + int zmax, zmin; //z-coordinates of first and last slices before rotation + int zmaxminuszmintimes100; //precomputed values to save time in loops + int c100minusDepthCueInt, c100minusDepthCueSurf; + boolean DepthCueIntLessThan100, DepthCueSurfLessThan100; + boolean OpacityOrNearestPt, OpacityAndNotNearestPt; + boolean MeanVal, BrightestPt; + int xsintheta, xcostheta; + int zsintheta, zcostheta, xsinthetainit, xcosthetainit; + byte[] pixels; + int projsize = projwidth * projheight; + + //find z-coordinates of first and last slices + zmax = zcenter + projwidth/2; + zmin = zcenter - projwidth/2; + zmaxminuszmintimes100 = 100 * (zmax-zmin); + c100minusDepthCueInt = 100 - depthCueInt; + c100minusDepthCueSurf = 100 - depthCueSurf; + DepthCueIntLessThan100 = (depthCueInt < 100); + DepthCueSurfLessThan100 = (depthCueSurf < 100); + OpacityOrNearestPt = ((projectionMethod==nearestPoint) || (opacity>0)); + OpacityAndNotNearestPt = ((opacity>0) && (projectionMethod!=nearestPoint)); + MeanVal = (projectionMethod==meanValue); + BrightestPt = (projectionMethod==brightestPoint); + xcosthetainit = (left - xcenter - 1) * costheta; + xsinthetainit = (left - xcenter - 1) * sintheta; + for (int k=1; k<=nSlices; k++) { + pixels = (byte[])stack.getPixels(k); + z = (int)((k-1)*sliceInterval+0.5) - zcenter; + zcostheta = z * costheta; + zsintheta = z * sintheta; + offsetinit = ((projheight-bottom+top)/2) * projwidth +(projwidth - right + left)/2 - projwidth; + for (int j=top; j= transparencyLower)) { + xnew = (xcostheta + zsintheta)/BIGPOWEROF2 + xcenter - left; + znew = (zcostheta - xsintheta)/BIGPOWEROF2 + zcenter; + offset = offsetinit + xnew; + if ((offset>=projsize) || (offset<0)) + offset = 0; + if (OpacityOrNearestPt) { + if (znew(brightCueArray[offset]&0xff)) || (thispixel==(brightCueArray[offset]&0xff)) && (znew>cueZBuffer[offset])) { + brightCueArray[offset] = (byte)thispixel; //use z-buffer to ensure that if depth-cueing is on, + cueZBuffer[offset] = (short)znew; //the closer of two equally-bright points is displayed. + projArray[offset] = (byte)((depthCueInt*thispixel/100 + + c100minusDepthCueInt*thispixel*(zmax-znew)/zmaxminuszmintimes100)); + } + } else { + if (thispixel > (projArray[offset]&0xff)) + projArray[offset] = (byte)thispixel; + } + } // if BrightestPt + } //end if thispixel in range + } // for i (all pixels in row) + } // for j (all rows) + } // for k (all slices) + } // DoOneProjectionY() + + + /** Projects each pixel of a volume (stack of slices) onto a plane as the volume rotates about the z-axis. */ + private void doOneProjectionZ (int nSlices, int xcenter, int ycenter, int zcenter, int projwidth, int projheight, int costheta, int sintheta) { + int thispixel; //current pixel to be projected + int offset, offsetinit; //precomputed offsets into an image buffer + int z; //z-coordinate of points in current slice before rotation + int xnew, ynew; //y- and z-coordinates of current point after rotation + int zmax, zmin; //z-coordinates of first and last slices before rotation + int zmaxminuszmintimes100; //precomputed values to save time in loops + int c100minusDepthCueInt, c100minusDepthCueSurf; + boolean DepthCueIntLessThan100, DepthCueSurfLessThan100; + boolean OpacityOrNearestPt, OpacityAndNotNearestPt; + boolean MeanVal, BrightestPt; + int xsintheta, xcostheta, ysintheta, ycostheta; + int xsinthetainit, xcosthetainit, ysinthetainit, ycosthetainit; + byte[] pixels; + int projsize = projwidth * projheight; + + //find z-coordinates of first and last slices + //zmax = zcenter + projwidth/2; + //zmin = zcenter - projwidth/2; + zmax = (int)((nSlices-1)*sliceInterval+0.5) - zcenter; + zmin = -zcenter; + + zmaxminuszmintimes100 = 100 * (zmax-zmin); + c100minusDepthCueInt = 100 - depthCueInt; + c100minusDepthCueSurf = 100 - depthCueSurf; + DepthCueIntLessThan100 = (depthCueInt < 100); + DepthCueSurfLessThan100 = (depthCueSurf < 100); + OpacityOrNearestPt = ((projectionMethod==nearestPoint) || (opacity>0)); + OpacityAndNotNearestPt = ((opacity>0) && (projectionMethod!=nearestPoint)); + MeanVal = (projectionMethod==meanValue); + BrightestPt = (projectionMethod==brightestPoint); + xcosthetainit = (left - xcenter - 1) * costheta; + xsinthetainit = (left - xcenter - 1) * sintheta; + ycosthetainit = (top - ycenter - 1) * costheta; + ysinthetainit = (top - ycenter - 1) * sintheta; + offsetinit = ((projheight-bottom+top)/2) * projwidth + (projwidth - right + left)/2 - 1; + for (int k=1; k<=nSlices; k++) { + pixels = (byte[])stack.getPixels(k); + z = (int)((k-1)*sliceInterval+0.5) - zcenter; + ycostheta = ycosthetainit; + ysintheta = ysinthetainit; + for (int j=top; jPicBaseAddr); + int lineIndex = j*imageWidth; + //read each pixel in current row and project it + for (int i=left; i= transparencyLower)) { + xnew = (xcostheta - ysintheta)/BIGPOWEROF2 + xcenter - left; + ynew = (xsintheta + ycostheta)/BIGPOWEROF2 + ycenter - top; + offset = offsetinit + ynew * projwidth + xnew; + if ((offset>=projsize) || (offset<0)) + offset = 0; + if (OpacityOrNearestPt) { + if (z(brightCueArray[offset]&0xff)) || (thispixel==(brightCueArray[offset]&0xff)) && (z>cueZBuffer[offset])) { + brightCueArray[offset] = (byte)thispixel; //use z-buffer to ensure that if depth-cueing is on, + cueZBuffer[offset] = (short)z; //the closer of two equally-bright points is displayed. + projArray[offset] = (byte)((depthCueInt*(thispixel)/100 + c100minusDepthCueInt*(thispixel)*(zmax-z)/zmaxminuszmintimes100)); + } + } else { + //p = (BYTE *)(projaddr + offset); + if (thispixel > (projArray[offset]&0xff)) + projArray[offset] = (byte)thispixel; + } + } // else BrightestPt + } //if thispixel in range + } //for i (all pixels in row) + } // for j (all rows of BoundRect) + } // for k (all slices) + //new ImagePlus("f", new FloatProcessor(projwidth,projheight,f,null)).show(); + } // end doOneProjectionZ() + + private ImagePlus zScale(ImagePlus imp, boolean showProgress) { + if (imp.getBitDepth()==16 || imp.getBitDepth()==32) + IJ.run(imp, "8-bit", ""); + IJ.showStatus("Z Scaling..."); + ImageStack stack1 = imp.getStack(); + int depth1 = stack1.getSize(); + ImagePlus imp2 = null; + String title = imp.getTitle(); + ImageProcessor ip = imp.getProcessor(); + ColorModel cm = ip.getColorModel(); + int width1 = imp.getWidth(); + int height1 = imp.getHeight(); + Rectangle r = ip.getRoi(); + int width2 = r.width; + int height2 = r.height; + int depth2 = (int)(stack1.getSize()*sliceInterval+0.5); + imp2 = NewImage.createImage(title, width2, height2, depth2, isRGB?24:8, NewImage.FILL_BLACK); + if (imp2==null || depth2!=imp2.getStackSize()) return null; + ImageStack stack2 = imp2.getStack(); + ImageProcessor xzPlane1 = ip.createProcessor(width2, depth1); + xzPlane1.setInterpolate(true); + ImageProcessor xzPlane2; + int[] line = new int[width2]; + for (int y=0; y3) && cimg==null) { + IJ.error("A 2 or 3 image stack, or a HyperStack, required"); + return; + } + int type = imp.getType(); + if (cimg==null && !(type==ImagePlus.GRAY8 || type==ImagePlus.GRAY16)) { + IJ.error("8-bit or 16-bit grayscale stack required"); + return; + } + if (!imp.lock()) + return; + Undo.reset(); + String title = imp.getTitle()+" (RGB)"; + if (cimg!=null) + compositeToRGB(cimg, title); + else if (type==ImagePlus.GRAY16) { + sixteenBitsToRGB(imp); + } else { + ImagePlus imp2 = imp.createImagePlus(); + imp2.setStack(title, imp.getStack()); + ImageConverter ic = new ImageConverter(imp2); + ic.convertRGBStackToRGB(); + imp2.show(); + } + imp.unlock(); + } + + /** Converts the specified multi-channel (composite) image to RGB. */ + public static void convertToRGB(ImagePlus imp) { + if (!imp.isComposite()) + throw new IllegalArgumentException("Multi-channel image required"); + RGBStackConverter converter = new RGBStackConverter(); + ImageWindow win = imp.getWindow(); + Point location = null; + if (win!=null) { + location = win.getLocation(); + imp.hide(); + } + converter.image = imp; + converter.run(""); + if (win!=null) { + ImageWindow.setNextLocation(location); + imp.show(); + } + } + + void compositeToRGB(CompositeImage imp, String title) { + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + int images = channels*slices*frames; + if (channels==images) { + compositeImageToRGB(imp, title); + return; + } + width = imp.getWidth(); + height = imp.getHeight(); + imageSize = width*height*4.0/(1024.0*1024.0); + channels1 = imp.getNChannels(); + slices1 = slices2 = imp.getNSlices(); + frames1 = frames2 = imp.getNFrames(); + int c1 = imp.getChannel(); + int z1 = imp.getSlice(); + int t2 = imp.getFrame(); + if (image!=null) { + slices2 = slices1; + frames2 = frames1; + keep = false; + } else { + if (!showDialog()) + return; + } + //IJ.log("HyperStackReducer-2: "+keep+" "+channels2+" "+slices2+" "+frames2); + String title2 = keep?WindowManager.getUniqueName(imp.getTitle()):imp.getTitle(); + ImagePlus imp2 = imp.createHyperStack(title2, 1, slices2, frames2, 24); + convertHyperstack(imp, imp2); + if (imp.getWindow()==null && !keep) { + imp.setImage(imp2); + imp.setOverlay(imp2.getOverlay()); + return; + } + imp2.setOpenAsHyperStack(slices2>1||frames2>1); + imp2.show(); + if (!keep) { + imp.changes = false; + imp.close(); + } + if (imp2.getWindow()!=null) + IJ.selectWindow(imp2.getID()); + } + + public void convertHyperstack(ImagePlus imp, ImagePlus imp2) { + int slices = imp2.getNSlices(); + int frames = imp2.getNFrames(); + int c1 = imp.getChannel(); + int z1 = imp.getSlice(); + int t1 = imp.getFrame(); + int i = 1; + int c = 1; + ImageStack stack = imp.getStack(); + ImageStack stack2 = imp2.getStack(); + imp.setPositionWithoutUpdate(c1, 1, 1); + ImageProcessor ip = imp.getProcessor(); + double min = ip.getMin(); + double max = ip.getMax(); + boolean hsbStack = imp.getProp("HSB_Stack")!=null; + for (int z=1; z<=slices; z++) { + if (slices==1) z = z1; + for (int t=1; t<=frames; t++) { + //IJ.showProgress(i++, n); + if (frames==1) t = t1; + //ip = stack.getProcessor(n1); + imp.setPositionWithoutUpdate(c1, z, t); + boolean isHSB = hsbStack && imp.getNChannels()==3; + int n2 = imp2.getStackIndex(c1, z, t); + if (isHSB) { + ImagePlus hsbImp = new Duplicator().run(imp, 1, 3, z, z, t, t); + ImageConverter ic = new ImageConverter(hsbImp); + ic.convertHSBToRGB(); + stack2.setPixels(hsbImp.getProcessor().getPixels(), n2); + } else { + Image img = imp.getImage(); + stack2.setPixels((new ColorProcessor(img)).getPixels(), n2); + } + } + } + imp.setPosition(c1, z1, t1); + imp2.resetStack(); + imp2.setPosition(1, 1, 1); + + //Added by Marcel Boeglin 2013.09.26 + Overlay overlay = imp.getOverlay(); + if (overlay!=null) { + int firstC = c1, lastC = c1, firstZ = z1, lastZ = z1, firstT = t1, lastT = t1; + if (imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.COMPOSITE) { + firstC = 1; + lastC = imp.getNChannels(); + } + if (slices2>1) {firstZ = 1; lastZ = slices2;} + if (frames2>1) {firstT = 1; lastT = frames2;} + Overlay overlay2 = overlay.duplicate(); + if (slices2>1 && frames2>1) + overlay2.crop(firstC, lastC, firstZ, lastZ, firstT, lastT);//imp2 is hyperstack : ROI's hypercoordinates are conserved but only those with C = 1 are displayed + else + overlay2.crop(c1, c1, firstZ, lastZ, firstT, lastT); //simple stack + imp2.setOverlay(overlay2); + } + } + + void compositeImageToRGB(CompositeImage imp, String title) { + if (imp.getMode()==IJ.COMPOSITE) { + ImagePlus imp2 = imp.createImagePlus(); + imp.updateImage(); + imp2.setProcessor(title, new ColorProcessor(imp.getImage())); + //Added by Marcel Boeglin 2013.09.26 + Overlay overlay = imp.getOverlay(); + Overlay overlay2 = null; + if (overlay!=null) { + overlay2 = overlay.duplicate(); + overlay2.crop(1, imp.getNChannels()); + imp2.setOverlay(overlay2); + } + if (image!=null && imp.getWindow()==null) { + imp.setImage(imp2); + imp.setOverlay(overlay2); + } else + imp2.show(); + return; + } + ImageStack stack = new ImageStack(imp.getWidth(), imp.getHeight()); + int c = imp.getChannel(); + int n = imp.getNChannels(); + for (int i=1; i<=n; i++) { + imp.setPositionWithoutUpdate(i, 1, 1); + stack.addSlice(null, new ColorProcessor(imp.getImage())); + } + imp.setPosition(c, 1, 1); + ImagePlus imp2 = imp.createImagePlus(); + imp2.setStack(title, stack); + Object info = imp.getProperty("Info"); + if (info!=null) imp2.setProperty("Info", info); + imp2.setProperties(imp.getPropertiesAsArray()); + Overlay overlay = imp.getOverlay(); + Overlay overlay2 = null; + if (overlay!=null) { + overlay2 = overlay.duplicate(); + overlay2.crop(1, imp.getNChannels()); + imp2.setOverlay(overlay2); + } + if (image!=null && imp.getWindow()==null) { + imp.setImage(imp2); + imp.setOverlay(overlay2); + } else { + imp2.show(); + imp2.setSlice(c); + } + } + + void sixteenBitsToRGB(ImagePlus imp) { + Roi roi = imp.getRoi(); + int width, height; + Rectangle r; + if (roi!=null) { + r = roi.getBounds(); + width = r.width; + height = r.height; + } else + r = new Rectangle(0,0,imp.getWidth(),imp.getHeight()); + ImageProcessor ip; + ImageStack stack1 = imp.getStack(); + ImageStack stack2 = new ImageStack(r.width, r.height); + for (int i=1; i<=stack1.getSize(); i++) { + ip = stack1.getProcessor(i); + ip.setRoi(r); + ImageProcessor ip2 = ip.crop(); + ip2 = ip2.convertToByte(true); + stack2.addSlice(null, ip2); + } + ImagePlus imp2 = imp.createImagePlus(); + imp2.setStack(imp.getTitle()+" (RGB)", stack2); + ImageConverter ic = new ImageConverter(imp2); + ic.convertRGBStackToRGB(); + imp2.show(); + } + + boolean showDialog() { + GenericDialog gd = new GenericDialog("Convert to RGB"); + gd.setInsets(10, 20, 5); + gd.addMessage("Create RGB image with:"); + gd.setInsets(0, 35, 0); + if (slices1!=1) gd.addCheckbox("Slices ("+slices1+")", true); + gd.setInsets(0, 35, 0); + if (frames1!=1) gd.addCheckbox("Frames ("+frames1+")", true); + gd.setInsets(5, 20, 0); + gd.addMessage(getNewDimensions()+" "); + gd.setInsets(15, 20, 0); + gd.addCheckbox("Keep source", keep); + gd.addDialogListener(this); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + else + return true; + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (IJ.isMacOSX()) IJ.wait(100); + if (slices1!=1) slices2 = gd.getNextBoolean()?slices1:1; + if (frames1!=1) frames2 = gd.getNextBoolean()?frames1:1; + keep = gd.getNextBoolean(); + if (!IJ.isMacro()) staticKeep = keep; + ((Label)gd.getMessage()).setText(getNewDimensions()); + return true; + } + + String getNewDimensions() { + String s1 = slices2>1?"x"+slices2:""; + String s2 = frames2>1?"x"+frames2:""; + String s = width+"x"+height+s1+s2; + s += " ("+(int)Math.round(imageSize*slices2*frames2)+"MB)"; + return(s); + } + + +} diff --git a/src/ij/plugin/RGBStackMerge.java b/src/ij/plugin/RGBStackMerge.java new file mode 100644 index 0000000..0e645db --- /dev/null +++ b/src/ij/plugin/RGBStackMerge.java @@ -0,0 +1,453 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import java.awt.image.*; + +/** This plugin implements the Image/Color/Merge Channels command. */ +public class RGBStackMerge implements PlugIn { + private static String none = "*None*"; + private static int maxChannels = 7; + private static String[] colors = {"red", "green", "blue", "gray", "cyan", "magenta", "yellow"}; + private static boolean staticCreateComposite = true; + private static boolean staticKeep; + private static boolean staticIgnoreLuts; + private ImagePlus imp; + private byte[] blank; + private boolean ignoreLuts; + private boolean autoFillDisabled; + private String firstChannelName; + + public void run(String arg) { + imp = WindowManager.getCurrentImage(); + mergeStacks(); + } + + public static ImagePlus mergeChannels(ImagePlus[] images, boolean keepSourceImages) { + RGBStackMerge rgbsm = new RGBStackMerge(); + return rgbsm.mergeHyperstacks(images, keepSourceImages); + } + + /** Combines up to seven grayscale stacks into one RGB or composite stack. */ + public void mergeStacks() { + int[] wList = WindowManager.getIDList(); + if (wList==null) { + error("No images are open."); + return; + } + + String[] titles = new String[wList.length+1]; + for (int i=0; i1) { + error("Source hyperstacks cannot have more than 1 channel."); + return; + } + if (img.getNSlices()!=slices || img.getNFrames()!=frames) { + error("Source hyperstacks must have the same dimensions."); + return; + } + mergeHyperstacks = true; + } // isHyperStack + if (img.getWidth()!=width || images[i].getHeight()!=height) { + error("The source images or stacks must have the same width and height."); + return; + } + if (createComposite && img.getBitDepth()!=bitDepth) { + error("The source images must have the same bit depth."); + return; + } + } + + ImageStack[] stacks = new ImageStack[maxChannels]; + for (int i=0; i2) + extraIChannels++; + if (images[i].getBitDepth()==24) + isRGB = true; + } + } + if (isRGB && extraIChannels>0) { + imp2 = mergeUsingRGBProjection(images, createComposite); + } else if ((createComposite&&!isRGB) || mergeHyperstacks) { + imp2 = mergeHyperstacks(images, keep); + if (imp2==null) return; + } else { + ImageStack rgb = mergeStacks(width, height, stackSize, stacks[0], stacks[1], stacks[2], keep); + imp2 = new ImagePlus("RGB", rgb); + if (createComposite) { + imp2 = CompositeConverter.makeComposite(imp2); + imp2.setTitle("Composite"); + } + } + for (int i=0; i=0; i--) { + if (titles!=null && titles[i].startsWith(str) && (firstChannelName==null||titles[i].contains(firstChannelName))) { + name = titles[i]; + if (channel==1) { + if (name==null || name.length()<3) + return none; + firstChannelName = name.substring(3); + } + break; + } + } + if (name==null) { + for (int i=titles.length-1; i>=0; i--) { + int index = titles[i].indexOf(colors[channel-1]); + if (titles!=null && index!=-1 && (firstChannelName==null||titles[i].contains(firstChannelName))) { + name = titles[i]; + if (channel==1 && index>0) + firstChannelName = name.substring(0, index-1); + break; + } + } + } + if (channel==1 && name==null) + autoFillDisabled = true; + if (name!=null) + return name; + else + return none; + } + + public ImagePlus mergeHyperstacks(ImagePlus[] images, boolean keep) { + int n = images.length; + int channels = 0; + for (int i=0; i1?"Merged":"Composite"; + ImagePlus imp2 = new ImagePlus(title, stack2); + imp2.setDimensions(channels, slices, frames); + imp2 = new CompositeImage(imp2, IJ.COMPOSITE); + boolean allGrayLuts = true; + for (int c=0; c0) { + if (!Toolbar.getToolName().equals("roundrect")) + IJ.setTool("roundrect"); + } + return true; + } + + public static Color getDefaultStrokeColor() { + return defaultStrokeColor; + } + + public static float getDefaultStrokeWidth() { + return (float)defaultStrokeWidth; + } + +} diff --git a/src/ij/plugin/Resizer.java b/src/ij/plugin/Resizer.java new file mode 100644 index 0000000..fa502e2 --- /dev/null +++ b/src/ij/plugin/Resizer.java @@ -0,0 +1,404 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import ij.util.Tools; +import ij.plugin.frame.Recorder; +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +/** This plugin implements the Edit/Crop and Image/Adjust/Size commands. */ +public class Resizer implements PlugIn, TextListener, ItemListener { + public static final int IN_PLACE=16, SCALE_T=32; + private static int newWidth; + private static int newHeight; + private static boolean constrain = true; + private static boolean averageWhenDownsizing = true; + private static int interpolationMethod = ImageProcessor.BILINEAR; + private String[] methods = ImageProcessor.getInterpolationMethods(); + private Vector fields, checkboxes; + private double origWidth, origHeight; + private boolean sizeToHeight; + + public void run(String arg) { + boolean crop = arg.equals("crop"); + ImagePlus imp = IJ.getImage(); + ImageProcessor ip = imp.getProcessor(); + Roi roi = imp.getRoi(); + int bitDepth = imp.getBitDepth(); + double min = ip.getMin(); + double max = ip.getMax(); + if (!imp.okToDeleteRoi()) + return; + if ((roi==null||!roi.isArea()) && crop) { + IJ.error(crop?"Crop":"Resize", "Area selection required"); + return; + } + if (!imp.lock()) { + IJ.log("<>"); + return; + } + Rectangle r = ip.getRoi(); + origWidth = r.width;; + origHeight = r.height; + sizeToHeight=false; + boolean restoreRoi = crop && roi!=null && roi.getType()!=Roi.RECTANGLE; + if (roi!=null) { + Rectangle b = roi.getBounds(); + int w = ip.getWidth(); + int h = ip.getHeight(); + if (b.x<0 || b.y<0 || b.x+b.width>w || b.y+b.height>h) { + ShapeRoi shape1 = new ShapeRoi(roi); + ShapeRoi shape2 = new ShapeRoi(new Roi(0, 0, w, h)); + roi = shape2.and(shape1); + if (roi.getBounds().width==0 || roi.getBounds().height==0) { + if (IJ.isMacro()) + IJ.log("Selection is outside image"); + else + throw new IllegalArgumentException("Selection is outside image"); + } + if (restoreRoi) imp.setRoi(roi); + } + } + int stackSize= imp.getStackSize(); + int z1 = imp.getStackSize(); + int t1 = 0; + int z2=0, t2=0; + int saveMethod = interpolationMethod; + if (crop) { + Rectangle bounds = roi.getBounds(); + newWidth = bounds.width; + newHeight = bounds.height; + interpolationMethod = ImageProcessor.NONE; + } else { + if (newWidth==0 || newHeight==0) { + newWidth = (int)origWidth/2; + newHeight = (int)origHeight/2; + } + if (constrain) newHeight = (int)(newWidth*(origHeight/origWidth)); + if (stackSize>1) { + newWidth = (int)origWidth; + newHeight = (int)origHeight; + } + GenericDialog gd = new GenericDialog("Resize"); + gd.addNumericField("Width (pixels):", newWidth, 0); + gd.addNumericField("Height (pixels):", newHeight, 0); + if (imp.isHyperStack()) { + z1 = imp.getNSlices(); + t1 = imp.getNFrames(); + } + if (z1==stackSize) + gd.addNumericField("Depth (images):", z1, 0); + else if (z1>1 && z11) + gd.addNumericField("Time (frames):", t1, 0); + gd.addCheckbox("Constrain aspect ratio", constrain); + gd.addCheckbox("Average when downsizing", averageWhenDownsizing); + gd.addChoice("Interpolation:", methods, methods[interpolationMethod]); + fields = gd.getNumericFields(); + if (!IJ.macroRunning()) { + for (int i=0; i<2; i++) + ((TextField)fields.elementAt(i)).addTextListener(this); + } + checkboxes = gd.getCheckboxes(); + if (!IJ.macroRunning()) + ((Checkbox)checkboxes.elementAt(0)).addItemListener(this); + gd.showDialog(); + if (gd.wasCanceled()) { + imp.unlock(); + return; + } + newWidth = (int)gd.getNextNumber(); + newHeight = (int)gd.getNextNumber(); + if (z1==stackSize || (z1>1 && z11) + t2 = (int)gd.getNextNumber(); + if (gd.invalidNumber()) { + IJ.error("Width or height are invalid."); + imp.unlock(); + return; + } + constrain = gd.getNextBoolean(); + averageWhenDownsizing = gd.getNextBoolean(); + interpolationMethod = gd.getNextChoiceIndex(); + if (constrain && newWidth==0) + sizeToHeight = true; + if (newWidth<=0.0 && !constrain) newWidth = 50; + if (newHeight<=0.0) newHeight = 50; + } + + if (!crop && constrain) { + if (sizeToHeight) + newWidth = (int)Math.round(newHeight*(origWidth/origHeight)); + else + newHeight = (int)Math.round(newWidth*(origHeight/origWidth)); + } + ip.setInterpolationMethod(interpolationMethod); + Undo.setup(crop?Undo.TRANSFORM:Undo.TYPE_CONVERSION, imp); + + if (roi!=null || newWidth!=origWidth || newHeight!=origHeight) { + try { + StackProcessor sp = new StackProcessor(imp.getStack(), ip); + ImageStack s2 = sp.resize(newWidth, newHeight, averageWhenDownsizing); + int newSize = s2.getSize(); + if (s2.getWidth()>0 && newSize>0) { + if (restoreRoi) + imp.deleteRoi(); + Calibration cal = imp.getCalibration(); + if (cal.scaled()) { + cal.pixelWidth *= origWidth/newWidth; + cal.pixelHeight *= origHeight/newHeight; + } + if (crop&&roi!=null&&(cal.xOrigin!=0.0||cal.yOrigin!=0.0)) { + cal.xOrigin -= roi.getBounds().x; + cal.yOrigin -= roi.getBounds().y; + } + imp.setStack(null, s2); + if (crop && roi!=null) { + Overlay overlay = imp.getOverlay(); + if (overlay!=null && !imp.getHideOverlay()) { + Overlay overlay2 = overlay.crop(roi.getBounds()); + imp.setOverlay(overlay2); + } + } else { + Overlay overlay = imp.getOverlay(); + if (overlay!=null && !imp.getHideOverlay()) + imp.setOverlay(overlay.scale(newWidth/origWidth,newHeight/origHeight)); + else + imp.setOverlay(null); + } + if (restoreRoi && roi!=null) { + roi.setLocation(0, 0); + imp.setRoi(roi); + imp.draw(); + } + } + if (stackSize>1 && newSize0 && z2!=z1) + imp2 = zScale(imp, z2, interpolationMethod+IN_PLACE); + if (t2>0 && t2!=t1) + imp2 = zScale(imp2!=null?imp2:imp, t2, interpolationMethod+IN_PLACE+SCALE_T); + imp.unlock(); + if (imp2!=null && imp2!=imp) { + imp.changes = false; + imp.close(); + imp2.show(); + } else if (crop && (bitDepth==16 || bitDepth==32)) { + imp.setDisplayRange(min, max); + imp.updateAndDraw(); + } + Scaler.record(imp, newWidth, newHeight, 1, interpolationMethod); + } + + public ImagePlus zScale(ImagePlus imp, int newDepth, int interpolationMethod) { + ImagePlus imp2 = null; + if (imp.isHyperStack()) + imp2 = zScaleHyperstack(imp, newDepth, interpolationMethod); + else { + boolean inPlace = (interpolationMethod&IN_PLACE)!=0; + interpolationMethod = interpolationMethod&15; + int stackSize = imp.getStackSize(); + int bitDepth = imp.getBitDepth(); + imp2 = resizeZ(imp, newDepth, interpolationMethod); + if (imp2==null) + return null; + double min = imp.getDisplayRangeMin(); + double max = imp.getDisplayRangeMax(); + imp2.setDisplayRange(min, max); + } + if (imp2==null) + return null; + if (imp2!=imp) { + if (imp.isComposite()) { + imp2 = new CompositeImage(imp2, ((CompositeImage)imp).getMode()); + ((CompositeImage)imp2).copyLuts(imp); + } else + imp2.setLut(imp.getProcessor().getLut()); + } + imp2.setCalibration(imp.getCalibration()); + Calibration cal = imp2.getCalibration(); + if (cal.scaled()) cal.pixelDepth *= (double)imp.getNSlices()/imp2.getNSlices(); + Object info = imp.getProperty("Info"); + if (info!=null) imp2.setProperty("Info", info); + imp2.setProperties(imp.getPropertiesAsArray()); + if (imp.isHyperStack()) + imp2.setOpenAsHyperStack(imp.isHyperStack()); + return imp2; + } + + private ImagePlus zScaleHyperstack(ImagePlus imp, int depth2, int interpolationMethod) { + boolean inPlace = (interpolationMethod&IN_PLACE)!=0; + boolean scaleT = (interpolationMethod&SCALE_T)!=0; + interpolationMethod = interpolationMethod&15; + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + int slices2 = slices; + int frames2 = frames; + int bitDepth = imp.getBitDepth(); + if (slices==1 && frames>1) + scaleT = true; + if (scaleT) + frames2 = depth2; + else + slices2 = depth2; + double scale = (double)(depth2-1)/slices; + if (scaleT) scale = (double)(depth2-1)/frames; + ImageStack stack1 = imp.getStack(); + int width = stack1.getWidth(); + int height = stack1.getHeight(); + ImagePlus imp2 = IJ.createImage(imp.getTitle(), bitDepth+"-bit", width, height, channels*slices2*frames2); + if (imp2==null) return null; + imp2.setDimensions(channels, slices2, frames2); + ImageStack stack2 = imp2.getStack(); + ImageProcessor ip = imp.getProcessor(); + int count = 0; + if (scaleT) { + IJ.showStatus("T Scaling..."); + ImageProcessor xtPlane1 = ip.createProcessor(width, frames); + xtPlane1.setInterpolationMethod(interpolationMethod); + ImageProcessor xtPlane2; + Object xtpixels1 = xtPlane1.getPixels(); + int last = slices*channels*height-1; + for (int z=1; z<=slices; z++) { + for (int c=1; c<=channels; c++) { + for (int y=0; y9) decimalPlaces = 9; + return decimalPlaces; + } + + public static Roi enlarge(Roi roi, double pixels) { + if (pixels==0) + return roi; + int type = roi.getType(); + int n = (int)Math.round(pixels); + if (type==Roi.RECTANGLE || type==Roi.OVAL) + return enlargeRectOrOval(roi, n); + if (n<0) + return shrink(roi, -n); + Rectangle bounds = roi.getBounds(); + int width = bounds.width; + int height = bounds.height; + width += 2*n + 2; + height += 2*n + 2; + ImageProcessor ip = new ByteProcessor(width, height); + ip.invert(); + roi.setLocation(n+1, n+1); + ip.setColor(0); + ip.fill(roi); + ip.setThreshold(0, 0, ImageProcessor.NO_LUT_UPDATE); + Roi roi2 = (new ThresholdToSelection()).convert(ip); + Rectangle bounds2 = roi2.getBounds(); + int xoffset = bounds2.x - (n+1); + int yoffset = bounds2.y - (n+1); + roi.setLocation(bounds.x, bounds.y); + FloatProcessor edm = new EDM().makeFloatEDM (ip, 0, false); + edm.setThreshold(0, n, ImageProcessor.NO_LUT_UPDATE); + roi2 = (new ThresholdToSelection()).convert(edm); + if (roi2==null) + return roi; + roi2.copyAttributes(roi); + roi2.setLocation(bounds.x-n+xoffset, bounds.y-n+yoffset); + if (roi.getStroke()!=null) + roi2.setStroke(roi.getStroke()); + return roi2; + } + + private static Roi enlargeRectOrOval(Roi roi, int n) { + Rectangle bounds = roi.getBounds(); + bounds.x -= n; + bounds.y -= n; + bounds.width += 2*n; + bounds.height += 2*n; + if (bounds.width<=0 || bounds.height<=0) + return roi; + Roi roi2 = null; + if (roi.getType()==Roi.RECTANGLE) + roi2 = new Roi(bounds.x, bounds.y, bounds.width, bounds.height); + else + roi2 = new OvalRoi(bounds.x, bounds.y, bounds.width, bounds.height); + roi2.copyAttributes(roi); + return roi2; + } + + private static Roi shrink(Roi roi, int n) { + Rectangle bounds = roi.getBounds(); + int width = bounds.width + 2; + int height = bounds.height + 2; + ImageProcessor ip = new ByteProcessor(width, height); + roi.setLocation(1, 1); + ip.setColor(255); + ip.fill(roi); + roi.setLocation(bounds.x, bounds.y); + FloatProcessor edm = new EDM().makeFloatEDM (ip, 0, false); + edm.setThreshold(n+1, Float.MAX_VALUE, ImageProcessor.NO_LUT_UPDATE); + Roi roi2 = (new ThresholdToSelection()).convert(edm); + if (roi2==null) + return roi; + Rectangle bounds2 = roi2.getBounds(); + if (bounds2.width<=0 && bounds2.height<=0) + return roi; + roi2.copyAttributes(roi); + roi2.setLocation(bounds.x+bounds2.x-1, bounds.y+bounds2.y-1); + return roi2; + } + + public static Roi enlarge255(Roi roi, double pixels) { + if (pixels==0) + return roi; + int type = roi.getType(); + int n = (int)Math.round(pixels); + if (type==Roi.RECTANGLE || type==Roi.OVAL) + return enlargeRectOrOval(roi, n); + if (n<0) + return shrink255(roi, -n); + Rectangle bounds = roi.getBounds(); + int width = bounds.width; + int height = bounds.height; + width += 2*n + 2; + height += 2*n + 2; + ImageProcessor ip = new ByteProcessor(width, height); + ip.invert(); + roi.setLocation(n+1, n+1); + ip.setColor(0); + ip.fill(roi); + ip.setThreshold(0, 0, ImageProcessor.NO_LUT_UPDATE); + Roi roi2 = (new ThresholdToSelection()).convert(ip); + Rectangle bounds2 = roi2.getBounds(); + int xoffset = bounds2.x - (n+1); + int yoffset = bounds2.y - (n+1); + roi.setLocation(bounds.x, bounds.y); + boolean bb = Prefs.blackBackground; + Prefs.blackBackground = true; + new EDM().toEDM(ip); + Prefs.blackBackground = bb; + ip.setThreshold(0, n, ImageProcessor.NO_LUT_UPDATE); + roi2 = (new ThresholdToSelection()).convert(ip); + if (roi2==null) + return roi; + roi2.copyAttributes(roi); + roi2.setLocation(bounds.x-n+xoffset, bounds.y-n+yoffset); + if (roi.getStroke()!=null) + roi2.setStroke(roi.getStroke()); + return roi2; + } + + private static Roi shrink255(Roi roi, int n) { + Rectangle bounds = roi.getBounds(); + int width = bounds.width + 2; + int height = bounds.height + 2; + ImageProcessor ip = new ByteProcessor(width, height); + roi.setLocation(1, 1); + ip.setColor(255); + ip.fill(roi); + roi.setLocation(bounds.x, bounds.y); + boolean bb = Prefs.blackBackground; + Prefs.blackBackground = true; + new EDM().toEDM(ip); + Prefs.blackBackground = bb; + ip.setThreshold(n+1, 255, ImageProcessor.NO_LUT_UPDATE); + Roi roi2 = (new ThresholdToSelection()).convert(ip); + if (roi2==null) + return roi; + Rectangle bounds2 = roi2.getBounds(); + if (bounds2.width<=0 && bounds2.height<=0) + return roi; + roi2.copyAttributes(roi); + roi2.setLocation(bounds.x+bounds2.x-1, bounds.y+bounds2.y-1); + return roi2; + } + +} diff --git a/src/ij/plugin/RoiInterpolator.java b/src/ij/plugin/RoiInterpolator.java new file mode 100644 index 0000000..b5dacb2 --- /dev/null +++ b/src/ij/plugin/RoiInterpolator.java @@ -0,0 +1,105 @@ +package ij.plugin; +import ij.IJ; +import ij.ImagePlus; +import ij.ImageStack; +import ij.process.*; +import ij.gui.Roi; +import ij.plugin.filter.ThresholdToSelection; +import ij.plugin.frame.RoiManager; +import java.awt.Rectangle; +import java.util.ArrayList; + +/** This class interpolates between ROIs in the ROI Manager. + * @author Michael Doube + * @author Johannes Schindelin +*/ +public class RoiInterpolator implements PlugIn { + int[][] idt; + int w, h; + + public void run(String arg) { + RoiManager roiman = RoiManager.getInstance(); + if (roiman==null || roiman.getCount()<2){ + IJ.error("RoiInterpolator", "Please populate the ROI Manager with at least two ROIs"); + return; + } + Roi[] rois = roiman.getRoisAsArray(); + int xmax = 0; + int xmin = Integer.MAX_VALUE; + int ymax = 0; + int ymin = Integer.MAX_VALUE; + int zmax = 1; + int zmin = Integer.MAX_VALUE; + ArrayList templateSlices = new ArrayList(); + for (Roi roi : rois){ + int slice = roiman.getSliceNumber(roi.getName()); + if (!templateSlices.contains(new Integer(slice))) + templateSlices.add(new Integer(slice)); + if (slice==0) //ignore non-slice associated ROIs + continue; + zmin = Math.min(slice, zmin); + zmax = Math.max(slice, zmax); + Rectangle bounds = roi.getBounds(); + xmin = Math.min(xmin, bounds.x); + ymin = Math.min(ymin, bounds.y); + xmax = Math.max(xmax, bounds.x + bounds.width); + ymax = Math.max(ymax, bounds.y + bounds.height); + } + if (templateSlices.size()<2) { + IJ.error("RoiInterpolator", "ROIs are all on the same slice, nothing to interpolate"); + return; + } + //create the binary stack + final int stackW = xmax - xmin + 1; + final int stackH = ymax - ymin + 1; + final int nSlices = zmax - zmin + 1; + ImageStack stack = new ImageStack(stackW, stackH); + for (int s=0; s4) // rounded rectangle + type = Roi.FREEROI; + if (type==Roi.OVAL||type==Roi.TRACED_ROI) + type = Roi.FREEROI; + roi2 = new PolygonRoi(poly.xpoints, poly.ypoints,poly.npoints, type); + } + roi2.copyAttributes(roi); + return roi2; + } + + private static Roi rotateShape(ShapeRoi roi, double angle, double xcenter, double ycenter) { + Shape shape = roi.getShape(); + AffineTransform at = new AffineTransform(); + at.rotate(angle, xcenter, ycenter); + Rectangle r = roi.getBounds(); + at.translate(r.x, r.y); + Shape shape2 = at.createTransformedShape(shape); + Roi roi2 = new ShapeRoi(shape2); + roi2.copyAttributes(roi); + return roi2; + } + +} diff --git a/src/ij/plugin/RoiScaler.java b/src/ij/plugin/RoiScaler.java new file mode 100644 index 0000000..bbef31a --- /dev/null +++ b/src/ij/plugin/RoiScaler.java @@ -0,0 +1,168 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.Measurements; +import java.awt.*; +import java.awt.geom.*; + +/** This plugin implements the Edit/Selection/Scale command. */ +public class RoiScaler implements PlugIn { + private static double defaultXScale = 1.5; + private static double defaultYScale = 1.5; + private double xscale; + private double yscale; + private boolean centered; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + Roi roi = imp.getRoi(); + if (roi==null) { + IJ.error("Scale", "This command requires a selection"); + return; + } + if (!IJ.isMacro() && !imp.okToDeleteRoi()) + return; + if (!showDialog()) + return; + if (!IJ.macroRunning()) { + defaultXScale = xscale; + defaultYScale = yscale; + } + Roi roi2 = scale(roi, xscale, yscale, centered); + if (roi2==null) + return; + Undo.setup(Undo.ROI, imp); + roi = (Roi)roi.clone(); + imp.setRoi(roi2); + Roi.setPreviousRoi(roi); + } + + public boolean showDialog() { + GenericDialog gd = new GenericDialog("Scale Selection"); + gd.addNumericField("X scale factor:", defaultXScale, 2, 4, ""); + gd.addNumericField("Y scale factor:", defaultYScale, 2, 4, ""); + gd.addCheckbox("Centered", false); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + xscale = gd.getNextNumber(); + yscale = gd.getNextNumber(); + centered = gd.getNextBoolean(); + return true; + } + + public static Roi scale(Roi roi, double xscale, double yscale, boolean centered) { + if (roi instanceof ShapeRoi) + return scaleShape((ShapeRoi)roi, xscale, yscale, centered); + else if (roi instanceof TextRoi) + return scaleText((TextRoi)roi, xscale, yscale, centered); + else if (roi instanceof ImageRoi) + return scaleImage((ImageRoi)roi, xscale, yscale, centered); + FloatPolygon poly = roi.getFloatPolygon(); + int type = roi.getType(); + if (type==Roi.LINE) { + Line line = (Line)roi; + double x1=line.x1d; + double y1=line.y1d; + double x2=line.x2d; + double y2=line.y2d; + poly = new FloatPolygon(); + poly.addPoint(x1, y1); + poly.addPoint(x2, y2); + } + ImageStatistics stats = null; + if (centered) { + ImagePlus imp = roi.getImage(); + if (imp==null) { + Rectangle r = roi.getBounds(); + imp = IJ.createImage("Untitled", "8-bit black", r.x+r.width, r.y+r.height, 1); + } + ImageProcessor ip = imp.getProcessor(); + ip.setRoi(roi); + stats = ImageStatistics.getStatistics(imp.getProcessor(), Measurements.CENTROID, null); + if (roi.isLine()) { + Rectangle r = roi.getBounds(); + stats.xCentroid = r.x + Math.round(r.width/2.0); + stats.yCentroid = r.y + Math.round(r.height/2.0); + } + } + for (int i=0; i4) // rounded rectangle + type = Roi.FREEROI; + if (type==Roi.OVAL||type==Roi.TRACED_ROI) + type = Roi.FREEROI; + roi2 = new PolygonRoi(poly.xpoints, poly.ypoints,poly.npoints, type); + } + roi2.copyAttributes(roi); + double width = roi.getStrokeWidth(); + if (width!=0) + roi2.setStrokeWidth(width*xscale); + return roi2; + } + + private static Roi scaleShape(ShapeRoi roi, double xscale, double yscale, boolean centered) { + Rectangle r = roi.getBounds(); + Shape shape = roi.getShape(); + AffineTransform at = new AffineTransform(); + at.scale(xscale, yscale); + if (!centered) + at.translate(r.x, r.y); + Shape shape2 = at.createTransformedShape(shape); + Roi roi2 = new ShapeRoi(shape2); + if (centered) { + int xbase = (int)(centered?r.x-(r.width*xscale-r.width)/2.0:r.x); + int ybase = (int)(centered?r.y-(r.height*yscale-r.height)/2.0:r.y); + roi2.setLocation(xbase, ybase); + } + roi2.copyAttributes(roi); + double width = roi.getStrokeWidth(); + if (width!=0) + roi2.setStrokeWidth(width*xscale); + return roi2; + } + + private static Roi scaleText(TextRoi roi, double xscale, double yscale, boolean centered) { + Rectangle bounds = roi.getBounds(); + int x = (int)Math.round(bounds.x*xscale); + int y = (int)Math.round(bounds.y*yscale); + Font font = roi.getCurrentFont(); + font = font.deriveFont((float)(font.getSize()*yscale)); + Roi roi2 = new TextRoi(x, y, roi.getText(), font); + roi2.copyAttributes(roi); + return roi2; + } + + private static Roi scaleImage(ImageRoi roi, double xscale, double yscale, boolean centered) { + roi = (ImageRoi)roi.clone(); + ImageProcessor ip2 = roi.getProcessor(); + //ip2.setInterpolationMethod(interpolationMethod); + int newWidth = (int)Math.round(ip2.getWidth()*xscale); + int newHeight = (int)Math.round(ip2.getHeight()*yscale); + ip2 = ip2.resize(newWidth, newHeight, true); + roi.setProcessor(ip2); + Rectangle bounds = roi.getBounds(); + int x = (int)Math.round(bounds.x*xscale); + int y = (int)Math.round(bounds.y*yscale); + roi.setLocation(x,y); + roi.copyAttributes(roi); + return roi; + } + +} diff --git a/src/ij/plugin/ScaleBar.java b/src/ij/plugin/ScaleBar.java new file mode 100644 index 0000000..e0d59ed --- /dev/null +++ b/src/ij/plugin/ScaleBar.java @@ -0,0 +1,760 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.measure.*; +import java.awt.event.*; + +/** This plugin implements the Analyze/Tools/Scale Bar command. + * Divakar Ramachandran added options to draw a background + * and use a serif font on 23 April 2006. + * Remi Berthoz added an option to draw vertical scale + * bars on 17 September 2021. +*/ +public class ScaleBar implements PlugIn { + + static final String[] locations = {"Upper Right", "Lower Right", "Lower Left", "Upper Left", "At Selection"}; + static final int UPPER_RIGHT=0, LOWER_RIGHT=1, LOWER_LEFT=2, UPPER_LEFT=3, AT_SELECTION=4; + static final String[] colors = {"White","Black","Light Gray","Gray","Dark Gray","Red","Green","Blue","Yellow"}; + static final String[] bcolors = {"None","Black","White","Dark Gray","Gray","Light Gray","Yellow","Blue","Green","Red"}; + static final String[] checkboxLabels = {"Horizontal", "Vertical", "Bold Text", "Hide Text", "Serif Font", "Overlay"}; + final static String SCALE_BAR = "|SB|"; + + private static final ScaleBarConfiguration sConfig = new ScaleBarConfiguration(); + private ScaleBarConfiguration config = new ScaleBarConfiguration(sConfig); + + ImagePlus imp; + int hBarWidthInPixels; + int vBarHeightInPixels; + int roiX, roiY, roiWidth, roiHeight; + boolean userRoiExists; + boolean[] checkboxStates = new boolean[6]; + + Rectangle hBackground = new Rectangle(); + Rectangle hBar = new Rectangle(); + Rectangle hText = new Rectangle(); + Rectangle vBackground = new Rectangle(); + Rectangle vBar = new Rectangle(); + Rectangle vText = new Rectangle(); + + /** + * This method is called when the plugin is loaded. 'arg', which + * may be blank, is the argument specified for this plugin in + * IJ_Props.txt. + */ + public void run(String arg) { + imp = WindowManager.getCurrentImage(); + if (imp == null) { + IJ.noImage(); + return; + } + // Snapshot before anything, so we can revert if the user cancels the action. + imp.getProcessor().snapshot(); + + userRoiExists = parseCurrentROI(); + boolean userOKed = askUserConfiguration(userRoiExists); + + if (!userOKed) { + removeScalebar(); + return; + } + + if (!IJ.isMacro()) + persistConfiguration(); + updateScalebar(!config.labelAll); + } + + /** + * Remove the scalebar drawn by this plugin. + * + * If the scalebar was drawn without the overlay by another + * instance of the plugin (it is drawn into the image), then + * we cannot remove it. + * + * If the scalebar was drawn using the overlay by another + * instance of the plugin, then we can remove it. + * + * With or without the overlay, we can remove a scalebar + * drawn by this instance of the plugin. + */ + void removeScalebar() { + // Revert with Undo, in case "Use Overlay" is not ticked + imp.getProcessor().reset(); + imp.updateAndDraw(); + // Remove overlay drawn by this plugin, in case "Use Overlay" is ticked + Overlay overlay = imp.getOverlay(); + if (overlay!=null) { + overlay.remove(SCALE_BAR); + imp.draw(); + } + } + + /** + * If there is a user selected ROI, set the class variables {roiX} + * and {roiY}, {roiWidth}, {roiHeight} to the corresponding + * features of the ROI, and return true. Otherwise, return false. + */ + boolean parseCurrentROI() { + Roi roi = imp.getRoi(); + if (roi == null) return false; + + Rectangle r = roi.getBounds(); + roiX = r.x; + roiY = r.y; + roiWidth = r.width; + roiHeight = r.height; + return true; + } + + /** + * There is no hard codded value for the width of the scalebar, + * when the plugin is called for the first time in an ImageJ + * instance, a defautl value for the width will be computed by + * this method. + */ + void computeDefaultBarWidth(boolean currentROIExists) { + Calibration cal = imp.getCalibration(); + ImageWindow win = imp.getWindow(); + double mag = (win!=null)?win.getCanvas().getMagnification():1.0; + if (mag>1.0) + mag = 1.0; + + double pixelWidth = cal.pixelWidth; + if (pixelWidth==0.0) + pixelWidth = 1.0; + double pixelHeight = cal.pixelHeight; + if (pixelHeight==0.0) + pixelHeight = 1.0; + double imageWidth = imp.getWidth()*pixelWidth; + double imageHeight = imp.getHeight()*pixelHeight; + + if (currentROIExists && roiX>=0 && roiWidth>10) { + // If the user has a ROI, set the bar width according to ROI width. + config.hBarWidth = roiWidth*pixelWidth; + } + else if (config.hBarWidth<=0.0 || config.hBarWidth>0.67*imageWidth) { + // If the bar is of negative width or too wide for the image, + // set the bar width to 80 pixels. + config.hBarWidth = (80.0*pixelWidth)/mag; + if (config.hBarWidth>0.67*imageWidth) + // If 80 pixels is too much, do 2/3 of the image. + config.hBarWidth = 0.67*imageWidth; + if (config.hBarWidth>5.0) + // If the resulting size is larger than 5 units, round the value. + config.hBarWidth = (int) config.hBarWidth; + } + + if (currentROIExists && roiY>=0 && roiHeight>10) { + config.vBarHeight = roiHeight*pixelHeight; + } + else if (config.vBarHeight<=0.0 || config.vBarHeight>0.67*imageHeight) { + config.vBarHeight = (80.0*pixelHeight)/mag; + if (config.vBarHeight>0.67*imageHeight) + // If 80 pixels is too much, do 2/3 of the image. + config.vBarHeight = 0.67*imageHeight; + if (config.vBarHeight>5.0) + // If the resulting size is larger than 5 units, round the value. + config.vBarHeight = (int) config.vBarHeight; + } + } + + /** + * Genreate & draw the configuration dialog. + * + * Return the value of dialog.wasOKed() when the user clicks OK + * or Cancel. + */ + boolean askUserConfiguration(boolean currentROIExists) { + // Update the user configuration if there is an ROI, or if + // the defined bar width is negative (it is if it has never + // been set in this ImageJ instance). + if (currentROIExists) { + config.location = locations[AT_SELECTION]; + } + if (IJ.isMacro()) + config.updateFrom(new ScaleBarConfiguration()); + if (config.hBarWidth <= 0 || config.vBarHeight <= 0 || currentROIExists) { + computeDefaultBarWidth(currentROIExists); + } + + // Draw a first preview scalebar, with the default or presisted + // configuration. + updateScalebar(true); + + // Create & show the dialog, then return. + boolean multipleSlices = imp.getStackSize() > 1; + GenericDialog dialog = new BarDialog(getHUnit(), getVUnit(), config.hDigits, config.vDigits, multipleSlices); + DialogListener dialogListener = new BarDialogListener(multipleSlices); + dialog.addDialogListener(dialogListener); + dialog.showDialog(); + return dialog.wasOKed(); + } + + /** + * Store the active configuration into the static variable that + * is persisted across calls of the plugin. + * + * The "active" configuration is normally the one reflected by + * the dialog. + */ + void persistConfiguration() { + sConfig.updateFrom(config); + } + + /** + * Return the X unit strings defined in the image calibration. + */ + String getHUnit() { + String hUnits = imp.getCalibration().getXUnits(); + if (hUnits.equals("microns")) + hUnits = IJ.micronSymbol+"m"; + return hUnits; + } + + /** + * Return the Y unit strings defined in the image calibration. + */ + String getVUnit() { + String vUnits = imp.getCalibration().getYUnits(); + if (vUnits.equals("microns")) + vUnits = IJ.micronSymbol+"m"; + return vUnits; + } + + /** + * Create & draw the scalebar using an Overlay. + */ + Overlay createScaleBarOverlay() throws MissingRoiException { + Overlay overlay = new Overlay(); + + Color color = getColor(); + Color bcolor = getBColor(); + + int fontType = config.boldText?Font.BOLD:Font.PLAIN; + String face = config.serifFont?"Serif":"SanSerif"; + Font font = new Font(face, fontType, config.fontSize); + ImageProcessor ip = imp.getProcessor(); + ip.setFont(font); + + setElementsPositions(ip); + + if (bcolor != null) { + if (config.showHorizontal) { + Roi hBackgroundRoi = new Roi(hBackground.x, hBackground.y, hBackground.width, hBackground.height); + hBackgroundRoi.setFillColor(bcolor); + overlay.add(hBackgroundRoi, SCALE_BAR); + } + if (config.showVertical) { + Roi vBackgroundRoi = new Roi(vBackground.x, vBackground.y, vBackground.width, vBackground.height); + vBackgroundRoi.setFillColor(bcolor); + overlay.add(vBackgroundRoi, SCALE_BAR); + } + } + + if (config.showHorizontal) { + Roi hBarRoi = new Roi(hBar.x, hBar.y, hBar.width, hBar.height); + hBarRoi.setFillColor(color); + overlay.add(hBarRoi, SCALE_BAR); + } + if (config.showVertical) { + Roi vBarRoi = new Roi(vBar.x, vBar.y, vBar.width, vBar.height); + vBarRoi.setFillColor(color); + overlay.add(vBarRoi, SCALE_BAR); + } + + if (!config.hideText) { + if (config.showHorizontal) { + TextRoi hTextRoi = new TextRoi(hText.x, hText.y, getHLabel(), font); + hTextRoi.setStrokeColor(color); + overlay.add(hTextRoi, SCALE_BAR); + } + if (config.showVertical) { + TextRoi vTextRoi = new TextRoi(vText.x, vText.y + vText.height, getVLabel(), font); + vTextRoi.setStrokeColor(color); + vTextRoi.setAngle(90.0); + overlay.add(vTextRoi, SCALE_BAR); + } + } + + return overlay; + } + + /** + * Returns the text to draw near the scalebar ( ). + */ + String getHLabel() { + return IJ.d2s(config.hBarWidth, config.hDigits) + " " + getHUnit(); + } + + /** + * Returns the text to draw near the scalebar ( ). + */ + String getVLabel() { + return IJ.d2s(config.vBarHeight, config.vDigits) + " " + getVUnit(); + } + + /** + * Returns the width of the box that contains the horizontal scalebar and + * its label. + */ + int getHBoxWidthInPixels() { + updateFont(); + ImageProcessor ip = imp.getProcessor(); + int hLabelWidth = config.hideText ? 0 : ip.getStringWidth(getHLabel()); + int hBoxWidth = Math.max(hBarWidthInPixels, hLabelWidth); + return (config.showHorizontal ? hBoxWidth : 0); + } + + /** + * Returns the height of the box that contains the horizontal scalebar and + * its label. + */ + int getHBoxHeightInPixels() { + int hLabelHeight = config.hideText ? 0 : config.fontSize; + int hBoxHeight = config.barThicknessInPixels + (int) (hLabelHeight * 1.25); + return (config.showHorizontal ? hBoxHeight : 0); + } + + /** + * Returns the height of the box that contains the vertical scalebar and + * its label. + */ + int getVBoxHeightInPixels() { + updateFont(); + ImageProcessor ip = imp.getProcessor(); + int vLabelHeight = config.hideText ? 0 : ip.getStringWidth(getVLabel()); + int vBoxHeight = Math.max(vBarHeightInPixels, vLabelHeight); + return (config.showVertical ? vBoxHeight : 0); + } + + /** + * Returns the width of the box that contains the vertical scalebar and + * its label. + */ + int getVBoxWidthInPixels() { + int vLabelWidth = config.hideText ? 0 : config.fontSize; + int vBoxWidth = config.barThicknessInPixels + (int) (vLabelWidth * 1.25); + return (config.showVertical ? vBoxWidth : 0); + } + + /** + * Returns the size of margins that should be displayed between the scalebar + * elements and the image edge. + */ + int getOuterMarginSizeInPixels() { + int imageWidth = imp.getWidth(); + int imageHeight = imp.getHeight(); + return (imageWidth + imageHeight) / 100; + } + + /** + * Retruns the size of margins that should be displayed between the scalebar + * elements and the edge of the element's backround. + */ + int getInnerMarginSizeInPixels() { + int maxWidth = Math.max(getHBoxWidthInPixels(), getVBoxHeightInPixels()); + int margin = Math.max(maxWidth/20, 2); + return config.bcolor.equals("None") ? 0 : margin; + } + + void updateFont() { + int fontType = config.boldText?Font.BOLD:Font.PLAIN; + String font = config.serifFont?"Serif":"SanSerif"; + ImageProcessor ip = imp.getProcessor(); + ip.setFont(new Font(font, fontType, config.fontSize)); + ip.setAntialiasedText(true); + } + + /** + * Sets the positions x y of hBackground and vBackground based on + * the current configuration. + */ + void setBackgroundBoxesPositions(ImageProcessor ip) throws MissingRoiException { + Calibration cal = imp.getCalibration(); + hBarWidthInPixels = (int)(config.hBarWidth/cal.pixelWidth); + vBarHeightInPixels = (int)(config.vBarHeight/cal.pixelHeight); + + boolean hTextTop = config.showVertical && (config.location.equals(locations[UPPER_LEFT]) || config.location.equals(locations[UPPER_RIGHT])); + + int imageWidth = imp.getWidth(); + int imageHeight = imp.getHeight(); + int hBoxWidth = getHBoxWidthInPixels(); + int hBoxHeight = getHBoxHeightInPixels(); + int vBoxWidth = getVBoxWidthInPixels(); + int vBoxHeight = getVBoxHeightInPixels(); + int outerMargin = getOuterMarginSizeInPixels(); + int innerMargin = getInnerMarginSizeInPixels(); + + hBackground.width = innerMargin + hBoxWidth + innerMargin; + hBackground.height = innerMargin + hBoxHeight + innerMargin; + vBackground.width = innerMargin + vBoxWidth + innerMargin; + vBackground.height = innerMargin + vBoxHeight + innerMargin; + + if (config.location.equals(locations[UPPER_RIGHT])) { + hBackground.x = imageWidth - outerMargin - innerMargin - vBoxWidth + (config.showVertical ? config.barThicknessInPixels : 0) - hBoxWidth - innerMargin; + hBackground.y = outerMargin; + vBackground.x = imageWidth - outerMargin - innerMargin - vBoxWidth - innerMargin; + vBackground.y = outerMargin + (hTextTop ? hBoxHeight - config.barThicknessInPixels : 0); + hBackground.width += (config.showVertical ? vBoxWidth - config.barThicknessInPixels : 0); + + } else if (config.location.equals(locations[LOWER_RIGHT])) { + hBackground.x = imageWidth - outerMargin - innerMargin - vBoxWidth - hBoxWidth + (config.showVertical ? config.barThicknessInPixels : 0) - innerMargin; + hBackground.y = imageHeight - outerMargin - innerMargin - hBoxHeight - innerMargin; + vBackground.x = imageWidth - outerMargin - innerMargin - vBoxWidth - innerMargin; + vBackground.y = imageHeight - outerMargin - innerMargin - hBoxHeight + (config.showHorizontal ? config.barThicknessInPixels : 0) - vBoxHeight - innerMargin; + vBackground.height += (config.showHorizontal ? hBoxHeight - config.barThicknessInPixels : 0); + + } else if (config.location.equals(locations[UPPER_LEFT])) { + hBackground.x = outerMargin + (config.showVertical ? vBackground.width - 2*innerMargin - config.barThicknessInPixels : 0); + hBackground.y = outerMargin; + vBackground.x = outerMargin; + vBackground.y = outerMargin + (hTextTop ? hBoxHeight - config.barThicknessInPixels : 0); + hBackground.width += (config.showVertical ? vBoxWidth - config.barThicknessInPixels : 0); + hBackground.x -= (config.showVertical ? vBoxWidth - config.barThicknessInPixels : 0); + + } else if (config.location.equals(locations[LOWER_LEFT])) { + hBackground.x = outerMargin + (config.showVertical ? vBackground.width - 2*innerMargin - config.barThicknessInPixels : 0); + hBackground.y = imageHeight - outerMargin - innerMargin - hBoxHeight - innerMargin; + vBackground.x = outerMargin; + vBackground.y = imageHeight - outerMargin - innerMargin - hBoxHeight + (config.showHorizontal ? config.barThicknessInPixels : 0) - vBoxHeight - innerMargin; + vBackground.height += (config.showHorizontal ? hBoxHeight - config.barThicknessInPixels : 0); + + } else { + if (!userRoiExists) + throw new MissingRoiException(); + + hBackground.x = roiX; + hBackground.y = roiY; + vBackground.x = roiX; + vBackground.y = roiY; + } + } + + /** + * Sets the rectangles x y positions for scalebar elements (hBar, hText, vBar, vText), + * based on the current configuration. Also sets the width and height of the rectangles. + * + * The position of each rectangle is relative to hBackground and vBackground, + * so setBackgroundBoxesPositions() must run before this method computes positions. + * This method calls setBackgroundBoxesPositions(). + */ + void setElementsPositions(ImageProcessor ip) throws MissingRoiException { + + setBackgroundBoxesPositions(ip); + + int hBoxWidth = getHBoxWidthInPixels(); + int hBoxHeight = getHBoxHeightInPixels(); + + int vBoxWidth = getVBoxWidthInPixels(); + int vBoxHeight = getVBoxHeightInPixels(); + + int innerMargin = getInnerMarginSizeInPixels(); + + boolean right = config.location.equals(locations[LOWER_RIGHT]) || config.location.equals(locations[UPPER_RIGHT]); + boolean upper = config.location.equals(locations[UPPER_RIGHT]) || config.location.equals(locations[UPPER_LEFT]); + boolean hTextTop = config.showVertical && upper; + + hBar.x = hBackground.x + innerMargin + (hBoxWidth - hBarWidthInPixels)/2 + (config.showVertical && !right && upper ? vBoxWidth - config.barThicknessInPixels : 0); + hBar.y = hBackground.y + innerMargin + (hTextTop ? hBoxHeight - config.barThicknessInPixels : 0); + hBar.width = hBarWidthInPixels; + hBar.height = config.barThicknessInPixels; + + hText.height = config.hideText ? 0 : config.fontSize; + hText.width = config.hideText ? 0 : ip.getStringWidth(getHLabel()); + hText.x = hBackground.x + innerMargin + (hBoxWidth - hText.width)/2 + (config.showVertical && !right && upper ? vBoxWidth - config.barThicknessInPixels : 0); + hText.y = hTextTop ? (hBackground.y + innerMargin - (int)(config.fontSize*0.25)) : (hBar.y + hBar.height); + + vBar.width = config.barThicknessInPixels; + vBar.height = vBarHeightInPixels; + vBar.x = vBackground.x + (right ? innerMargin : vBackground.width - config.barThicknessInPixels - innerMargin); + vBar.y = vBackground.y + innerMargin + (vBoxHeight - vBar.height)/2; + + vText.height = config.hideText ? 0 : ip.getStringWidth(getVLabel()); + vText.width = config.hideText ? 0 : config.fontSize; + vText.x = right ? (vBar.x + vBar.width) : (vBar.x - vBoxWidth + config.barThicknessInPixels - (int)(config.fontSize*0.25)); + vText.y = vBackground.y + innerMargin + (vBoxHeight - vText.height)/2; + } + + Color getColor() { + Color c = Color.black; + if (config.color.equals(colors[0])) c = Color.white; + else if (config.color.equals(colors[2])) c = Color.lightGray; + else if (config.color.equals(colors[3])) c = Color.gray; + else if (config.color.equals(colors[4])) c = Color.darkGray; + else if (config.color.equals(colors[5])) c = Color.red; + else if (config.color.equals(colors[6])) c = Color.green; + else if (config.color.equals(colors[7])) c = Color.blue; + else if (config.color.equals(colors[8])) c = Color.yellow; + return c; + } + + // Div., mimic getColor to write getBColor for bkgnd + Color getBColor() { + if (config.bcolor==null || config.bcolor.equals(bcolors[0])) return null; + Color bc = Color.white; + if (config.bcolor.equals(bcolors[1])) bc = Color.black; + else if (config.bcolor.equals(bcolors[3])) bc = Color.darkGray; + else if (config.bcolor.equals(bcolors[4])) bc = Color.gray; + else if (config.bcolor.equals(bcolors[5])) bc = Color.lightGray; + else if (config.bcolor.equals(bcolors[6])) bc = Color.yellow; + else if (config.bcolor.equals(bcolors[7])) bc = Color.blue; + else if (config.bcolor.equals(bcolors[8])) bc = Color.green; + else if (config.bcolor.equals(bcolors[9])) bc = Color.red; + return bc; + } + + /** + * Draw the scale bar, based on the current configuration. + * + * If {previewOnly} is true, only the active slice will be + * labeled with a scalebar. If it is false, all slices of + * the stack will be labeled. + * + * This method chooses whether to use an overlay or the + * drawing tool to create the scalebar. + */ + void updateScalebar(boolean previewOnly) { + removeScalebar(); + + Overlay scaleBarOverlay; + try { + scaleBarOverlay = createScaleBarOverlay(); + } catch (MissingRoiException e) { + return; // Simply don't draw the scalebar. + } + + Overlay impOverlay = imp.getOverlay(); + if (impOverlay==null) { + impOverlay = new Overlay(); + } + + if (config.useOverlay) { + for (Roi roi : scaleBarOverlay) + impOverlay.add(roi); + imp.setOverlay(impOverlay); + } else { + if (previewOnly) { + ImageProcessor ip = imp.getProcessor(); + drawOverlayOnProcessor(scaleBarOverlay, ip); + imp.updateAndDraw(); + } else { + ImageStack stack = imp.getStack(); + for (int i=1; i<=stack.size(); i++) { + ImageProcessor ip = stack.getProcessor(i); + drawOverlayOnProcessor(scaleBarOverlay, ip); + imp.updateAndDraw(); + } + imp.setStack(stack); + } + } + } + + void drawOverlayOnProcessor(Overlay overlay, ImageProcessor processor) { + if (processor.getBitDepth() == 8 || processor.getBitDepth() == 24) { + // drawOverlay() only works for 8-bits and RGB + processor.drawOverlay(overlay); + return; + } + // Generate a buffer ImageProcessor, completely black. + // We will draw the overlay on it, then loop over each pixel, + // to replace drawn pixels in the original processor. + ImageProcessor ip = new ByteProcessor(imp.getWidth(), imp.getHeight()); + ip.setCalibrationTable(processor.getCalibrationTable()); + LUT lut = ip.getLut(); + for (Roi roi : overlay) { + Color fillColor = roi.getFillColor(); + if (fillColor != null) { + int i = processor.getBestIndex(fillColor); + roi.setFillColor(new Color(lut.getRed(i), lut.getGreen(i), lut.getBlue(i))); + // Below, when looping on each pixel of the buffer, we detect whether it was + // drawn by checking if the pixel value is greater than zero. Hence, we cannot + // put zero-valued pixels when the Roi is black, or these pixels will not + // be part of the drawing. + if (roi.getFillColor().equals(Color.BLACK)) roi.setFillColor(new Color(1, 1, 1)); + } + Color strokeColor = roi.getStrokeColor(); + if (strokeColor != null) { + int i = processor.getBestIndex(strokeColor); + roi.setStrokeColor(new Color(lut.getRed(i), lut.getGreen(i), lut.getBlue(i))); + if (roi.getStrokeColor().equals(Color.BLACK)) roi.setStrokeColor(new Color(1, 1, 1)); + } + } + ip.drawOverlay(overlay); + for (int y = 0; y < ip.getHeight(); y++) + for (int x = 0; x < ip.getWidth(); x++) { + int p = ip.get(x, y); + if (p > 0) { + p = (int) (p * ((processor.getMax() - processor.getMin()) / 255d) + (float)processor.getMin()); + if (processor.getBitDepth() == 32) + p = Float.floatToIntBits(p); + processor.putPixel(x, y, p); + } + } + } + + class BarDialog extends GenericDialog { + + BarDialog(String hUnits, String vUnits, int hDigits, int vDigits, boolean multipleSlices) { + super("Scale Bar"); + + addNumericField("Width in "+hUnits+": ", config.hBarWidth, hDigits); + addNumericField("Height in "+vUnits+": ", config.vBarHeight, vDigits); + addNumericField("Thickness in pixels: ", config.barThicknessInPixels, 0); + addNumericField("Font size: ", config.fontSize, 0); + addChoice("Color: ", colors, config.color); + addChoice("Background: ", bcolors, config.bcolor); + addChoice("Location: ", locations, config.location); + checkboxStates[0] = config.showHorizontal; checkboxStates[1] = config.showVertical; + checkboxStates[2] = config.boldText; checkboxStates[3] = config.hideText; + checkboxStates[4] = config.serifFont; checkboxStates[5] = config.useOverlay; + setInsets(10, 25, 0); + addCheckboxGroup(3, 2, checkboxLabels, checkboxStates); + + // For simplicity of the itemStateChanged() method below, + // is is best to keep the "Label all slices" checkbox in + // the last position. + if (multipleSlices) { + setInsets(0, 25, 0); + addCheckbox("Label all slices", config.labelAll); + } + } + } //BarDialog inner class + + class BarDialogListener implements DialogListener { + + boolean multipleSlices; + + public BarDialogListener(boolean multipleSlices) { + super(); + this.multipleSlices = multipleSlices; + } + + @Override + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + config.hBarWidth = gd.getNextNumber(); + config.vBarHeight = gd.getNextNumber(); + config.barThicknessInPixels = (int)gd.getNextNumber(); + config.fontSize = (int)gd.getNextNumber(); + config.color = gd.getNextChoice(); + config.bcolor = gd.getNextChoice(); + config.location = gd.getNextChoice(); + config.showHorizontal = gd.getNextBoolean(); + config.showVertical = gd.getNextBoolean(); + config.boldText = gd.getNextBoolean(); + config.hideText = gd.getNextBoolean(); + config.serifFont = gd.getNextBoolean(); + config.useOverlay = gd.getNextBoolean(); + if (multipleSlices) + config.labelAll = gd.getNextBoolean(); + if (!config.showHorizontal && !config.showVertical && IJ.isMacro()) { + // Previous versions of this plugin did not handle vertical scale bars: + // the macro syntax was different in that "height" meant "thickness" of + // the horizontal scalebar. + // If the conditional above is true, then the macro syntax is the old + // one, so we swap a few config variables. + config.showHorizontal = true; + config.barThicknessInPixels = (int)config.vBarHeight; + config.vBarHeight = 0.0; + } + + String widthString = ((TextField) gd.getNumericFields().elementAt(0)).getText(); + boolean hasDecimalPoint = false; + config.hDigits = 0; + for (int i = 0; i < widthString.length(); i++) { + if (hasDecimalPoint) + config.hDigits += 1; + if (widthString.charAt(i) == '.') + hasDecimalPoint = true; + } + + String heightString = ((TextField) gd.getNumericFields().elementAt(1)).getText(); + hasDecimalPoint = false; + config.vDigits = 0; + for (int i = 0; i < heightString.length(); i++) { + if (hasDecimalPoint) + config.vDigits += 1; + if (heightString.charAt(i) == '.') + hasDecimalPoint = true; + } + + updateScalebar(true); + return true; + } + } + + class MissingRoiException extends Exception { + MissingRoiException() { + super("Scalebar location is set to AT_SELECTION but there is no selection on the image."); + } + } //MissingRoiException inner class + + static class ScaleBarConfiguration { + + private static int defaultBarHeight = 4; + + boolean showHorizontal; + boolean showVertical; + double hBarWidth; + double vBarHeight; + int hDigits; // The number of digits after the decimal point that the user input in the dialog for vBarWidth. + int vDigits; + int barThicknessInPixels; + String location; + String color; + String bcolor; + boolean boldText; + boolean hideText; + boolean serifFont; + boolean useOverlay; + int fontSize; + boolean labelAll; + + /** + * Create ScaleBarConfiguration with default values. + */ + ScaleBarConfiguration() { + this.showHorizontal = true; + this.showVertical = false; + this.hBarWidth = -1; + this.vBarHeight = -1; + this.barThicknessInPixels = defaultBarHeight; + this.location = locations[LOWER_RIGHT]; + this.color = colors[0]; + this.bcolor = bcolors[0]; + this.boldText = true; + this.hideText = false; + this.serifFont = false; + this.useOverlay = true; + this.fontSize = 14; + this.labelAll = false; + } + + /** + * Copy constructor. + */ + ScaleBarConfiguration(ScaleBarConfiguration model) { + this.updateFrom(model); + } + + void updateFrom(ScaleBarConfiguration model) { + this.showHorizontal = model.showHorizontal; + this.showVertical = model.showVertical; + this.hBarWidth = model.hBarWidth; + this.vBarHeight = model.vBarHeight; + this.hDigits = model.hDigits; + this.vDigits = model.vDigits; + this.barThicknessInPixels = model.barThicknessInPixels; + this.location = locations[LOWER_RIGHT]; + this.color = model.color; + this.bcolor = model.bcolor; + this.boldText = model.boldText; + this.serifFont = model.serifFont; + this.hideText = model.hideText; + this.useOverlay = model.useOverlay; + this.fontSize = model.fontSize; + this.labelAll = model.labelAll; + } + } //ScaleBarConfiguration inner class + +} //ScaleBar class diff --git a/src/ij/plugin/Scaler.java b/src/ij/plugin/Scaler.java new file mode 100644 index 0000000..82cfe52 --- /dev/null +++ b/src/ij/plugin/Scaler.java @@ -0,0 +1,452 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import ij.util.Tools; +import ij.plugin.frame.Recorder; +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +/** This plugin implements the Image/Scale command. */ +public class Scaler implements PlugIn, TextListener, FocusListener { + private ImagePlus imp; + private static String xstr = "0.5"; + private static String ystr = "0.5"; + private String zstr = "1.0"; + private static int newWidth, newHeight; + private int newDepth; + private boolean doZScaling; + private static boolean averageWhenDownsizing = true; + private static boolean newWindow = true; + private static int staticInterpolationMethod = ImageProcessor.BILINEAR; + private int interpolationMethod = staticInterpolationMethod; + private String[] methods = ImageProcessor.getInterpolationMethods(); + private static boolean fillWithBackground; + private static boolean processStack = true; + private double xscale, yscale, zscale; + private String title = "Untitled"; + private Vector fields; + private double bgValue; + private boolean constainAspectRatio = true; + private TextField xField, yField, zField, widthField, heightField, depthField; + private Rectangle r; + private Object fieldWithFocus; + private int oldDepth; + + public void run(String arg) { + imp = IJ.getImage(); + Roi roi = imp.getRoi(); + ImageProcessor ip = imp.getProcessor(); + if (roi!=null && !roi.isArea()) + ip.resetRoi(); + if (!showDialog(ip)) + return; + doZScaling = newDepth>0 && newDepth!=oldDepth; + if (doZScaling) { + newWindow = true; + processStack = true; + } + if ((ip.getWidth()>1 && ip.getHeight()>1) || newWindow) + ip.setInterpolationMethod(interpolationMethod); + else + ip.setInterpolationMethod(ImageProcessor.NONE); + ip.setBackgroundValue(bgValue); + imp.startTiming(); + try { + if (newWindow && imp.getStackSize()>1 && processStack) { + ImagePlus imp2 = createNewStack(imp, ip, newWidth, newHeight, newDepth); + if (imp2!=null) { + imp2.show(); + imp2.changes = true; + } + } else + scale(imp); + } + catch(OutOfMemoryError o) { + IJ.outOfMemory("Scale"); + } + IJ.showProgress(1.0); + record(imp, newWidth, newHeight, newDepth, interpolationMethod); + } + + /** Returns a scaled copy of this image or ROI, where the + 'options' string can contain 'none', 'bilinear'. 'bicubic', + 'slice' and 'constrain'. + */ + public static ImagePlus resize(ImagePlus imp, int dstWidth, int dstHeight, int dstDepth, String options) { + if (options==null) + options = ""; + Scaler scaler = new Scaler(); + if (options.contains("none")) + scaler.interpolationMethod = ImageProcessor.NONE; + if (options.contains("bicubic")) + scaler.interpolationMethod = ImageProcessor.BICUBIC; + if (scaler.xscale==0) { + scaler.xscale = (double)dstWidth/imp.getWidth(); + scaler.yscale = (double)dstHeight/imp.getHeight(); + scaler.zscale = (double)dstDepth/imp.getStackSize(); + } + boolean processStack = imp.getStackSize()>1 && !options.contains("slice"); + //return new ImagePlus("Untitled", ip.resize(dstWidth, dstHeight, useAveraging)); + Roi roi = imp.getRoi(); + ImageProcessor ip = imp.getProcessor(); + if (roi!=null && !roi.isArea()) + ip.resetRoi(); + scaler.doZScaling = dstDepth!=1; + if (scaler.doZScaling) + scaler.processStack = true; + return scaler.createNewStack(imp, ip, dstWidth, dstHeight, dstDepth); + } + + private ImagePlus createNewStack(ImagePlus imp, ImageProcessor ip, int newWidth, int newHeight, int newDepth) { + int nSlices = imp.getStackSize(); + int w=imp.getWidth(), h=imp.getHeight(); + ImagePlus imp2 = imp.createImagePlus(); + Rectangle r = ip.getRoi(); + boolean crop = r.width!=imp.getWidth() || r.height!=imp.getHeight(); + ImageStack stack1 = imp.getStack(); + ImageStack stack2 = new ImageStack(newWidth, newHeight); + boolean virtualStack = stack1.isVirtual(); + double min = imp.getDisplayRangeMin(); + double max = imp.getDisplayRangeMax(); + ImageProcessor ip1, ip2; + int method = interpolationMethod; + if (w==1 || h==1) + method = ImageProcessor.NONE; + for (int i=1; i<=nSlices; i++) { + IJ.showStatus("Scale: " + i + "/" + nSlices); + ip1 = stack1.getProcessor(i); + String label = stack1.getSliceLabel(i); + if (crop) { + ip1.setRoi(r); + ip1 = ip1.crop(); + } + ip1.setInterpolationMethod(method); + ip2 = ip1.resize(newWidth, newHeight, averageWhenDownsizing); + if (ip2!=null) + stack2.addSlice(label, ip2); + IJ.showProgress(i, nSlices); + } + imp2.setStack(title, stack2); + if (virtualStack) + imp2.setDisplayRange(min, max); + Calibration cal = imp2.getCalibration(); + if (cal.scaled()) { + cal.pixelWidth *= 1.0/xscale; + cal.pixelHeight *= 1.0/yscale; + } + Overlay overlay = imp.getOverlay(); + if (overlay!=null && !imp.getHideOverlay() && !doZScaling) + imp2.setOverlay(overlay.scale(xscale, yscale)); + IJ.showProgress(1.0); + int[] dim = imp.getDimensions(); + imp2.setDimensions(dim[2], dim[3], dim[4]); + if (imp.isComposite()) { + imp2 = new CompositeImage(imp2, ((CompositeImage)imp).getMode()); + ((CompositeImage)imp2).copyLuts(imp); + } + if (imp.isHyperStack()) + imp2.setOpenAsHyperStack(true); + if (doZScaling) { + Resizer resizer = new Resizer(); + resizer.setAverageWhenDownsizing(averageWhenDownsizing); + imp2 = resizer.zScale(imp2, newDepth, interpolationMethod); + } + return imp2; + } + + private void scale(ImagePlus imp) { + ImageProcessor ip = imp.getProcessor(); + if (newWindow) { + Rectangle r = ip.getRoi(); + ImagePlus imp2 = imp.createImagePlus(); + imp2.setProcessor(title, ip.resize(newWidth, newHeight, averageWhenDownsizing)); + Calibration cal = imp2.getCalibration(); + if (cal.scaled()) { + cal.pixelWidth *= 1.0/xscale; + cal.pixelHeight *= 1.0/yscale; + } + Overlay overlay = imp.getOverlay(); + if (overlay!=null && !imp.getHideOverlay()) + imp2.setOverlay(overlay.scale(xscale, yscale)); + imp2.show(); + imp.trimProcessor(); + imp2.trimProcessor(); + imp2.changes = true; + } else { + if (processStack && imp.getStackSize()>1) { + Undo.reset(); + StackProcessor sp = new StackProcessor(imp.getStack(), ip); + sp.scale(xscale, yscale, bgValue); + } else { + ip.snapshot(); + Undo.setup(Undo.FILTER, imp); + ip.setSnapshotCopyMode(true); + ip.scale(xscale, yscale); + ip.setSnapshotCopyMode(false); + } + imp.deleteRoi(); + imp.updateAndDraw(); + imp.changes = true; + } + } + + public static void record(ImagePlus imp, int w2, int h2, int d2, int method) { + if (!Recorder.scriptMode()) + return; + String options = ""; + if (method==ImageProcessor.NONE) + options = "none"; + else if (method==ImageProcessor.BICUBIC) + options = "bicubic"; + else + options = "bilinear"; + Recorder.recordCall("imp = imp.resize("+w2+", "+h2+(d2>0&&d2!=imp.getStackSize()?", "+d2:"")+", \""+options+"\");"); + } + + boolean showDialog(ImageProcessor ip) { + String options = Macro.getOptions(); + boolean isMacro = options!=null; + if (isMacro) { + if (options.contains(" interpolate")) + options = options.replaceAll(" interpolate", " interpolation=Bilinear"); + else if (!options.contains(" interpolation=")) + options = options+" interpolation=None"; + if (options.contains("width=")&&options.contains(" height=")) { + xstr = "-"; + ystr = "-"; + if (options.contains(" depth=")) + zstr = "-"; + else + zstr = "1.0"; + } + Macro.setOptions(options); + interpolationMethod = ImageProcessor.BILINEAR; + } + int bitDepth = imp.getBitDepth(); + int stackSize = imp.getStackSize(); + boolean isStack = stackSize>1; + oldDepth = stackSize; + if (isStack && !isMacro) { + xstr = "1.0"; + ystr = "1.0"; + zstr = "1.0"; + } + r = ip.getRoi(); + int width = newWidth; + if (width==0) width = r.width; + int height = (int)Math.round(((double)width*r.height/r.width)); + xscale = Tools.parseDouble(xstr, 0.0); + yscale = Tools.parseDouble(ystr, 0.0); + zscale = 1.0; + if (xscale!=0.0 && yscale!=0.0) { + width = (int)Math.round(r.width*xscale); + height = (int)Math.round(r.height*yscale); + } else { + xstr = "-"; + ystr = "-"; + } + GenericDialog gd = new GenericDialog("Scale"); + gd.addStringField("X Scale:", xstr); + gd.addStringField("Y Scale:", ystr); + if (isStack) + gd.addStringField("Z Scale:", zstr); + gd.setInsets(5, 0, 5); + gd.addStringField("Width (pixels):", ""+width); + gd.addStringField("Height (pixels):", ""+height); + if (isStack) { + String label = "Depth (images):"; + if (imp.isHyperStack()) { + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + if (slices==1&&frames>1) { + label = "Depth (frames):"; + oldDepth = frames; + } else { + label = "Depth (slices):"; + oldDepth = slices; + } + } + gd.addStringField(label, ""+oldDepth); + } + fields = gd.getStringFields(); + if (fields!=null) { + for (int i=0; i0.0 && yscale>0.0) { + newWidth = (int)Math.round(r.width*xscale); + newHeight = (int)Math.round(r.height*yscale); + } + if (isStack) { + newDepth = (int)Math.round(Tools.parseDouble(gd.getNextString(), 0)); + if (zscale>0.0) { + int nSlices = stackSize; + if (imp.isHyperStack()) { + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + if (slices==1&&frames>1) + nSlices = frames; + else + nSlices = slices; + } + newDepth = (int)Math.round(nSlices*zscale); + } + } + interpolationMethod = gd.getNextChoiceIndex(); + if (bitDepth==8 || bitDepth==24) + fillWithBackground = gd.getNextBoolean(); + averageWhenDownsizing = gd.getNextBoolean(); + if (isStack && !hyperstack) + processStack = gd.getNextBoolean(); + if (hyperstack) + processStack = true; + newWindow = gd.getNextBoolean(); + if (xscale==0.0) { + xscale = (double)newWidth/r.width; + yscale = (double)newHeight/r.height; + } + gd.setSmartRecording(true); + title = gd.getNextString(); + if (fillWithBackground) { + Color bgc = Toolbar.getBackgroundColor(); + if (bitDepth==8) + bgValue = ip.getBestIndex(bgc); + else if (bitDepth==24) + bgValue = bgc.getRGB(); + } else + bgValue = 0.0; + if (!isMacro) + staticInterpolationMethod = interpolationMethod; + return true; + } + + public void textValueChanged(TextEvent e) { + if (xField==null || yField==null) + return; + Object source = e.getSource(); + double newXScale = xscale; + double newYScale = yscale; + double newZScale = zscale; + if (source==xField && fieldWithFocus==xField) { + String newXText = xField.getText(); + newXScale = Tools.parseDouble(newXText,0); + if (newXScale==0) return; + if (newXScale!=xscale) { + int newWidth = (int)Math.round(newXScale*r.width); + widthField.setText(""+newWidth); + if (constainAspectRatio) { + yField.setText(newXText); + int newHeight = (int)Math.round(newXScale*r.height); + heightField.setText(""+newHeight); + } + } + } else if (source==yField && fieldWithFocus==yField) { + String newYText = yField.getText(); + newYScale = Tools.parseDouble(newYText,0); + if (newYScale==0) return; + if (newYScale!=yscale) { + int newHeight = (int)Math.round(newYScale*r.height); + heightField.setText(""+newHeight); + } + } else if (source==zField && fieldWithFocus==zField) { + String newZText = zField.getText(); + newZScale = Tools.parseDouble(newZText,0); + if (newZScale==0) return; + if (newZScale!=zscale) { + int nSlices = imp.getStackSize(); + if (imp.isHyperStack()) { + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + if (slices==1&&frames>1) + nSlices = frames; + else + nSlices = slices; + } + int newDepth= (int)Math.round(newZScale*nSlices); + depthField.setText(""+newDepth); + } + } else if (source==widthField && fieldWithFocus==widthField) { + int newWidth = (int)Math.round(Tools.parseDouble(widthField.getText(), 0.0)); + if (newWidth!=0) { + int newHeight = (int)Math.round(newWidth*(double)r.height/r.width); + heightField.setText(""+newHeight); + xField.setText("-"); + yField.setText("-"); + newXScale = 0.0; + newYScale = 0.0; + } + } else if (source==depthField && fieldWithFocus==depthField) { + int newDepth = (int)Math.round(Tools.parseDouble(depthField.getText(), 0.0)); + if (newDepth!=0) { + zField.setText("-"); + newZScale = 0.0; + } + } + xscale = newXScale; + yscale = newYScale; + zscale = newZScale; + } + + public void focusGained(FocusEvent e) { + fieldWithFocus = e.getSource(); + if (fieldWithFocus==widthField) + constainAspectRatio = true; + else if (fieldWithFocus==yField) + constainAspectRatio = false; + } + + public void focusLost(FocusEvent e) {} + +} diff --git a/src/ij/plugin/ScreenGrabber.java b/src/ij/plugin/ScreenGrabber.java new file mode 100644 index 0000000..6718079 --- /dev/null +++ b/src/ij/plugin/ScreenGrabber.java @@ -0,0 +1,90 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; + +/** This plugin implements the Plugins/Utilities/Capture Screen + and Plugins/Utilities/Capture Image commands. Note that these + commands may not work on Linux if windows translucency or + special effects are enabled in the windows manager. */ +public class ScreenGrabber implements PlugIn { + private static int delay = 10; + + public void run(String arg) { + ImagePlus imp2 = null; + if (arg.equals("image") || arg.equals("flatten")) + imp2 = captureImage(); + else if (arg.equals("delay")) + imp2 = captureDelayed(); + else + imp2 = captureScreen(); + if (imp2!=null) + imp2.show(); + } + + private ImagePlus captureDelayed() { + GenericDialog gd = new GenericDialog("Delayed Capture"); + gd.addNumericField("Delay (seconds):", delay, 0); + gd.showDialog(); + if (gd.wasCanceled()) + return null; + int delay = (int)gd.getNextNumber(); + if (delay<0) return null; + if (delay>60) delay=60; + for (int i=0; i4 && i==delay-2) IJ.beep(); + } + return captureScreen(); + } + + + /** Captures the entire screen and returns it as an ImagePlus. */ + public ImagePlus captureScreen() { + ImagePlus imp = null; + try { + Robot robot = new Robot(); + Rectangle r = GUI.getScreenBounds(IJ.getInstance()); // screen showing "ImageJ" window + Image img = robot.createScreenCapture(r); + if (img!=null) imp = new ImagePlus("Screenshot", img); + } catch(Exception e) {} + return imp; + } + + /** Captures the active image window and returns it as an ImagePlus. */ + public ImagePlus captureImage() { + ImagePlus imp = IJ.getImage(); + if (imp==null) { + IJ.noImage(); + return null; + } + ImageWindow win = imp.getWindow(); + if (win==null) return null; + win.toFront(); + IJ.wait(500); + Point loc = win.getLocation(); + ImageCanvas ic = win.getCanvas(); + Rectangle bounds = ic.getBounds(); + loc.x += bounds.x; + loc.y += bounds.y; + Rectangle r = new Rectangle(loc.x, loc.y, bounds.width, bounds.height); + ImagePlus imp2 = null; + Image img = null; + boolean wasHidden = ic.hideZoomIndicator(true); + IJ.wait(250); + try { + Robot robot = new Robot(); + img = robot.createScreenCapture(r); + } catch(Exception e) { } + ic.hideZoomIndicator(wasHidden); + if (img!=null) { + String title = WindowManager.getUniqueName(imp.getTitle()); + imp2 = new ImagePlus(title, img); + } + return imp2; + } + +} + diff --git a/src/ij/plugin/Selection.java b/src/ij/plugin/Selection.java new file mode 100644 index 0000000..cfeb31f --- /dev/null +++ b/src/ij/plugin/Selection.java @@ -0,0 +1,970 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.frame.*; +import ij.macro.Interpreter; +import ij.plugin.filter.*; +import ij.util.Tools; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.util.Vector; +import java.awt.geom.*; + + +/** This plugin implements the commands in the Edit/Selection submenu. */ +public class Selection implements PlugIn, Measurements { + private ImagePlus imp; + private float[] kernel = {1f, 1f, 1f, 1f, 1f}; + private float[] kernel3 = {1f, 1f, 1f}; + private static int bandSize = 15; // pixels + private static Color linec, fillc; + private static int lineWidth = 1; + private static boolean smooth; + private static boolean adjust; + + + + public void run(String arg) { + imp = WindowManager.getCurrentImage(); + if (arg.equals("add")) { + addToRoiManager(imp); + return; + } + if (imp==null) { + if (!(IJ.isMacro()&&arg.equals("none"))) + IJ.noImage(); + return; + } + if (arg.equals("all")) { + if (imp.okToDeleteRoi()) { + imp.saveRoi(); + imp.setRoi(0,0,imp.getWidth(),imp.getHeight()); + } + } else if (arg.equals("none")) { + if (imp.okToDeleteRoi()) + imp.deleteRoi(); + } else if (arg.equals("restore")) + imp.restoreRoi(); + else if (arg.equals("spline")) + fitSpline(); + else if (arg.equals("interpolate")) + interpolate(); + else if (arg.equals("circle")) + fitCircle(imp); + else if (arg.equals("ellipse")) + createEllipse(imp); + else if (arg.equals("hull")) + convexHull(imp); + else if (arg.equals("mask")) + createMask(imp); + else if (arg.equals("from")) + createSelectionFromMask(imp); + else if (arg.equals("inverse")) + invert(imp); + else if (arg.equals("toarea")) + lineToArea(imp); + else if (arg.equals("toline")) + areaToLine(imp); + else if (arg.equals("properties")) + {setProperties("Properties ", imp.getRoi()); imp.draw();} + else if (arg.equals("band")) + makeBand(imp); + else if (arg.equals("tobox")) + toBoundingBox(imp); + else if (arg.equals("rotate")) + rotate(imp); + else if (arg.equals("enlarge")) + enlarge(imp); + else if (arg.equals("rect")) + fitRectangle(imp); + } + + private void rotate(ImagePlus imp) { + if (!imp.okToDeleteRoi()) return; + Roi roi = imp.getRoi(); + if (IJ.macroRunning()) { + String options = Macro.getOptions(); + if (options!=null && (options.indexOf("grid=")!=-1||options.indexOf("interpolat")!=-1)) { + IJ.run("Rotate... ", options); // run Image>Transform>Rotate + return; + } + } + (new RoiRotator()).run(""); + } + + private void enlarge(ImagePlus imp) { + if (!imp.okToDeleteRoi()) return; + Roi roi = imp.getRoi(); + if (roi!=null) { + Undo.setup(Undo.ROI, imp); + roi = (Roi)roi.clone(); + (new RoiEnlarger()).run(""); + } else + noRoi("Enlarge"); + } + + /* + if selection is closed shape, create a circle with the same area and centroid, otherwise use
+ the Pratt method to fit a circle to the points that define the line or multi-point selection.
+ Reference: Pratt V., Direct least-squares fitting of algebraic surfaces", Computer Graphics, Vol. 21, pages 145-152 (1987).
+ Original code: Nikolai Chernov's MATLAB script for Newton-based Pratt fit.
+ (http://www.math.uab.edu/~chernov/cl/MATLABcircle.html)
+ Java version: https://github.com/mdoube/BoneJ/blob/master/src/org/doube/geometry/FitCircle.java
+ Authors: Nikolai Chernov, Michael Doube, Ved Sharma + */ + void fitCircle(ImagePlus imp) { + if (!imp.okToDeleteRoi()) return; + Roi roi = imp.getRoi(); + if (roi==null) { + noRoi("Fit Circle"); + return; + } + + if (roi.isArea()) { //create circle with the same area and centroid + Undo.setup(Undo.ROI, imp); + ImageProcessor ip = imp.getProcessor(); + ip.setRoi(roi); + ImageStatistics stats = ImageStatistics.getStatistics(ip, Measurements.AREA+Measurements.CENTROID, null); + double r = Math.sqrt(stats.pixelCount/Math.PI); + imp.deleteRoi(); + int d = (int)Math.round(2.0*r); + Roi roi2 = new OvalRoi((int)Math.round(stats.xCentroid-r), (int)Math.round(stats.yCentroid-r), d, d); + transferProperties(roi, roi2); + imp.setRoi(roi2); + return; + } + + Polygon poly = roi.getPolygon(); + int n=poly.npoints; + int[] x = poly.xpoints; + int[] y = poly.ypoints; + if (n<3) { + IJ.error("Fit Circle", "At least 3 points are required to fit a circle."); + return; + } + + // calculate point centroid + double sumx = 0, sumy = 0; + for (int i=0; iMath.abs(yold)) { + if (IJ.debugMode) IJ.log("Fit Circle: wrong direction: |ynew| > |yold|"); + xnew = 0; + break; + } + double Dy = A1 + xnew*(A22 + 16*xnew*xnew); + double xold = xnew; + xnew = xold - ynew/Dy; + if (Math.abs((xnew-xold)/xnew) < epsilon) + break; + if (iter >= IterMax) { + if (IJ.debugMode) IJ.log("Fit Circle: will not converge"); + xnew = 0; + } + if (xnew<0) { + if (IJ.debugMode) IJ.log("Fit Circle: negative root: x = "+xnew); + xnew = 0; + } + } + if (IJ.debugMode) IJ.log("Fit Circle: n="+n+", xnew="+IJ.d2s(xnew,2)+", iterations="+iterations); + + // calculate the circle parameters + double DET = xnew*xnew - xnew*Mz + Cov_xy; + double CenterX = (Mxz*(Myy-xnew)-Myz*Mxy)/(2*DET); + double CenterY = (Myz*(Mxx-xnew)-Mxz*Mxy)/(2*DET); + double radius = Math.sqrt(CenterX*CenterX + CenterY*CenterY + Mz + 2*xnew); + if (Double.isNaN(radius)) { + IJ.error("Fit Circle", "Points are collinear."); + return; + } + CenterX = CenterX + meanx; + CenterY = CenterY + meany; + Undo.setup(Undo.ROI, imp); + imp.deleteRoi(); + IJ.makeOval((int)Math.round(CenterX-radius), (int)Math.round(CenterY-radius), (int)Math.round(2*radius), (int)Math.round(2*radius)); + } + + void fitSpline() { + Roi roi = imp.getRoi(); + if (roi==null) + {noRoi("Spline"); return;} + int type = roi.getType(); + boolean segmentedSelection = type==Roi.POLYGON||type==Roi.POLYLINE; + if (!(segmentedSelection||type==Roi.FREEROI||type==Roi.TRACED_ROI||type==Roi.FREELINE)) + {IJ.error("Spline Fit", "Polygon or polyline selection required"); return;} + if (roi instanceof EllipseRoi) + return; + PolygonRoi p = (PolygonRoi)roi; + Undo.setup(Undo.ROI, imp); + if (!segmentedSelection && p.getNCoordinates()>3) { + if (p.subPixelResolution()) + p = trimFloatPolygon(p, p.getUncalibratedLength()); + else + p = trimPolygon(p, p.getUncalibratedLength()); + } + String options = Macro.getOptions(); + if (options!=null && options.indexOf("straighten")!=-1) + p.fitSplineForStraightening(); + else if (options!=null && options.indexOf("remove")!=-1) + p.removeSplineFit(); + else + p.fitSpline(); + imp.draw(); + LineWidthAdjuster.update(); + } + + void interpolate() { + Roi roi = imp.getRoi(); + if (roi==null) + {noRoi("Interpolate"); return;} + if (roi.getType()==Roi.POINT) + return; + if (IJ.isMacro()&&Macro.getOptions()==null) + Macro.setOptions("interval=1"); + GenericDialog gd = new GenericDialog("Interpolate"); + gd.addNumericField("Interval:", 1.0, 1, 4, "pixel"); + gd.addCheckbox("Smooth", IJ.isMacro()?false:smooth); + gd.addCheckbox("Adjust interval to match", IJ.isMacro()?false:adjust); + gd.showDialog(); + if (gd.wasCanceled()) + return; + double interval = gd.getNextNumber(); + smooth = gd.getNextBoolean(); + Undo.setup(Undo.ROI, imp); + adjust = gd.getNextBoolean(); + int sign = adjust ? -1 : 1; + FloatPolygon poly = roi.getInterpolatedPolygon(sign*interval, smooth); + int t = roi.getType(); + int type = roi.isLine()?Roi.FREELINE:Roi.FREEROI; + if (t==Roi.POLYGON && interval>1.0) + type = Roi.POLYGON; + if ((t==Roi.RECTANGLE||t==Roi.OVAL||t==Roi.FREEROI) && interval>=8.0) + type = Roi.POLYGON; + if ((t==Roi.LINE||t==Roi.FREELINE) && interval>=8.0) + type = Roi.POLYLINE; + if (t==Roi.POLYLINE && interval>=8.0) + type = Roi.POLYLINE; + ImageCanvas ic = imp.getCanvas(); + if (poly.npoints<=150 && ic!=null && ic.getMagnification()>=12.0) + type = roi.isLine()?Roi.POLYLINE:Roi.POLYGON; + Roi p = new PolygonRoi(poly,type); + if (roi.getStroke()!=null) + p.setStrokeWidth(roi.getStrokeWidth()); + p.setStrokeColor(roi.getStrokeColor()); + p.setName(roi.getName()); + transferProperties(roi, p); + imp.setRoi(p); + } + + private static void transferProperties(Roi roi1, Roi roi2) { + if (roi1==null || roi2==null) + return; + roi2.setStrokeColor(roi1.getStrokeColor()); + if (roi1.getStroke()!=null) + roi2.setStroke(roi1.getStroke()); + roi2.setDrawOffset(roi1.getDrawOffset()); + } + + PolygonRoi trimPolygon(PolygonRoi roi, double length) { + int[] x = roi.getXCoordinates(); + int[] y = roi.getYCoordinates(); + int n = roi.getNCoordinates(); + x = smooth(x, n); + y = smooth(y, n); + float[] curvature = getCurvature(x, y, n); + Rectangle r = roi.getBounds(); + double threshold = rodbard(length); + //IJ.log("trim: "+length+" "+threshold); + double distance = Math.sqrt((x[1]-x[0])*(x[1]-x[0])+(y[1]-y[0])*(y[1]-y[0])); + x[0] += r.x; y[0]+=r.y; + int i2 = 1; + int x1,y1,x2=0,y2=0; + for (int i=1; i=threshold) { + x[i2] = x2 + r.x; + y[i2] = y2 + r.y; + i2++; + distance = 0.0; + } + } + int type = roi.getType()==Roi.FREELINE?Roi.POLYLINE:Roi.POLYGON; + if (type==Roi.POLYLINE && distance>0.0) { + x[i2] = x2 + r.x; + y[i2] = y2 + r.y; + i2++; + } + PolygonRoi p = new PolygonRoi(x, y, i2, type); + if (roi.getStroke()!=null) + p.setStrokeWidth(roi.getStrokeWidth()); + p.setStrokeColor(roi.getStrokeColor()); + p.setName(roi.getName()); + imp.setRoi(p); + return p; + } + + double rodbard(double x) { + // y = c*((a-x/(x-d))^(1/b) + // a=3.9, b=.88, c=712, d=44 + double ex; + if (x == 0.0) + ex = 5.0; + else + ex = Math.exp(Math.log(x/700.0)*0.88); + double y = 3.9-44.0; + y = y/(1.0+ex); + return y+44.0; + } + + int[] smooth(int[] a, int n) { + FloatProcessor fp = new FloatProcessor(n, 1); + for (int i=0; i=threshold) { + x[i2] = (float)x2; + y[i2] = (float)y2; + i2++; + distance = 0.0; + } + } + int type = roi.getType()==Roi.FREELINE?Roi.POLYLINE:Roi.POLYGON; + if (type==Roi.POLYLINE && distance>0.0) { + x[i2] = (float)x2; + y[i2] = (float)y2; + i2++; + } + PolygonRoi p = new PolygonRoi(x, y, i2, type); + if (roi.getStroke()!=null) + p.setStrokeWidth(roi.getStrokeWidth()); + p.setStrokeColor(roi.getStrokeColor()); + p.setDrawOffset(roi.getDrawOffset()); + p.setName(roi.getName()); + imp.setRoi(p); + return p; + } + + float[] smooth(float[] a, int n) { + FloatProcessor fp = new FloatProcessor(n, 1); + for (int i=0; iAdjust>Threshold\n"+ + "The current image is not a mask and has not\n"+ + "been thresholded."); + return; + } + int threshold = ip.isInvertedLut()?255:0; + if (Prefs.blackBackground) + threshold = (threshold==255)?0:255; + ip.setThreshold(threshold, threshold, ImageProcessor.NO_LUT_UPDATE); + if (!IJ.isMacro()) + IJ.log("Create Selection: threshold not set; assumed to be "+threshold+"-"+threshold); + IJ.runPlugIn("ij.plugin.filter.ThresholdToSelection", ""); + } + + void invert(ImagePlus imp) { + Roi roi = imp.getRoi(); + if (roi == null) + {run("all"); return;} + if (!roi.isArea()) + {IJ.error("Inverse", "Area selection required"); return;} + Roi inverse = roi.getInverse(imp); + Undo.setup(Undo.ROI, imp); + imp.setRoi(inverse); + } + + private void lineToArea(ImagePlus imp) { + Roi roi = imp.getRoi(); + if (roi==null || !roi.isLine()) + {IJ.error("Line to Area", "Line selection required"); return;} + Undo.setup(Undo.ROI, imp); + Roi roi2 = lineToArea(roi); + imp.setRoi(roi2); + Roi.setPreviousRoi(roi); + } + + /** Converts a line selection into an area selection. */ + public static Roi lineToArea(Roi roi) { + return Roi.convertLineToArea(roi); + } + + void areaToLine(ImagePlus imp) { + Roi roi = imp.getRoi(); + if (roi==null || !roi.isArea()) { + IJ.error("Area to Line", "Area selection required"); + return; + } + Undo.setup(Undo.ROI, imp); + Polygon p = roi.getPolygon(); + FloatPolygon fp = (roi.subPixelResolution()) ? roi.getFloatPolygon() : null; + if (p==null && fp==null) + return; + int type1 = roi.getType(); + if (type1==Roi.COMPOSITE) { + IJ.error("Area to Line", "Composite selections cannot be converted to lines."); + return; + } + if (fp==null && type1==Roi.TRACED_ROI) { + for (int i=0; i=imp.getWidth()) p.xpoints[i]=imp.getWidth()-1; + if (p.ypoints[i]>=imp.getHeight()) p.ypoints[i]=imp.getHeight()-1; + } + } + int type2 = Roi.POLYLINE; + if (type1==Roi.OVAL||type1==Roi.FREEROI||type1==Roi.TRACED_ROI + ||((roi instanceof PolygonRoi)&&((PolygonRoi)roi).isSplineFit())) + type2 = Roi.FREELINE; + Roi roi2 = fp==null ? new PolygonRoi(p, type2) : new PolygonRoi(fp, type2); + transferProperties(roi, roi2); + Rectangle2D.Double bounds = roi.getFloatBounds(); + roi2.setLocation(bounds.x - 0.5, bounds.y -0.5); //area and line roi coordinates are 0.5 pxl different + imp.setRoi(roi2); + } + + void toBoundingBox(ImagePlus imp) { + Roi roi = imp.getRoi(); + if (roi==null) { + noRoi("To Bounding Box"); + return; + } + if (!imp.okToDeleteRoi()) + return; + Undo.setup(Undo.ROI, imp); + Rectangle r = roi.getBounds(); + imp.deleteRoi(); + Roi roi2 = new Roi(r.x, r.y, r.width, r.height); + transferProperties(roi, roi2); + imp.setRoi(roi2); + } + + void addToRoiManager(ImagePlus imp) { + if (IJ.macroRunning() && Interpreter.isBatchModeRoiManager()) + IJ.error("run(\"Add to Manager\") may not work in batch mode macros"); + Frame frame = WindowManager.getFrame("ROI Manager"); + if (frame==null) + IJ.run("ROI Manager..."); + if (imp==null) return; + Roi roi = imp.getRoi(); + if (roi==null) return; + frame = WindowManager.getFrame("ROI Manager"); + if (frame==null || !(frame instanceof RoiManager)) + IJ.error("ROI Manager not found"); + RoiManager rm = (RoiManager)frame; + boolean altDown= IJ.altKeyDown(); + IJ.setKeyUp(IJ.ALL_KEYS); + if (altDown && !IJ.macroRunning()) + IJ.setKeyDown(KeyEvent.VK_SHIFT); + if (roi.getState()==Roi.CONSTRUCTING) { //wait (up to 2 sec.) until ROI finished + long start = System.currentTimeMillis(); + while (true) { + IJ.wait(10); + if (roi.getState()!=Roi.CONSTRUCTING) + break; + if ((System.currentTimeMillis()-start)>2000) { + IJ.beep(); + IJ.error("Add to Manager", "Selection is not complete"); + return; + } + } + } + if (Recorder.record && imp.getStackSize()>1) { + if (imp.isHyperStack()) { + int C = imp.getChannel(); + int Z = imp.getSlice(); + int T = imp.getFrame(); + if (Recorder.scriptMode()) { + Recorder.recordCall("roi = imp.getRoi();"); + Recorder.recordCall("roi.setPosition("+C+", "+Z+", "+T+");"); + } else + Recorder.record("Roi.setPosition", C, Z, T); + } else { + int position = imp.getCurrentSlice(); + if (Recorder.scriptMode()) { + Recorder.recordCall("roi = imp.getRoi();"); + Recorder.recordCall("roi.setPosition("+position+");"); + } else + Recorder.record("Roi.setPosition", position); + } + } + rm.allowRecording(true); + rm.runCommand("add"); + rm.allowRecording(false); + IJ.setKeyUp(IJ.ALL_KEYS); + } + + boolean setProperties(String title, Roi roi) { + if ((roi instanceof PointRoi) && Toolbar.getMultiPointMode() && IJ.altKeyDown()) { + ((PointRoi)roi).displayCounts(); + return true; + } + Frame f = WindowManager.getFrontWindow(); + if (f!=null && f.getTitle().indexOf("3D Viewer")!=-1) + return false; + if (roi==null) { + IJ.error("This command requires a selection."); + return false; + } + RoiProperties rp = new RoiProperties(title, roi); + boolean ok = rp.showDialog(); + if (IJ.debugMode) + IJ.log(roi.getDebugInfo()); + return ok; + } + + private void makeBand(ImagePlus imp) { + Roi roi = imp.getRoi(); + if (roi==null) { + noRoi("Make Band"); + return; + } + Roi roiOrig = roi; + if (!roi.isArea()) { + IJ.error("Make Band", "Area selection required"); + return; + } + Calibration cal = imp.getCalibration(); + double pixels = bandSize; + double size = pixels*cal.pixelWidth; + int decimalPlaces = 0; + if ((int)size!=size) + decimalPlaces = 2; + GenericDialog gd = new GenericDialog("Make Band"); + gd.addNumericField("Band Size:", size, decimalPlaces, 4, cal.getUnits()); + gd.showDialog(); + if (gd.wasCanceled()) + return; + size = gd.getNextNumber(); + if (Double.isNaN(size)) { + IJ.error("Make Band", "invalid number"); + return; + } + int n = (int)Math.round(size/cal.pixelWidth); + if (n >255) { + IJ.error("Make Band", "Cannot make bands wider that 255 pixels"); + return; + } + int width = imp.getWidth(); + int height = imp.getHeight(); + Rectangle r = roi.getBounds(); + ImageProcessor ip = roi.getMask(); + if (ip==null) { + ip = new ByteProcessor(r.width, r.height); + ip.invert(); + } + ImageProcessor mask = new ByteProcessor(width, height); + mask.insert(ip, r.x, r.y); + ImagePlus edm = new ImagePlus("mask", mask); + boolean saveBlackBackground = Prefs.blackBackground; + Prefs.blackBackground = false; + int saveType = EDM.getOutputType(); + EDM.setOutputType(EDM.BYTE_OVERWRITE); + IJ.run(edm, "Distance Map", ""); + EDM.setOutputType(saveType); + Prefs.blackBackground = saveBlackBackground; + ip = edm.getProcessor(); + ip.setThreshold(0, n, ImageProcessor.NO_LUT_UPDATE); + int xx=-1, yy=-1; + for (int x=r.x; x=0||yy>=0) + break; + } + int count = IJ.doWand(edm, xx, yy, 0, null); + if (count<=0) { + IJ.error("Make Band", "Unable to make band"); + return; + } + ShapeRoi roi2 = new ShapeRoi(edm.getRoi()); + if (!(roi instanceof ShapeRoi)) + roi = new ShapeRoi(roi); + ShapeRoi roi1 = (ShapeRoi)roi; + roi2 = roi2.not(roi1); + Undo.setup(Undo.ROI, imp); + transferProperties(roiOrig, roi2); + imp.setRoi(roi2); + bandSize = n; + } + + /* Fits a minimum area rectangle into a ROI, by searching for the minimum area bounding rectangles + * among the ones having a side that is colinear with an edge of the convex hull. + * + * Loosely based on: + * H. Freeman and R. Shapira. 1975. Determining the minimum-area encasing rectangle for an arbitrary + * closed curve. Commun. ACM 18, 7 (July 1975), 409–413. DOI:https://doi.org/10.1145/360881.360919 + */ + private void fitRectangle(ImagePlus imp) { + if (!imp.okToDeleteRoi()) return; + long startTime = System.currentTimeMillis(); + Roi roi = imp.getRoi(); + if (roi == null) + {noRoi("Fit Rectangle"); return;} + if (roi instanceof Line || roi.isDrawingTool()) + {IJ.error("Fit Rectangle", "Area selection, point selection, or segmented or free line required"); return;} + if (!roi.isArea()) { + // check number of points and colinearity before proceeding + FloatPolygon poly = roi.getFloatPolygon(); + int n = poly.npoints; + if (n < 3) + {IJ.error("Fit Rectangle", "At least three points are required"); return;} + float[] x = poly.xpoints; + float[] y = poly.ypoints; + boolean colinear = true; + for(int i=2; i area) { + minArea = area; + minFD = maxLD; + min_hmin = hmin; + min_hmax = hmax; + + imin = imax; + i2min = i2max; + jmin = jmax; + } + } + double pd = ((xp[i2min] - xp[imin]) * (yp[jmin] - yp[imin]) - (xp[jmin] - xp[imin]) * (yp[i2min] - yp[imin])) / Math.sqrt(Math.pow(xp[i2min] - xp[imin], 2) + Math.pow(yp[i2min] - yp [imin], 2)); // signed feret diameter + double pairAngle = Math.atan2( yp[i2min]- yp[imin], xp[i2min]- xp[imin]); + double minAngle = pairAngle + Math.PI/2; + + // rectangle center and signed full height + double xm = xp[imin] + Math.cos(pairAngle) * (min_hmax + min_hmin)/2 + Math.cos(minAngle) * pd/2; + double ym = yp[imin] + Math.sin(pairAngle) * (min_hmax + min_hmin)/2 + Math.sin(minAngle) * pd/2; + double hm = min_hmax - min_hmin; + + if (minFD > Math.abs(hm)) { // ensure control axis is parallel to longer side + pairAngle = pairAngle - Math.PI/2; + minFD = Math.abs(hm); + hm = pd; + } + + if (pairAngle * hm > 0) hm = -hm; // ensure first control point at the top + + double x1 = xm + Math.cos(pairAngle) * hm/2; + double y1 = ym + Math.sin(pairAngle) * hm/2; + double x2 = xm - Math.cos(pairAngle) * hm/2; + double y2 = ym - Math.sin(pairAngle) * hm/2; + Undo.setup(Undo.ROI, imp); + imp.deleteRoi(); + Roi roi2 = new RotatedRectRoi(x1, y1, x2, y2, minFD); + transferProperties(roi, roi2); + imp.setRoi(roi2); + IJ.showTime(imp, startTime, "Fit Rectangle ", 1); + } + } + + void noRoi(String command) { + IJ.error(command, "This command requires a selection"); + } + +} diff --git a/src/ij/plugin/SimpleCommands.java b/src/ij/plugin/SimpleCommands.java new file mode 100644 index 0000000..b992341 --- /dev/null +++ b/src/ij/plugin/SimpleCommands.java @@ -0,0 +1,274 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.io.Opener; +import ij.text.TextWindow; +import ij.measure.ResultsTable; +import ij.plugin.frame.Editor; +import java.awt.Desktop; +import java.awt.Frame; +import java.io.File; + +/** This plugin implements the Plugins/Utilities/Unlock, Image/Rename + and Plugins/Utilities/Search commands. */ +public class SimpleCommands implements PlugIn { + static String searchArg; + private static String[] choices = {"Locked Image", "Clipboard", "Undo Buffer"}; + private static int choiceIndex = 0; + + public void run(String arg) { + if (arg.equals("search")) + search(); + else if (arg.equals("import")) + Opener.openResultsTable(""); + else if (arg.equals("table")) + Opener.openTable(""); + else if (arg.equals("rename")) + rename(); + else if (arg.equals("reset")) + reset(); + else if (arg.equals("about")) + aboutPluginsHelp(); + else if (arg.equals("install")) + installation(); + else if (arg.equals("set")) + setSliceLabel(); + else if (arg.equals("remove")) + removeStackLabels(); + else if (arg.equals("itor")) + imageToResults(); + else if (arg.equals("rtoi")) + resultsToImage(); + else if (arg.equals("display")) + IJ.runMacroFile("ij.jar:ShowAllLuts", null); + else if (arg.equals("missing")) + showMissingPluginsMessage(); + else if (arg.equals("fonts")) + showFonts(); + else if (arg.equals("opencp")) + openControlPanel(); + else if (arg.equals("magic")) + installMagicMontageTools(); + else if (arg.equals("interactive")) + openInteractiveModeEditor(); + else if (arg.startsWith("showdir")) + showDirectory(arg.replace("showdir", "")); + else if (arg.equals("measure")) + measureStack(); + } + + private synchronized void showFonts() { + Thread t = new Thread(new Runnable() { + public void run() {IJ.runPlugIn("ij.plugin.Text", "");} + }); + t.start(); + } + + private void reset() { + GenericDialog gd = new GenericDialog(""); + gd.addChoice("Reset:", choices, choices[choiceIndex]); + gd.showDialog(); + if (gd.wasCanceled()) return; + choiceIndex = gd.getNextChoiceIndex(); + switch (choiceIndex) { + case 0: unlock(); break; + case 1: resetClipboard(); break; + case 2: resetUndo(); break; + } + } + + private void unlock() { + ImagePlus imp = IJ.getImage(); + boolean wasUnlocked = imp.lockSilently(); + if (wasUnlocked) + IJ.showStatus("\""+imp.getTitle()+"\" is not locked"); + else { + IJ.showStatus("\""+imp.getTitle()+"\" is now unlocked"); + IJ.beep(); + } + imp.unlock(); + } + + private void resetClipboard() { + ImagePlus.resetClipboard(); + IJ.showStatus("Clipboard reset"); + } + + private void resetUndo() { + Undo.setup(Undo.NOTHING, null); + IJ.showStatus("Undo reset"); + } + + private void rename() { + ImagePlus imp = IJ.getImage(); + GenericDialog gd = new GenericDialog("Rename"); + gd.addStringField("Title:", imp.getTitle(), 30); + gd.showDialog(); + if (!gd.wasCanceled()) + imp.setTitle(gd.getNextString()); + } + + private void search() { + searchArg = IJ.runMacroFile("ij.jar:Search", searchArg); + } + + private void installation() { + String url = IJ.URL+"/docs/install/"; + if (IJ.isMacintosh()) + url += "osx.html"; + else if (IJ.isWindows()) + url += "windows.html"; + else if (IJ.isLinux()) + url += "linux.html"; + IJ.runPlugIn("ij.plugin.BrowserLauncher", url); + } + + private void aboutPluginsHelp() { + IJ.showMessage("\"About Plugins\" Submenu", + "Plugins packaged as JAR files can add entries\n"+ + "to this submenu. There is an example at\n \n"+ + IJ.URL+"/plugins/jar-demo.html"); + } + + private void setSliceLabel() { + ImagePlus imp = IJ.getImage(); + ImageStack stack = imp.getStack(); + int n = imp.getCurrentSlice(); + String label = stack.getSliceLabel(n); + String label2 = label; + if (label2==null) + label2 = ""; + GenericDialog gd = new GenericDialog("Set Slice Label ("+n+")"); + gd.addStringField("Label:", label2, 30); + gd.showDialog(); + if (!gd.wasCanceled()) { + label2 = gd.getNextString(); + if (label2!=label) { + if (label2.length()==0) + label2 = null; + stack.setSliceLabel(label2, n); + imp.setProp("Slice_Label", label2); + imp.repaintWindow(); + } + } + } + + private void removeStackLabels() { + ImagePlus imp = IJ.getImage(); + ImageStack stack = imp.getStack(); + int size = imp.getStackSize(); + for (int i=1; i<=size; i++) + stack.setSliceLabel(null, i); + if (size==1) + imp.setProp("Slice_Label", null); + imp.repaintWindow(); + } + + private void imageToResults() { + ImagePlus imp = IJ.getImage(); + ImageProcessor ip = imp.getProcessor(); + ResultsTable rt = ResultsTable.createTableFromImage(ip); + rt.show("Results"); + } + + private void resultsToImage() { + ResultsTable rt = ResultsTable.getResultsTable(); + if (rt==null || rt.size()==0) { + IJ.error("Results to Image", "The Results table is empty"); + return; + } + ImageProcessor ip = rt.getTableAsImage(); + if (ip==null) return; + new ImagePlus("Results Table", ip).show(); + } + + private void openControlPanel() { + Prefs.set("Control_Panel.@Main", "51 22 92 426"); + Prefs.set("Control_Panel.Help.Examples", "144 107 261 373"); + IJ.run("Control Panel...", ""); + } + + private void showMissingPluginsMessage() { + IJ.showMessage("Path Randomization", + "Plugins were not loaded due to macOS Path Randomization.\n"+ + "To work around this problem, move ImageJ.app out of the\n"+ + "ImageJ folder and then copy it back. More information is at\n \n"+ + IJ.URL+"/docs/install/osx.html#randomization"); + } + + private void installMagicMontageTools() { + String name = "MagicMontageTools.txt"; + String path = "/macros/"+name; + MacroInstaller mi = new MacroInstaller(); + if (IJ.shiftKeyDown()) + Toolbar.showCode(name, mi.openFromIJJar(path)); + else + try { + mi.installFromIJJar(path); + } catch (Exception e) {} + } + + private void openInteractiveModeEditor() { + Editor ed = new Editor(); + ed.setSize(600, 500); + ed.create(Editor.INTERACTIVE_NAME, ""); + } + + private void showDirectory(String arg) { + arg = arg.toLowerCase(); + String path = IJ.getDir(arg); + if (path == null) { + if (arg.equals("image")) { + if (WindowManager.getCurrentImage()==null) + IJ.noImage(); + else + IJ.error("No file is associated with front image"); + } else + IJ.error("Folder not found: " + arg); + return; + } + File dir = new File(path); + if (!dir.exists()) { + IJ.error("Folder not found: " + arg); + return; + } + if (arg.equals("image")&& IJ.getImage()!=null) { + File imgPath = new File(IJ.getDir("image")); + if (!imgPath.exists()) { + IJ.error("Image not found"); + return; + } + } + if (IJ.debugMode) IJ.log("Show Folder: arg="+arg+", path="+path); + String msg1 = ""; + if (IJ.isLinux()) try { + if (IJ.debugMode) IJ.log(" trying xdg-open "+path); + Runtime.getRuntime().exec(new String[] {"xdg-open", path} ); + return; + } catch (Exception e2) { + msg1 = "xdg-open error: "+e2; + } + try { + if (IJ.debugMode) IJ.log(" trying Desktop.open "+dir); + Desktop desktop = Desktop.getDesktop(); + desktop.open(dir); + } catch (Exception e) { + String msg2 = "Desktop.open error: "+e; + if (msg1.length()>0) + msg2 = msg1+"\n"+msg2; + IJ.error("Show Folder", msg2); + } + } + + private void measureStack() { + ImagePlus imp = IJ.getImage(); + if (imp.isLocked()) { + IJ.showStatus("Image is locked: \""+imp.getTitle()+"\""); + IJ.beep(); + } else + IJ.runMacroFile("ij.jar:MeasureStack", null); + return; + } + +} diff --git a/src/ij/plugin/Slicer.java b/src/ij/plugin/Slicer.java new file mode 100644 index 0000000..ab7aee3 --- /dev/null +++ b/src/ij/plugin/Slicer.java @@ -0,0 +1,753 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.*; +import ij.util.Tools; +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +/** Implements the Image/Stacks/Reslice command. Known shortcomings: + for FREELINE or POLYLINE ROI, spatial calibration is ignored: + the image is sampled at constant _pixel_ increments (distance 1), so + (if y/x aspect ratio != 1 in source image) one dimension in the output is not + homogeneous (i.e. pixelWidth not the same everywhere). +*/ +public class Slicer implements PlugIn, TextListener, ItemListener { + + private static final String[] starts = {"Top", "Left", "Bottom", "Right"}; + private static String startAtS = starts[0]; + private static boolean rotateS; + private static boolean flipS; + private static int sliceCountS = 1; + + private String startAt = starts[0]; + private boolean rotate; + private boolean flip; + private int sliceCount = 1; + private boolean nointerpolate = Prefs.avoidResliceInterpolation; + private double inputZSpacing = 1.0; + private double outputZSpacing = 1.0; + private int outputSlices = 1; + private boolean noRoi; + private boolean rgb, notFloat; + private Vector fields, checkboxes; + private Label message; + private ImagePlus imp; + private double gx1, gy1, gx2, gy2, gLength; + private Color lineColor = new Color(1f, 1f, 0f, 0.4f); + + // Variables used by getIrregularProfile and doIrregularSetup + private int n; + private double[] x; + private double[] y; + private int xbase; + private int ybase; + private double length; + private double[] segmentLengths; + private double[] dx; + private double[] dy; + + public void run(String arg) { + imp = WindowManager.getCurrentImage(); + if (imp==null) { + IJ.noImage(); + return; + } + int stackSize = imp.getStackSize(); + Roi roi = imp.getRoi(); + int roiType = roi!=null?roi.getType():0; + // stack required except for ROI = none or RECT + if (stackSize<2 && roi!=null && roiType!=Roi.RECTANGLE) { + IJ.error("Reslice...", "Stack required"); + return; + } + // permissible ROI types: none,RECT,*LINE + if (roi!=null && roiType!=Roi.RECTANGLE && roiType!=Roi.LINE && roiType!=Roi.POLYLINE && roiType!=Roi.FREELINE) { + IJ.error("Reslice...", "Line or rectangular selection required"); + return; + } + if (!showDialog(imp)) + return; + long startTime = System.currentTimeMillis(); + ImagePlus imp2 = null; + rgb = imp.getType()==ImagePlus.COLOR_RGB; + notFloat = !rgb && imp.getType()!=ImagePlus.GRAY32; + if (imp.isHyperStack()) + imp2 = resliceHyperstack(imp); + else + imp2 = reslice(imp); + if (imp2==null) + return; + ImageProcessor ip = imp.getProcessor(); + double min = ip.getMin(); + double max = ip.getMax(); + if (!rgb) imp2.getProcessor().setMinAndMax(min, max); + imp2.show(); + if (noRoi) + imp.deleteRoi(); + else + imp.draw(); + IJ.showStatus(IJ.d2s(((System.currentTimeMillis()-startTime)/1000.0),2)+" seconds"); + } + + public ImagePlus reslice(ImagePlus imp) { + ImagePlus imp2; + Roi roi = imp.getRoi(); + int roiType = roi!=null?roi.getType():0; + Calibration origCal = imp.getCalibration(); + boolean globalCalibration = false; + if (nointerpolate) {// temporarily clear spatial calibration + globalCalibration = imp.getGlobalCalibration()!=null; + imp.setGlobalCalibration(null); + Calibration tmpCal = origCal.copy(); + tmpCal.pixelWidth = 1.0; + tmpCal.pixelHeight = 1.0; + tmpCal.pixelDepth = 1.0; + imp.setCalibration(tmpCal); + inputZSpacing = 1.0; + if (roiType!=Roi.LINE) + outputZSpacing = 1.0; + } + double zSpacing = inputZSpacing/imp.getCalibration().pixelWidth; + if (roi==null || roiType==Roi.RECTANGLE || roiType==Roi.LINE) { + imp2 = resliceRectOrLine(imp); + } else {// we assert roiType==Roi.POLYLINE || roiType==Roi.FREELINE + String status = imp.getStack().isVirtual()?"":null; + IJ.showStatus("Reslice..."); + ImageProcessor ip2 = getSlice(imp, 0.0, 0.0, 0.0, 0.0, status); + imp2 = new ImagePlus("Reslice of "+imp.getShortTitle(), ip2); + } + if (nointerpolate) { // restore calibration + if (globalCalibration) + imp.setGlobalCalibration(origCal); + imp.setCalibration(origCal); + } + // create Calibration for new stack + // start from previous cal and swap appropriate fields + boolean horizontal = false; + boolean vertical = false; + if (roi==null || roiType==Roi.RECTANGLE) { + if (startAt.equals(starts[0]) || startAt.equals(starts[2])) + horizontal = true; + else + vertical = true; + } + if (roi!=null && roiType==Roi.LINE) { + Line l = (Line)roi; + horizontal = (l.y2-l.y1)==0; + vertical = (l.x2-l.x1)==0; + } + if (imp2==null) return null; + imp2.setCalibration(imp.getCalibration()); + Calibration cal = imp2.getCalibration(); + if (horizontal) { + cal.pixelWidth = origCal.pixelWidth; + cal.pixelHeight = origCal.pixelDepth/zSpacing; + cal.pixelDepth = origCal.pixelHeight*outputZSpacing; + } else if (vertical) { + cal.pixelWidth = origCal.pixelHeight; + cal.pixelHeight = origCal.pixelDepth/zSpacing; + //cal.pixelWidth = origCal.pixelDepth/zSpacing; + //cal.pixelHeight = origCal.pixelHeight; + cal.pixelDepth = origCal.pixelWidth*outputZSpacing;; + } else { // oblique line, polyLine or freeline + if (origCal.pixelHeight==origCal.pixelWidth) { + cal.pixelWidth = origCal.pixelWidth; + cal.pixelHeight=origCal.pixelDepth/zSpacing; + cal.pixelDepth = origCal.pixelWidth*outputZSpacing; + } else { + cal.pixelWidth = cal.pixelHeight=cal.pixelDepth=1.0; + cal.setUnit("pixel"); + } + } + double tmp; + if (rotate) {// if rotated flip X and Y + tmp = cal.pixelWidth; + cal.pixelWidth = cal.pixelHeight; + cal.pixelHeight = tmp; + } + return imp2; + } + + ImagePlus resliceHyperstack(ImagePlus imp) { + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + if (slices==1) + return resliceTimeLapseHyperstack(imp); + int c1 = imp.getChannel(); + int z1 = imp.getSlice(); + int t1 = imp.getFrame(); + int width = imp.getWidth(); + int height = imp.getHeight(); + ImagePlus imp2 = null; + ImageStack stack2 = null; + Roi roi = imp.getRoi(); + for (int t=1; t<=frames; t++) { + for (int c=1; c<=channels; c++) { + ImageStack tmp1Stack = new ImageStack(width, height); + for (int z=1; z<=slices; z++) { + imp.setPositionWithoutUpdate(c, z, t); + tmp1Stack.addSlice(null, imp.getProcessor()); + } + ImagePlus tmp1 = new ImagePlus("tmp", tmp1Stack); + tmp1.setCalibration(imp.getCalibration()); + tmp1.setRoi(roi); + ImagePlus tmp2 = reslice(tmp1); + int slices2 = tmp2.getStackSize(); + if (imp2==null) { + imp2 = tmp2.createHyperStack("Reslice of "+imp.getTitle(), channels, slices2, frames, tmp2.getBitDepth()); + stack2 = imp2.getStack(); + } + ImageStack tmp2Stack = tmp2.getStack(); + for (int z=1; z<=slices2; z++) { + imp.setPositionWithoutUpdate(c, z, t); + int n2 = imp2.getStackIndex(c, z, t); + stack2.setPixels(tmp2Stack.getPixels(z), n2); + } + } + } + imp.setPosition(c1, z1, t1); + if (channels>1 && imp.isComposite()) { + imp2 = new CompositeImage(imp2, ((CompositeImage)imp).getMode()); + ((CompositeImage)imp2).copyLuts(imp); + } + return imp2; + } + + ImagePlus resliceTimeLapseHyperstack(ImagePlus imp) { + int channels = imp.getNChannels(); + int frames = imp.getNFrames(); + int c1 = imp.getChannel(); + int t1 = imp.getFrame(); + int width = imp.getWidth(); + int height = imp.getHeight(); + ImagePlus imp2 = null; + ImageStack stack2 = null; + Roi roi = imp.getRoi(); + int z = 1; + for (int c=1; c<=channels; c++) { + ImageStack tmp1Stack = new ImageStack(width, height); + for (int t=1; t<=frames; t++) { + imp.setPositionWithoutUpdate(c, z, t); + tmp1Stack.addSlice(null, imp.getProcessor()); + } + ImagePlus tmp1 = new ImagePlus("tmp", tmp1Stack); + tmp1.setCalibration(imp.getCalibration()); + tmp1.setRoi(roi); + ImagePlus tmp2 = reslice(tmp1); + int frames2 = tmp2.getStackSize(); + if (imp2==null) { + imp2 = tmp2.createHyperStack("Reslice of "+imp.getTitle(), channels, 1, frames2, tmp2.getBitDepth()); + stack2 = imp2.getStack(); + } + ImageStack tmp2Stack = tmp2.getStack(); + for (int t=1; t<=frames2; t++) { + imp.setPositionWithoutUpdate(c, z, t); + int n2 = imp2.getStackIndex(c, z, t); + stack2.setPixels(tmp2Stack.getPixels(z), n2); + } + } + imp.setPosition(c1, 1, t1); + if (channels>1 && imp.isComposite()) { + imp2 = new CompositeImage(imp2, ((CompositeImage)imp).getMode()); + ((CompositeImage)imp2).copyLuts(imp); + } + return imp2; + } + + boolean showDialog(ImagePlus imp) { + Calibration cal = imp.getCalibration(); + if (cal.pixelDepth<0.0) + cal.pixelDepth = -cal.pixelDepth; + String units = cal.getUnits(); + if (cal.pixelWidth==0.0) + cal.pixelWidth = 1.0; + inputZSpacing = cal.pixelDepth; + double outputSpacing = cal.pixelDepth; + Roi roi = imp.getRoi(); + boolean line = roi!=null && roi.getType()==Roi.LINE; + if (line) saveLineInfo(roi); + String macroOptions = Macro.getOptions(); + boolean macroRunning = macroOptions!=null; + if (macroRunning) { + if (macroOptions.indexOf("input=")!=-1) + macroOptions = macroOptions.replaceAll("slice=", "slice_count="); + macroOptions = macroOptions.replaceAll("slice=", "output="); + Macro.setOptions(macroOptions); + nointerpolate = false; + } else { + startAt = startAtS; + rotate = rotateS; + flip = flipS; + sliceCount = sliceCountS; + } + GenericDialog gd = new GenericDialog("Reslice"); + gd.addNumericField("Output spacing ("+units+"):", outputSpacing, 3); + if (line) { + if (!IJ.isMacro()) outputSlices=sliceCount; + gd.addNumericField("Slice_count:", outputSlices, 0); + } else + gd.addChoice("Start at:", starts, startAt); + gd.addCheckbox("Flip vertically", flip); + gd.addCheckbox("Rotate 90 degrees", rotate); + gd.addCheckbox("Avoid interpolation", nointerpolate); + gd.setInsets(0, 32, 0); + gd.addMessage("(use 1 pixel spacing)"); + gd.setInsets(15, 0, 0); + gd.addMessage("Voxel size: "+d2s(cal.pixelWidth)+"x"+d2s(cal.pixelHeight) + +"x"+d2s(cal.pixelDepth)+" "+cal.getUnit()); + gd.setInsets(5, 0, 0); + gd.addMessage("Output size: "+getSize(cal.pixelDepth,outputSpacing,outputSlices)+" "); + fields = gd.getNumericFields(); + if (!macroRunning) { + for (int i=0; iProperties correct?."); + return null; + } + boolean virtualStack = imp.getStack().isVirtual(); + String status = null; + ImagePlus imp2 = null; + ImageStack stack2 = null; + boolean isStack = imp.getStackSize()>1; + IJ.resetEscape(); + boolean macro = IJ.isMacro(); + for (int i=0; i1?(i+1)+"/"+outputSlices+", ":""; + ImageProcessor ip = getSlice(imp, x1, y1, x2, y2, status); + if (macro) + IJ.showProgress(i,outputSlices-1); + else + drawLine(x1, y1, x2, y2, imp); + if (stack2==null) { + stack2 = createOutputStack(imp, ip); + if (stack2==null || stack2.getSize()0) makePolygon(count, outSpacing); + } + String size = getSize(inputZSpacing, outSpacing, count); + message.setText("Output Size: "+size); + } + + String getSize(double inSpacing, double outSpacing, int count) { + int size = getOutputStackSize(inSpacing, outSpacing, count); + int mem = getAvailableMemory(); + String available = mem!=-1?" ("+mem+"MB free)":""; + if (message!=null) + message.setForeground(mem!=-1&&size>mem?Color.red:Color.black); + if (size>0) + return size+"MB"+available; + else + return "<1MB"+available; + } + + void makePolygon(int count, double outSpacing) { + int[] x = new int[4]; + int[] y = new int[4]; + Calibration cal = imp.getCalibration(); + double cx = cal.pixelWidth; //corrects preview for x calibration + double cy = cal.pixelHeight; //corrects preview for y calibration + x[0] = (int)gx1; + y[0] = (int)gy1; + x[1] = (int)gx2; + y[1] = (int)gy2; + double dx = gx2 - gx1; + double dy = gy2 - gy1; + double nrm = Math.sqrt(dx*dx + dy*dy)/outSpacing; + double xInc = -(dy/(cx*nrm)); //cx scales the x increment + double yInc = (dx/(cy*nrm)); //cy scales the y increment + x[2] = x[1] + (int)(xInc*count); + y[2] = y[1] + (int)(yInc*count); + x[3] = x[0] + (int)(xInc*count); + y[3] = y[0] + (int)(yInc*count); + imp.setRoi(new PolygonRoi(x, y, 4, PolygonRoi.FREEROI)); + } + + int getOutputStackSize(double inSpacing, double outSpacing, int count) { + Roi roi = imp.getRoi(); + int width = imp.getWidth(); + int height = imp.getHeight(); + if (roi!=null) { + Rectangle r = roi.getBounds(); + width = r.width; + width = r.height; + } + int type = roi!=null?roi.getType():0; + int stackSize = imp.getStackSize(); + double size = 0.0; + if (type==Roi.RECTANGLE) { + size = width*height*stackSize; + if (outSpacing>0&&!nointerpolate) size *= inSpacing/outSpacing; + } else + size = gLength*count*stackSize; + int bits = imp.getBitDepth(); + switch (bits) { + case 16: size*=2; break; + case 24: case 32: size*=4; break; + } + return (int)Math.round(size/1048576.0); + } + + int getAvailableMemory() { + long max = IJ.maxMemory(); + if (max==0) return -1; + long inUse = IJ.currentMemory(); + long available = max - inUse; + return (int)((available+524288L)/1048576L); + } +} diff --git a/src/ij/plugin/SpecifyROI.java b/src/ij/plugin/SpecifyROI.java new file mode 100644 index 0000000..4dcf0ba --- /dev/null +++ b/src/ij/plugin/SpecifyROI.java @@ -0,0 +1,227 @@ +package ij.plugin; +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.util.Tools; +import ij.measure.Calibration; + +/** + * This plugin implements the Edit/Selection/Specify command.

+ + * New update, correctly handling existing oval ROIs, the case that + * "Centered" is already selected when the plugin starts, and always + * restoring the original ROI when the dialog is cancelled (JW, 2008/02/22) + * + * Enhancing the original plugin created by Jeffrey Kuhn, this one takes, + * in addition to width and height and the option to have an oval ROI from + * the original program, x & y coordinates, slice number, and the option to have + * the x & y coordinates centered or in default top left corner of ROI. + * The original creator is Jeffrey Kuhn, The University of Texas at Austin, + * jkuhn@ccwf.cc.utexas.edu + * + * @author Joachim Wesner + * @author Anthony Padua + * + */ +public class SpecifyROI implements PlugIn, DialogListener { + private static double width, height, xRoi, yRoi; + private static boolean oval; + private static boolean square; + private static boolean centered; + private static boolean scaledUnits; + private final static int WIDTH = 0, HEIGHT = 1, X_ROI = 2, Y_ROI = 3; //sequence of NumericFields + private final static int OVAL = 0, SQUARE = 1, CENTERED = 2, SCALED_UNITS = 3; //sequence of Checkboxes + private static Rectangle prevRoi; + private static double prevPixelWidth = 1.0; + private int iSlice; + private boolean bAbort; + private ImagePlus imp; + private Vector fields, checkboxes; + private int stackSize; + + public void run(String arg) { + imp = IJ.getImage(); + if (imp == null) return; + if (!imp.okToDeleteRoi()) + return; + stackSize = imp.getStackSize(); + Roi roi = imp.getRoi(); + Calibration cal = imp.getCalibration(); + if (roi!=null && roi.getBounds().equals(prevRoi) && cal.pixelWidth==prevPixelWidth) + roi = null; + if (roi!=null) { + boolean rectOrOval = roi!=null && (roi.getType()==Roi.RECTANGLE||roi.getType()==Roi.OVAL); + oval = rectOrOval && (roi.getType()==Roi.OVAL); // Handle existing oval ROI + Rectangle r = roi.getBounds(); + width = r.width; + height = r.height; + xRoi = r.x; + yRoi = r.y; + if (scaledUnits && cal.scaled()) { + xRoi = xRoi*cal.pixelWidth; + yRoi = yRoi*cal.pixelHeight; + width = width*cal.pixelWidth; + height = height*cal.pixelHeight; + } + if (centered) { // Make xRoi and yRoi consistent when centered mode is active + xRoi += width/2.0; + yRoi += height/2.0; + } + } else if (!validDialogValues()) { + width = imp.getWidth()/2; + height = imp.getHeight()/2; + xRoi = width/2; + yRoi = height/2; + } + iSlice = imp.getCurrentSlice(); + showDialog(); + } + + boolean validDialogValues() { + Calibration cal = imp.getCalibration(); + double pw=cal.pixelWidth, ph=cal.pixelHeight; + if (width/pw<1 || height/ph<1) + return false; + if (xRoi/pw>imp.getWidth() || yRoi/ph>imp.getHeight()) + return false; + return true; + } + + /** + * Creates a dialog box, allowing the user to enter the requested + * width, height, x & y coordinates, slice number for a Region Of Interest, + * option for oval, and option for whether x & y coordinates to be centered. + */ + void showDialog() { + Calibration cal = imp.getCalibration(); + int digits = 0; + if (scaledUnits && cal.scaled()) + digits = 2; + Roi roi = imp.getRoi(); + if (roi==null) + drawRoi(); + GenericDialog gd = new GenericDialog("Specify"); + gd.addNumericField("Width:", width, digits); + gd.addNumericField("Height:", height, digits); + gd.addNumericField("X coordinate:", xRoi, digits); + gd.addNumericField("Y coordinate:", yRoi, digits); + if (stackSize>1) + gd.addNumericField("Slice:", iSlice, 0); + gd.addCheckbox("Oval", oval); + gd.addCheckbox("Constrain square/circle", square); + gd.addCheckbox("Centered",centered); + if (cal.scaled()) { + boolean unitsMatch = cal.getXUnit().equals(cal.getYUnit()); + String units = unitsMatch ? cal.getUnits() : cal.getXUnit()+" x "+cal.getYUnit(); + gd.addCheckbox("Scaled units ("+units+")", scaledUnits); + } + fields = gd.getNumericFields(); + gd.addDialogListener(this); + gd.showDialog(); + if (gd.wasCanceled()) { + if (roi==null) + imp.deleteRoi(); + else // *ALWAYS* restore initial ROI when cancelled + imp.setRoi(roi); + } + } + + void drawRoi() { + double xPxl = xRoi; + double yPxl = yRoi; + if (centered) { + xPxl -= width/2; + yPxl -= height/2; + } + double widthPxl = width; + double heightPxl = height; + Calibration cal = imp.getCalibration(); + if (scaledUnits && cal.scaled()) { + xPxl /= cal.pixelWidth; + yPxl /= cal.pixelHeight; + widthPxl /= cal.pixelWidth; + heightPxl /= cal.pixelHeight; + prevPixelWidth = cal.pixelWidth; + } + Roi roi; + if (oval) + roi = new OvalRoi(xPxl, yPxl, widthPxl, heightPxl); + else + roi = new Roi(xPxl, yPxl, widthPxl, heightPxl); + imp.setRoi(roi); + prevRoi = roi.getBounds(); + //prevPixelWidth = cal.pixelWidth; + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (IJ.isMacOSX()) IJ.wait(50); + Calibration cal = imp.getCalibration(); + width = gd.getNextNumber(); + height = gd.getNextNumber(); + xRoi = gd.getNextNumber(); + yRoi = gd.getNextNumber(); + if (stackSize>1) + iSlice = (int) gd.getNextNumber(); + oval = gd.getNextBoolean(); + square = gd.getNextBoolean(); + centered = gd.getNextBoolean(); + if (cal.scaled()) + scaledUnits = gd.getNextBoolean(); + if (gd.invalidNumber() || width<=0 || height<=0) + return false; + // + Vector numFields = gd.getNumericFields(); + Vector checkboxes = gd.getCheckboxes(); + boolean newWidth = false, newHeight = false, newXY = false; + if (e!=null && e.getSource()==checkboxes.get(SQUARE) && square) { + width = 0.5*(width+height); //make square: same width&height + height = width; + newWidth = true; + newHeight = true; + } + if (e!=null && e.getSource()==checkboxes.get(CENTERED)) { + double shiftBy = centered ? 0.5 : -0.5; //'centered' changed: + xRoi += shiftBy * width; //shift x, y to keep roi the same + yRoi += shiftBy * height; + newXY = true; + } + if (square && width!=height && e!=null) { //in 'square' mode, synchronize width&height + if (e.getSource()==numFields.get(WIDTH)) { + height = width; + newHeight = true; + } else if (e.getSource()==numFields.get(HEIGHT)) { + width = height; + newWidth = true; + } + } + if (e!=null && cal.scaled() && e.getSource()==checkboxes.get(SCALED_UNITS)) { + double xFactor = scaledUnits ? cal.pixelWidth : 1./cal.pixelWidth; + double yFactor = scaledUnits ? cal.pixelHeight : 1./cal.pixelHeight; + width *= xFactor; //transform everything to keep roi the same + height *= yFactor; + xRoi *= xFactor; + yRoi *= yFactor; + newWidth = true; newHeight = true; newXY = true; + } + int digits = (scaledUnits || (int)width!=width) ? 2 : 0; + if (newWidth) + ((TextField)(numFields.get(WIDTH))).setText(IJ.d2s(width, digits)); + if (newHeight) + ((TextField)(numFields.get(HEIGHT))).setText(IJ.d2s(height, digits)); + digits = (scaledUnits || (int)xRoi!=xRoi || (int)yRoi!=yRoi) ? 2 : 0; + if (newXY) { + ((TextField)(numFields.get(X_ROI))).setText(IJ.d2s(xRoi, digits)); + ((TextField)(numFields.get(Y_ROI))).setText(IJ.d2s(yRoi, digits)); + } + + if (stackSize>1 && iSlice>0 && iSlice<=stackSize) + imp.setSlice(iSlice); + if (!newWidth && !newHeight && !newXY) // don't draw if an update will come immediately + drawRoi(); + return true; + } + +} diff --git a/src/ij/plugin/StackCombiner.java b/src/ij/plugin/StackCombiner.java new file mode 100644 index 0000000..ad09c7f --- /dev/null +++ b/src/ij/plugin/StackCombiner.java @@ -0,0 +1,159 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; + +/** + This plugin implements the Image/Stacks/Combine command. + It combines two stacks (w1xh1xd1 and w2xh2xd2) to create a new + w1+w2 x max(h1,h2) x max(d1,d2) stack. For example, a 256x256x40 + and a 256x256x30 stack would be combined into one 512x256x40 stack. + If "Vertical" is checked, create a new max(w1+w2) x (h1+h2) x max(d1,d2) stack. + Unused areas in the combined stack are filled with the background color. +*/ +public class StackCombiner implements PlugIn { + ImagePlus imp1; + ImagePlus imp2; + static boolean vertical; + + public void run(String arg) { + if (!showDialog()) + return; + if (imp1.getBitDepth()!=imp2.getBitDepth()) { + error(); + return; + } + int[] dim1 = imp1.getDimensions(); + int[] dim2 = imp2.getDimensions(); + if (imp1.isHyperStack() || imp2.isHyperStack()) { + if (dim1[2]!=dim2[2] || dim1[3]!=dim2[3] || dim1[4]!=dim2[4]) { + IJ.error("StackCombiner", "Hyperstacks must have identical CZT dimensions"); + return; + } + } + ImageStack stack1 = imp1.getStack(); + ImageStack stack2 = imp2.getStack(); + ImageStack stack3 = vertical?combineVertically(stack1, stack2):combineHorizontally(stack1, stack2); + imp1.changes = false; + imp1.close(); + imp2.changes = false; + imp2.close(); + ImagePlus imp3 = imp1.createImagePlus(); + imp3.setStack(stack3); + if (imp1.isHyperStack()) + imp3.setDimensions(dim1[2],dim1[3],dim1[4]); + if (imp1.isComposite()) { + imp3 = new CompositeImage(imp3, imp1.getCompositeMode()); + imp3.setDimensions(dim1[2],dim1[3],dim1[4]); + } + imp3.setTitle("Combined Stacks"); + imp3.show(); + } + + public ImageStack combineHorizontally(ImageStack stack1, ImageStack stack2) { + int d1 = stack1.getSize(); + int d2 = stack2.getSize(); + int d3 = Math.max(d1, d2); + int w1 = stack1.getWidth(); + int h1 = stack1.getHeight(); + int w2 = stack2.getWidth(); + int h2 = stack2.getHeight(); + int w3 = w1 + w2; + int h3 = Math.max(h1, h2); + ImageStack stack3 = new ImageStack(w3, h3, stack1.getColorModel()); + ImageProcessor ip = stack1.getProcessor(1); + ImageProcessor ip1, ip2, ip3; + Color background = Toolbar.getBackgroundColor(); + for (int i=1; i<=d3; i++) { + IJ.showProgress((double)i/d3); + ip3 = ip.createProcessor(w3, h3); + if (h1!=h2) { + ip3.setColor(background); + ip3.fill(); + } + if (i<=d1) { + ip3.insert(stack1.getProcessor(1),0,0); + if (stack2!=stack1) + stack1.deleteSlice(1); + } + if (i<=d2) { + ip3.insert(stack2.getProcessor(1),w1,0); + stack2.deleteSlice(1); + } + stack3.addSlice(null, ip3); + } + return stack3; + } + + public ImageStack combineVertically(ImageStack stack1, ImageStack stack2) { + int d1 = stack1.getSize(); + int d2 = stack2.getSize(); + int d3 = Math.max(d1, d2); + int w1 = stack1.getWidth(); + int h1 = stack1.getHeight(); + int w2 = stack2.getWidth(); + int h2 = stack2.getHeight(); + int w3 = Math.max(w1, w2); + int h3 = h1 + h2; + ImageStack stack3 = new ImageStack(w3, h3, stack1.getColorModel()); + ImageProcessor ip = stack1.getProcessor(1); + ImageProcessor ip1, ip2, ip3; + Color background = Toolbar.getBackgroundColor(); + for (int i=1; i<=d3; i++) { + IJ.showProgress((double)i/d3); + ip3 = ip.createProcessor(w3, h3); + if (w1!=w2) { + ip3.setColor(background); + ip3.fill(); + } + if (i<=d1) { + ip3.insert(stack1.getProcessor(1),0,0); + if (stack2!=stack1) + stack1.deleteSlice(1); + } + if (i<=d2) { + ip3.insert(stack2.getProcessor(1),0,h1); + stack2.deleteSlice(1); + } + stack3.addSlice(null, ip3); + } + return stack3; + } + + boolean showDialog() { + int[] wList = WindowManager.getIDList(); + if (wList==null || wList.length<2) { + error(); + return false; + } + String[] titles = new String[wList.length]; + for (int i=0; i1 && slices==1) + choice = "frame"; + else if (slices>1) + choice = "slice"; + GenericDialog gd = new GenericDialog("Add"); + gd.addChoice("Add", choices, choice); + gd.addCheckbox("Prepend", false); + gd.showDialog(); + if (gd.wasCanceled()) + return; + choice = gd.getNextChoice(); + boolean prepend = gd.getNextBoolean(); + if (!imp.lock()) + return; + ImageStack stack = imp.getStack(); + LUT[] luts = null; + if (choice.equals("frame")) { // add time point + int index = imp.getStackIndex(channels, slices, t1); + if (prepend) + index = 0; + for (int i=0; i=1; t--) { + int index = imp.getStackIndex(channels, z1, t); + if (prepend) + index = (t-1)*channels*slices; + for (int i=0; i=minIndex) { + ImageProcessor ip = stack.getProcessor(1).duplicate(); + ip.setColor(0); ip.fill(); + stack.addSlice(null, ip, index); + index -= channels; + } + channels++; + } + imp.setStack(stack, channels, slices, frames); + if (luts!=null) { + LUT[] luts2 = new LUT[luts.length+1]; + int index = 0; + for (int i=0; i1) list.add("channel"); + if (slices>1) list.add("slice"); + if (frames>1) list.add("frame"); + String[] choices = new String[list.size()]; + list.toArray(choices); + String choice = choices[0]; + if (frames>1 && slices==1) + choice = "frame"; + else if (slices>1) + choice = "slice"; + String options = Macro.getOptions(); + if (IJ.isMacro() && options!=null && !options.contains("delete=")) { + if (options.contains("delete")) + Macro.setOptions("delete=frame"); + else + Macro.setOptions("delete=slice"); + } + if (IJ.isMacro() && options==null && (imp.isComposite() && imp.getStackSize()==imp.getNChannels())) + Macro.setOptions("delete=channel"); + GenericDialog gd = new GenericDialog("Delete"); + gd.addChoice("Delete current", choices, choice); + gd.showDialog(); + if (gd.wasCanceled()) return; + choice = gd.getNextChoice(); + if (!imp.lock()) return; + ImageStack stack = imp.getStack(); + LUT[] luts = null; + if (choice.equals("frame")) { // delete time point + for (int z=slices; z>=1; z--) { + int index = imp.getStackIndex(channels, z, t1); + for (int i=0; i=1; t--) { + int index = imp.getStackIndex(channels, z1, t); + for (int i=0; i0) { + stack.deleteSlice(index); + index -= channels; + } + channels--; + } + //imp.setDimensions(channels, slices, frames); + imp.setStack(stack, channels, slices, frames); + if (luts!=null) { + for (int i=c1-1; i30 && !IJ.isMacro()) { + boolean ok = IJ.showMessageWithCancel("Convert to Images?", + "Are you sure you want to convert this\nstack to " + +size+" separate windows?"); + if (!ok) { + imp.unlock(); + return; + } + } + Calibration cal = imp.getCalibration(); + CompositeImage cimg = imp.isComposite()?(CompositeImage)imp:null; + if (imp.getNChannels()!=imp.getStackSize()) cimg = null; + Overlay overlay = imp.getOverlay(); + int lastImageID = 0; + for (int i=1; i<=size; i++) { + String label = stack.getShortSliceLabel(i); + if (label!=null && (label.contains("/") || label.contains("\\") || label.contains(":"))) + label = null; + String title = label!=null&&!label.equals("")?label:getTitle(imp, i); + ImageProcessor ip = stack.getProcessor(i); + if (cimg!=null) { + LUT lut = cimg.getChannelLut(i); + if (lut!=null) { + ip.setColorModel(lut); + ip.setMinAndMax(lut.min, lut.max); + } + } + ImagePlus imp2 = new ImagePlus(title, ip); + imp2.setCalibration(cal); + String info = stack.getSliceLabel(i); + if (info!=null && !info.equals(label)) + imp2.setProperty("Info", info); + imp2.setIJMenuBar(i==size); + if (overlay!=null) { + Overlay overlay2 = new Overlay(); + for (int j=0; j0) + imp2.setOverlay(overlay2); + } + if (i==size) + lastImageID = imp2.getID(); + imp2.show(); + } + imp.changes = false; + ImageWindow win = imp.getWindow(); + if (win!=null) + win.close(); + else if (Interpreter.isBatchMode()) + Interpreter.removeBatchModeImage(imp); + imp.unlock(); + } + + String getTitle(ImagePlus imp, int n) { + String digits = "00000000"+n; + return getShortTitle(imp)+"-"+digits.substring(digits.length()-4,digits.length()); + } + + /** Returns a shortened version of image name that does not + include spaces or a file name extension. */ + private String getShortTitle(ImagePlus imp) { + String title = imp.getTitle(); + int index = title.indexOf(' '); + if (index>-1) + title = title.substring(0, index); + index = title.lastIndexOf('.'); + if (index>0) + title = title.substring(0, index); + return title; + } + +} + diff --git a/src/ij/plugin/StackInserter.java b/src/ij/plugin/StackInserter.java new file mode 100644 index 0000000..f3e9283 --- /dev/null +++ b/src/ij/plugin/StackInserter.java @@ -0,0 +1,78 @@ +package ij.plugin; +import java.awt.*; +import java.io.*; +import ij.*; +import ij.gui.*; +import ij.process.*; + +/** This plugin, which implements the Image/Stacks/Tools/Insert + command, inserts an image or stack into another image or stack. */ +public class StackInserter implements PlugIn { + + private static int index1; + private static int index2; + private static int x, y; + + public void run(String arg) { + int[] wList = WindowManager.getIDList(); + if (wList==null) { + IJ.showMessage("Stack Inserter", "No windows are open."); + return; + } + if (wList.length==1) { + IJ.showMessage("Stack Inserter", "At least two windows must be open."); + return; + } + String[] titles = new String[wList.length]; + for (int i=0; i=titles.length)index1 = 0; + if (index2>=titles.length)index2 = 0; + GenericDialog gd = new GenericDialog("Stack Inserter"); + gd.addChoice("Source: ", titles, titles[index1]); + gd.addChoice("Destination: ", titles, titles[index2]); + gd.addNumericField("X Location: ", 0, 0); + gd.addNumericField("Y Location: ", 0, 0); + gd.showDialog(); + if (gd.wasCanceled()) + return; + index1 = gd.getNextChoiceIndex(); + index2 = gd.getNextChoiceIndex(); + x = (int)gd.getNextNumber(); + y = (int)gd.getNextNumber(); + String title1 = titles[index1]; + String title2 = titles[index2]; + ImagePlus imp1 = WindowManager.getImage(wList[index1]); + ImagePlus imp2 = WindowManager.getImage(wList[index2]); + if (imp1.getType()!= imp2.getType()) { + IJ.showMessage("Stack Inserter", "The source and destination must be the same type."); + return; + } + if (imp1== imp2) { + IJ.showMessage("Stack Inserter", "The source and destination must be different."); + return; + } + insert(imp1, imp2, x, y); + } + + public void insert(ImagePlus imp1, ImagePlus imp2, int x, int y) { + ImageStack stack1 = imp1.getStack(); + ImageStack stack2 = imp2.getStack(); + int size1 = stack1.getSize(); + int size2 = stack2.getSize(); + ImageProcessor ip1, ip2; + for (int i=1; i<=size2; i++) { + ip1 = stack1.getProcessor(i<=size1?i:size1); + ip2 = stack2.getProcessor(i); + ip2.insert(ip1, x, y); + stack2.setPixels(ip2.getPixels(), i); + } + imp2.setStack(null, stack2); + } + +} diff --git a/src/ij/plugin/StackMaker.java b/src/ij/plugin/StackMaker.java new file mode 100644 index 0000000..808b2c3 --- /dev/null +++ b/src/ij/plugin/StackMaker.java @@ -0,0 +1,80 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.plugin.*; +import ij.util.Tools; + +/** The plugin implements the Image/Stacks/Tools/Montage to Stack command. + It creates a w*h image stack from an wxh image montage. + This is the opposite of what the "Make Montage" command does. + 2010.04.20,TF: Final stack can be cropped to remove border around frames. +*/ +public class StackMaker implements PlugIn { + private int rows; + private int columns; + private int border; + + public void run(String arg) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) { + IJ.noImage(); + return; + } + if (imp.getStackSize()>1) { + IJ.error("This command requires a montage"); + return; + } + String options = Macro.getOptions(); + if (options!=null) { + options = options.replace("images_per_row=", "columns="); + options = options.replace("images_per_column=", "rows="); + } + columns = info("xMontage", imp, 2); + rows = info("yMontage", imp, 2); + String montageHeight = (String)imp.getProperty("yMontage"); + if (montageHeight!=null) + rows = Integer.parseInt(montageHeight); + GenericDialog gd = new GenericDialog("Stack Maker"); + gd.addNumericField("Columns: ", columns, 0); + gd.addNumericField("Rows: ", rows, 0); + gd.addNumericField("Border width: ", border, 0); + gd.showDialog(); + if (gd.wasCanceled()) + return; + columns = (int)gd.getNextNumber(); + rows = (int)gd.getNextNumber(); + border = (int)gd.getNextNumber(); + if (rows==0 || columns==0) + return; + ImageStack stack = makeStack(imp.getProcessor(), rows, columns, border); + new ImagePlus("Stack", stack).show(); + } + + private int info(String key, ImagePlus imp, int value) { + String svalue = imp.getStringProperty(key); + if (svalue!=null) + value = Integer.parseInt(svalue); + return value; + } + + public ImageStack makeStack(ImageProcessor ip, int rows, int columns, int border) { + int stackSize = rows*columns; + int width = ip.getWidth()/columns; + int height = ip.getHeight()/rows; + //IJ.log("makeStack: "+rows+" "+columns+" "+border+" "+width+" "+height); + ImageStack stack = new ImageStack(width, height); + for (int y=0; y0) { + int cropwidth = width-border-border/2; + int cropheight = height-border-border/2; + StackProcessor sp = new StackProcessor(stack,ip); + stack = sp.crop(border, border, cropwidth, cropheight); + } + return stack; + } +} diff --git a/src/ij/plugin/StackPlotter.java b/src/ij/plugin/StackPlotter.java new file mode 100644 index 0000000..0cddaf1 --- /dev/null +++ b/src/ij/plugin/StackPlotter.java @@ -0,0 +1,93 @@ +/** + * This plugin, which implements the Image/Stacks/Plot XY Profile command, + * generates a stack of plots with the same vertical scale. + * Source image is a stack or hyperstack. + * Line or rectangle selection is required. + * @author Jerome Parent +*/ + +package ij.plugin; +import ij.*; +import ij.gui.*; +import java.awt.*; + +public class StackPlotter implements PlugIn { + + private int channel = 1; + private int slice = 1; + private int frame = 1; + private int frames = 1; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + //Check if Roi is defined + if (imp.getRoi() == null) { + IJ.error("Stack Plotter", "Line or rectangular selection required"); + return; + } + //Check if Image is a Stack + int dim = imp.getNDimensions(); + if (dim < 3) { + IJ.error("Stack Plotter","This plugin requires a stack"); + return; + } + //Get Stack size + int length = 0; + if (dim==3) + length = imp.getImageStackSize(); + // Plot stack over frames information, improvement will be to select the dimension to plot over + boolean plotFrames = true; + if (dim>3) { + channel = imp.getChannel(); + slice = imp.getSlice(); + frame = imp.getFrame(); + length = frames = imp.getNFrames(); + if (dim==4 && length==1) { + plotFrames = false; + length = imp.getNSlices(); + } + } else + slice = imp.getCurrentSlice(); + + //Get a profile plot for each frame in the stack + //Store min and max value of all Profile across the stack + ProfilePlot[] pPlot = new ProfilePlot[length]; + double ymin = 0; + double ymax = 0; + for (int i=0; i 3) { + if (plotFrames) + imp.setPosition(channel,slice,i+1); + else + imp.setPosition(channel,i+1,frame); + } + pPlot[i] = new ProfilePlot(imp); + if (pPlot[i] == null) return; + if (pPlot[i].getMin() < ymin) ymin = pPlot[i].getMin(); + if (pPlot[i].getMax() > ymax) ymax = pPlot[i].getMax(); + } + //Save current Min and Max values of profile plot + double pp_min = ProfilePlot.getFixedMin(); + double pp_max = ProfilePlot.getFixedMax(); + //Set same Min Max values for all plots + ProfilePlot.setMinAndMax(ymin,ymax); + + //Make a profile stack + Plot plot = pPlot[0].getPlot(); + Dimension size = plot.getSize(); + ImageStack stack = new ImageStack(size.width,size.height); + for (int i=0; i< length; i++) { + plot = pPlot[i].getPlot(); + stack.addSlice(plot.getProcessor()); + } + ImagePlus output = new ImagePlus("Profile Plots",stack); + output.show(); + output.setSlice(slice); + if (dim==3) imp.setPosition(slice); + if (dim>3) imp.setPosition(channel,slice,frame); + //reset profile plot Min and May + ProfilePlot.setMinAndMax(pp_min,pp_max); + } + +} diff --git a/src/ij/plugin/StackReducer.java b/src/ij/plugin/StackReducer.java new file mode 100644 index 0000000..485828a --- /dev/null +++ b/src/ij/plugin/StackReducer.java @@ -0,0 +1,99 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.Calibration; +import java.awt.*; + +/** This plugin implements the Image/Stacks/Tools/Reduce command. */ +public class StackReducer implements PlugIn { + ImagePlus imp; + private static int factor = 2; + private boolean hyperstack, reduceSlices; + + public void run(String arg) { + imp = WindowManager.getCurrentImage(); + if (imp==null) + {IJ.noImage(); return;} + ImageStack stack = imp.getStack(); + int size = stack.size(); + if (size==1 || (imp.getNChannels()==size&&imp.isComposite())) + {IJ.error("Stack or hyperstack required"); return;} + if (!showDialog(stack)) + return; + if (hyperstack) + reduceHyperstack(imp, factor, reduceSlices); + else + reduceStack(imp, factor); + } + + public boolean showDialog(ImageStack stack) { + hyperstack = imp.isHyperStack(); + boolean showCheckbox = false; + if (hyperstack && imp.getNSlices()>1 && imp.getNFrames()>1) + showCheckbox = true; + else if (hyperstack && imp.getNSlices()>1) + reduceSlices = true; + int n = stack.size(); + GenericDialog gd = new GenericDialog("Reduce Size"); + gd.addNumericField("Reduction Factor:", factor, 0); + if (showCheckbox) + gd.addCheckbox("Reduce in Z-Dimension", false); + gd.showDialog(); + if (gd.wasCanceled()) return false; + factor = (int) gd.getNextNumber(); + if (showCheckbox) + reduceSlices = gd.getNextBoolean(); + return true; + } + + public void reduceStack(ImagePlus imp, int factor) { + ImageStack stack = imp.getStack(); + boolean virtual = stack.isVirtual(); + int n = stack.size(); + ImageStack stack2 = new ImageStack(stack.getWidth(), stack.getHeight()); + for (int i=1; i<=n; i+=factor) { + if (virtual) IJ.showProgress(i, n); + stack2.addSlice(stack.getSliceLabel(i), stack.getProcessor(i)); + } + imp.setStack(null, stack2); + if (virtual) { + IJ.showProgress(1.0); + imp.setTitle(imp.getTitle()); + } + Calibration cal = imp.getCalibration(); + if (cal.scaled()) cal.pixelDepth *= factor; + } + + public void reduceHyperstack(ImagePlus imp, int factor, boolean reduceSlices) { + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + int zfactor = reduceSlices?factor:1; + int tfactor = reduceSlices?1:factor; + ImageStack stack = imp.getStack(); + ImageStack stack2 = new ImageStack(imp.getWidth(), imp.getHeight()); + boolean virtual = stack.isVirtual(); + int slices2 = slices/zfactor + ((slices%zfactor)!=0?1:0); + int frames2 = frames/tfactor + ((frames%tfactor)!=0?1:0); + int n = channels*slices2*frames2; + int count = 1; + for (int t=1; t<=frames; t+=tfactor) { + for (int z=1; z<=slices; z+=zfactor) { + for (int c=1; c<=channels; c++) { + int i = imp.getStackIndex(c, z, t); + IJ.showProgress(i, n); + ImageProcessor ip = stack.getProcessor(imp.getStackIndex(c, z, t)); + //IJ.log(count++ +" "+i+" "+c+" "+z+" "+t); + stack2.addSlice(stack.getSliceLabel(i), ip); + } + } + } + imp.setStack(stack2, channels, slices2, frames2); + Calibration cal = imp.getCalibration(); + if (cal.scaled()) cal.pixelDepth *= zfactor; + if (virtual) imp.setTitle(imp.getTitle()); + IJ.showProgress(1.0); + } + +} diff --git a/src/ij/plugin/StackReverser.java b/src/ij/plugin/StackReverser.java new file mode 100644 index 0000000..1ee5e44 --- /dev/null +++ b/src/ij/plugin/StackReverser.java @@ -0,0 +1,45 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.measure.Calibration; + +/** This plugin implements the Image/Transform/Flip Z and + Image/Stacks/Tools/Reverse commands. */ +public class StackReverser implements PlugIn { + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + if (imp.getStackSize()==1) { + IJ.error("Flip Z", "This command requires a stack"); + return; + } + if (imp.isHyperStack()) { + IJ.error("Flip Z", "This command does not currently work with hyperstacks."); + return; + } + flipStack(imp); + } + + public void flipStack(ImagePlus imp) { + ImageStack stack = imp.getStack(); + int n = stack.size(); + if (n==1) + return; + Calibration cal = imp.getCalibration(); + double min = cal.getCValue(imp.getDisplayRangeMin()); + double max = cal.getCValue(imp.getDisplayRangeMax()); + ImageStack stack2 = new ImageStack(imp.getWidth(), imp.getHeight(), n); + for (int i=1; i<=n; i++) { + stack2.setPixels(stack.getPixels(i), n-i+1); + stack2.setSliceLabel(stack.getSliceLabel(i), n-i+1); + } + stack2.setColorModel(stack.getColorModel()); + imp.setStack(stack2); + if (imp.isComposite()) { + ((CompositeImage)imp).reset(); + imp.updateAndDraw(); + } + IJ.setMinAndMax(imp, min, max); + } + +} diff --git a/src/ij/plugin/StackWriter.java b/src/ij/plugin/StackWriter.java new file mode 100644 index 0000000..a4961de --- /dev/null +++ b/src/ij/plugin/StackWriter.java @@ -0,0 +1,264 @@ +package ij.plugin; +import java.awt.*; +import java.io.*; +import java.text.DecimalFormat; +import java.util.*; +import ij.*; +import ij.io.*; +import ij.gui.*; +import ij.measure.Calibration; +import ij.process.*; +import ij.plugin.frame.Recorder; +import ij.macro.Interpreter; +import ij.util.Tools; + +/** This plugin, which saves the images in a stack as separate files, + implements the File/Save As/Image Sequence command. */ +public class StackWriter implements PlugIn { + private static final String DIR_KEY = "save.sequence.dir"; + private static String[] choices = {"BMP", "FITS", "GIF", "JPEG", "PGM", "PNG", "Raw", "Text", "TIFF", "ZIP"}; + private static String staticFileType = "TIFF"; + private String fileType = "TIFF"; + private int ndigits = 4; + private boolean useLabels; + private boolean firstTime = true; + private int startAt; + private boolean hyperstack; + private int[] dim; + private ImagePlus imp; + private String directory; + private String format = "tiff"; + private String name; + + /** Saves the specified image as a sequence of images. */ + public static void save(ImagePlus imp, String directoryPath, String options) { + StackWriter sw = new StackWriter(); + sw.imp = imp; + sw.format = Tools.getStringFromList(options, "format=", sw.format); + sw.name = Tools.getStringFromList(options, "name="); + sw.ndigits = (int)Tools.getNumberFromList(options, "digits=", sw.ndigits); + sw.useLabels = options.contains(" use"); + sw.run(directoryPath); + } + + + public void run(String arg) { + if (imp==null) + imp = WindowManager.getCurrentImage(); + if (imp==null || (imp!=null && imp.getStackSize()<2&&!IJ.isMacro())) { + IJ.error("Stack Writer", "This command requires a stack."); + return; + } + int stackSize = imp.getStackSize(); + if (name==null) { + name = imp.getTitle(); + int dotIndex = name.lastIndexOf("."); + if (dotIndex>=0) + name = name.substring(0, dotIndex); + } + hyperstack = imp.isHyperStack(); + LUT[] luts = null; + int lutIndex = 0; + int nChannels = imp.getNChannels(); + if (hyperstack) { + dim = imp.getDimensions(); + if (imp.isComposite()) + luts = ((CompositeImage)imp).getLuts(); + if (firstTime && ndigits==4) { + ndigits = 3; + firstTime = false; + } + } + if (arg!=null && arg.length()>0) + directory = arg; + else { + if (!showDialog(imp)) + return; + } + File d = new File(directory); + if (d==null || !d.isDirectory()) { + IJ.error("File>Save As>Image Sequence", "Directory not found: "+directory); + return; + } + int number = 0; + if (ndigits<1) ndigits = 1; + if (ndigits>8) ndigits = 8; + int maxImages = (int)Math.pow(10,ndigits); + if (stackSize>maxImages && !useLabels && !hyperstack) { + IJ.error("Stack Writer", "More than " + ndigits + +" digits are required to generate \nunique file names for "+stackSize+" images."); + return; + } + if (format.equals("fits") && !FileSaver.okForFits(imp)) + return; + if (format.equals("text")) + format = "text image"; + String extension = "." + format; + if (format.equals("tiff")) + extension = ".tif"; + else if (format.equals("text image")) + extension = ".txt"; + Overlay overlay = imp.getOverlay(); + boolean isOverlay = overlay!=null && !imp.getHideOverlay(); + if (!(format.equals("jpeg")||format.equals("png"))) + isOverlay = false; + ImageStack stack = imp.getStack(); + ImagePlus imp2 = new ImagePlus(); + imp2.setTitle(imp.getTitle()); + Calibration cal = imp.getCalibration(); + int nSlices = stack.size(); + String path,label=null; + imp.lock(); + for (int i=1; i<=nSlices; i++) { + IJ.showStatus("writing: "+i+"/"+nSlices); + IJ.showProgress(i, nSlices); + ImageProcessor ip = stack.getProcessor(i); + if (isOverlay) { + imp.setSliceWithoutUpdate(i); + ip = imp.flatten().getProcessor(); + } else if (luts!=null && nChannels>1 && hyperstack) { + ip.setColorModel(luts[lutIndex++]); + if (lutIndex>=luts.length) lutIndex = 0; + } + imp2.setProcessor(null, ip); + String label2 = stack.getSliceLabel(i); + imp2.setProp("Slice_Label", null); + if (label2!=null) { + if (label2.contains("\n")) + imp2.setProperty("Info", label2); + else + imp2.setProp("Slice_Label", label2);; + } else { + Properties props = imp2.getProperties(); + if (props!=null) props.remove("Info"); + } + imp2.setCalibration(cal); + String digits = getDigits(number++); + if (useLabels) { + label = stack.getShortSliceLabel(i, 111); + if (label!=null && label.equals("")) label = null; + if (label!=null) label = label.replaceAll("/","-"); + } + if (label==null) + path = directory+name+digits+extension; + else + path = directory+label+extension; + if (i==1) { + File f = new File(path); + if (f.exists()) { + if (!IJ.isMacro() && !IJ.showMessageWithCancel("Overwrite files?", + "One or more files will be overwritten if you click \"OK\".\n \n"+path)) { + imp.unlock(); + IJ.showStatus(""); + IJ.showProgress(1.0); + return; + } + } + } + if (Recorder.record) + Recorder.disablePathRecording(); + imp2.setOverlay(null); + if (overlay!=null && format.equals("tiff")) { + Overlay overlay2 = overlay.duplicate(); + overlay2.crop(i, i); + if (overlay2.size()>0) { + for (int j=0; j1) { + cs = "00000000"+c; + cs = "_c"+cs.substring(cs.length()-ndigits); + } + if (dim[3]>1) { + zs = "00000000"+z; + zs = "_z"+zs.substring(zs.length()-ndigits); + } + if (dim[4]>1) { + ts = "00000000"+t; + ts = "_t"+ts.substring(ts.length()-ndigits); + } + return ts+zs+cs; + } else { + String digits = "00000000"+(startAt+n); + return digits.substring(digits.length()-ndigits); + } + } + +} + diff --git a/src/ij/plugin/Stack_Statistics.java b/src/ij/plugin/Stack_Statistics.java new file mode 100644 index 0000000..de7fa50 --- /dev/null +++ b/src/ij/plugin/Stack_Statistics.java @@ -0,0 +1,54 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.plugin.filter.Analyzer; +import ij.measure.*; +import ij.gui.Roi; +import java.awt.Rectangle; + +/** This plugin implements the Image/Stacks/Statistics command. */ +public class Stack_Statistics implements PlugIn { + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + int measurements = Analyzer.getMeasurements(); + Analyzer.setMeasurements(measurements | Measurements.LIMIT); + ImageStatistics stats = new StackStatistics(imp); + Analyzer.setMeasurements(measurements); + ResultsTable rt = Analyzer.getResultsTable(); + rt.incrementCounter(); + Roi roi = imp.getRoi(); + if (roi!=null && !roi.isArea()) { + imp.deleteRoi(); + roi = null; + } + double stackVoxels = 0.0; + double images = imp.getStackSize(); + if (roi==null) + stackVoxels = imp.getWidth()*imp.getHeight()*images; + else if (roi.getType()==Roi.RECTANGLE) { + Rectangle r = roi.getBounds(); + stackVoxels = r.width*r.height*images; + } else { + Analyzer.setMeasurements(measurements & ~Measurements.LIMIT); + ImageStatistics stats2 = new StackStatistics(imp); + Analyzer.setMeasurements(measurements); + stackVoxels = stats2.longPixelCount; + } + Calibration cal = imp.getCalibration(); + String units = cal.getUnits(); + double scale = cal.pixelWidth*cal.pixelHeight*cal.pixelDepth; + rt.addValue("Voxels", stats.longPixelCount); + if (scale!=1.0) + rt.addValue("Volume("+units+"^3)", stats.longPixelCount*scale); + rt.addValue("%Volume", stats.longPixelCount*100.0/stackVoxels); + rt.addValue("Mean", stats.mean); + rt.addValue("StdDev", stats.stdDev); + rt.addValue("Min", stats.min); + rt.addValue("Max", stats.max); + rt.addValue("Mode", stats.dmode); + rt.addValue("Median", stats.median); + rt.show("Results"); + } + +} diff --git a/src/ij/plugin/Startup.java b/src/ij/plugin/Startup.java new file mode 100644 index 0000000..9a91e5e --- /dev/null +++ b/src/ij/plugin/Startup.java @@ -0,0 +1,110 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.macro.Interpreter; +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.util.Vector; + +/** This plugin implements the Edit/Options/Startup command. */ + public class Startup implements PlugIn, ItemListener { + private static String NAME = "RunAtStartup.ijm"; + private GenericDialog gd; + private static final String[] code = { + "[Select from list]", + "Black background", + "Set default directory", + "Debug mode", + "10-bit (0-1023) range", + "12-bit (0-4095) range", + "Splash Screen", + "Bolder selections", + "Add to overlay", + "Flip FITS images" + }; + private String macro = ""; + private int originalLength; + + public void run(String arg) { + macro = getStartupMacro(); + String macro2 = macro; + if (!showDialog()) + return; + if (!macro.equals(macro2)) { + if (!runMacro(macro)) + return; + saveStartupMacro(macro); + } + } + + public String getStartupMacro() { + String macro = IJ.openAsString(IJ.getDirectory("macros")+NAME); + if (macro==null || macro.startsWith("Error:")) + return null; + else + return macro; + } + + private void saveStartupMacro(String macro) { + IJ.saveString(macro, IJ.getDirectory("macros")+NAME); + } + + private boolean showDialog() { + gd = new GenericDialog("Startup Macro"); + String text = "Macro code contained in this text area\nexecutes when ImageJ starts up."; + Font font = new Font("SansSerif", Font.PLAIN, 14); + gd.setInsets(5,15,0); + gd.addMessage(text, font); + gd.setInsets(5, 10, 0); + gd.addTextAreas(macro, null, 15, 50); + gd.addChoice("Add code:", code, code[0]); + Vector choices = gd.getChoices(); + if (choices!=null) { + Choice choice = (Choice)choices.elementAt(0); + choice.addItemListener(this); + } + gd.showDialog(); + macro = gd.getNextText(); + return !gd.wasCanceled(); + } + + private boolean runMacro(String macro) { + Interpreter interp = new Interpreter(); + interp.run(macro, null); + if (interp.wasError()) + return false; + else + return true; + } + + public void itemStateChanged(ItemEvent e) { + Choice choice = (Choice)e.getSource(); + String item = choice.getSelectedItem(); + String statement = null; + if (item.equals(code[1])) + statement = "setOption(\"BlackBackground\", true);\n"; + else if (item.equals(code[2])) + statement = "File.setDefaultDir(getDir(\"downloads\"));\n"; + else if (item.equals(code[3])) + statement = "setOption(\"DebugMode\", true);\n"; + else if (item.equals(code[4])) + statement = "call(\"ij.ImagePlus.setDefault16bitRange\", 10);\n"; + else if (item.equals(code[5])) + statement = "call(\"ij.ImagePlus.setDefault16bitRange\", 12);\n"; + else if (item.equals(code[6])) + statement = "run(\"About ImageJ...\");\nwait(3000);\nclose(\"About ImageJ\");\n"; + else if (item.equals(code[7])) + statement = "Roi.setDefaultStrokeWidth(2);\n"; + else if (item.equals(code[8])) + statement = "setOption(\"Add to overlay\", true);\n"; + else if (item.equals(code[9])) + statement = "setOption(\"FlipFitsImages\", false);\n"; + if (statement!=null) { + TextArea ta = gd.getTextArea1(); + ta.insert(statement, ta.getCaretPosition()); + if (IJ.isMacOSX()) ta.requestFocus(); + } + } + +} diff --git a/src/ij/plugin/Straightener.java b/src/ij/plugin/Straightener.java new file mode 100644 index 0000000..d67dbba --- /dev/null +++ b/src/ij/plugin/Straightener.java @@ -0,0 +1,230 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.Calibration; +import java.awt.*; + +/** This plugin implements the Edit/Selection/Straighten command. */ +public class Straightener implements PlugIn { + static boolean processStack; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + Roi roi = imp.getRoi(); + boolean rotatedRectangle = roi!=null && (roi instanceof RotatedRectRoi); + if (roi==null || !(roi.isLine()||rotatedRectangle)) { + IJ.error("Straightener", "Line, or rotated rectangle, selection required"); + return; + } + if (rotatedRectangle) { + IJ.run(imp, "Duplicate...", " "); + return; + } + if (!imp.lock()) return; + int width = (int)Math.round(roi.getStrokeWidth()); + int originalWidth = width; + boolean isMacro = IJ.macroRunning() && Macro.getOptions()!=null; + int stackSize = imp.getStackSize(); + if (stackSize==1) processStack = false; + String newTitle = WindowManager.getUniqueName(imp.getTitle()); + if (width<=1 || isMacro || stackSize>1) { + if (width<=1) width = 20; + GenericDialog gd = new GenericDialog("Straightener"); + gd.addStringField("Title:", newTitle, 15); + gd.addNumericField("Line Width:", width, 0, 3, "pixels"); + if (stackSize>1) + gd.addCheckbox("Process Entire Stack", processStack); + gd.showDialog(); + if (gd.wasCanceled()) {imp.unlock(); return;} + newTitle = gd.getNextString(); + width = (int)gd.getNextNumber(); + Line.setWidth(width); + if (stackSize>1) + processStack = gd.getNextBoolean(); + } + roi = (Roi)imp.getRoi().clone(); + int type = roi.getType(); + if (type==Roi.FREELINE) + IJ.run(imp, "Fit Spline", ""); + ImageProcessor ip2 = null; + ImagePlus imp2 = null; + if (processStack) { + ImageStack stack2 = straightenStack(imp, roi, width); + imp2 = new ImagePlus(newTitle, stack2); + } else { + ip2 = straighten(imp, roi, width); + imp2 = new ImagePlus(newTitle, ip2); + } + imp.unlock(); + if (imp2==null) + return; + Calibration cal = imp.getCalibration(); + if (cal.pixelWidth==cal.pixelHeight) + imp2.setCalibration(cal); + imp2.show(); + if (isMacro) Line.setWidth(originalWidth); + } + + public ImageProcessor straighten(ImagePlus imp, Roi roi, int width) { + ImageProcessor ip2; + if (imp.getBitDepth()==24 && roi.getType()!=Roi.LINE) + ip2 = straightenRGB(imp, width); + else if (imp.isComposite() && ((CompositeImage)imp).getMode()==IJ.COMPOSITE) { + if (roi.getType()==Roi.LINE) + ip2 = rotateCompositeLine(imp, width); + else + ip2 = straightenComposite(imp, width); + } else if (roi.getType()==Roi.LINE) + ip2 = rotateLine(imp, width); + else + ip2 = straightenLine(imp, width); + return ip2; + } + + public ImageStack straightenStack(ImagePlus imp, Roi roi, int width) { + int current = imp.getCurrentSlice(); + int n = imp.getStackSize(); + ImageStack stack2 = null; + for (int i=1; i<=n; i++) { + IJ.showProgress(i, n); + imp.setSlice(i); + ImageProcessor ip2 = straighten(imp, roi, width); + if (stack2==null) + stack2 = new ImageStack(ip2.getWidth(), ip2.getHeight()); + stack2.addSlice(null, ip2); + } + imp.setSlice(current); + return stack2; + } + + public ImageProcessor straightenLine(ImagePlus imp, int width) { + Roi tempRoi = imp.getRoi(); + if (tempRoi == null) return null; //roi may have changed asynchronously + if (tempRoi instanceof Line) { + FloatPolygon fp = ((Line)tempRoi).getFloatPoints(); + tempRoi = new PolygonRoi(fp.xpoints, fp.ypoints, 2, Roi.POLYLINE); + } else if (!(tempRoi instanceof PolygonRoi)) + return null; + PolygonRoi roi = (PolygonRoi)tempRoi; + if (roi==null) + return null; + if (roi.getState()==Roi.CONSTRUCTING) + roi.exitConstructingMode(); + if (roi.isSplineFit()) + roi.removeSplineFit(); + int type = roi.getType(); + int n = roi.getNCoordinates(); + double len = roi.getLength(); + roi.fitSplineForStraightening(); + if (roi.getNCoordinates()<2) + return null; + FloatPolygon p = roi.getFloatPolygon(); + n = p.npoints; + ImageProcessor ip = imp.getProcessor(); + ImageProcessor ip2 = new FloatProcessor(n, width); + //ImageProcessor distances = null; + //if (IJ.debugMode) distances = new FloatProcessor(n, 1); + float[] pixels = (float[])ip2.getPixels(); + double x1, y1; + // the following will be taken as the previous point; extrapolate back one pixel + double x2 = p.xpoints[0]-(p.xpoints[1]-p.xpoints[0]); + double y2 = p.ypoints[0]-(p.ypoints[1]-p.ypoints[0]); + if (width<=1) + ip2.putPixelValue(0, 0, ip.getInterpolatedValue(x2, y2)); + for (int i=0; i0); + } + if (!processStack) IJ.showProgress(n, n); + if (type==Roi.FREELINE) + roi.removeSplineFit(); + else + imp.draw(); + if (imp.getBitDepth()!=24) { + ip2.setColorModel(ip.getColorModel()); + ip2.resetMinAndMax(); + } + return ip2; + } + + public ImageProcessor rotateLine(ImagePlus imp, int width) { + Roi roi = imp.getRoi(); + if (roi==null || roi.getType()!=Roi.LINE) + throw new IllegalArgumentException("Straight line selection expected"); + ImageProcessor ip2 = imp.getBitDepth()==24?straightenRGB(imp, width):straightenLine(imp, width); + return ip2; + } + + ImageProcessor straightenRGB(ImagePlus imp, int width) { + int w=imp.getWidth(), h=imp.getHeight(); + int size = w*h; + byte[] r = new byte[size]; + byte[] g = new byte[size]; + byte[] b = new byte[size]; + ColorProcessor cp = (ColorProcessor)imp.getProcessor(); + cp.getRGB(r, g, b); + Roi roi = imp.getRoi(); + if (roi == null) return null; + ImagePlus imp2 = new ImagePlus("red", new ByteProcessor(w, h, r, null)); + imp2.setRoi((Roi)roi.clone()); + ImageProcessor red = straightenLine(imp2, width); + if (red==null) return null; + imp2 = new ImagePlus("green", new ByteProcessor(w, h, g, null)); + imp2.setRoi((Roi)roi.clone()); + ImageProcessor green = straightenLine(imp2, width); + if (green==null) return null; + imp2 = new ImagePlus("blue", new ByteProcessor(w, h, b, null)); + imp2.setRoi((Roi)roi.clone()); + ImageProcessor blue = straightenLine(imp2, width); + if (blue==null) return null; + ColorProcessor cp2 = new ColorProcessor(red.getWidth(), red.getHeight()); + red = red.convertToByte(false); + green = green.convertToByte(false); + blue = blue.convertToByte(false); + cp2.setRGB((byte[])red.getPixels(), (byte[])green.getPixels(), (byte[])blue.getPixels()); + imp.setRoi(imp2.getRoi()); + return cp2; + } + + ImageProcessor straightenComposite(ImagePlus imp, int width) { + Image img = imp.getImage(); + ImagePlus imp2 = new ImagePlus("temp", new ColorProcessor(img)); + imp2.setRoi(imp.getRoi()); + ImageProcessor ip2 = straightenRGB(imp2, width); + imp.setRoi(imp2.getRoi()); + return ip2; + } + + ImageProcessor rotateCompositeLine(ImagePlus imp, int width) { + Image img = imp.getImage(); + ImagePlus imp2 = new ImagePlus("temp", new ColorProcessor(img)); + imp2.setRoi(imp.getRoi()); + ImageProcessor ip2 = rotateLine(imp2, width); + return ip2; + } + +} diff --git a/src/ij/plugin/SubHyperstackMaker.java b/src/ij/plugin/SubHyperstackMaker.java new file mode 100644 index 0000000..38e914e --- /dev/null +++ b/src/ij/plugin/SubHyperstackMaker.java @@ -0,0 +1,184 @@ +package ij.plugin; +import ij.*; +import ij.gui.GenericDialog; +import ij.process.ImageProcessor; +import ij.process.LUT; +import java.util.ArrayList; +import java.util.List; +import java.awt.Color; + +/** + * This plugin is used by the Image/Stacks/Tools/Make Substack + * command to create substacks of hyperstacks. + * + * @author Curtis Rueden + */ +public class SubHyperstackMaker implements PlugIn { + + public void run(String arg) { + // verify input image is appropriate + ImagePlus input = WindowManager.getCurrentImage(); + if (input == null) { + IJ.showMessage("No image open."); + return; + } + if (input.getStackSize() == 1) { + IJ.showMessage("Image is not a stack."); + return; + } + int cCount = input.getNChannels(); + int zCount = input.getNSlices(); + int tCount = input.getNFrames(); + boolean hasC = cCount > 1; + boolean hasZ = zCount > 1; + boolean hasT = tCount > 1; + + // prompt for C, Z and T ranges + GenericDialog gd = new GenericDialog("Subhyperstack Maker"); + gd.addMessage("Enter a range (e.g. 2-14), a range with increment\n" + + "(e.g. 1-100-2) or a list (e.g. 7,9,25,27)", null, Color.darkGray); + if (hasC) gd.addStringField("Channels:", "1-" + cCount, 40); + if (hasZ) gd.addStringField("Slices:", "1-" + zCount, 40); + if (hasT) gd.addStringField("Frames:", "1-" + tCount, 40); + gd.showDialog(); + if (gd.wasCanceled()) return; + String cString = hasC ? gd.getNextString() : "1"; + String zString = hasZ ? gd.getNextString() : "1"; + String tString = hasT ? gd.getNextString() : "1"; + + // compute subhyperstack + ImagePlus output = makeSubhyperstack(input, cString, zString, tString); + + // display result + output.show(); + } + + public static ImagePlus makeSubhyperstack(ImagePlus input, String cString, String zString, String tString) { + ArrayList cList = parseList(cString, input.getNChannels()); + ArrayList zList = parseList(zString, input.getNSlices()); + ArrayList tList = parseList(tString, input.getNFrames()); + return makeSubhyperstack(input, cList, zList, tList); + } + + public static ImagePlus makeSubhyperstack(ImagePlus input, List cList, List zList, List tList) { + // validate inputs + if (cList.size() == 0) + throw new IllegalArgumentException("Must specify at least one channel"); + if (zList.size() == 0) + throw new IllegalArgumentException("Must specify at least one slice"); + if (tList.size() == 0) + throw new IllegalArgumentException("Must specify at least one frame"); + + ImageStack inputStack = input.getImageStack(); + + int cCount = input.getNChannels(); + int zCount = input.getNSlices(); + int tCount = input.getNFrames(); + + for (int c : cList) + check("C", c, cCount); + for (int z : zList) + check("Z", z, zCount); + for (int t : tList) + check("T", t, tCount); + + // create output image + String title = WindowManager.getUniqueName(input.getTitle()); + ImagePlus output = IJ.createHyperStack(title, input.getWidth(), input.getHeight(), cList.size(), zList.size(), tList.size(), input.getBitDepth()); + //ImagePlus output = input.createHyperStack(title, cList.size(), zList.size(), tList.size(), input.getBitDepth()); + ImageStack outputStack = output.getImageStack(); + + // add specified planes to subhyperstack + int oc = 0, oz, ot; + for (int c : cList) { + oc++; + oz = 0; + for (int z : zList) { + oz++; + ot = 0; + for (int t : tList) { + ot++; + int i = input.getStackIndex(c, z, t); + int oi = output.getStackIndex(oc, oz, ot); + String label = inputStack.getSliceLabel(i); + ImageProcessor ip = inputStack.getProcessor(i); + outputStack.setSliceLabel(label, oi); + outputStack.setPixels(ip.getPixels(), oi); + //IJ.log(" "+c + " "+z+" "+t+" "+i +" "+oi+" "+outputStack.getProcessor(1).getPixelValue(0,0)); + } + } + } + output.setStack(outputStack); + + // propagate composite image settings, if appropriate + if (input instanceof CompositeImage) { + CompositeImage compositeInput = (CompositeImage) input; + CompositeImage compositeOutput = + new CompositeImage(output, compositeInput.getMode()); + oc = 0; + for (int c : cList) { + oc++; + LUT table = compositeInput.getChannelLut(c); + compositeOutput.setChannelLut(table, oc); + compositeOutput.setPositionWithoutUpdate(oc, 1, 1); + compositeInput.setPositionWithoutUpdate(c, 1, 1); + double min = compositeInput.getDisplayRangeMin(); + double max = compositeInput.getDisplayRangeMax(); + compositeOutput.setDisplayRange(min, max); + } + output = compositeOutput; + } + output.setCalibration(input.getCalibration()); + return output; + } + + private static void check(String name, int index, int count) { + if (index < 1 || index > count) { + throw new IllegalArgumentException("Invalid " + name + " index: " + + index); + } + } + + private static ArrayList parseList(String planeString, int count) { + ArrayList list = new ArrayList(); + for (String token : planeString.split("\\s*,\\s*")) { + int dash1 = token.indexOf("-"); + int dash2 = token.lastIndexOf("-"); + if (dash1 < 0) { + // single number + int index; + try { + index = Integer.parseInt(token); + } catch (NumberFormatException exc) { + throw new IllegalArgumentException("Invalid number: " + token); + } + if (index < 1 || index > count) + throw new IllegalArgumentException("Invalid number: " + token); + list.add(Integer.parseInt(token)); + } else { + // range, with or without increment + int min, max, step; + try { + min = Integer.parseInt(token.substring(0, dash1)); + if (dash1 == dash2) { + // range (e.g. 2-14) + max = Integer.parseInt(token.substring(dash1 + 1)); + step = 1; + } else { + // range with increment (e.g. 1-100-2) + max = Integer.parseInt(token.substring(dash1 + 1, dash2)); + step = Integer.parseInt(token.substring(dash2 + 1)); + } + } catch (NumberFormatException exc) { + throw new IllegalArgumentException("Invalid range: " + token); + } + if (min < 1 || min > max || max > count || step < 1) + throw new IllegalArgumentException("Invalid range: " + token); + for (int index = min; index <= max; index += step) + list.add(index); + } + } + return list; + } + +} diff --git a/src/ij/plugin/SubstackMaker.java b/src/ij/plugin/SubstackMaker.java new file mode 100644 index 0000000..64057d1 --- /dev/null +++ b/src/ij/plugin/SubstackMaker.java @@ -0,0 +1,229 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.plugin.frame.Recorder; +import ij.io.FileInfo; +import java.awt.Color; + +/** + * This plugin implements the Image/Stacks/Tools/Make Substack command. + * What it does is extracts selected images from a stack to make a new substack. + * It takes three types of inputs: a range of images (e.g. 2-14), a range of images + * with an increment (e.g. 2-14-3), or a list of images (e.g. 7,9,25,27,34,132). + * It then copies those images from the active stack to a new stack in the order + * of listing or range. + * + * @author Anthony Padua + * @author Daniel Barboriak, MD + * @author Neuroradiology + * @author Duke University Medical Center + * + * @author Ved P. Sharma, Ph.D. + * @author Anatomy and Structural Biology + * @author Albert Einstein College of Medicine + * + */ + +public class SubstackMaker implements PlugIn { + private static boolean staticDelete; + private boolean delete; + private boolean methodCall; + + public void run(String arg) { + ImagePlus imp = IJ.getImage(); + if (imp.isHyperStack() || imp.isComposite()) { + (new SubHyperstackMaker()).run(""); + return; + } + String userInput = showDialog(); + if (userInput==null) + return; + ImagePlus imp2 = makeSubstack(imp, userInput); + if (imp2!=null) + imp2.show(); + } + + /** + * Extracts selected slices from a stack to make a new substack. + * Takes three types of inputs: a range of images (e.g. "2-14"), a range of + * images with an increment (e.g. "2-14-3"), or a list of images (e.g. "7,9,25,27"). + * Precede with 'delete ' (e.g. "delete 2-14") and the slices will be deleted + * from the stack. + */ + public static ImagePlus run(ImagePlus imp, String rangeOrList) { + SubstackMaker sm = new SubstackMaker(); + sm.delete = rangeOrList.contains("delete "); + if (sm.delete) + rangeOrList = rangeOrList.replace("delete ",""); + sm.methodCall = true; + ImagePlus imp2 = sm.makeSubstack(imp, rangeOrList); + if (sm.delete) + return imp; + else + return imp2; + } + + public ImagePlus makeSubstack(ImagePlus imp, String userInput) { + String stackTitle = "Substack ("+userInput+")"; + if (stackTitle.length()>25) { + int idxA = stackTitle.indexOf(",",18); + int idxB = stackTitle.lastIndexOf(","); + if(idxA>=1 && idxB>=1){ + String strA = stackTitle.substring(0,idxA); + String strB = stackTitle.substring(idxB+1); + stackTitle = strA + ", ... " + strB; + } + } + ImagePlus imp2 = null; + try { + int idx1 = userInput.indexOf("-"); + if (idx1>=1) { // input displayed in range + String rngStart = userInput.substring(0, idx1); + String rngEnd = userInput.substring(idx1+1); + Integer obj = new Integer(rngStart); + int first = obj.intValue(); + int inc = 1; + int idx2 = rngEnd.indexOf("-"); + if (idx2>=1) { + String rngEndAndInc = rngEnd; + rngEnd = rngEndAndInc.substring(0, idx2); + String rngInc = rngEndAndInc.substring(idx2+1); + obj = new Integer(rngInc); + inc = obj.intValue(); + } + obj = new Integer(rngEnd); + int last = obj.intValue(); + imp2 = stackRange(imp, first, last, inc, stackTitle); + } else { + int count = 1; // count # of slices to extract + for (int j=0; j0) { + String num = userInput.substring(0,idx2); + Integer obj = new Integer(num); + numList[i] = obj.intValue(); + userInput = userInput.substring(idx2+1); + } else { + String num = userInput; + Integer obj = new Integer(num); + numList[i] = obj.intValue(); + } + } + imp2 = stackList(imp, count, numList, stackTitle); + } + } catch (Exception e) { + IJ.error("Substack Maker", "Invalid input string: \n \n \""+userInput+"\""); + } + return imp2; + } + + String showDialog() { + String options = Macro.getOptions(); + boolean isMacro = options!=null; + if (options!=null && !options.contains("slices=")) { + Macro.setOptions(options.replace("channels=", "slices=")); + Macro.setOptions(options.replace("frames=", "slices=")); + } + if (!isMacro) delete = staticDelete; + GenericDialog gd = new GenericDialog("Substack Maker"); + gd.setInsets(10,45,0); + gd.addMessage("Enter a range (e.g. 2-14), a range with increment\n(e.g. 1-100-2) or a list (e.g. 7,9,25,27)", null, Color.darkGray); + gd.addStringField("Slices:", "", 40); + gd.addCheckbox("Delete slices from original stack", delete); + gd.showDialog(); + if (gd.wasCanceled()) + return null; + else { + String userInput = gd.getNextString(); + delete = gd.getNextBoolean(); + if (!isMacro) staticDelete = delete; + if (delete) + Recorder.recordCall("SubstackMaker.run(imp, \""+"delete "+userInput+"\");"); + else + Recorder.recordCall("imp2 = SubstackMaker.run(imp, \""+userInput+"\");"); + return userInput; + } + } + + // extract specific slices + ImagePlus stackList(ImagePlus imp, int count, int[] numList, String stackTitle) throws Exception { + ImageStack stack = imp.getStack(); + ImageStack stack2 = null; + boolean virtualStack = stack.isVirtual(); + double min = imp.getDisplayRangeMin(); + double max = imp.getDisplayRangeMax(); + Roi roi = imp.getRoi(); + for (int i=0, j=0; i400 || stack.isVirtual(); + for (int i= first, j=0; i<= last; i+=inc) { + if (showProgress) IJ.showProgress(i,last); + int currSlice = i-j; + ImageProcessor ip2 = stack.getProcessor(currSlice); + ip2.setRoi(roi); + ip2 = ip2.crop(); + if (stack2==null) + stack2 = new ImageStack(ip2.getWidth(), ip2.getHeight()); + stack2.addSlice(stack.getSliceLabel(currSlice), ip2); + if (delete) { + stack.deleteSlice(currSlice); + j++; + } + } + if (delete) { + imp.setStack(stack); + // next three lines for updating the scroll bar + ImageWindow win = imp.getWindow(); + StackWindow swin = (StackWindow) win; + if (swin!=null) + swin.updateSliceSelector(); + } + ImagePlus substack = imp.createImagePlus(); + substack.setStack(title, stack2); + substack.setCalibration(imp.getCalibration()); + if (virtualStack) + substack.setDisplayRange(min, max); + return substack; + } +} diff --git a/src/ij/plugin/SurfacePlotter.java b/src/ij/plugin/SurfacePlotter.java new file mode 100644 index 0000000..5eb90f9 --- /dev/null +++ b/src/ij/plugin/SurfacePlotter.java @@ -0,0 +1,468 @@ +package ij.plugin; +import ij.*; +import ij.plugin.filter.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import java.awt.image.*; +import java.math.*; +import java.util.*; +import ij.measure.*; + + +public class SurfacePlotter implements PlugIn { + + static final int fontSize = 14; + static int plotWidth = 350; + static int polygonMultiplier = 100; + static boolean oneToOne; + static boolean firstTime = true; + + static boolean showWireframe=false; + static boolean showGrayscale=true; + static boolean showAxis=true; + static boolean whiteBackground=false; + static boolean blackFill=false; + static boolean smooth = true; + + ImagePlus img; + int[] x,y; + boolean invertedLut; + double angleInDegrees = 35; + double angle = (angleInDegrees/360.0)*2.0*Math.PI; + double angle2InDegrees = 15.0; + double angle2 = (angle2InDegrees/360.0)*2.0*Math.PI; + double yinc2 = Math.sin(angle2); + double p1x, p1y; // left bottom corner + double p2x, p2y; // center bottom corner + double p3x, p3y; // right bottom corner + + LookUpTable lut; + + public void run(String arg) { + img = WindowManager.getCurrentImage(); + if (img==null) + {IJ.noImage(); return;} + if (img.getType()==ImagePlus.COLOR_RGB) + {IJ.error("Surface Plotter", "Grayscale or pseudo-color image required"); return;} + invertedLut = img.getProcessor().isInvertedLut(); + if (firstTime) { + if (invertedLut) + whiteBackground = true; + firstTime = false; + } + if (!showDialog()) + return; + + int stackFlags = IJ.setupDialog(img, 0); + if(stackFlags == PlugInFilter.DONE) + return; + Date start = new Date(); + lut = img.createLut(); + + if (stackFlags==PlugInFilter.DOES_STACKS && img.getStack().getSize()>1){ + ImageStack stackSource = img.getStack(); + ImageProcessor ip = stackSource.getProcessor(1); + ImageProcessor plot = makeSurfacePlot(ip); + ImageStack stack = new ImageStack(plot.getWidth(), plot.getHeight()); + stack.setColorModel(plot.getColorModel()); + for (int i=1;i<=stackSource.getSize();i++) + stack.addSlice(null, plot.duplicate().getPixels()); + stack.setPixels(plot.getPixels(), 1); + ImagePlus plots = new ImagePlus("Surface Plot", stack); + plots.show(); + for (int i=2;i<=stackSource.getSize();i++) { + IJ.showStatus("Drawing slice " + i + "..." + " (" + (100*(i-1)/stackSource.getSize()) + "% done)"); + ip = stackSource.getProcessor(i); + plot = makeSurfacePlot(ip); + ImageWindow win = plots.getWindow(); + if (win!=null && win.isClosed()) break; + stack.setPixels(plot.getPixels(), i); + plots.setSlice(i); + } + } else { + ImageProcessor plot = makeSurfacePlot(img.getProcessor()); + new ImagePlus("Surface Plot", plot).show(); + } + + Date end = new Date(); + long lstart = start.getTime(); + long lend = end.getTime(); + long difference = lend - lstart; + IJ.register(SurfacePlotter.class); + IJ.showStatus("Done in "+difference+" msec." ); + } + + boolean showDialog() { + GenericDialog gd = new GenericDialog("Surface Plotter"); + //gd.addNumericField("Plot Width (pixels):", plotWidth, 0); + //gd.addNumericField("Angle (-90-90 degrees):", angleInDegrees, 0); + gd.addNumericField("Polygon Multiplier (10-200%):", polygonMultiplier, 0); + gd.addCheckbox("Draw_Wireframe", showWireframe); + gd.addCheckbox("Shade", showGrayscale); + gd.addCheckbox("Draw_Axis", showAxis); + gd.addCheckbox("Source Background is Lighter", whiteBackground); + gd.addCheckbox("Fill Plot Background with Black", blackFill); + gd.addCheckbox("One Polygon Per Line", oneToOne); + gd.addCheckbox("Smooth", smooth); + gd.showDialog(); + if (gd.wasCanceled()) + return false; + //plotWidth = (int) gd.getNextNumber(); + //angleInDegrees = gd.getNextNumber(); + polygonMultiplier = (int)gd.getNextNumber(); + showWireframe = gd.getNextBoolean(); + showGrayscale = gd.getNextBoolean(); + showAxis = gd.getNextBoolean(); + whiteBackground = gd.getNextBoolean(); + blackFill = gd.getNextBoolean(); + oneToOne = gd.getNextBoolean(); + smooth = gd.getNextBoolean(); + if (showWireframe && !showGrayscale) + blackFill = false; + if (polygonMultiplier>400) polygonMultiplier = 400; + if (polygonMultiplier<10) polygonMultiplier = 10; + return true; + } + + public ImageProcessor makeSurfacePlot(ImageProcessor ip) { + ip = ip.duplicate(); + Rectangle roi = img.getProcessor().getRoi(); + ip.setRoi(roi); + if (!(ip instanceof ByteProcessor)) { + ip.setMinAndMax(img.getProcessor().getMin(), img.getProcessor().getMax()); + ip = ip.convertToByte(true); + ip.setRoi(roi); + } + double angle = (angleInDegrees/360.0)*2.0*Math.PI; + int polygons = (int)(plotWidth*(polygonMultiplier/100.0)/4); + if (oneToOne) + polygons = roi.height; + double xinc = 0.8*plotWidth*Math.sin(angle)/polygons; + double yinc = 0.8*plotWidth*Math.cos(angle)/polygons; + IJ.showProgress(0.01); + ip.setInterpolate(!oneToOne); + ip = ip.resize(plotWidth, polygons); + int width = ip.getWidth(); + int height = ip.getHeight(); + double min = ip.getMin(); + double max = ip.getMax(); + + if(invertedLut) ip.invert(); + if(whiteBackground) ip.invert(); + if (smooth) ip.smooth(); + + x = new int[width+2]; + y = new int[width+2]; + double xstart = 10.0; + if (xinc<0.0) + xstart += Math.abs(xinc)*polygons; + ByteProcessor ipProfile =new ByteProcessor(width, (int)(256+width*yinc2)); + ipProfile.setValue(255); + ipProfile.fill(); + double ystart = yinc2*width; + int ybase = (int)(ystart+0.5); + int windowWidth =(int)(plotWidth+polygons*Math.abs(xinc) + 20.0); + int windowHeight = (int)(ipProfile.getHeight()+polygons*yinc + 10.0); + + if(showAxis){ + xstart += 50+20; + ystart += 10; + windowWidth += 60+20; + windowHeight += 20; + p1x = xstart; + p1y = ystart+255; + p2x = xstart+xinc*height;; + p2y = p1y+yinc*height; + p3x = p2x+width-1; + p3y = p2y- yinc2*width; + } + + if(showGrayscale) { + int v; + int[] column = new int[255]; + for(int row=0; row<255; row++) { + if(whiteBackground) + v = row; + else + v = 255-row; + column[row] = v; + } + int base = ipProfile.getHeight()-255; + for(int col=0; colipW) + ipW = (int) Math.abs(r*Math.cos(-aBaseR+aR)); + if((int) Math.abs(r*Math.sin(-aBaseR+aR))>ipH) + ipH = (int) Math.abs(r*Math.sin(-aBaseR+aR)); + + ipW *= 2; + ipH *= 2; + + int tW = w; + if(ipW>w) + tW = ipW; + ImageProcessor ipText = new ByteProcessor(tW, ipH); + ipText.setFont(new Font("SansSerif", Font.PLAIN, fontSize)); + ipText.setColor(Color.white); + ipText.fill(); + ipText.setColor(Color.black); + ipText.setAntialiasedText(true); + ipText.drawString(s, tW/2-w/2, ipH/2+h/2); + ipText.setInterpolate(true); + ipText.rotate(-a); + ipText.setRoi(tW/2-ipW/2, 0, ipW, ipH); + ipText = ipText.crop(); + + //new ImagePlus("test", ipText).show(); + //ip.copyBits(ipText, x, y, Blitter.COPY_TRANSPARENT); + + return ipText; + } + + void clearAboveProfile(ImageProcessor ipProfile, double[] profile, int width, double yinc2) { + byte[] pixels = (byte[])ipProfile.getPixels(); + double ydelta = 0.0; + int height = ipProfile.getHeight(); + for(int x=0; x10) { + plot.setRoi(0, trim, width, height-trim); + plot = plot.crop(); + } + return plot; + } + + + void fixLut(ImageProcessor ip) { + if(!lut.isGrayscale() && lut.getMapSize() == 256){ + + for(int y=0;y0) + width = wordsPerLine; + if (lines>1 && wordsPerLine!=0 && wordsPerLine!=wordsInPreviousLine) + throw new IOException("Line "+lines+ " is not the same length as the first line."); + if (wordsPerLine!=0) + wordsInPreviousLine = wordsPerLine; + wordsPerLine = 0; + if (lines%20==0 && width>1 && lines<=width) + IJ.showProgress(((double)lines/width)/2.0); + break; + case StreamTokenizer.TT_WORD: + words++; + wordsPerLine++; + break; + } + } + if (wordsPerLine==width) + lines++; // last line does not end with EOL + } + + void read(Reader r, int size, float[] pixels) throws IOException { + StreamTokenizer tok = new StreamTokenizer(r); + tok.resetSyntax(); + tok.wordChars(43, 43); + tok.wordChars(45, 127); + tok.whitespaceChars(0, 42); + tok.whitespaceChars(44, 44); + //tok.wordChars(33, 127); + //tok.whitespaceChars(0, ' '); + tok.whitespaceChars(128, 255); + //tok.parseNumbers(); + + int i = 0; + int inc = size/20; + if (inc<1) + inc = 1; + while (tok.nextToken() != StreamTokenizer.TT_EOF) { + if (tok.ttype==StreamTokenizer.TT_WORD) { + if (i==0) + firstTok = tok.sval; + pixels[i++] = (float)Tools.parseDouble(tok.sval, Double.NaN); + if (i==size) + break; + if (i%inc==0) + IJ.showProgress(0.5+((double)i/size)/2.0); + } + } + IJ.showProgress(1.0); + } + +} diff --git a/src/ij/plugin/TextWriter.java b/src/ij/plugin/TextWriter.java new file mode 100644 index 0000000..d61e068 --- /dev/null +++ b/src/ij/plugin/TextWriter.java @@ -0,0 +1,34 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.io.*; +import ij.text.*; +import ij.plugin.frame.Editor; +import java.awt.*; + +/** This plugin implements the File/Save As/Text command, which saves the + contents of Editor windows and TextWindows (e.g., "Log" and "Results"). */ +public class TextWriter implements PlugIn { + + public void run(String arg) { + saveText(); + } + + void saveText() { + Frame frame = WindowManager.getFrontWindow(); + if (frame!=null && (frame instanceof TextWindow)) { + TextPanel tp = ((TextWindow)frame).getTextPanel(); + tp.saveAs(""); + } else if (frame!=null && (frame instanceof Editor)) { + Editor ed = (Editor)frame; + ed.saveAs(); + } else { + IJ.error("Save As Text", + "This command requires a TextWindow, such\n" + + "as the \"Log\" window, or an Editor window. Use\n" + + "File>Save>Text Image to save an image as text."); + } + } + +} + diff --git a/src/ij/plugin/ThreadLister.java b/src/ij/plugin/ThreadLister.java new file mode 100644 index 0000000..57f60a2 --- /dev/null +++ b/src/ij/plugin/ThreadLister.java @@ -0,0 +1,93 @@ +package ij.plugin; +import java.io.*; +import ij.*; +import ij.text.*; +import javax.swing.SwingUtilities; + +/** +* Displays thread information in a text window. +* +* This code is from the book _Java in a Nutshell_ by David Flanagan. +* Written by David Flanagan. +*/ +public class ThreadLister implements PlugIn { + + public void run(String arg) { + if (IJ.getApplet()!=null) + return; + CharArrayWriter caw = new CharArrayWriter(); + PrintWriter pw = new PrintWriter(caw); + try { + listAllThreads(pw); + new TextWindow("Threads", caw.toString(), 420, 420); + } catch + (Exception e) {} + + // cause an exception on the EDT + if (IJ.altKeyDown()) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + ((Object) null).toString(); + } + }); + } + // cause an exception off the EDT + //((Object) null).toString(); + + } + + + // Display info about a thread. + private static void print_thread_info(PrintWriter out, Thread t, + String indent) { + if (t == null) return; + out.print(indent + "Thread: " + t.getName() + + " Priority: " + t.getPriority() + + (t.isDaemon()?" Daemon":"") + + (t.isAlive()?"":" Not Alive") + "\n"); + } + + // Display info about a thread group and its threads and groups + private static void list_group(PrintWriter out, ThreadGroup g, + String indent) { + if (g == null) return; + int num_threads = g.activeCount(); + int num_groups = g.activeGroupCount(); + Thread[] threads = new Thread[num_threads]; + ThreadGroup[] groups = new ThreadGroup[num_groups]; + + g.enumerate(threads, false); + g.enumerate(groups, false); + + out.println(indent + "Thread Group: " + g.getName() + + " Max Priority: " + g.getMaxPriority() + + (g.isDaemon()?" Daemon":"") + "\n"); + + for(int i = 0; i < num_threads; i++) + print_thread_info(out, threads[i], indent + " "); + for(int i = 0; i < num_groups; i++) + list_group(out, groups[i], indent + " "); + } + + // Find the root thread group and list it recursively + public static void listAllThreads(PrintWriter out) { + ThreadGroup current_thread_group; + ThreadGroup root_thread_group; + ThreadGroup parent; + + // Get the current thread group + current_thread_group = Thread.currentThread().getThreadGroup(); + + // Now go find the root thread group + root_thread_group = current_thread_group; + parent = root_thread_group.getParent(); + while(parent != null) { + root_thread_group = parent; + parent = parent.getParent(); + } + + // And list it, recursively + list_group(out, root_thread_group, ""); + } + +} diff --git a/src/ij/plugin/Thresholder.java b/src/ij/plugin/Thresholder.java new file mode 100644 index 0000000..2f0ae35 --- /dev/null +++ b/src/ij/plugin/Thresholder.java @@ -0,0 +1,439 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.frame.Recorder; +import ij.plugin.filter.PlugInFilter; +import ij.plugin.frame.ThresholdAdjuster; +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +/** This plugin implements the Process/Binary/Make Binary + and Convert to Mask commands. */ +public class Thresholder implements PlugIn, Measurements, ItemListener { + + public static final String[] methods = AutoThresholder.getMethods(); + public static final String[] backgrounds = {"Default", "Dark", "Light"}; + private double minThreshold; + private double maxThreshold; + private boolean autoThreshold; + private boolean showLegacyDialog = true; + private static boolean fill1 = true; + private static boolean fill2 = true; + private static boolean useBW = true; + private boolean useLocal = true; + private boolean listThresholds; + private boolean convertToMask; + private String method = methods[0]; + private String background = backgrounds[0]; + private static boolean staticUseLocal = true; + private static boolean staticListThresholds; + private static String staticMethod = methods[0]; + private static String staticBackground = backgrounds[0]; + private ImagePlus imp; + private Vector choices; + + public void run(String arg) { + convertToMask = arg.equals("mask"); + if (arg.equals("skip") || convertToMask) + showLegacyDialog = false; + ImagePlus imp = IJ.getImage(); + if (imp.getStackSize()==1) { + Undo.setup(Undo.TRANSFORM, imp); + applyThreshold(imp, false); + } else + convertStack(imp); + IJ.showProgress(1.0); + } + + void convertStack(ImagePlus imp) { + if (imp.getStack().isVirtual()) { + IJ.error("Thresholder", "This command does not work with virtual stacks.\nUse Image>Duplicate to convert to a normal stack."); + return; + } + showLegacyDialog = false; + boolean thresholdSet = imp.isThreshold(); + this.imp = imp; + if (!IJ.isMacro()) { + method = staticMethod; + background = staticBackground; + useLocal = staticUseLocal; + listThresholds = staticListThresholds; + if (!thresholdSet) + updateThreshold(imp); + } else { + String macroOptions = Macro.getOptions(); + if (macroOptions!=null && macroOptions.indexOf("slice ")!=-1) + Macro.setOptions(macroOptions.replaceAll("slice ", "only ")); + useLocal = false; + } + boolean saveBlackBackground = Prefs.blackBackground; + boolean oneSlice = false; + GenericDialog gd = new GenericDialog("Convert Stack to Binary"); + gd.addChoice("Method:", methods, method); + gd.addChoice("Background:", backgrounds, background); + gd.addCheckbox("Calculate threshold for each image", useLocal); + gd.addCheckbox("Only convert current image", oneSlice); + gd.addCheckbox("Black background (of binary masks)", Prefs.blackBackground); + gd.addCheckbox("List thresholds", listThresholds); + choices = gd.getChoices(); + if (choices!=null) { + ((Choice)choices.elementAt(0)).addItemListener(this); + ((Choice)choices.elementAt(1)).addItemListener(this); + } + gd.showDialog(); + if (gd.wasCanceled()) + return; + this.imp = null; + method = gd.getNextChoice(); + background = gd.getNextChoice(); + useLocal = gd.getNextBoolean(); + oneSlice = gd.getNextBoolean(); + Prefs.blackBackground = gd.getNextBoolean(); + listThresholds = gd.getNextBoolean(); + if (!IJ.isMacro()) { + staticMethod = method; + staticBackground = background; + staticUseLocal = useLocal; + staticListThresholds = listThresholds; + } + if (oneSlice) { + useLocal = false; + listThresholds = false; + if (oneSlice && imp.getBitDepth()!=8) { + IJ.error("Thresholder", "8-bit stack required to process a single slice."); + return; + } + } + Undo.reset(); + if (useLocal) + convertStackToBinary(imp); + else + applyThreshold(imp, oneSlice); + Prefs.blackBackground = saveBlackBackground; + if (thresholdSet) { + if (imp.getProcessor().getLutUpdateMode()!=ImageProcessor.NO_LUT_UPDATE) + imp.getProcessor().resetThreshold(); + imp.updateAndDraw(); + } + } + + private void applyThreshold(ImagePlus imp, boolean oneSlice) { + imp.deleteRoi(); + ImageProcessor ip = imp.getProcessor(); + ip.resetBinaryThreshold(); // remove any invisible threshold set by Make Binary or Convert to Mask + int type = imp.getType(); + if (type==ImagePlus.GRAY16 || type==ImagePlus.GRAY32) { + applyShortOrFloatThreshold(imp); + return; + } + if (!imp.lock()) return; + double saveMinThreshold = ip.getMinThreshold(); + double saveMaxThreshold = ip.getMaxThreshold(); + autoThreshold = saveMinThreshold==ImageProcessor.NO_THRESHOLD; + + boolean useBlackAndWhite = false; + boolean fill1 = true; + boolean fill2 = true; + String options = Macro.getOptions(); + boolean modernMacro = options!=null && !(options.contains("thresholded")||options.contains("remaining")); + if (autoThreshold || modernMacro || (IJ.macroRunning()&&options==null)) + showLegacyDialog = false; + int fcolor=255, bcolor=0; + + if (showLegacyDialog) { + GenericDialog gd = new GenericDialog("Make Binary"); + gd.addCheckbox("Thresholded pixels to foreground color", fill1); + gd.addCheckbox("Remaining pixels to background color", fill2); + gd.addMessage(""); + gd.addCheckbox("Black foreground, white background", useBW); + gd.showDialog(); + if (gd.wasCanceled()) + {imp.unlock(); return;} + fill1 = gd.getNextBoolean(); + fill2 = gd.getNextBoolean(); + useBW = useBlackAndWhite = gd.getNextBoolean(); + int savePixel = ip.getPixel(0,0); + if (useBlackAndWhite) + ip.setColor(Color.black); + else + ip.setColor(Toolbar.getForegroundColor()); + ip.drawPixel(0,0); + fcolor = ip.getPixel(0,0); + if (useBlackAndWhite) + ip.setColor(Color.white); + else + ip.setColor(Toolbar.getBackgroundColor()); + ip.drawPixel(0,0); + bcolor = ip.getPixel(0,0); + ip.setColor(Toolbar.getForegroundColor()); + ip.putPixel(0,0,savePixel); + } else + convertToMask = true; + + if (type==ImagePlus.COLOR_RGB) { + ImageProcessor ip2 = updateColorThresholdedImage(imp); + if (ip2!=null) { + imp.setProcessor(ip2); + autoThreshold =false; + saveMinThreshold = 255; + saveMaxThreshold = 255; + type = ImagePlus.GRAY8; + } + } + if (type!=ImagePlus.GRAY8) + convertToByte(imp); + ip = imp.getProcessor(); + + if (autoThreshold) + autoThreshold(ip); + else { + if (Recorder.record && !Recorder.scriptMode() && (!IJ.isMacro()||Recorder.recordInMacros)) + Recorder.record("//setThreshold", (int)saveMinThreshold, (int)saveMaxThreshold); + minThreshold = saveMinThreshold; + maxThreshold = saveMaxThreshold; + } + + if (convertToMask && ip.isColorLut()) + ip.setColorModel(ip.getDefaultColorModel()); + ip.resetThreshold(); + + if (IJ.debugMode) IJ.log("Thresholder (apply): "+minThreshold+"-"+maxThreshold+" "+fcolor+" "+bcolor+" "+fill1+" "+fill2); + int[] lut = new int[256]; + for (int i=0; i<256; i++) { + if (i>=minThreshold && i<=maxThreshold) + lut[i] = fill1?fcolor:(byte)i; + else { + lut[i] = fill2?bcolor:(byte)i; + } + } + if (imp.getStackSize()>1 && !oneSlice) + new StackProcessor(imp.getStack(), ip).applyTable(lut); + else + ip.applyTable(lut); + if (convertToMask && !oneSlice) { + boolean invertedLut = imp.isInvertedLut(); + if ((invertedLut && Prefs.blackBackground) || (!invertedLut && !Prefs.blackBackground)) { + ip.invertLut(); + if (!IJ.isMacro() && ThresholdAdjuster.isDarkBackground() && !invertedLut && !Prefs.blackBackground) + IJ.log("\"Black background\" not set in Process>Binary>Options; inverting LUT"); + if (IJ.debugMode) IJ.log("Thresholder (inverting lut)"); + } + } + if (fill1 && fill2 && ((fcolor==0&&bcolor==255)||(fcolor==255&&bcolor==0))) { + imp.getProcessor().setThreshold(fcolor, fcolor, ImageProcessor.NO_LUT_UPDATE); + if (IJ.debugMode) IJ.log("Thresholder: "+fcolor+"-"+fcolor+" ("+(Prefs.blackBackground?"black":"white")+" background)"); + } + imp.updateAndRepaintWindow(); + imp.unlock(); + } + + private ImageProcessor updateColorThresholdedImage(ImagePlus imp) { + Object mask = imp.getProperty("Mask"); + if (mask==null || !(mask instanceof ByteProcessor)) + return null; + ImageProcessor maskIP = (ImageProcessor)mask; + if (maskIP.getWidth()!=imp.getWidth() || maskIP.getHeight()!=imp.getHeight()) + return null; + Object originalImage = imp.getProperty("OriginalImage"); + if (originalImage!=null && (originalImage instanceof ImagePlus)) { + ImagePlus imp2 = (ImagePlus)originalImage; + if (imp2.getBitDepth()==24 && imp2.getWidth()==imp.getWidth() && imp2.getHeight()==imp.getHeight()) { + imp.setProcessor(imp2.getProcessor()); + Undo.setup(Undo.TRANSFORM, imp); + } + } + return maskIP; + } + + private void applyShortOrFloatThreshold(ImagePlus imp) { + if (!imp.lock()) return; + int width = imp.getWidth(); + int height = imp.getHeight(); + int size = width*height; + boolean isFloat = imp.getType()==ImagePlus.GRAY32; + int currentSlice = imp.getCurrentSlice(); + int nSlices = imp.getStackSize(); + ImageStack stack1 = imp.getStack(); + ImageStack stack2 = new ImageStack(width, height); + ImageProcessor ip = imp.getProcessor(); + float t1 = (float)ip.getMinThreshold(); + float t2 = (float)ip.getMaxThreshold(); + if (t1==ImageProcessor.NO_THRESHOLD) { + double min = ip.getMin(); + double max = ip.getMax(); + ip = ip.convertToByte(true); + autoThreshold(ip); + t1 = (float)(min + (max-min)*(minThreshold/255.0)); + t2 = (float)(min + (max-min)*(maxThreshold/255.0)); + } + float value; + ImageProcessor ip1, ip2; + IJ.showStatus("Converting to mask"); + for (int i=1; i<=nSlices; i++) { + IJ.showProgress(i, nSlices); + String label = stack1.getSliceLabel(i); + ip1 = stack1.getProcessor(i); + ip2 = new ByteProcessor(width, height); + for (int j=0; j=t1 && value<=t2) + ip2.set(j, 255); + else + ip2.set(j, 0); + } + stack2.addSlice(label, ip2); + } + imp.setStack(null, stack2); + ImageStack stack = imp.getStack(); + stack.setColorModel(LookUpTable.createGrayscaleColorModel(!Prefs.blackBackground)); + imp.setStack(null, stack); + if (imp.isComposite()) { + CompositeImage ci = (CompositeImage)imp; + ci.setMode(IJ.GRAYSCALE); + ci.resetDisplayRanges(); + ci.updateAndDraw(); + } + imp.getProcessor().setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE); + if (IJ.debugMode) IJ.log("Thresholder16: 255-255 ("+(Prefs.blackBackground?"black":"white")+" background)"); + IJ.showStatus(""); + imp.unlock(); + } + + void convertStackToBinary(ImagePlus imp) { + int nSlices = imp.getStackSize(); + double[] minValues = listThresholds?new double[nSlices]:null; + double[] maxValues = listThresholds?new double[nSlices]:null; + int bitDepth = imp.getBitDepth(); + if (bitDepth!=8) { + IJ.showStatus("Converting to byte"); + ImageStack stack1 = imp.getStack(); + ImageStack stack2 = new ImageStack(imp.getWidth(), imp.getHeight()); + for (int i=1; i<=nSlices; i++) { + IJ.showProgress(i, nSlices); + String label = stack1.getSliceLabel(i); + ImageProcessor ip = stack1.getProcessor(i); + ip.resetMinAndMax(); + if (listThresholds) { + minValues[i-1] = ip.getMin(); + maxValues[i-1] = ip.getMax(); + } + stack2.addSlice(label, ip.convertToByte(true)); + } + imp.setStack(null, stack2); + } + ImageStack stack = imp.getStack(); + IJ.showStatus("Auto-thresholding"); + if (listThresholds) + IJ.log("Thresholding method: "+method); + for (int i=1; i<=nSlices; i++) { + IJ.showProgress(i, nSlices); + ImageProcessor ip = stack.getProcessor(i); + if (method.equals("Default") && background.equals("Default")) + ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.NO_LUT_UPDATE); + else + ip.setAutoThreshold(method, !background.equals("Light"), ImageProcessor.NO_LUT_UPDATE); + minThreshold = ip.getMinThreshold(); + maxThreshold = ip.getMaxThreshold(); + if (listThresholds) { + double t1 = minThreshold; + double t2 = maxThreshold; + if (bitDepth!=8) { + t1 = minValues[i-1] + (t1/255.0)*(maxValues[i-1]-minValues[i-1]); + t2 = minValues[i-1] + (t2/255.0)*(maxValues[i-1]-minValues[i-1]); + } + int digits = bitDepth==32?2:0; + IJ.log(" "+i+": "+IJ.d2s(t1,digits)+"-"+IJ.d2s(t2,digits)); + } + int[] lut = new int[256]; + for (int j=0; j<256; j++) { + if (j>=minThreshold && j<=maxThreshold) + lut[j] = (byte)255; + else + lut[j] = 0; + } + ip.applyTable(lut); + } + stack.setColorModel(LookUpTable.createGrayscaleColorModel(!Prefs.blackBackground)); + imp.setStack(null, stack); + imp.getProcessor().setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE); + if (imp.isComposite()) { + CompositeImage ci = (CompositeImage)imp; + ci.setMode(IJ.GRAYSCALE); + ci.resetDisplayRanges(); + ci.updateAndDraw(); + } + IJ.showStatus(""); + } + + void convertToByte(ImagePlus imp) { + ImageProcessor ip; + int currentSlice = imp.getCurrentSlice(); + ImageStack stack1 = imp.getStack(); + ImageStack stack2 = imp.createEmptyStack(); + int nSlices = imp.getStackSize(); + String label; + for(int i=1; i<=nSlices; i++) { + label = stack1.getSliceLabel(i); + ip = stack1.getProcessor(i); + ip.setMinAndMax(0, 255); + stack2.addSlice(label, ip.convertToByte(true)); + } + imp.setStack(null, stack2); + imp.setSlice(currentSlice); + imp.setCalibration(imp.getCalibration()); //update calibration + } + + /** Returns an 8-bit binary (0 and 255) threshold mask + * that has the same dimensions as this image. + * @see ij.process.ImageProcessor#createMask + * @see ij.ImagePlus#createThresholdMask + * @see ij.ImagePlus#createRoiMask + */ + public static ByteProcessor createMask(ImagePlus imp) { + ImageProcessor ip = imp.getProcessor(); + if (ip instanceof ColorProcessor) + throw new IllegalArgumentException("Non-RGB image requires"); + if (ip.getMinThreshold()==ImageProcessor.NO_THRESHOLD) + throw new IllegalArgumentException("Image must be thresholded"); + return ip.createMask(); + } + + void autoThreshold(ImageProcessor ip) { + ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.NO_LUT_UPDATE); + minThreshold = ip.getMinThreshold(); + maxThreshold = ip.getMaxThreshold(); + if (IJ.debugMode) IJ.log("Thresholder (auto): "+minThreshold+"-"+maxThreshold); + } + + public static void setMethod(String method) { + staticMethod = method; + } + + public static void setBackground(String background) { + staticBackground = background; + } + + public void itemStateChanged(ItemEvent e) { + if (imp==null) + return; + Choice choice = (Choice)e.getSource(); + if (choice==choices.elementAt(0)) + method = choice.getSelectedItem(); + else + background = choice.getSelectedItem(); + updateThreshold(imp); + } + + private void updateThreshold(ImagePlus imp) { + ImageProcessor ip = imp.getProcessor(); + if (method.equals("Default") && background.equals("Default")) + ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.RED_LUT); + else + ip.setAutoThreshold(method, !background.equals("Light"), ImageProcessor.RED_LUT); + imp.updateAndDraw(); + } + +} diff --git a/src/ij/plugin/URLOpener.java b/src/ij/plugin/URLOpener.java new file mode 100644 index 0000000..2e12f15 --- /dev/null +++ b/src/ij/plugin/URLOpener.java @@ -0,0 +1,198 @@ +package ij.plugin; +import java.awt.*; +import java.io.*; +import java.net.URL; +import java.util.*; +import ij.*; +import ij.io.*; +import ij.gui.*; +import ij.plugin.frame.*; + +/** Opens TIFFs, ZIP compressed TIFFs, DICOMs, GIFs and JPEGs using a URL. + TIFF file names must end in ".tif", ZIP file names must end + in ".zip" and DICOM file names must end in ".dcm". + Opens a Web page in the default browser if the URL ends with "/". +*/ +public class URLOpener implements PlugIn { + + private static String url = IJ.URL+"/images/clown.gif"; + + /** If 'urlOrName' is a URL, opens the image at that URL. If it is + a file name, opens the image with that name from the 'images.location' + URL in IJ_Props.txt. If it is blank, prompts for an image + URL and open the specified image. */ + public void run(String urlOrName) { + if (!urlOrName.equals("")) { + if (urlOrName.equals("cache")) + cacheSampleImages(); + else if (urlOrName.endsWith("StartupMacros.txt")) + openTextFile(urlOrName, true); + else { + double startTime = System.currentTimeMillis(); + String url = urlOrName.indexOf("://")>0?urlOrName:Prefs.getImagesURL()+urlOrName; + ImagePlus imp = new ImagePlus(url); + if (Recorder.record) + Recorder.recordCall("imp = IJ.openImage(\""+url+"\");"); + if (imp.getType()==ImagePlus.COLOR_RGB) + Opener.convertGrayJpegTo8Bits(imp); + WindowManager.checkForDuplicateName = true; + FileInfo fi = imp.getOriginalFileInfo(); + if (fi!=null && fi.fileType==FileInfo.RGB48) + imp = new CompositeImage(imp, IJ.COMPOSITE); + else if (imp.getNChannels()>1 && fi!=null && fi.description!=null && fi.description.indexOf("mode=")!=-1) { + int mode = IJ.COLOR; + if (fi.description.indexOf("mode=composite")!=-1) + mode = IJ.COMPOSITE; + else if (fi.description.indexOf("mode=gray")!=-1) + mode = IJ.GRAYSCALE; + imp = new CompositeImage(imp, mode); + } + if (fi!=null && (fi.url==null || fi.url.length()==0)) { + fi.url = url; + imp.setFileInfo(fi); + } + imp.show(Opener.getLoadRate(startTime,imp)); + String title = imp.getTitle(); + if (title!=null && (title.startsWith("flybrain") || title.startsWith("t1-head"))) + imp.setSlice(imp.getStackSize()/2); + } + return; + } + + GenericDialog gd = new GenericDialog("Enter a URL"); + gd.setInsets(10, 32, 0); + gd.addMessage("Enter URL of an image, macro or web page", null, Color.darkGray); + gd.addStringField("URL:", url, 45); + gd.showDialog(); + if (gd.wasCanceled()) + return; + url = gd.getNextString(); + url = url.trim(); + if (url.indexOf("://")==-1) + url = "http://" + url; + if (url.endsWith("/")) + IJ.runPlugIn("ij.plugin.BrowserLauncher", url.substring(0, url.length()-1)); + else if (url.endsWith(".html") || url.endsWith(".htm") || url.endsWith(".pdf") || url.indexOf(".html#")>0 || noExtension(url)) + IJ.runPlugIn("ij.plugin.BrowserLauncher", url); + else if (url.endsWith(".txt")||url.endsWith(".ijm")||url.endsWith(".js")||url.endsWith(".java")) + openTextFile(url, false); + else if (url.endsWith(".jar")||url.endsWith(".class")) + IJ.open(url); + else { + IJ.showStatus("Opening: " + url); + double startTime = System.currentTimeMillis(); + ImagePlus imp = new ImagePlus(url); + WindowManager.checkForDuplicateName = true; + FileInfo fi = imp.getOriginalFileInfo(); + if (fi!=null && fi.fileType==FileInfo.RGB48) + imp = new CompositeImage(imp, IJ.COMPOSITE); + else if (imp.getNChannels()>1 && fi!=null && fi.description!=null && fi.description.indexOf("mode=")!=-1) { + int mode = IJ.COLOR; + if (fi.description.indexOf("mode=composite")!=-1) + mode = IJ.COMPOSITE; + else if (fi.description.indexOf("mode=gray")!=-1) + mode = IJ.GRAYSCALE; + imp = new CompositeImage(imp, mode); + } + if (fi!=null && (fi.url==null || fi.url.length()==0)) { + fi.url = url; + imp.setFileInfo(fi); + } + imp.show(Opener.getLoadRate(startTime,imp)); + } + IJ.register(URLOpener.class); // keeps this class from being GC'd + } + + boolean noExtension(String url) { + int lastSlash = url.lastIndexOf("/"); + if (lastSlash==-1) lastSlash = 0; + int lastDot = url.lastIndexOf("."); + if (lastDot==-1 || lastDot6) + return true; // no extension + else + return false; + } + + void openTextFile(String urlString, boolean install) { + StringBuffer sb = null; + try { + URL url = new URL(urlString); + InputStream in = url.openStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + sb = new StringBuffer() ; + String line; + while ((line=br.readLine()) != null) + sb.append (line + "\n"); + in.close (); + } catch (IOException e) { + if (!(install&&urlString.endsWith("StartupMacros.txt"))) + IJ.error("URL Opener", ""+e); + sb = null; + } + if (sb!=null) { + if (install) + (new MacroInstaller()).install(new String(sb)); + else { + int index = urlString.lastIndexOf("/"); + if (index!=-1 && index<=urlString.length()-1) + urlString = urlString.substring(index+1); + (new Editor()).create(urlString, new String(sb)); + } + } + } + + private void cacheSampleImages() { + String[] names = getSampleImageNames(); + int n = names.length; + if (n==0) return; + String dir = IJ.getDirectory("imagej")+"samples"; + File f = new File(dir); + if (!f.exists()) { + boolean ok = f.mkdir(); + if (!ok) { + IJ.error("Unable to create directory:\n \n"+dir); + return; + } + } + IJ.resetEscape(); + for (int i=0; iOpen Samples"); + if (samplesMenu==null) + return new String[0]; + for (int i=0; i0||starty>0) + IJ.doWand(startx, starty, tolerance, mode+(Prefs.smoothWand?" smooth":"")); + return true; + } + + public static String getMode() { + return mode; + } + + public static double getTolerance() { + return tolerance; + } + + public static final void setStart(int x, int y) { + startx = x; + starty = y; + } + +} diff --git a/src/ij/plugin/WindowOrganizer.java b/src/ij/plugin/WindowOrganizer.java new file mode 100644 index 0000000..04dcf6f --- /dev/null +++ b/src/ij/plugin/WindowOrganizer.java @@ -0,0 +1,184 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.plugin.frame.ThresholdAdjuster; +import java.awt.*; + +/** This class implements the Window menu's "Show All", "Main Window", "Cascade" and "Tile" commands. */ +public class WindowOrganizer implements PlugIn { + + private static final int XSTART=4, YSTART=94, XOFFSET=8, YOFFSET=24,MAXSTEP=200,GAP=2; + private int titlebarHeight = IJ.isMacintosh()?40:20; + + public void run(String arg) { + if (arg.equals("imagej")) + {showImageJ(); return;} + int[] wList = WindowManager.getIDList(); + if (arg.equals("show")) + {showAll(wList); return;} + if (wList==null) { + IJ.noImage(); + return; + } + if (arg.equals("tile")) + tileWindows(wList); + else + cascadeWindows(wList); + } + + void tileWindows(int[] wList) { + Rectangle screen = GUI.getMaxWindowBounds(IJ.getInstance()); + int minWidth = Integer.MAX_VALUE; + int minHeight = Integer.MAX_VALUE; + boolean allSameSize = true; + int width=0, height=0; + double totalWidth = 0; + double totalHeight = 0; + for (int i=0; ihspace) + tileWidth = hspace; + int vspace = screen.height - YSTART; + if (tileHeight>vspace) + tileHeight = vspace; + int hloc, vloc; + boolean theyFit; + do { + hloc = XSTART; + vloc = YSTART; + theyFit = true; + int i = 0; + do { + i++; + if (hloc+tileWidth>screen.width) { + hloc = XSTART; + vloc = vloc + tileHeight; + if (vloc+tileHeight> screen.height) + theyFit = false; + } + hloc = hloc + tileWidth + GAP; + } while (theyFit && (iscreen.width) { + hloc = XSTART; + vloc = vloc + tileHeight; + } + ImageWindow win = getWindow(wList[i]); + if (win!=null) { + win.setLocation(hloc + screen.x, vloc + screen.y); + ImageCanvas canvas = win.getCanvas(); + while (win.getSize().width*0.85>=tileWidth && canvas.getMagnification()>0.03125) + canvas.zoomOut(0, 0); + win.toFront(); + ImagePlus imp = win.getImagePlus(); + if (imp!=null) imp.setIJMenuBar(i==nPics-1); + } + hloc += tileWidth + GAP; + } + } + + ImageWindow getWindow(int id) { + ImageWindow win = null; + ImagePlus imp = WindowManager.getImage(id); + if (imp!=null) + win = imp.getWindow(); + return win; + } + + void cascadeWindows(int[] wList) { + Rectangle screen = GUI.getMaxWindowBounds(IJ.getInstance()); + int x = XSTART; + int y = YSTART; + int xstep = 0; + int xstart = XSTART; + for (int i=0; iMAXSTEP) + xstep = MAXSTEP; + } + if (y+d.height*0.67>screen.height) { + xstart += xstep; + if (xstart+d.width*0.67>screen.width) + xstart = XSTART+XOFFSET; + x = xstart; + y = YSTART; + } + win.setLocation(x + screen.x, y + screen.y); + win.toFront(); + x += XOFFSET; + y += YOFFSET; + ImagePlus imp = win.getImagePlus(); + if (imp!=null) imp.setIJMenuBar(i==wList.length-1); + } + } + + void showImageJ() { + ImageJ ij = IJ.getInstance(); + if (ij!=null) + ij.toFront(); + } + + void showAll(int[] wList) { + if (wList!=null) { + for (int i=0; i>16; + int g = (c&0xff00)>>8; + int b = c&0xff; + bg = r+","+g+","+b; + bg = " \n Background value: " + bg + "\n"; + } + imp.deleteRoi(); + + int slices = imp.getStackSize(); + String msg = + "This plugin writes to a text file the XY coordinates and\n" + + "pixel value of all non-background pixels. Backround\n" + + "defaults to be the value of the pixel in the upper\n" + + "left corner of the image.\n \n" + + "If there is a selection, this dialog is skipped and the\n" + + "coordinates and values of pixels in the selection are saved.\n"; + + GenericDialog gd = new GenericDialog("Save XY Coordinates"); + gd.setInsets(0, 20, 0); + gd.addMessage(msg, null, Color.darkGray); + int digits = (int)background==background?0:4; + if (!rgb) { + gd.setInsets(5, 35, 3); + gd.addNumericField("Background value:", background, digits); + } + gd.setInsets(10, 35, 0); + gd.addCheckbox("Invert y coordinates off (0 at top of image)", invertY); + gd.setInsets(0, 35, 0); + gd.addCheckbox("Suppress Log output", suppress); + if (slices>1) { + gd.setInsets(0, 35, 0); + gd.addCheckbox("Process all "+slices+" images", processStack); + } + gd.showDialog(); + if (gd.wasCanceled()) + return; + if (!rgb) + background = gd.getNextNumber(); + invertY = gd.getNextBoolean(); + suppress = gd.getNextBoolean(); + if (slices>1) + processStack = gd.getNextBoolean(); + else + processStack = false; + if (!processStack) slices = 1; + + SaveDialog sd = new SaveDialog("Save Coordinates as Text...", imp.getTitle(), ".txt"); + String name = sd.getFileName(); + if (name == null) + return; + String directory = sd.getDirectory(); + PrintWriter pw = null; + try { + FileOutputStream fos = new FileOutputStream(directory+name); + BufferedOutputStream bos = new BufferedOutputStream(fos); + pw = new PrintWriter(bos); + } + catch (IOException e) { + IJ.error("Save XY Coordinates", "Error saving coordinates:\n "+e.getMessage()); + return; + } + + IJ.showStatus("Saving coordinates..."); + int count = 0; + float v; + int c,r,g,b; + int type = imp.getType(); + ImageStack stack = imp.getStack(); + boolean nanBackground = Double.isNaN(background); + for (int z=0; z1) ip = stack.getProcessor(z+1); + String zstr = slices>1?z+"\t":""; + for (int i=0; i>16; + g = (c&0xff00)>>8; + b = c&0xff; + pw.println(x+"\t"+(invertY?y:height-1-y)+"\t"+zstr+r+"\t"+g+"\t"+b); + } else + pw.println(x+"\t"+(invertY?y:height-1-y)+"\t"+zstr+(int)v); + count++; + } + } // x + if (slices==1&&y%10==0) IJ.showProgress((double)(height-y)/height); + } // y + if (slices>1) IJ.showProgress(z+1, slices); + String img = slices>1?"-"+(z+1):""; + if (!suppress) + IJ.log(imp.getTitle() + img+": " + count + " pixels (" + IJ.d2s(count*100.0/(width*height)) + "%)\n"); + count = 0; + } // z + IJ.showProgress(1.0); + IJ.showStatus(""); + pw.close(); + } + + private void saveSelectionCoordinates(ImagePlus imp) { + SaveDialog sd = new SaveDialog("Save Coordinates as Text...", imp.getTitle(), ".csv"); + String name = sd.getFileName(); + if (name == null) + return; + String dir = sd.getDirectory(); + Roi roi = imp.getRoi(); + ImageProcessor ip = imp.getProcessor(); + ImageProcessor mask = roi.getMask(); + Rectangle r = roi.getBounds(); + ResultsTable rt = new ResultsTable(); + boolean rgb = imp.getBitDepth()==24; + for (int y=0; y>16); + rt.addValue("Green", (c&0xff00)>>8); + rt.addValue("Blue", c&0xff); + } else + rt.addValue("Value", ip.getPixelValue(r.x+x,r.y+y)); + } + } + } + rt.save(dir+name); + } + +} diff --git a/src/ij/plugin/XY_Reader.java b/src/ij/plugin/XY_Reader.java new file mode 100644 index 0000000..63ee71f --- /dev/null +++ b/src/ij/plugin/XY_Reader.java @@ -0,0 +1,58 @@ +package ij.plugin; +import ij.*; +import ij.process.*; +import ij.gui.*; +import java.awt.*; +import ij.measure.*; +import ij.plugin.TextReader; + +/** This plugin implements the File/Import/XY Coordinates command. It reads a + two column text file, such as those created by File/Save As/XY Coordinates, + as a polygon ROI. The ROI is displayed in the current image or, if the image + is too small, in a new blank image. +*/ +public class XY_Reader implements PlugIn { + + public void run(String arg) { + TextReader tr = new TextReader(); + ImageProcessor ip = tr.open(); + if (ip==null) + return; + int width = ip.getWidth(); + int height = ip.getHeight(); + if (width!=2 || height<3) { + IJ.showMessage("XY Reader", "Two column text file required"); + return; + } + float[] x = new float[height]; + float[] y = new float[height]; + boolean allIntegers = true; + double length = 0.0; + for (int i=0; i0) { + double dx = x[i] - x[i-1]; + double dy = y[i] - y[i-1]; + length += Math.sqrt(dx*dx+dy*dy); + } + } + Roi roi = null; + int type = length/x.length>10?Roi.POLYGON:Roi.FREEROI; + if (allIntegers) + roi = new PolygonRoi(Roi.toIntR(x), Roi.toIntR(y), height, type); + else + roi = new PolygonRoi(x, y, height, type); + Rectangle r = roi.getBoundingRect(); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null || imp.getWidth()1; + if (options==null && slices>1 && frames>1 && (!isPlotMaker ||firstTime)) { + showingDialog = true; + GenericDialog gd = new GenericDialog("Profiler"); + gd.addChoice("Profile", choices, choice); + gd.showDialog(); + if (gd.wasCanceled()) + return null; + choice = gd.getNextChoice(); + timeProfile = choice.equals(choices[0]); + } + if (options!=null) + timeProfile = frames>1 && !options.contains("z"); + if (timeProfile) + size = frames; + else + size = slices; + float[] values = new float[size]; + Calibration cal = imp.getCalibration(); + ResultsTable rt = new ResultsTable(); + Analyzer analyzer = new Analyzer(imp, rt); + int measurements = Analyzer.getMeasurements(); + boolean showResults = !isPlotMaker && measurements!=0 && measurements!=LIMIT; + measurements |= MEAN; + if (showResults) { + if (!Analyzer.resetCounter()) + return null; + } + ImageStack stack = imp.getStack(); + boolean showProgress = size>400 || stack.isVirtual(); + for (int i=1; i<=size; i++) { + if (showProgress) + IJ.showProgress(i,size); + int index = 1; + if (timeProfile) + index = imp.getStackIndex(c, z, i); + else + index = imp.getStackIndex(c, i, t); + ImageProcessor ip = stack.getProcessor(index); + if (minThreshold!=ImageProcessor.NO_THRESHOLD) + ip.setThreshold(minThreshold,maxThreshold,ImageProcessor.NO_LUT_UPDATE); + ip.setRoi(roi); + ImageStatistics stats = ImageStatistics.getStatistics(ip, measurements, cal); + analyzer.saveResults(stats, roi); + values[i-1] = (float)stats.mean; + } + if (showResults) + rt.show("Results"); + return values; + } + + private float[] getZAxisProfile(Roi roi, double minThreshold, double maxThreshold) { + ImageStack stack = imp.getStack(); + if (firstTime) { + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + timeProfile = slices==1 && frames>1; + } + int size = stack.size(); + boolean showProgress = size>400 || stack.isVirtual(); + float[] values = new float[size]; + Calibration cal = imp.getCalibration(); + ResultsTable rt = new ResultsTable(); + Analyzer analyzer = new Analyzer(imp, rt); + int measurements = Analyzer.getMeasurements(); + boolean showResults = !isPlotMaker && measurements!=0 && measurements!=LIMIT; + boolean showingLabels = firstTime && showResults && ((measurements&LABELS)!=0 || (measurements&SLICE)!=0); + measurements |= MEAN; + if (showResults) { + if (!Analyzer.resetCounter()) + return null; + } + boolean isLine = roi!=null && roi.isLine(); + int current = imp.getCurrentSlice(); + for (int i=1; i<=size; i++) { + if (showProgress) + IJ.showProgress(i,size); + if (showingLabels) + imp.setSlice(i); + ImageProcessor ip = stack.getProcessor(i); + if (minThreshold!=ImageProcessor.NO_THRESHOLD) + ip.setThreshold(minThreshold,maxThreshold,ImageProcessor.NO_LUT_UPDATE); + ip.setRoi(roi); + ImageStatistics stats = null; + if (isLine) + stats = getLineStatistics(roi, ip, measurements, cal); + else + stats = ImageStatistics.getStatistics(ip, measurements, cal); + analyzer.saveResults(stats, roi); + values[i-1] = (float)stats.mean; + } + if (showResults) + rt.show("Results"); + if (showingLabels) + imp.setSlice(current); + return values; + } + + private ImageStatistics getLineStatistics(Roi roi, ImageProcessor ip, int measurements, Calibration cal) { + ImagePlus imp = new ImagePlus("", ip); + imp.setRoi(roi); + ProfilePlot profile = new ProfilePlot(imp); + double[] values = profile.getProfile(); + ImageProcessor ip2 = new FloatProcessor(values.length, 1, values); + return ImageStatistics.getStatistics(ip2, measurements, cal); + } + +} + diff --git a/src/ij/plugin/ZProjector.java b/src/ij/plugin/ZProjector.java new file mode 100644 index 0000000..514ee15 --- /dev/null +++ b/src/ij/plugin/ZProjector.java @@ -0,0 +1,823 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.plugin.filter.*; +import ij.plugin.frame.Recorder; +import ij.measure.Measurements; +import java.lang.*; +import java.awt.*; +import java.awt.event.*; +import java.util.Arrays; + +/** This plugin performs a z-projection of the input stack. Type of + output image is same as type of input image. + @author Patrick Kelly +*/ +public class ZProjector implements PlugIn { + public static final int AVG_METHOD = 0; + public static final int MAX_METHOD = 1; + public static final int MIN_METHOD = 2; + public static final int SUM_METHOD = 3; + public static final int SD_METHOD = 4; + public static final int MEDIAN_METHOD = 5; + public static final String[] METHODS = + {"Average Intensity", "Max Intensity", "Min Intensity", "Sum Slices", "Standard Deviation", "Median"}; + private static final String METHOD_KEY = "zproject.method"; + private int method = (int)Prefs.get(METHOD_KEY, AVG_METHOD); + + private static final int BYTE_TYPE = 0; + private static final int SHORT_TYPE = 1; + private static final int FLOAT_TYPE = 2; + + public static final String lutMessage = + "Stacks with inverter LUTs may not project correctly.\n" + +"To create a standard LUT, invert the stack (Edit/Invert)\n" + +"and invert the LUT (Image/Lookup Tables/Invert LUT)."; + + /** Image to hold z-projection. */ + private ImagePlus projImage = null; + + /** Image stack to project. */ + private ImagePlus imp = null; + + /** Projection starts from this slice. */ + private int startSlice = 1; + /** Projection ends at this slice. */ + private int stopSlice = 1; + /** Project all time points? */ + private boolean allTimeFrames = true; + + private String color = ""; + private boolean isHyperstack; + private boolean simpleComposite; + private int increment = 1; + private int sliceCount; + + public ZProjector() { + } + + /** Construction of ZProjector with image to be projected. */ + public ZProjector(ImagePlus imp) { + setImage(imp); + } + + /** Performs projection on the entire stack using the specified method and returns + the result, where 'method' is "avg", "min", "max", "sum", "sd" or "median". + Add " all" to 'method' to project all hyperstack time points. */ + public static ImagePlus run(ImagePlus imp, String method) { + return run(imp, method, 1, imp.getStackSize()); + } + + /** Performs projection using the specified method and stack range, and returns + the result, where 'method' is "avg", "min", "max", "sum", "sd" or "median". + Add " all" to 'method' to project all hyperstack time points.
+ Example: http://imagej.nih.gov/ij/macros/js/ProjectionDemo.js + */ + public static ImagePlus run(ImagePlus imp, String method, int startSlice, int stopSlice) { + ZProjector zp = new ZProjector(imp); + zp.setStartSlice(startSlice); + zp.setStopSlice(stopSlice); + zp.isHyperstack = imp.isHyperStack(); + if (zp.isHyperstack && startSlice==1 && stopSlice==imp.getStackSize()) + zp.setDefaultBounds(); + if (method==null) return null; + method = method.toLowerCase(); + int m = -1; + if (method.startsWith("av")) m = AVG_METHOD; + else if (method.startsWith("max")) m = MAX_METHOD; + else if (method.startsWith("min")) m = MIN_METHOD; + else if (method.startsWith("sum")) m = SUM_METHOD; + else if (method.startsWith("sd")) m = SD_METHOD; + else if (method.startsWith("median")) m = MEDIAN_METHOD; + if (m<0) + throw new IllegalArgumentException("Invalid projection method: "+method); + zp.allTimeFrames = method.contains("all"); + zp.setMethod(m); + zp.doProjection(true); + return zp.getProjection(); + } + + /** Explicitly set image to be projected. This is useful if + ZProjection_ object is to be used not as a plugin but as a + stand alone processing object. */ + public void setImage(ImagePlus imp) { + this.imp = imp; + startSlice = 1; + stopSlice = imp.getStackSize(); + } + + public void setStartSlice(int slice) { + if (imp==null || slice < 1 || slice > imp.getStackSize()) + return; + startSlice = slice; + } + + public void setStopSlice(int slice) { + if (imp==null || slice < 1 || slice > imp.getStackSize()) + return; + stopSlice = slice; + } + + public void setMethod(int projMethod){ + method = projMethod; + } + + /** Retrieve results of most recent projection operation.*/ + public ImagePlus getProjection() { + return projImage; + } + + public void run(String arg) { + imp = IJ.getImage(); + if (imp==null) { + IJ.noImage(); + return; + } + + // Make sure input image is a stack. + if(imp.getStackSize()==1) { + IJ.error("Z Project", "Stack required"); + return; + } + + // Check for inverting LUT. + if (imp.getProcessor().isInvertedLut()) { + if (!IJ.showMessageWithCancel("ZProjection", lutMessage)) + return; + } + + setDefaultBounds(); + + // Build control dialog + GenericDialog gd = buildControlDialog(startSlice,stopSlice); + gd.showDialog(); + if (gd.wasCanceled()) return; + + if (!imp.lock()) return; // exit if in use + long tstart = System.currentTimeMillis(); + gd.setSmartRecording(true); + int startSlice2 = startSlice; + int stopSlice2 = stopSlice; + setStartSlice((int)gd.getNextNumber()); + setStopSlice((int)gd.getNextNumber()); + boolean rangeChanged = startSlice!=startSlice2 || stopSlice!=stopSlice2; + startSlice2 = startSlice; + stopSlice2 = stopSlice; + gd.setSmartRecording(false); + method = gd.getNextChoiceIndex(); + Prefs.set(METHOD_KEY, method); + if (isHyperstack) + allTimeFrames = imp.getNFrames()>1&&imp.getNSlices()>1?gd.getNextBoolean():false; + doProjection(true); + + if (arg.equals("") && projImage!=null) { + long tstop = System.currentTimeMillis(); + if (simpleComposite) IJ.run(projImage, "Grays", ""); + projImage.show("ZProjector: " +IJ.d2s((tstop-tstart)/1000.0,2)+" seconds"); + } + + imp.unlock(); + IJ.register(ZProjector.class); + if (Recorder.scriptMode()) { + String m = getMethodAsString(); + if (isHyperstack && allTimeFrames) + m = m + " all"; + String range = ""; + if (rangeChanged) + range = ","+startSlice2+","+stopSlice2; + Recorder.recordCall("imp = ZProjector.run(imp,\""+m+"\""+range+");"); + } + + } + + private String getMethodAsString() { + switch (method) { + case AVG_METHOD: return "avg"; + case MAX_METHOD: return "max"; + case MIN_METHOD: return "min"; + case SUM_METHOD: return "sum"; + case SD_METHOD: return "sd"; + case MEDIAN_METHOD: return "median"; + default: return "avg"; + } + } + + private void setDefaultBounds() { + int stackSize = imp.getStackSize(); + int channels = imp.getNChannels(); + int frames = imp.getNFrames(); + int slices = imp.getNSlices(); + isHyperstack = imp.isHyperStack()||( ij.macro.Interpreter.isBatchMode()&&((frames>1&&frames1&&slices1) + stopSlice = nSlices; + else + stopSlice = imp.getNFrames(); + } else + stopSlice = stackSize; + } + + public void doRGBProjection() { + doRGBProjection(imp.getStack()); + } + + //Added by Marcel Boeglin 2013.09.23 + public void doRGBProjection(boolean handleOverlay) { + doRGBProjection(imp.getStack()); + Overlay overlay = imp.getOverlay(); + if (handleOverlay && overlay!=null) + projImage.setOverlay(projectRGBHyperStackRois(overlay)); + } + + private void doRGBProjection(ImageStack stack) { + ImageStack[] channels = ChannelSplitter.splitRGB(stack, true); + ImagePlus red = new ImagePlus("Red", channels[0]); + ImagePlus green = new ImagePlus("Green", channels[1]); + ImagePlus blue = new ImagePlus("Blue", channels[2]); + imp.unlock(); + ImagePlus saveImp = imp; + imp = red; + color = "(red)"; doProjection(); + ImagePlus red2 = projImage; + imp = green; + color = "(green)"; doProjection(); + ImagePlus green2 = projImage; + imp = blue; + color = "(blue)"; doProjection(); + ImagePlus blue2 = projImage; + int w = red2.getWidth(), h = red2.getHeight(), d = red2.getStackSize(); + if (method==SD_METHOD) { + ImageProcessor r = red2.getProcessor(); + ImageProcessor g = green2.getProcessor(); + ImageProcessor b = blue2.getProcessor(); + double max = 0; + double rmax = r.getStats().max; if (rmax>max) max=rmax; + double gmax = g.getStats().max; if (gmax>max) max=gmax; + double bmax = b.getStats().max; if (bmax>max) max=bmax; + double scale = 255/max; + r.multiply(scale); g.multiply(scale); b.multiply(scale); + red2.setProcessor(r.convertToByte(false)); + green2.setProcessor(g.convertToByte(false)); + blue2.setProcessor(b.convertToByte(false)); + } + RGBStackMerge merge = new RGBStackMerge(); + ImageStack stack2 = merge.mergeStacks(w, h, d, red2.getStack(), green2.getStack(), blue2.getStack(), true); + imp = saveImp; + projImage = new ImagePlus(makeTitle(), stack2); + } + + /** Builds dialog to query users for projection parameters. + @param start starting slice to display + @param stop last slice */ + protected GenericDialog buildControlDialog(int start, int stop) { + GenericDialog gd = new GenericDialog("ZProjection"); + gd.addNumericField("Start slice:",startSlice,0/*digits*/); + gd.addNumericField("Stop slice:",stopSlice,0/*digits*/); + gd.addChoice("Projection type", METHODS, METHODS[method]); + if (isHyperstack && imp.getNFrames()>1&& imp.getNSlices()>1) + gd.addCheckbox("All time frames", allTimeFrames); + return gd; + } + + /** Performs actual projection using specified method. */ + public void doProjection() { + if (imp==null) + return; + if (imp.getBitDepth()==24) { + doRGBProjection(); + return; + } + sliceCount = 0; + if (methodMEDIAN_METHOD) + method = AVG_METHOD; + for (int slice=startSlice; slice<=stopSlice; slice+=increment) + sliceCount++; + if (method==MEDIAN_METHOD) { + projImage = doMedianProjection(); + return; + } + + // Create new float processor for projected pixels. + FloatProcessor fp = new FloatProcessor(imp.getWidth(),imp.getHeight()); + ImageStack stack = imp.getStack(); + RayFunction rayFunc = getRayFunction(method, fp); + if (IJ.debugMode==true) { + IJ.log("\nProjecting stack from: "+startSlice + +" to: "+stopSlice); + } + + // Determine type of input image. Explicit determination of + // processor type is required for subsequent pixel + // manipulation. This approach is more efficient than the + // more general use of ImageProcessor's getPixelValue and + // putPixel methods. + int ptype; + if (stack.getProcessor(1) instanceof ByteProcessor) ptype = BYTE_TYPE; + else if (stack.getProcessor(1) instanceof ShortProcessor) ptype = SHORT_TYPE; + else if (stack.getProcessor(1) instanceof FloatProcessor) ptype = FLOAT_TYPE; + else { + IJ.error("Z Project", "Non-RGB stack required"); + return; + } + + // Do the projection + int sliceCount = 0; + for (int n=startSlice; n<=stopSlice; n+=increment) { + if (!isHyperstack) { + IJ.showStatus("ZProjection " + color +": " + n + "/" + stopSlice); + IJ.showProgress(n-startSlice, stopSlice-startSlice); + } + projectSlice(stack.getPixels(n), rayFunc, ptype); + sliceCount++; + } + + // Finish up projection. + if (method==SUM_METHOD) { + if (imp.getCalibration().isSigned16Bit()) + fp.subtract(sliceCount*32768.0); + fp.resetMinAndMax(); + projImage = new ImagePlus(makeTitle(), fp); + } else if (method==SD_METHOD) { + rayFunc.postProcess(); + fp.resetMinAndMax(); + projImage = new ImagePlus(makeTitle(), fp); + } else { + rayFunc.postProcess(); + projImage = makeOutputImage(imp, fp, ptype); + } + + if(projImage==null) + IJ.error("Z Project", "Error computing projection."); + } + + //Added by Marcel Boeglin 2013.09.23 + /** Performs actual projection using specified method. + If handleOverlay, adds stack overlay + elements from startSlice to stopSlice to projection. */ + public void doProjection(boolean handleOverlay) { + if (isHyperstack) + doHyperStackProjection(allTimeFrames); + else if (imp.getType()==ImagePlus.COLOR_RGB) + doRGBProjection(handleOverlay); + else { + doProjection(); + Overlay overlay = imp.getOverlay(); + if (handleOverlay && overlay!=null) + projImage.setOverlay(projectStackRois(overlay)); + } + if (projImage!=null) + projImage.setCalibration(imp.getCalibration()); + } + + //Added by Marcel Boeglin 2013.09.23 + private Overlay projectStackRois(Overlay overlay) { + if (overlay==null) return null; + Overlay overlay2 = overlay.create(); + Roi roi; + int s; + for (Roi r : overlay.toArray()) { + s = r.getPosition(); + roi = (Roi)r.clone(); + if (s>=startSlice && s<=stopSlice || s==0) { + roi.setPosition(s); + overlay2.add(roi); + } + } + return overlay2; + } + + public void doHyperStackProjection(boolean allTimeFrames) { + int start = startSlice; + int stop = stopSlice; + int firstFrame = 1; + int lastFrame = imp.getNFrames(); + if (!allTimeFrames) + firstFrame = lastFrame = imp.getFrame(); + ImageStack stack = new ImageStack(imp.getWidth(), imp.getHeight()); + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + if (slices==1) { + slices = imp.getNFrames(); + firstFrame = lastFrame = 1; + } + int frames = lastFrame-firstFrame+1; + increment = channels; + boolean rgb = imp.getBitDepth()==24; + for (int frame=firstFrame; frame<=lastFrame; frame++) { + IJ.showStatus(""+ (frame-firstFrame) + "/" + (lastFrame-firstFrame)); + IJ.showProgress(frame-firstFrame, lastFrame-firstFrame); + for (int channel=1; channel<=channels; channel++) { + startSlice = (frame-1)*channels*slices + (start-1)*channels + channel; + stopSlice = (frame-1)*channels*slices + (stop-1)*channels + channel; + if (rgb) + doHSRGBProjection(imp); + else + doProjection(); + stack.addSlice(null, projImage.getProcessor()); + } + } + projImage = new ImagePlus(makeTitle(), stack); + projImage.setDimensions(channels, 1, frames); + if (channels>1) { + projImage = new CompositeImage(projImage, 0); + ((CompositeImage)projImage).copyLuts(imp); + if (method==SUM_METHOD || method==SD_METHOD) + ((CompositeImage)projImage).resetDisplayRanges(); + } + if (frames>1) + projImage.setOpenAsHyperStack(true); + Overlay overlay = imp.getOverlay(); + if (overlay!=null) { + startSlice = start; + stopSlice = stop; + if (imp.getType()==ImagePlus.COLOR_RGB) + projImage.setOverlay(projectRGBHyperStackRois(overlay)); + else + projImage.setOverlay(projectHyperStackRois(overlay)); + } + IJ.showProgress(1, 1); + } + + //Added by Marcel Boeglin 2013.09.22 + private Overlay projectRGBHyperStackRois(Overlay overlay) { + if (overlay==null) return null; + int frames = projImage.getNFrames(); + int t1 = imp.getFrame(); + Overlay overlay2 = overlay.create(); + Roi roi; + int c, z, t; + for (Roi r : overlay.toArray()) { + c = r.getCPosition(); + z = r.hasHyperStackPosition()?r.getZPosition():0; + t = r.getTPosition(); + roi = (Roi)r.clone(); + if (z>=startSlice && z<=stopSlice || z==0 || c==0 || t==0) { + if (frames==1 && t!=t1 && t!=0)//current time frame + continue; + roi.setPosition(t); + overlay2.add(roi); + } + } + return overlay2; + } + + //Added by Marcel Boeglin 2013.09.22 + private Overlay projectHyperStackRois(Overlay overlay) { + if (overlay==null) return null; + int t1 = imp.getFrame(); + int channels = projImage.getNChannels(); + int slices = 1; + int frames = projImage.getNFrames(); + Overlay overlay2 = overlay.create(); + Roi roi; + int c, z, t; + int size = channels * slices * frames; + for (Roi r : overlay.toArray()) { + c = r.getCPosition(); + z = r.getZPosition(); + t = r.getTPosition(); + roi = (Roi)r.clone(); + if (size==channels) {//current time frame + if (z>=startSlice && z<=stopSlice && t==t1 || c==0) { + roi.setPosition(c); + overlay2.add(roi); + } + } + else if (size==frames*channels) {//all time frames + if (z>=startSlice && z<=stopSlice) + roi.setPosition(c, 1, t); + else if (z==0) + roi.setPosition(c, 0, t); + else continue; + overlay2.add(roi); + } + } + return overlay2; + } + + private void doHSRGBProjection(ImagePlus rgbImp) { + ImageStack stack = rgbImp.getStack(); + ImageStack stack2 = new ImageStack(stack.getWidth(), stack.getHeight()); + for (int i=startSlice; i<=stopSlice; i++) + stack2.addSlice(null, stack.getProcessor(i)); + startSlice = 1; + stopSlice = stack2.getSize(); + doRGBProjection(stack2); + } + + private RayFunction getRayFunction(int method, FloatProcessor fp) { + switch (method) { + case AVG_METHOD: case SUM_METHOD: + return new AverageIntensity(fp, sliceCount); + case MAX_METHOD: + return new MaxIntensity(fp); + case MIN_METHOD: + return new MinIntensity(fp); + case SD_METHOD: + return new StandardDeviation(fp, sliceCount); + default: + IJ.error("Z Project", "Unknown method."); + return null; + } + } + + /** Generate output image whose type is same as input image. */ + private ImagePlus makeOutputImage(ImagePlus imp, FloatProcessor fp, int ptype) { + int width = imp.getWidth(); + int height = imp.getHeight(); + float[] pixels = (float[])fp.getPixels(); + ImageProcessor oip=null; + + // Create output image consistent w/ type of input image. + int size = pixels.length; + switch (ptype) { + case BYTE_TYPE: + oip = imp.getProcessor().createProcessor(width,height); + byte[] pixels8 = (byte[])oip.getPixels(); + for (int i=0; ifpixels[i]) + fpixels[i] = (pixels[i]&0xff); + } + } + + public void projectSlice(short[] pixels) { + for(int i=0; ifpixels[i]) + fpixels[i] = pixels[i]&0xffff; + } + } + + public void projectSlice(float[] pixels) { + for (int i=0; ifpixels[i]) + fpixels[i] = pixels[i]; + } + } + + } // end MaxIntensity + + /** Compute min intensity projection. */ + class MinIntensity extends RayFunction { + private float[] fpixels; + private int len; + + /** Simple constructor since no preprocessing is necessary. */ + public MinIntensity(FloatProcessor fp) { + fpixels = (float[])fp.getPixels(); + len = fpixels.length; + for (int i=0; i1) { + stdDev = (n*sum2[i]-sum[i]*sum[i])/n; + if (stdDev>0.0) + result[i] = (float)Math.sqrt(stdDev/(n-1.0)); + else + result[i] = 0f; + } else + result[i] = 0f; + } + } + + } // end StandardDeviation + +} // end ZProjection + + diff --git a/src/ij/plugin/Zoom.java b/src/ij/plugin/Zoom.java new file mode 100644 index 0000000..ff5061e --- /dev/null +++ b/src/ij/plugin/Zoom.java @@ -0,0 +1,277 @@ +package ij.plugin; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import ij.plugin.frame.Recorder; +import java.awt.*; + +/** This plugin implements the commands in the Image/Zoom submenu. */ +public class Zoom implements PlugIn { + + public static void toSelection(ImagePlus imp) { + Zoom zoom = new Zoom(); + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) + zoom.zoomToSelection(imp, ic); + } + + public static void set(ImagePlus imp, double magnification) { + Zoom zoom = new Zoom(); + zoom.setZoom(imp, magnification, -1, -1); + } + + public static void set(ImagePlus imp, double magnification, int x, int y) { + Zoom zoom = new Zoom(); + zoom.setZoom(imp, magnification, x, y); + } + + public static void in(ImagePlus imp) { + ImageCanvas ic = imp.getCanvas(); + if (ic==null) return; + waitUntilActivated(imp); + int x = ic.screenX(imp.getWidth()/2); + int y = ic.screenY(imp.getHeight()/2); + ic.zoomIn(x, y); + if (ic.getMagnification()<=1.0) + imp.repaintWindow(); + } + + public static void out(ImagePlus imp) { + ImageCanvas ic = imp.getCanvas(); + if (ic==null) return; + waitUntilActivated(imp); + int x = ic.screenX(imp.getWidth()/2); + int y = ic.screenY(imp.getHeight()/2); + ic.zoomOut(x, y); + if (ic.getMagnification()<=1.0) + imp.repaintWindow(); + } + + public static void unzoom(ImagePlus imp) { + ImageCanvas ic = imp.getCanvas(); + if (ic!=null) { + waitUntilActivated(imp); + ic.unzoom(); + } + } + + public static void maximize(ImagePlus imp) { + ImageWindow win = imp.getWindow(); + if (win!=null) { + waitUntilActivated(imp); + win.maximize(); + IJ.wait(100); + } + } + + private static void waitUntilActivated(ImagePlus imp) { + int count = 0; + boolean isCanvas = imp.getCanvas()!=null; + if (isCanvas) { + long t0 = System.currentTimeMillis(); + while (!imp.windowActivated() && (System.currentTimeMillis()-t0)<=1000) { + IJ.wait(10); + count++; + } + } + if (IJ.debugMode) + IJ.log("Zoom: "+ count+" "+imp.windowActivated()+" "+isCanvas+" "+imp); + } + + public void run(String arg) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) { + IJ.noImage(); + return; + } + ImageCanvas ic = imp.getCanvas(); + if (ic==null) return; + if (ic instanceof PlotCanvas && !((PlotCanvas)ic).isFrozen()) { + ((PlotCanvas)ic).zoom(arg); + return; + } + Point loc = ic.getCursorLoc(); + if (!ic.cursorOverImage()) { + Rectangle srcRect = ic.getSrcRect(); + loc.x = srcRect.x + srcRect.width/2; + loc.y = srcRect.y + srcRect.height/2; + } + int x = ic.screenX(loc.x); + int y = ic.screenY(loc.y); + if (arg.equals("in")) { + waitUntilActivated(imp); + ic.zoomIn(x, y); + if (ic.getMagnification()<=1.0) + imp.repaintWindow(); + Recorder.recordCall("Zoom.in(imp);"); + } else if (arg.equals("out")) { + waitUntilActivated(imp); + ic.zoomOut(x, y); + if (ic.getMagnification()<1.0) + imp.repaintWindow(); + Recorder.recordCall("Zoom.out(imp);"); + } else if (arg.equals("orig")) { + unzoom(imp); + Recorder.recordCall("Zoom.unzoom(imp);"); + } else if (arg.equals("100%")) { + waitUntilActivated(imp); + ic.zoom100Percent(); + } else if (arg.equals("to")) { + zoomToSelection(imp, ic); + Recorder.recordCall("Zoom.toSelection(imp);"); + } else if (arg.equals("set")) { + setZoom(imp, -1, -1, -1); + } else if (arg.equals("max")) { + maximize(imp); + Recorder.recordCall("Zoom.maximize(imp);"); + } else if (arg.equals("scale")) + scaleToFit(imp); + } + + void zoomToSelection(ImagePlus imp, ImageCanvas ic) { + waitUntilActivated(imp); + Roi roi = imp.getRoi(); + ic.unzoom(); + if (roi==null) return; + Rectangle w = imp.getWindow().getBounds(); + Rectangle r = roi.getBounds(); + double mag = ic.getMagnification(); + int marginw = (int)((w.width - mag * imp.getWidth())); + int marginh = (int)((w.height - mag * imp.getHeight())); + int x = r.x+r.width/2; + int y = r.y+r.height/2; + mag = ic.getHigherZoomLevel(mag); + while (r.width*mag 100) jpegQuality = 100; + this.jpegQuality = jpegQuality; + File file = new File(path); + raFile = new RandomAccessFile(file, "rw"); + raFile.setLength(0); + imp.startTiming(); + + // G e t s t a c k p r o p e r t i e s + boolean isComposite = imp.isComposite(); + boolean isHyperstack = imp.isHyperStack(); + boolean isOverlay = imp.getOverlay()!=null && !imp.getHideOverlay(); + xDim = imp.getWidth(); //image width + yDim = imp.getHeight(); //image height + zDim = imp.getStackSize(); //number of frames in video + boolean saveFrames=false, saveSlices=false, saveChannels=false; + int channels = imp.getNChannels(); + int slices = imp.getNSlices(); + int frames = imp.getNFrames(); + int channel = imp.getChannel(); + int slice = imp.getSlice(); + int frame = imp.getFrame(); + if (isHyperstack || isComposite) { + if (frames>1) { + saveFrames = true; + zDim = frames; + } else if (slices>1) { + saveSlices = true; + zDim = slices; + } else if (channels>1) { + saveChannels = true; + zDim = channels; + } else + isHyperstack = false; + } + + if (imp.getType()==ImagePlus.COLOR_RGB || isComposite || biCompression==JPEG_COMPRESSION || isOverlay) + bytesPerPixel = 3; //color and JPEG-compressed files + else + bytesPerPixel = 1; //gray 8, 16, 32 bit and indexed color: all written as 8 bit + boolean writeLUT = bytesPerPixel==1; // QuickTime reads the avi palette also for PNG + linePad = 0; + int minLineLength = bytesPerPixel*xDim; + if (biCompression==NO_COMPRESSION && minLineLength%4!=0) + linePad = 4 - minLineLength%4; //uncompressed lines written must be a multiple of 4 bytes + frameDataSize = (bytesPerPixel*xDim+linePad)*yDim; + int microSecPerFrame = (int)Math.round((1.0/getFrameRate(imp))*1.0e6); + int dwChunkId = biCompression==NO_COMPRESSION ? FOURCC_00db : FOURCC_00dc; + long sizeEstimate = bytesPerPixel*xDim*yDim*(long)zDim; + //boolean writeAVI2index = true;//frameDataSize*zDim > 1000000000; + int nAvixChunksEstimate = (int)(sizeEstimate/JUNK_SIZE_THRESHOLD); //estimated number of AVIX junks + endHeadPointer = 4096+((nAvixChunksEstimate*16+1000)/1024)*1024; //reserve plenty of space for 'indx' + + // W r i t e A V I f i l e h e a d e r + writeString("RIFF"); // signature + chunkSizeHere(); // size of file (nesting level 0) + writeString("AVI "); // RIFF type + writeString("LIST"); // first LIST chunk, which contains information on data decoding + chunkSizeHere(); // size of LIST (nesting level 1) + writeString("hdrl"); // LIST chunk type + writeString("avih"); // Write the avih sub-CHUNK + writeInt(0x38); // length of the avih sub-CHUNK (38H) not including the + // the first 8 bytes for avihSignature and the length + writeInt(microSecPerFrame); // dwMicroSecPerFrame - Write the microseconds per frame + writeInt(0); // dwMaxBytesPerSec (maximum data rate of the file in bytes per second) + writeInt(0); // dwPaddingGranularity (for header length?), previously dwReserved1, usually set to zero. + writeInt(0x10); // dwFlags - just set the bit for AVIF_HASINDEX + // 10H AVIF_HASINDEX: The AVI file has an idx1 chunk containing + // an index at the end of the file. For good performance, all + // AVI files should contain an index. + writeInt(zDim); // dwTotalFrames - total frame number + writeInt(0); // dwInitialFrames -Initial frame for interleaved files. + // Noninterleaved files should specify 0. + writeInt(1); // dwStreams - number of streams in the file - here 1 video and zero audio. + writeInt(0); // dwSuggestedBufferSize + writeInt(xDim); // dwWidth - image width in pixels + writeInt(yDim); // dwHeight - image height in pixels + writeInt(0); // dwReserved[4] + writeInt(0); + writeInt(0); + writeInt(0); + + // W r i t e s t r e a m i n f o r m a t i o n + writeString("LIST"); // List of stream headers + chunkSizeHere(); // size of LIST (nesting level 2) + writeString("strl"); // LIST chunk type: stream list + writeString("strh"); // stream header + writeInt(56); // Write the length of the strh sub-CHUNK + writeString("vids"); // fccType - type of data stream - here 'vids' for video stream + writeString("DIB "); // 'DIB ' for Microsoft Device Independent Bitmap. + writeInt(0); // dwFlags + writeInt(0); // wPriority, wLanguage + writeInt(0); // dwInitialFrames + writeInt(1); // dwScale + writeInt((int)Math.round(getFrameRate(imp))); // dwRate - frame rate for video streams + writeInt(0); // dwStart - this field is usually set to zero + writeInt(zDim); // dwLength - playing time of AVI file as defined by scale and rate + // Set equal to the number of frames + writeInt(0); // dwSuggestedBufferSize for reading the stream. + // Typically, this contains a value corresponding to the largest chunk + // in a stream. + writeInt(-1); // dwQuality - encoding quality given by an integer between + // 0 and 10,000. If set to -1, drivers use the default + // quality value. + writeInt(0); // dwSampleSize. 0 means that each frame is in its own chunk + writeShort((short)0); // left of rcFrame if stream has a different size than dwWidth*dwHeight(unused) + writeShort((short)0); // top + writeShort((short)0); // right + writeShort((short)0); // bottom + // end of 'strh' chunk, stream format follows + writeString("strf"); // stream format chunk + chunkSizeHere(); // size of 'strf' chunk (nesting level 3) + writeInt(40); // biSize - Write header size of BITMAPINFO header structure + // Applications should use this size to determine which BITMAPINFO header structure is + // being used. This size includes this biSize field. + writeInt(xDim); // biWidth - width in pixels + writeInt(yDim); // biHeight - image height in pixels. (May be negative for uncompressed + // video to indicate vertical flip). + writeShort(1); // biPlanes - number of color planes in which the data is stored + writeShort((short)(8*bytesPerPixel)); // biBitCount - number of bits per pixel # + writeInt(biCompression); // biCompression - type of compression used (uncompressed: NO_COMPRESSION=0) + int biSizeImage = // Image Buffer. Quicktime needs 3 bytes also for 8-bit png + (biCompression==NO_COMPRESSION)?0:xDim*yDim*bytesPerPixel; + writeInt(biSizeImage); // biSizeImage (buffer size for decompressed mage) may be 0 for uncompressed data + writeInt(0); // biXPelsPerMeter - horizontal resolution in pixels per meter + writeInt(0); // biYPelsPerMeter - vertical resolution in pixels per meter + writeInt(writeLUT ? 256:0); // biClrUsed (color table size; for 8-bit only) + writeInt(0); // biClrImportant - specifies that the first x colors of the color table + // are important to the DIB. If the rest of the colors are not available, + // the image still retains its meaning in an acceptable manner. When this + // field is set to zero, all the colors are important, or, rather, their + // relative importance has not been computed. + if (writeLUT) // write color lookup table + writeLUT(imp.getProcessor()); + chunkEndWriteSize(); //'strf' chunk finished (nesting level 3) + + writeString("strn"); // Use 'strn' to provide a zero terminated text string describing the stream + writeInt(16); // length of the strn sub-CHUNK (must be even) + writeString("ImageJ AVI \0"); //must be 16 bytes as given above (including the terminating 0 byte) + pointer2indx = raFile.getFilePointer(); + writeString("indx"); // 'indx' chunk type: Index of indices + chunkSizeHere(); // size of 'indx' (nesting level 3) + writeShort(4); // wLongsPerEntry = 4 ('Longs' are 32-bit here!) + writeByte(0); // bIndexSubType=0 + writeByte(0); // bIndexType=0: AVI_INDEX_OF_INDEXES + pointer2indxNEntriesInUse = raFile.getFilePointer(); + writeInt(0); // nEntriesInUse, will be filled in later + writeInt(dwChunkId); // dwChunkId, '00dc' or '00db' + writeInt(0); writeInt(0); writeInt(0); // dwReserved[3] + pointer2indxNextEntry = raFile.getFilePointer(); + chunkEndWriteSize(); //'indx' chunk finished (nesting level 3), will be modified by writeMainIndxEntry + writeString("JUNK"); // write a JUNK chunk for padding (will be moved and shortened by writeMainIndxEntry) + chunkSizeHere(); // size of 'JUNK' for padding (nesting level 3) + raFile.seek(endHeadPointer); // we continue here + chunkEndWriteSize(); // 'JUNK' finished (nesting level 3) + chunkEndWriteSize(); // LIST 'strl' finished (nesting level 2) + chunkEndWriteSize(); // LIST 'hdrl' finished (nesting level 1) + + // P r e p a r e f o r w r i t i n g d a t a + if (biCompression == NO_COMPRESSION) + bufferWrite = new byte[frameDataSize]; + else + raOutputStream = new RaOutputStream(raFile); //needed for writing compressed formats + //int maxChunkLength = 0; // needed for dwSuggestedBufferSize + int[] dataChunkOffset = new int[zDim]; // remember chunk positions... + int[] dataChunkLength = new int[zDim]; // ... and sizes for the index + + int currentFilePart = 0;// 0 is inside RIFF AVI (AVI 1.0 compatible), >0 is RIFF AVIX (data chunk of AVI 2.0) + + // W r i t e f r a m e d a t a a n d i n d i c e s + boolean writeAVI2index = false; // see whether we need an AVI2 index (large files only) + int iFrame = 0; + while (iFrame < zDim) { + if (currentFilePart > 0) { // open new RIFF AVIX chunk + writeString("RIFF"); + chunkSizeHere(); // size of chunk (nesting level 0) + writeString("AVIX"); // RIFF type + //IJ.log("AVIX starts at iFrame="+iFrame); + } + writeString("LIST"); // this LIST chunk contains the AVI-2 style index and the actual data + chunkSizeHere(); // size of LIST (nesting level 1) + long moviPointer = raFile.getFilePointer(); + writeString("movi"); // write LIST type 'movi' + + int firstFrameInChunk = iFrame; + + // W r i t e s i n g l e f r a m e + while (iFrame JUNK_SIZE_THRESHOLD) + break; // make sure we don't get over 1GB + } // while (iFrame MAX_INDX_SIZE) { + raFile.close(); + throw new RuntimeException("AVI_Writer ERROR: Index Size Overflow"); + } + long savePosition = raFile.getFilePointer(); + raFile.seek(pointer2indxNextEntry); + writeLong(ix00pointer); + writeInt(dwSize); + writeInt(nFrames); + pointer2indxNextEntry += 16; + nIndxEntries++; + writeString("JUNK"); // write a JUNK chunk for padding + chunkSizeHere(); // size of 'JUNK' for padding goes here + raFile.seek(endHeadPointer);// end of the padded range + chunkEndWriteSize(); // 'JUNK' finished (nesting level 3) + raFile.seek(pointer2indx+4); + writeInt((int)(pointer2indxNextEntry - pointer2indx - 8)); //write new size of 'indx' + raFile.seek(pointer2indxNEntriesInUse); + writeInt(nIndxEntries); //write new number of 'indx' entries + raFile.seek(savePosition); + } + + /** Write Grayscale (or indexed color) data. Lines are + * padded to a length that is a multiple of 4 bytes. */ + private void writeByteFrame(ImageProcessor ip) throws IOException { + ip = ip.convertToByte(true); + byte[] pixels = (byte[])ip.getPixels(); + int width = ip.getWidth(); + int height = ip.getHeight(); + int c, offset, index = 0; + for (int y=height-1; y>=0; y--) { + offset = y*width; + for (int x=0; x=0; y--) { + offset = y*width; + for (int x=0; x>8); //green + bufferWrite[index++] = (byte)((c&0xff0000)>>16); // red + } + for (int i = 0; i60.0) rate = 60.0; + return rate; + } + + private void writeString(String s) throws IOException { + byte[] bytes = s.getBytes("UTF8"); + raFile.write(bytes); + } + + /** Write 8-byte int with Intel (little-endian) byte order + * (note: RandomAccessFile.writeInt has other byte order than AVI) */ + private void writeLong(long v) throws IOException { + for (int i=0; i<8; i++) { + raFile.write((int)(v & 0xFFL)); + v = v>>>8; + } + //IJ.log("long: 0x"+Long.toHexString(v)+"="+v); + } + + /** Write 4-byte int with Intel (little-endian) byte order + * (note: RandomAccessFile.writeInt has other byte order than AVI) */ + private void writeInt(int v) throws IOException { + raFile.write(v & 0xFF); + raFile.write((v >>> 8) & 0xFF); + raFile.write((v >>> 16) & 0xFF); + raFile.write((v >>> 24) & 0xFF); + //IJ.log("int: 0x"+Integer.toHexString(v)+"="+v); + } + + /** Write 2-byte short with Intel (little-endian) byte order + * (note: RandomAccessFile.writeShort has other byte order than AVI) */ + private void writeShort(int v) throws IOException { + raFile.write(v & 0xFF); + raFile.write((v >>> 8) & 0xFF); + } + + /** Write a byte */ + private void writeByte(int v) throws IOException { + raFile.write(v & 0xFF); + } + + /** An output stream directed to a RandomAccessFile (starting at the current position) */ + class RaOutputStream extends OutputStream { + RandomAccessFile raFile; + RaOutputStream (RandomAccessFile raFile) { + this.raFile = raFile; + } + public void write (int b) throws IOException { + //IJ.log("stream: byte"); + raFile.writeByte(b); //just for completeness, usually not used by image encoders + } + public void write (byte[] b) throws IOException { + //IJ.log("stream: array len="+b.length); + raFile.write(b); + } + public void write (byte[] b, int off, int len) throws IOException { + //IJ.log("stream: array="+b.length+" off="+off+" len="+len); + raFile.write(b, off, len); + } + } + +} diff --git a/src/ij/plugin/filter/Analyzer.java b/src/ij/plugin/filter/Analyzer.java new file mode 100644 index 0000000..285d634 --- /dev/null +++ b/src/ij/plugin/filter/Analyzer.java @@ -0,0 +1,1070 @@ +package ij.plugin.filter; +import java.awt.*; +import java.util.Vector; +import java.util.Properties; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import ij.text.*; +import ij.plugin.MeasurementsWriter; +import ij.plugin.Straightener; +import ij.plugin.frame.RoiManager; +import ij.util.Tools; +import ij.macro.Interpreter; + +/** This plugin implements ImageJ's Analyze/Measure and Analyze/Set Measurements commands. */ +public class Analyzer implements PlugInFilter, Measurements { + + private static boolean drawLabels = true; + private String arg; + private ImagePlus imp; + private ResultsTable rt; + private int measurements; + private StringBuffer min,max,mean,sd; + private boolean disableReset; + private boolean resultsUpdated; + + // Order must agree with order of checkboxes in Set Measurements dialog box + private static final int[] list = {AREA,MEAN,STD_DEV,MODE,MIN_MAX, + CENTROID,CENTER_OF_MASS,PERIMETER,RECT,ELLIPSE,SHAPE_DESCRIPTORS, FERET, + INTEGRATED_DENSITY,MEDIAN,SKEWNESS,KURTOSIS,AREA_FRACTION,STACK_POSITION, + LIMIT,LABELS,INVERT_Y,SCIENTIFIC_NOTATION,ADD_TO_OVERLAY,NaN_EMPTY_CELLS}; + + private static final String MEASUREMENTS = "measurements"; + private static final String MARK_WIDTH = "mark.width"; + private static final String PRECISION = "precision"; + //private static int counter; + private static boolean unsavedMeasurements; + public static Color darkBlue = new Color(0,0,160); + private static int systemMeasurements = Prefs.getInt(MEASUREMENTS,AREA+MEAN+MIN_MAX); + public static int markWidth; + public static int precision = Prefs.getInt(PRECISION,3); + private static float[] umeans = new float[MAX_STANDARDS]; + private static ResultsTable systemRT = new ResultsTable(); + private static int redirectTarget; + private static String redirectTitle = ""; + private static ImagePlus redirectImage; // non-displayed images + static int firstParticle, lastParticle; + private static boolean summarized; + private static boolean switchingModes; + private static boolean showMin = true; + private static boolean showAngle = true; + + public Analyzer() { + rt = systemRT; + rt.showRowNumbers(true); + rt.setPrecision((systemMeasurements&SCIENTIFIC_NOTATION)!=0?-precision:precision); + rt.setNaNEmptyCells((systemMeasurements&NaN_EMPTY_CELLS)!=0); + measurements = systemMeasurements; + } + + /** Constructs a new Analyzer using the specified ImagePlus object + and the current measurement options and default results table. */ + public Analyzer(ImagePlus imp) { + this(); + this.imp = imp; + } + + /** Construct a new Analyzer using an ImagePlus object and a ResultsTable. */ + public Analyzer(ImagePlus imp, ResultsTable rt) { + this(imp, Analyzer.getMeasurements(), rt); + } + + /** Construct a new Analyzer using an ImagePlus object and private + measurement options and a ResultsTable. */ + public Analyzer(ImagePlus imp, int measurements, ResultsTable rt) { + this.imp = imp; + this.measurements = measurements; + if (rt==null) + rt = new ResultsTable(); + rt.setPrecision((systemMeasurements&SCIENTIFIC_NOTATION)!=0?-precision:precision); + rt.setNaNEmptyCells((systemMeasurements&NaN_EMPTY_CELLS)!=0); + this.rt = rt; + } + + public int setup(String arg, ImagePlus imp) { + this.arg = arg; + this.imp = imp; + IJ.register(Analyzer.class); + if (arg.equals("set")) { + doSetDialog(); + return DONE; + } else if (arg.equals("sum")) { + summarize(); + return DONE; + } else if (arg.equals("clear")) { + if (IJ.macroRunning()) + unsavedMeasurements = false; + resetCounter(); + return DONE; + } else + return DOES_ALL+NO_CHANGES; + } + + public void run(ImageProcessor ip) { + measure(); + Roi roi = imp.getRoi(); + if (roi==null || roi.getType()!=Roi.POINT) + displayResults(); + if ((measurements&ADD_TO_OVERLAY)!=0) + addRoiToOverlay(); + } + + private void addRoiToOverlay() { + Roi roi = imp.getRoi(); + if (roi==null) + return; + roi = (Roi)roi.clone(); + if (imp.getStackSize()>1) { + if (imp.isHyperStack()||imp.isComposite()) + roi.setPosition(0, imp.getSlice(), imp.getFrame()); + else + roi.setPosition(imp.getCurrentSlice()); + } + if (roi.getName()==null) + roi.setName(""+rt.size()); + //roi.setName(IJ.getString("Label:", "m"+rt.size())); + roi.setIgnoreClipRect(true); + Overlay overlay = imp.getOverlay(); + if (overlay==null) + overlay = new Overlay(); + if (drawLabels) + overlay.drawLabels(true); + if (!overlay.getDrawNames()) + overlay.drawNames(true); + overlay.setLabelColor(Color.white); + overlay.drawBackgrounds(true); + overlay.add(roi); + imp.setOverlay(overlay); + if (roi.getType()==Roi.COMPOSITE && Toolbar.getToolId()==Toolbar.OVAL && Toolbar.getBrushSize()>0) + imp.deleteRoi(); // delete ROIs created with the selection brush tool + } + + void doSetDialog() { + String NONE = "None"; + String[] titles; + int[] wList = WindowManager.getIDList(); + if (wList==null) { + titles = new String[1]; + titles[0] = NONE; + } else { + titles = new String[wList.length+1]; + titles[0] = NONE; + for (int i=0; i9) prec = 9; + boolean notationChanged = (oldMeasurements&SCIENTIFIC_NOTATION)!=(systemMeasurements&SCIENTIFIC_NOTATION); + if (prec!=precision || notationChanged) { + precision = prec; + rt.setPrecision((systemMeasurements&SCIENTIFIC_NOTATION)!=0?-precision:precision); + if (rt.size()>0) + rt.show("Results"); + } + } + + void setOptions(GenericDialog gd) { + int oldMeasurements = systemMeasurements; + int previous = 0; + boolean b = false; + for (int i=0; i1 && !IJ.isResultsWindow() && IJ.getInstance()!=null) + rt.reset(); + if ((oldMeasurements&(~SCIENTIFIC_NOTATION))!=(systemMeasurements&(~SCIENTIFIC_NOTATION))&&IJ.isResultsWindow()) { + rt.setPrecision((systemMeasurements&SCIENTIFIC_NOTATION)!=0?-precision:precision); + clearSummary(); + rt.update(systemMeasurements, imp, null); + } + if ((systemMeasurements&LABELS)==0) + systemRT.disableRowLabels(); + } + + /** Measures the image or selection and adds the results to the default results table. */ + public void measure() { + String lastHdr = rt.getColumnHeading(ResultsTable.LAST_HEADING); + if (lastHdr==null || lastHdr.charAt(0)!='M') { + if (!reset()) return; + } + firstParticle = lastParticle = 0; + Roi roi = imp.getRoi(); + if (roi!=null && roi.getType()==Roi.POINT) { + measurePoint(roi); + return; + } + if (roi!=null && roi.isLine()) { + measureLength(roi); + return; + } + if (roi!=null && roi.getType()==Roi.ANGLE) { + measureAngle(roi); + return; + } + ImageStatistics stats; + if (isRedirectImage()) { + stats = getRedirectStats(measurements, roi); + if (stats==null) return; + } else + stats = imp.getStatistics(measurements); + if (!IJ.isResultsWindow() && IJ.getInstance()!=null) + reset(); + saveResults(stats, roi); + } + + /* + void showHeadings() { + String[] headings = rt.getHeadings(); + int columns = headings.length; + if (columns==0) + return; + IJ.log("Headings: "+headings.length+" "+rt.getColumnHeading(ResultsTable.LAST_HEADING)); + for (int i=0; i0 && !disableReset) + ok = resetCounter(); + if (ok && rt.getColumnHeading(ResultsTable.LAST_HEADING)==null) + rt.setDefaultHeadings(); + return ok; + } + + /** Returns true if an image is selected in the "Redirect To:" + popup menu of the Analyze/Set Measurements dialog box. */ + public static boolean isRedirectImage() { + return redirectTarget!=0; + } + + /** Set the "Redirect To" image. Pass 'null' as the + argument to disable redirected sampling. */ + public static void setRedirectImage(ImagePlus imp) { + if (imp==null) { + redirectTarget = 0; + redirectTitle = null; + redirectImage = null; + } else { + redirectTarget = imp.getID(); + redirectTitle = imp.getTitle(); + if (imp.getWindow()==null) + redirectImage = imp; + } + } + + private ImagePlus getRedirectImageOrStack(ImagePlus cimp) { + ImagePlus rimp = getRedirectImage(cimp); + if (rimp!=null) { + int depth = rimp.getStackSize(); + if (depth>1 && depth==cimp.getStackSize() && rimp.getCurrentSlice()!=cimp.getCurrentSlice()) + rimp.setSlice(cimp.getCurrentSlice()); + } + return rimp; + } + + /** Returns the image selected in the "Redirect To:" popup + menu of the Analyze/Set Measurements dialog, or null + if "None" is selected, the image was not found or the + image is not the same size as currentImage. */ + public static ImagePlus getRedirectImage(ImagePlus cimp) { + ImagePlus rimp = WindowManager.getImage(redirectTarget); + if (rimp==null) + rimp = redirectImage; + if (rimp==null) { + IJ.error("Analyzer", "Redirect image (\""+redirectTitle+"\")\n" + + "not found."); + redirectTarget = 0; + Macro.abort(); + return null; + } + if (rimp.getWidth()!=cimp.getWidth() || rimp.getHeight()!=cimp.getHeight()) { + IJ.error("Analyzer", "Redirect image (\""+redirectTitle+"\") \n" + + "is not the same size as the current image."); + Macro.abort(); + return null; + } + return rimp; + } + + ImageStatistics getRedirectStats(int measurements, Roi roi) { + ImagePlus redirectImp = getRedirectImageOrStack(imp); + if (redirectImp==null) + return null; + ImageProcessor ip = redirectImp.getProcessor(); + if (imp.getTitle().equals("mask") && imp.getBitDepth()==8) { + ip.setMask(imp.getProcessor()); + ip.setRoi(0, 0, imp.getWidth(), imp.getHeight()); + } else + ip.setRoi(roi); + return ImageStatistics.getStatistics(ip, measurements, redirectImp.getCalibration()); + } + + void measurePoint(Roi roi) { + if (rt.size()>0) { + if (!IJ.isResultsWindow()) + reset(); + int index = rt.getColumnIndex("X"); + if ((index<0 || !rt.columnExists(index)) && rt.getLastColumn()1) + stack = imp2.getStack(); + PointRoi pointRoi = roi instanceof PointRoi?(PointRoi)roi:null; + for (int i=0; i0 && position<=stack.size()) + ip = stack.getProcessor(position); + else + ip = imp2.getProcessor(); + ip.setRoi((int)Math.round(p.xpoints[i]), (int)Math.round(p.ypoints[i]), 1, 1); + ImageStatistics stats = ImageStatistics.getStatistics(ip, measurements, imp2.getCalibration()); + stats.xCenterOfMass = p.xpoints[i]; + stats.yCenterOfMass = p.ypoints[i]; + PointRoi point = new PointRoi(p.xpoints[i], p.ypoints[i]); + point.setPosition(position); + if (pointRoi!=null && pointRoi.getNCounters()>1) { + int[] counters = pointRoi.getCounters(); + if (counters!=null && i0) { + if (!IJ.isResultsWindow()) reset(); + int index = rt.getColumnIndex("Angle"); + if (index<0 || !rt.columnExists(index)) { + clearSummary(); + rt.update(measurements, imp, roi); + } + } + ImageProcessor ip = imp.getProcessor(); + ip.setRoi(roi.getPolygon()); + ImageStatistics stats = new ImageStatistics(); + saveResults(stats, roi); + } + + void measureLength(Roi roi) { + ImagePlus imp2 = isRedirectImage()?getRedirectImageOrStack(imp):null; + if (imp2!=null) + imp2.setRoi(roi); + else + imp2 = imp; + if (rt.size()>0) { + if (!IJ.isResultsWindow()) reset(); + boolean update = false; + int index = rt.getColumnIndex("Length"); + if (index<0 || !rt.columnExists(index)) + update=true; + if (roi.getType()==Roi.LINE) { + index = rt.getColumnIndex("Angle"); + if (index<0 || !rt.columnExists(index)) update=true; + } + if (update) { + clearSummary(); + rt.update(measurements, imp2, roi); + } + } + if ((measurements&(AREA+MEAN+STD_DEV+MODE+MIN_MAX+CENTROID+MEDIAN))==0) { + incrementCounter(); + rt.addValue("Length", roi.getLength()); + if (roi.getType()==Roi.LINE && showAngle) { + Line line = (Line)roi; + rt.addValue("Angle", line.getFloatAngle(line.x1d,line.y1d,line.x2d,line.y2d)); + } + if ((measurements&LABELS)!=0) + rt.addLabel("Label", getFileName()); + return; + } + boolean straightLine = roi.getType()==Roi.LINE; + int lineWidth = (int)Math.round(roi.getStrokeWidth()); + ImageProcessor ip2 = imp2.getProcessor(); + double minThreshold = ip2.getMinThreshold(); + double maxThreshold = ip2.getMaxThreshold(); + int limit = (Analyzer.getMeasurements()&LIMIT)!=0?LIMIT:0; + boolean calibrated = imp2.getCalibration().calibrated(); + Rectangle saveR = null; + Calibration globalCal = calibrated?imp2.getGlobalCalibration():null; + Calibration localCal = null; + if (globalCal!=null) { + imp2.setGlobalCalibration(null); + localCal = imp2.getCalibration().copy(); + imp2.setCalibration(globalCal); + } if (lineWidth>1) { + saveR = ip2.getRoi(); + ip2.setRoi(Roi.convertLineToArea(roi)); + } else if (calibrated && limit!=0) { + Calibration cal = imp2.getCalibration().copy(); + imp2.getCalibration().disableDensityCalibration(); + ProfilePlot profile = new ProfilePlot(imp2); + imp2.setCalibration(cal); + double[] values = profile.getProfile(); + if (values==null) return; + ip2 = new FloatProcessor(values.length, 1, values); + ip2 = convertToOriginalDepth(imp2, ip2); + ip2.setCalibrationTable(cal.getCTable()); + } else { + ProfilePlot profile = new ProfilePlot(imp2); + double[] values = profile.getProfile(); + if (values==null) return; + ip2 = new FloatProcessor(values.length, 1, values); + if (limit!=0) + ip2 = convertToOriginalDepth(imp2, ip2); + } + if (limit!=0 && minThreshold!=ImageProcessor.NO_THRESHOLD) + ip2.setThreshold(minThreshold,maxThreshold,ImageProcessor.NO_LUT_UPDATE); + ImageStatistics stats = ImageStatistics.getStatistics(ip2, AREA+MEAN+STD_DEV+MODE+MIN_MAX+MEDIAN+limit, imp2.getCalibration()); + if (saveR!=null) + ip2.setRoi(saveR); + if ((roi instanceof Line) && (measurements&CENTROID)!=0) { + FloatPolygon p = ((Line)roi).getFloatPoints(); + stats.xCentroid = p.xpoints[0] + (p.xpoints[1]-p.xpoints[0])/2.0; + stats.yCentroid = p.ypoints[0] + (p.ypoints[1]-p.ypoints[0])/2.0; + if (imp2!=null) { + Calibration cal = imp.getCalibration(); + stats.xCentroid = cal.getX(stats.xCentroid); + stats.yCentroid = cal.getY(stats.yCentroid, imp2.getHeight()); + } + } + saveResults(stats, roi); + if (globalCal!=null && localCal!=null) { + imp2.setGlobalCalibration(globalCal); + imp2.setCalibration(localCal); + } + } + + private ImageProcessor convertToOriginalDepth(ImagePlus imp, ImageProcessor ip) { + if (imp.getBitDepth()==8) + ip = ip.convertToByte(false); + else if (imp.getBitDepth()==16) + ip = ip.convertToShort(false); + return ip; + } + + + /** Saves the measurements specified in the "Set Measurements" dialog, + or by calling setMeasurements(), in the default results table. + */ + public void saveResults(ImageStatistics stats, Roi roi) { + if (rt.getColumnHeading(ResultsTable.LAST_HEADING)==null) + reset(); + clearSummary(); + incrementCounter(); + int counter = rt.size(); + if (counter<=MAX_STANDARDS && !(stats.umean==0.0&&counter==1&&umeans!=null && umeans[0]!=0f)) { + if (umeans==null) umeans = new float[MAX_STANDARDS]; + umeans[counter-1] = (float)stats.umean; + } + if ((measurements&LABELS)!=0) + rt.addLabel("Label", getFileName()); + if ((measurements&AREA)!=0) rt.addValue(ResultsTable.AREA,stats.area); + if ((measurements&MEAN)!=0) rt.addValue(ResultsTable.MEAN,stats.mean); + if ((measurements&STD_DEV)!=0) rt.addValue(ResultsTable.STD_DEV,stats.stdDev); + if ((measurements&MODE)!=0) rt.addValue(ResultsTable.MODE, stats.dmode); + if ((measurements&MIN_MAX)!=0) { + if (showMin) rt.addValue(ResultsTable.MIN,stats.min); + rt.addValue(ResultsTable.MAX,stats.max); + } + if ((measurements&CENTROID)!=0) { + rt.addValue(ResultsTable.X_CENTROID,stats.xCentroid); + rt.addValue(ResultsTable.Y_CENTROID,stats.yCentroid); + } + if ((measurements&CENTER_OF_MASS)!=0) { + rt.addValue(ResultsTable.X_CENTER_OF_MASS,stats.xCenterOfMass); + rt.addValue(ResultsTable.Y_CENTER_OF_MASS,stats.yCenterOfMass); + } + if ((measurements&PERIMETER)!=0 || (measurements&SHAPE_DESCRIPTORS)!=0) { + double perimeter; + if (roi!=null) + perimeter = roi.getLength(); + else + perimeter = imp!=null?imp.getWidth()*2+imp.getHeight()*2:0.0; + if ((measurements&PERIMETER)!=0) + rt.addValue(ResultsTable.PERIMETER,perimeter); + if ((measurements&SHAPE_DESCRIPTORS)!=0) { + double circularity = perimeter==0.0?0.0:4.0*Math.PI*(stats.area/(perimeter*perimeter)); + if (circularity>1.0) circularity = 1.0; + rt.addValue(ResultsTable.CIRCULARITY, circularity); + Polygon ch = null; + boolean isArea = roi==null || roi.isArea(); + double convexArea = roi!=null?getArea(roi.getConvexHull()):stats.pixelCount; + rt.addValue(ResultsTable.ASPECT_RATIO, isArea?stats.major/stats.minor:0.0); + rt.addValue(ResultsTable.ROUNDNESS, isArea?4.0*stats.area/(Math.PI*stats.major*stats.major):0.0); + rt.addValue(ResultsTable.SOLIDITY, isArea?stats.pixelCount/convexArea:Double.NaN); + if (rt.size()==1) { + rt.setDecimalPlaces(ResultsTable.CIRCULARITY, precision); + rt.setDecimalPlaces(ResultsTable.ASPECT_RATIO, precision); + rt.setDecimalPlaces(ResultsTable.ROUNDNESS, precision); + rt.setDecimalPlaces(ResultsTable.SOLIDITY, precision); + } + //rt.addValue(ResultsTable.CONVEXITY, getConvexPerimeter(roi, ch)/perimeter); + } + } + if ((measurements&RECT)!=0) { + if (roi!=null && roi.isLine()) { + Rectangle bounds = roi.getBounds(); + double rx = bounds.x; + double ry = bounds.y; + double rw = bounds.width; + double rh = bounds.height; + Calibration cal = imp!=null?imp.getCalibration():null; + if (cal!=null) { + rx = cal.getX(rx); + ry = cal.getY(ry, imp.getHeight()); + rw *= cal.pixelWidth; + rh *= cal.pixelHeight; + } + rt.addValue(ResultsTable.ROI_X, rx); + rt.addValue(ResultsTable.ROI_Y, ry); + rt.addValue(ResultsTable.ROI_WIDTH, rw); + rt.addValue(ResultsTable.ROI_HEIGHT, rh); + } else { + rt.addValue(ResultsTable.ROI_X,stats.roiX); + rt.addValue(ResultsTable.ROI_Y,stats.roiY); + rt.addValue(ResultsTable.ROI_WIDTH,stats.roiWidth); + rt.addValue(ResultsTable.ROI_HEIGHT,stats.roiHeight); + } + } + if ((measurements&ELLIPSE)!=0) { + rt.addValue(ResultsTable.MAJOR,stats.major); + rt.addValue(ResultsTable.MINOR,stats.minor); + rt.addValue(ResultsTable.ANGLE,stats.angle); + } + if ((measurements&FERET)!=0) { + boolean extras = true; + double FeretDiameter=Double.NaN, feretAngle=Double.NaN, minFeret=Double.NaN, + feretX=Double.NaN, feretY=Double.NaN; + Roi roi2 = roi; + if (roi2==null && imp!=null) + roi2 = new Roi(0, 0, imp.getWidth(), imp.getHeight()); + if (roi2!=null) { + double[] a = roi2.getFeretValues(); + if (a!=null) { + FeretDiameter = a[0]; + feretAngle = a[1]; + minFeret = a[2]; + feretX = a[3]; + feretY = a[4]; + } + } + rt.addValue(ResultsTable.FERET, FeretDiameter); + rt.addValue(ResultsTable.FERET_X, feretX); + rt.addValue(ResultsTable.FERET_Y, feretY); + rt.addValue(ResultsTable.FERET_ANGLE, feretAngle); + rt.addValue(ResultsTable.MIN_FERET, minFeret); + } + if ((measurements&INTEGRATED_DENSITY)!=0) { + rt.addValue(ResultsTable.INTEGRATED_DENSITY,stats.area*stats.mean); + rt.addValue(ResultsTable.RAW_INTEGRATED_DENSITY,stats.pixelCount*stats.umean); + } + if ((measurements&MEDIAN)!=0) rt.addValue(ResultsTable.MEDIAN, stats.median); + if ((measurements&SKEWNESS)!=0) rt.addValue(ResultsTable.SKEWNESS, stats.skewness); + if ((measurements&KURTOSIS)!=0) rt.addValue(ResultsTable.KURTOSIS, stats.kurtosis); + if ((measurements&AREA_FRACTION)!=0) rt.addValue(ResultsTable.AREA_FRACTION, stats.areaFraction); + if ((measurements&STACK_POSITION)!=0) { + boolean update = false; + if (imp!=null && (imp.isHyperStack()||imp.isComposite())) { + int[] position = imp.convertIndexToPosition(imp.getCurrentSlice()); + if (imp.getNChannels()>1) { + int index = rt.getColumnIndex("Ch"); + if (index<0 || !rt.columnExists(index)) update=true; + rt.addValue("Ch", position[0]); + } + if (imp.getNSlices()>1) { + int index = rt.getColumnIndex("Slice"); + if (index<0 || !rt.columnExists(index)) update=true; + rt.addValue("Slice", position[1]); + } + if (imp.getNFrames()>1) { + int index = rt.getColumnIndex("Frame"); + if (index<0 || !rt.columnExists(index)) update=true; + rt.addValue("Frame", position[2]); + } + } else { + int index = rt.getColumnIndex("Slice"); + if (index<0 || !rt.columnExists(index)) update=true; + rt.addValue("Slice", imp!=null?imp.getCurrentSlice():1.0); + } + if (update && rt==systemRT && IJ.isResultsWindow()) + rt.update(measurements, imp, roi); + } + if (roi!=null) { + if (roi.isLine()) { + rt.addValue("Length", roi.getLength()); + if (roi.getType()==Roi.LINE && showAngle) { + Line line = (Line)roi; + rt.addValue("Angle", line.getFloatAngle(line.x1d,line.y1d,line.x2d,line.y2d)); + } + } else if (roi.getType()==Roi.ANGLE) { + double angle = ((PolygonRoi)roi).getAngle(); + if (Prefs.reflexAngle) angle = 360.0-angle; + rt.addValue("Angle", angle); + } else if (roi instanceof PointRoi) + savePoints((PointRoi)roi); + } + if ((measurements&LIMIT)!=0 && imp!=null && imp.getBitDepth()!=24) { + rt.addValue(ResultsTable.MIN_THRESHOLD, stats.lowerThreshold); + rt.addValue(ResultsTable.MAX_THRESHOLD, stats.upperThreshold); + } + if (roi instanceof RotatedRectRoi) { + double[] p = ((RotatedRectRoi)roi).getParams(); + double dx = p[2] - p[0]; + double dy = p[3] - p[1]; + double length = Math.sqrt(dx*dx+dy*dy); + Calibration cal = imp!=null?imp.getCalibration():null; + double pw = 1.0; + if (cal!=null && cal.pixelWidth==cal.pixelHeight) + pw = cal.pixelWidth; + rt.addValue("RRLength", length*pw); + rt.addValue("RRWidth", p[4]*pw); + } + int group = roi!=null?roi.getGroup():0; + if (group>0) { + rt.addValue("Group", group); + String name = Roi.getGroupName(group); + if (name!=null) + rt.addValue("GroupName", name); + } + } + + private void clearSummary() { + if (summarized && rt.size()>=4 && "Max".equals(rt.getLabel(rt.size()-1))) { + for (int i=0; i<4; i++) + rt.deleteRow(rt.size()-1); + rt.show("Results"); + summarized = false; + } + } + + final double getArea(Polygon p) { + if (p==null) return Double.NaN; + int carea = 0; + int iminus1; + for (int i=0; i0 && !Toolbar.getMultiPointMode()) { + ip.setColor(Toolbar.getForegroundColor()); + ip.setLineWidth(markWidth); + ip.moveTo(ix,iy); + ip.lineTo(ix,iy); + imp.updateAndDraw(); + ip.setLineWidth(Line.getWidth()); + } + rt.addValue("X", cal.getX(x)); + rt.addValue("Y", cal.getY(y, imp.getHeight())); + int position = roi.getPosition(); + if (imp.isHyperStack() || imp.isComposite()) { + int channel = imp.getChannel(); + int slice = imp.getSlice(); + int frame = imp.getFrame(); + if (position>0) { + int[] pos = imp.convertIndexToPosition(position); + channel = pos[0]; + slice = pos[1]; + frame = pos[2]; + } + if (imp.getNChannels()>1) + rt.addValue("Ch", channel); + if (imp.getNSlices()>1) + rt.addValue("Slice", slice); + if (imp.getNFrames()>1) + rt.addValue("Frame", frame); + } else if (imp.getStackSize()>1) { + if (position==0) + position = imp.getCurrentSlice(); + rt.addValue("Slice", position); + } + int[] info = roi.getCounterInfo(); + if (info!=null) { + rt.addValue("Counter", info[0]); + rt.addValue("Count", info[1]); + } + if (imp.getProperty("FHT")!=null) { + double center = imp.getWidth()/2.0; + y = imp.getHeight()-y-1; + double r = Math.sqrt((x-center)*(x-center) + (y-center)*(y-center)); + if (r<1.0) r = 1.0; + double theta = Math.atan2(y-center, x-center); + theta = theta*180.0/Math.PI; + if (theta<0) theta = 360.0+theta; + rt.addValue("R", (imp.getWidth()/r)*cal.pixelWidth); + rt.addValue("Theta", theta); + } + } + + String getFileName() { + String s = ""; + if (imp!=null) { + if (redirectTarget!=0) { + ImagePlus rImp = WindowManager.getImage(redirectTarget); + if (rImp==null) rImp = redirectImage; + if (rImp!=null) s = rImp.getTitle(); + } else + s = imp.getTitle(); + //int len = s.length(); + //if (len>4 && s.charAt(len-4)=='.' && !Character.isDigit(s.charAt(len-1))) + // s = s.substring(0,len-4); + Roi roi = imp.getRoi(); + String roiName = roi!=null?roi.getName():null; + if (roiName!=null && !roiName.contains(".")) { + if (roiName.length()>30) + roiName = roiName.substring(0,27) + "..."; + s += ":"+roiName; + } + if (imp.getStackSize()>1) { + ImageStack stack = imp.getStack(); + int currentSlice = imp.getCurrentSlice(); + String label = stack.getShortSliceLabel(currentSlice); + String colon = s.equals("")?"":":"; + if (label!=null && !label.equals("")) + s += colon+label; + else + s += colon+currentSlice; + } + } + return s; + } + + /** Writes the last row in the system results table to the Results window. */ + public void displayResults() { + if (rt.columnDeleted()) + return; + int counter = rt.size(); + if (counter==1) + IJ.setColumnHeadings(rt.getColumnHeadings()); + TextPanel tp = IJ.isResultsWindow()?IJ.getTextPanel():null; + int lineCount = tp!=null?IJ.getTextPanel().getLineCount():0; + if (counter>lineCount+1) { // delete rt rows added by particle analyzer + int n = counter - lineCount - 1; + int index = lineCount; + for (int i=0; i1 && rt.getColumnIndex("Group")>=0 && rt.getValue("Group",counter-1)>0) { + rt.show("Results"); + resultsUpdated = true; + } else + IJ.write(rt.getRowAsString(counter-1)); + } + + /** Redisplays the results table. */ + public void updateHeadings() { + rt.show("Results"); + } + + /** Converts a number to a formatted string with a tab at the end. */ + public String n(double n) { + String s; + if (Math.round(n)==n) + s = ResultsTable.d2s(n,0); + else + s = ResultsTable.d2s(n,precision); + return s+"\t"; + } + + void incrementCounter() { + if (rt==null) rt = systemRT; + rt.incrementCounter(); + unsavedMeasurements = true; + } + + public void summarize() { + if (summarized) + return; + int n = rt.size(); + if (n<2) + return; + String[] headings = rt.getHeadings(); + int columns = headings.length; + if (columns==0) + return; + int first = "Label".equals(headings[0])?1:0; + double[] min = new double[columns]; + double[] max = new double[columns]; + double[] sum = new double[columns]; + double[] sum2 = new double[columns]; + for (int i=0; imax[col]) max[col]=v; + sum[col]+=v; + sum2[col]+=v*v; + } + } + rt.incrementCounter(); rt.setLabel("Mean", n+0); + rt.incrementCounter(); rt.setLabel("SD", n+1); + rt.incrementCounter(); rt.setLabel("Min", n+2); + rt.incrementCounter(); rt.setLabel("Max", n+3); + for (int col=first; col0 && lineCount>0 && unsavedMeasurements && !macro && ij!=null && !ij.quitting()) { + YesNoCancelDialog d = new YesNoCancelDialog(ij, "ImageJ", "Save "+counter+" measurements?"); + if (d.cancelPressed()) + return false; + else if (d.yesPressed()) { + if (!(new MeasurementsWriter()).save("")) + return false; + } + } + umeans = null; + systemRT.reset(); + RoiManager.resetMultiMeasureResults(); + unsavedMeasurements = false; + if (tp!=null) tp.clear(); + summarized = false; + return true; + } + + public static void setUnsavedMeasurements(boolean b) { + unsavedMeasurements = b; + } + + // Returns the measurement options defined in the Set Measurements dialog. */ + public static int getMeasurements() { + return systemMeasurements; + } + + /** Sets the system-wide measurement options. */ + public static void setMeasurements(int measurements) { + systemMeasurements = measurements; + } + + /** Sets the specified system-wide measurement option. */ + public static void setMeasurement(int option, boolean state) { + if (state) { + systemMeasurements |= option; + if ((option&ADD_TO_OVERLAY)!=0) + drawLabels = true; + } else + systemMeasurements &= ~option; + } + + /** Called once when ImageJ quits. */ + public static void savePreferences(Properties prefs) { + prefs.put(MEASUREMENTS, Integer.toString(systemMeasurements)); + //prefs.put(MARK_WIDTH, Integer.toString(markWidth)); + prefs.put(PRECISION, Integer.toString(precision)); } + + /** Returns an array containing the first 20 uncalibrated means. */ + public static float[] getUMeans() { + return umeans; + } + + /** Returns the default results table. This table should only + be displayed in a the "Results" window. */ + public static ResultsTable getResultsTable() { + systemRT.showRowNumbers(true); + return systemRT; + } + + /** Returns the number of digits displayed to the right of decimal point. */ + public static int getPrecision() { + return precision; + } + + /** Sets the number of digits displayed to the right of decimal point. */ + public static void setPrecision(int decimalPlaces) { + if (decimalPlaces<0) decimalPlaces = 0; + if (decimalPlaces>9) decimalPlaces = 9; + precision = decimalPlaces; + } + + /** Returns an updated Y coordinate based on + the current "Invert Y Coordinates" flag. */ + public static int updateY(int y, int imageHeight) { + if ((systemMeasurements&INVERT_Y)!=0) + y = imageHeight-y-1; + return y; + } + + /** Returns an updated Y coordinate based on + the current "Invert Y Coordinates" flag. */ + public static double updateY(double y, int imageHeight) { + if ((systemMeasurements&INVERT_Y)!=0) + y = imageHeight-y-1; + return y; + } + + /** Sets the default headings ("Area", "Mean", etc.). */ + public static void setDefaultHeadings() { + systemRT.setDefaultHeadings(); + } + + public static void setOption(String option, boolean b) { + if (option.contains("min")) + showMin = b; + else if (option.contains("angle")) + showAngle = b; + } + + public static boolean addToOverlay() { + return ((getMeasurements()&ADD_TO_OVERLAY)!=0); + } + + public static void setResultsTable(ResultsTable rt) { + TextPanel tp = IJ.isResultsWindow()?IJ.getTextPanel():null; + if (tp!=null) + tp.clear(); + if (rt==null) + rt = new ResultsTable(); + rt.setPrecision((systemMeasurements&SCIENTIFIC_NOTATION)!=0?-precision:precision); + rt.setNaNEmptyCells((systemMeasurements&NaN_EMPTY_CELLS)!=0); + systemRT = rt; + summarized = false; + umeans = null; + unsavedMeasurements = false; + } + + public static void drawLabels(boolean b) { + drawLabels = b; + } + + /** Used by RoiManager.multiMeasure() to suppress save as dialogs. */ + public void disableReset(boolean b) { + disableReset = b; + } + +} + diff --git a/src/ij/plugin/filter/BackgroundSubtracter.java b/src/ij/plugin/filter/BackgroundSubtracter.java new file mode 100644 index 0000000..13b925a --- /dev/null +++ b/src/ij/plugin/filter/BackgroundSubtracter.java @@ -0,0 +1,829 @@ +package ij.plugin.filter; +import ij.*; +import ij.gui.*; +import ij.process.*; +import ij.measure.*; +import ij.util.Tools; +import java.awt.*; + + +/** Implements ImageJ's Subtract Background command. Based on the concept of the +rolling ball algorithm described in Stanley Sternberg's article, "Biomedical Image +Processing", IEEE Computer, January 1983. + +Imagine that the 2D grayscale image has a third (height) dimension by the image +value at every point in the image, creating a surface. A ball of given radius is +rolled over the bottom side of this surface; the hull of the volume reachable by +the ball is the background. + +With "Sliding Parabvoloid", the rolling ball is replaced by a sliding paraboloid +of rotation with the same curvature at its apex as a ball of a given radius. +A paraboloid has the advantage that suitable paraboloids can be found for any image +values, even if the pixel values are much larger than a typical object size (in pixels). +The paraboloid of rotation is approximated as parabolae in 4 directions: x, y and +the two 45-degree directions. Lines of the image in these directions are processed +by sliding a parabola against them. Obtaining the hull needs the parabola for a +given direction to be applied multiple times (after doing the other directions); +in this respect the current code is a compromise between accuracy and speed. + +For noise rejection, with the sliding paraboloid algorithm, a 3x3 maximum of the +background is applied. With both, rolling ball and sliding paraboloid, +the image used for calculating the background is slightly smoothened (3x3 average). +This can result in negative values after background subtraction. This preprocessing +can be disabled. + +In the sliding paraboloid algorithm, additional code has been added to avoid +subtracting corner objects as a background (note that a paraboloid or ball would +always touch the 4 corner pixels and thus make them background pixels). +This code assumes that corner particles reach less than 1/4 of the image size +into the image. + +Rolling ball code based on the NIH Image Pascal version by Michael Castle and Janice +Keller of the University of Michigan Mental Health Research Institute. +Sliding Paraboloid by Michael Schmid, 2007. + +Version 10-Jan-2008 +*/ +public class BackgroundSubtracter implements ExtendedPlugInFilter, DialogListener { + /* parameters from the dialog: */ + private static double staticRadius = 50; // default rolling ball radius + private static boolean staticLightBackground = Prefs.get("bs.background", true); + private static boolean staticSeparateColors; // whether to create a separate background for each color channel + private static boolean staticCreateBackground; // don't subtract background (e.g., for processing the background before subtracting) + private static boolean staticUseParaboloid; // use "Sliding Paraboloid" instead of rolling ball algorithm + private static boolean staticDoPresmooth = true; // smoothen the image before creating the background + private double radius = staticRadius; + private boolean lightBackground = staticLightBackground; + private boolean separateColors = staticSeparateColors; + private boolean createBackground = staticCreateBackground; + private boolean useParaboloid = staticUseParaboloid; + private boolean doPresmooth = staticDoPresmooth; + /* more class variables */ + private boolean isRGB; // whether we have an RGB image + private boolean previewing; + private final static int MAXIMUM = 0, MEAN = 1; //filter types of filter3x3 + private final static int X_DIRECTION = 0, Y_DIRECTION = 1, + DIAGONAL_1A = 2, DIAGONAL_1B = 3, DIAGONAL_2A = 4, DIAGONAL_2B = 5; //filter directions + private final static int DIRECTION_PASSES = 9; //number of passes for different directions + private int nPasses = DIRECTION_PASSES; + private int pass; + private int flags = DOES_ALL|FINAL_PROCESSING|KEEP_PREVIEW|PARALLELIZE_STACKS; + private boolean calledAsPlugin; + + + public int setup(String arg, ImagePlus imp) { + if (arg.equals("final")) { + imp.getProcessor().resetMinAndMax(); + return DONE; + } else + return flags; + } + + public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) { + isRGB = imp.getProcessor() instanceof ColorProcessor; + calledAsPlugin = true; + String options = Macro.getOptions(); + if (options!=null) { //macro + Macro.setOptions(options.replaceAll("white", "light")); + radius = 50; + lightBackground = false; + separateColors = false; + createBackground = false; + useParaboloid = false; + doPresmooth = true; + } + GenericDialog gd = new GenericDialog(command); + gd.addNumericField("Rolling ball radius:", radius, 1, 6, "pixels"); + gd.addCheckbox("Light background", lightBackground); + if (isRGB) gd.addCheckbox("Separate colors", separateColors); + gd.addCheckbox("Create background (don't subtract)", createBackground); + gd.addCheckbox("Sliding paraboloid", useParaboloid); + gd.addCheckbox("Disable smoothing", !doPresmooth); + gd.addPreviewCheckbox(pfr); + gd.addDialogListener(this); + previewing = true; + gd.addHelp(IJ.URL+"/docs/menus/process.html#background"); + gd.showDialog(); + previewing = false; + if (gd.wasCanceled()) return DONE; + IJ.register(this.getClass()); //protect static class variables (filter parameters) from garbage collection + if ((imp.getProcessor() instanceof FloatProcessor) && !createBackground) + flags |= SNAPSHOT; //FloatProcessors need the original to subtract it from the background + if (options==null) { // not a macro + staticRadius = radius; + staticLightBackground = lightBackground; + staticSeparateColors = separateColors; + staticCreateBackground = createBackground; + staticUseParaboloid = useParaboloid; + staticDoPresmooth = doPresmooth; + Prefs.set("bs.background", lightBackground); + } + return IJ.setupDialog(imp, flags); //ask whether to process all slices of stack (if a stack) + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + radius = gd.getNextNumber(); + if (radius <= 0.0001 || gd.invalidNumber()) + return false; + lightBackground = gd.getNextBoolean(); + if (isRGB) separateColors = gd.getNextBoolean(); + createBackground = gd.getNextBoolean(); + useParaboloid = gd.getNextBoolean(); + doPresmooth = !gd.getNextBoolean(); + return true; + } + + /** Background for any image type */ + public void run(ImageProcessor ip) { + if (isRGB && !separateColors) + rollingBallBrightnessBackground((ColorProcessor)ip, radius, createBackground, lightBackground, useParaboloid, doPresmooth, true); + else + rollingBallBackground(ip, radius, createBackground, lightBackground, useParaboloid, doPresmooth, true); + if (previewing && (ip instanceof FloatProcessor || ip instanceof ShortProcessor)) { + ip.resetMinAndMax(); + } + } + + /** Depracated. For compatibility with previous ImageJ versions */ + public void subtractRGBBackround(ColorProcessor ip, int ballRadius) { + rollingBallBrightnessBackground(ip, (double)ballRadius, false, lightBackground, false, true, true); + } + /** Depracated. For compatibility with previous ImageJ versions */ + public void subtractBackround(ImageProcessor ip, int ballRadius) { + rollingBallBackground(ip, (double)ballRadius, false, lightBackground, false, true, true); + } + + /** Create or subtract a background, based on the brightness of an RGB image (keeping + * the hue of each pixel unchanged) + * @param ip The RGB image. On output, it will become the background-subtracted image or + * the background (depending on createBackground). + * @param radius Radius of the rolling ball creating the background (actually a + * paraboloid of rotation with the same curvature) + * @param createBackground Whether to create a background, not to subtract it. + * @param lightBackground Whether the image has a light background. + * @param doPresmooth Whether the image should be smoothened (3x3 mean) before creating + * the background. With smoothing, the background will not necessarily + * be below the image data. + * @param correctCorners Whether the algorithm should try to detect corner particles to avoid + * subtracting them as a background. + */ + public void rollingBallBrightnessBackground(ColorProcessor ip, double radius, boolean createBackground, + boolean lightBackground, boolean useParaboloid, boolean doPresmooth, boolean correctCorners) { + int width = ip.getWidth(); + int height = ip.getHeight(); + byte[] H = new byte[width*height]; + byte[] S = new byte[width*height]; + byte[] B = new byte[width*height]; + ip.getHSB(H, S, B); + ByteProcessor bp = new ByteProcessor(width, height, B, null); + rollingBallBackground(bp, radius, createBackground, lightBackground, useParaboloid, doPresmooth, correctCorners); + ip.setHSB(H, S, (byte[])bp.getPixels()); + } + + /** Create or subtract a background, works for all image types. For RGB images, the + * background is subtracted from each channel separately + * @param ip The image. On output, it will become the background-subtracted image or + * the background (depending on createBackground). + * @param radius Radius of the rolling ball creating the background (actually a + * paraboloid of rotation with the same curvature) + * @param createBackground Whether to create a background, not to subtract it. + * @param lightBackground Whether the image has a light background. + * @param useParaboloid Whether to use the "sliding paraboloid" algorithm. + * @param doPresmooth Whether the image should be smoothened (3x3 mean) before creating + * the background. With smoothing, the background will not necessarily + * be below the image data. + * @param correctCorners Whether the algorithm should try to detect corner particles to avoid + * subtracting them as a background. + */ + public void rollingBallBackground(ImageProcessor ip, double radius, boolean createBackground, + boolean lightBackground, boolean useParaboloid, boolean doPresmooth, boolean correctCorners) { + boolean invertedLut = ip.isInvertedLut(); + boolean invert = (invertedLut && !lightBackground) || (!invertedLut && lightBackground); + RollingBall ball = null; + if (!useParaboloid) ball = new RollingBall(radius); + FloatProcessor fp = null; + for (int channelNumber=0; channelNumber65535f) value = 65535f; + + pixels[p] = (short)(value); + } + } else if (ip instanceof ByteProcessor) { + float offset = invert ? 255.5f : 0.5f; //includes 0.5 for rounding when converting float to byte + byte[] pixels = (byte[])ip.getPixels(); + for (int p=0; p255f) value = 255f; + + pixels[p] = (byte)(value); + } + } else if (ip instanceof ColorProcessor) { + float offset = invert ? 255.5f : 0.5f; + int[] pixels = (int[])ip.getPixels(); + int shift = 16 - 8*channelNumber; + + int byteMask = 255<>shift) - bgPixels[p] + offset; + if (value<0f) value = 0f; + + if (value>255f) value = 255f; + pixels[p] = (pxl&resetMask) | ((int)value<length. Will usually remain + * in the CPU cache and may therefore speed up the code. + * @param nextPoint Work array. Will hold the index of the next point with sufficient local + * curvature to get touched by the parabola. + * @param correctedEdges Should be a 2-element array used for output or null. + * @return The correctedEdges array (if non-null on input) with the two estimated + * edge pixel values corrected for edge particles. + */ + static float[] lineSlideParabola(float[] pixels, int start, int inc, int length, float coeff2, float[] cache, int[] nextPoint, float[] correctedEdges) { + float minValue = Float.MAX_VALUE; + int lastpoint = 0; + int firstCorner = length-1; // the first point except the edge that is touched + int lastCorner = 0; // the last point except the edge that is touched + float vPrevious1 = 0f; + float vPrevious2 = 0f; + float curvatureTest = 1.999f*coeff2; //not 2: numeric scatter of 2nd derivative + /* copy data to cache, determine the minimum, and find points with local curvature such + * that the parabola can touch them - only these need to be examined futher on */ + for (int i=0, p=start; i= 2 && vPrevious1+vPrevious1-vPrevious2-v < curvatureTest) { + nextPoint[lastpoint] = i-1; // point i-1 may be touched + lastpoint = i-1; + } + vPrevious2 = vPrevious1; + vPrevious1 = v; + } + nextPoint[lastpoint] = length-1; + nextPoint[length-1] = Integer.MAX_VALUE;// breaks the search loop + + int i1 = 0; // i1 and i2 will be the two points where the parabola touches + while (i1 0) searchTo = maxSearch; + } + } + if (i1 == 0) firstCorner = i2; + if (i2 == length-1) lastCorner = i1; + /* interpolate between the two points where the parabola touches: */ + for (int j=i1+1, p=start+j*inc; j= length) firstCorner = 0; // edge particles must be < 1/4 image size + if (4*(length - 1 - lastCorner) >= length) lastCorner = length - 1; + float v1 = cache[firstCorner]; + float v2 = cache[lastCorner]; + float slope = (v2-v1)/(lastCorner-firstCorner); // of the line through the two outermost non-edge touching points + float value0 = v1 - slope * firstCorner; // offset of this line + float coeff6 = 0; // coefficient of 6th order polynomial + float mid = 0.5f * (lastCorner + firstCorner); + for (int i=(length+2)/3; i<=(2*length)/3; i++) {// compare with mid-image pixels to detect vignetting + float dx = (i-mid)*2f/(lastCorner-firstCorner); + float poly6 = dx*dx*dx*dx*dx*dx - 1f; // the 6th order polynomial, zero at firstCorner and lastCorner + if (cache[i] < value0 + slope*i + coeff6*poly6) { + coeff6 = -(value0 + slope*i - cache[i])/poly6; + } + } + float dx = (firstCorner-mid)*2f/(lastCorner-firstCorner); + correctedEdges[0] = value0 + coeff6*(dx*dx*dx*dx*dx*dx - 1f) + coeff2*firstCorner*firstCorner; + dx = (lastCorner-mid)*2f/(lastCorner-firstCorner); + correctedEdges[1] = value0 + (length-1)*slope + coeff6*(dx*dx*dx*dx*dx*dx - 1f) + coeff2*(length-1-lastCorner)*(length-1-lastCorner); + } + return correctedEdges; + } //void lineSlideParabola + + /** Detect corner particles and adjust corner pixels if a particle is there. + * Analyzing the directions parallel to the edges and the diagonals, we + * average over the 3 correction values (found for the 3 directions) + */ + void correctCorners(FloatProcessor fp, float coeff2, float[] cache, int[] nextPoint) { + int width = fp.getWidth(); + int height = fp.getHeight(); + float[] pixels = (float[])fp.getPixels(); + float[] corners = new float[4]; //(0,0); (xmax,0); (ymax,0); (xmax,ymax) + float[] correctedEdges = new float[2]; + correctedEdges = lineSlideParabola(pixels, 0, 1, width, coeff2, cache, nextPoint, correctedEdges); + corners[0] = correctedEdges[0]; + corners[1] = correctedEdges[1]; + correctedEdges = lineSlideParabola(pixels, (height-1)*width, 1, width, coeff2, cache, nextPoint, correctedEdges); + corners[2] = correctedEdges[0]; + corners[3] = correctedEdges[1]; + correctedEdges = lineSlideParabola(pixels, 0, width, height, coeff2, cache, nextPoint, correctedEdges); + corners[0] += correctedEdges[0]; + corners[2] += correctedEdges[1]; + correctedEdges = lineSlideParabola(pixels, width-1, width, height, coeff2, cache, nextPoint, correctedEdges); + corners[1] += correctedEdges[0]; + corners[3] += correctedEdges[1]; + int diagLength = Math.min(width,height); //length of a 45-degree line from a corner + float coeff2diag = 2 * coeff2; + correctedEdges = lineSlideParabola(pixels, 0, 1+width, diagLength, coeff2diag, cache, nextPoint, correctedEdges); + corners[0] += correctedEdges[0]; + correctedEdges = lineSlideParabola(pixels, width-1, -1+width, diagLength, coeff2diag, cache, nextPoint, correctedEdges); + corners[1] += correctedEdges[0]; + correctedEdges = lineSlideParabola(pixels, (height-1)*width, 1-width, diagLength, coeff2diag, cache, nextPoint, correctedEdges); + corners[2] += correctedEdges[0]; + correctedEdges = lineSlideParabola(pixels, width*height-1, -1-width, diagLength, coeff2diag, cache, nextPoint, correctedEdges); + corners[3] += correctedEdges[0]; + if (pixels[0] > corners[0]/3) pixels[0] = corners[0]/3; + if (pixels[width-1] > corners[1]/3) pixels[width-1] = corners[1]/3; + if (pixels[(height-1)*width] > corners[2]/3) pixels[(height-1)*width] = corners[2]/3; + if (pixels[width*height-1] > corners[3]/3) pixels[width*height-1] = corners[3]/3; + //new ImagePlus("corner corrected",fp.duplicate()).show(); + } //void correctCorners + + // R O L L B A L L S E C T I O N + + /** Create background for a float image by rolling a ball over + * the image. */ + void rollingBallFloatBackground(FloatProcessor fp, float radius, boolean invert, + boolean doPresmooth, RollingBall ball) { + float[] pixels = (float[])fp.getPixels(); //this will become the background + boolean shrink = ball.shrinkFactor >1; + + showProgress(0.0); + if (invert) + for (int i=0; i 100) { + lastTime = time; + if (thread.isInterrupted()) return; + showProgress(0.1+0.8*y/(height+ballWidth)); + } + int nextLineToWriteInCache = (y+radius)%ballWidth; + int nextLineToRead = y + radius; //line of the input not touched yet + if (nextLineToRead=height) yend = height-1; + for (int x=-radius; x=width) xend = width-1; + for (int yp=y0, yBall=yBall0; yp<=yend; yp++,yBall++) { //for all points inside the ball + int cachePointer = (yp%ballWidth)*width+x0; + for (int xp=x0, bp=xBall0+yBall*ballWidth; xp<=xend; xp++, cachePointer++, bp++) { + float zReduced = cache[cachePointer] - zBall[bp]; + if (z > zReduced) //does this point imply a greater height? + z = zReduced; + } + } + for (int yp=y0, yBall=yBall0; yp<=yend; yp++,yBall++) //raise pixels to ball surface + for (int xp=x0, p=xp+yp*width, bp=xBall0+yBall*ballWidth; xp<=xend; xp++, p++, bp++) { + float zMin = z + zBall[bp]; + if (pixels[p] < zMin) + pixels[p] = zMin; + } + // if (x>=0&&y>=0&&x line0 + line0 = line1; + line1 = swap; //keep the other array for filling with new data + ySmallLine0++; + int sYPointer = (ySmallIndices[y]+1)*smallWidth; //points to line0 + 1 in smallImage + for (int x=0; x line1 + line1[x] = sPixels[sYPointer+xSmallIndices[x]] * xWeights[x] + + sPixels[sYPointer+xSmallIndices[x]+1] * (1f - xWeights[x]); + } + float weight = yWeights[y]; + for (int x=0, p=y*width; x + Example for shrinkFactor = 4: + small image pixel number | 0 | 1 | 2 | ... + full image pixel number | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |10 |11 | ... + smallIndex for interpolation(0) | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 2 | 2 | ... + (0) Note: This is smallIndex for the left pixel; for the right pixel used for interpolation + it is higher by one +

+ Returns "null" if the Roi Manager is not open or index is + out of range. + */ + public static String getName(String index) { + int i = (int)Tools.parseDouble(index, -1); + RoiManager instance = getInstance2(); + if (instance!=null && i>=0 && icmd + is not one of these strings. */ + public boolean runCommand(String cmd) { + cmd = cmd.toLowerCase(); + macro = true; + boolean ok = true; + if (cmd.equals("add")) { + boolean shift = IJ.shiftKeyDown(); + boolean alt = IJ.altKeyDown(); + if (Interpreter.isBatchMode()) { + shift = false; + alt = false; + } + add(shift, alt); + if (IJ.isJava18()&&IJ.isMacOSX()) + repaint(); + } else if (cmd.equals("add & draw")) + addAndDraw(false); + else if (cmd.equals("update")) + update(true); + else if (cmd.equals("update2")) + update(false); + else if (cmd.equals("delete")) + delete(false); + else if (cmd.equals("measure")) { + if (EventQueue.isDispatchThread()) + measure(getImage()); + else try { + // run on event dispatching thread for greater speed on Windows + final ImagePlus imp = getImage(); + EventQueue.invokeAndWait(new Runnable() { + public void run() { + measure(imp); + } + }); + } catch (Exception e) {} + } else if (cmd.equals("draw")) + drawOrFill(DRAW); + else if (cmd.equals("fill")) + drawOrFill(FILL); + else if (cmd.equals("label")) + drawOrFill(LABEL); + else if (cmd.equals("and")) + and(); + else if (cmd.equals("or") || cmd.equals("combine")) + combine(); + else if (cmd.equals("xor")) + xor(); + else if (cmd.equals("split")) + split(); + else if (cmd.equals("sort")) + sort(); + else if (cmd.startsWith("multi measure") || cmd.startsWith("multi-measure")) + multiMeasure(cmd); + else if (cmd.equals("multi plot")) + multiPlot(); + else if (cmd.equals("show all")) { + if (WindowManager.getCurrentImage()!=null) { + showAll(SHOW_ALL); + showAllCheckbox.setState(true); + } + } else if (cmd.equals("show none")) { + if (WindowManager.getCurrentImage()!=null) { + showAll(SHOW_NONE); + showAllCheckbox.setState(false); + } + } else if (cmd.equals("show all with labels")) { + labelsCheckbox.setState(true); + showAll(LABELS); + showAllCheckbox.setState(true); + if (Interpreter.isBatchMode()) IJ.wait(250); + } else if (cmd.equals("show all without labels")) { + showAllCheckbox.setState(true); + labelsCheckbox.setState(false); + showAll(NO_LABELS); + if (Interpreter.isBatchMode()) IJ.wait(250); + } else if (cmd.equals("deselect")||cmd.indexOf("all")!=-1) { + if (IJ.isMacOSX()) ignoreInterrupts = true; + deselect(); + IJ.wait(50); + } else if (cmd.equals("reset")) { + reset(); + } else if (cmd.equals("debug")) { + //IJ.log("Debug: "+debugCount); + //for (int i=0; icmd is not "Open", "Save" or "Rename", or if an error occurs. */ + public boolean runCommand(String cmd, String name) { + cmd = cmd.toLowerCase(); + macro = true; + if (cmd.equals("open")) { + boolean ok = open(Opener.makeFullPath(name)); + macro = false; + return ok; + } else if (cmd.equals("save")) { + boolean ok = false; + if (name!=null && name.endsWith(".roi")) + ok = saveOne(getIndexes(), name); + else + ok = save(name, false); + return ok; + } else if (cmd.equals("save selected")) { + if (name!=null && name.endsWith(".roi")) + return saveOne(getIndexes(), name); + else + return save(name, true); + } else if (cmd.equals("rename")) { + rename(name); + macro = false; + return true; + } else if (cmd.equals("set color")) { + Color color = Colors.decode(name, Color.cyan); + setProperties(color, -1, null); + macro = false; + return true; + } else if (cmd.equals("set fill color")) { + Color fillColor = Colors.decode(name, Color.cyan); + setProperties(null, -1, fillColor); + macro = false; + return true; + } else if (cmd.equals("set line width")) { + int lineWidth = (int)Tools.parseDouble(name, 0); + if (lineWidth>=0) + setProperties(null, lineWidth, null); + macro = false; + return true; + } else if (cmd.equals("associate")) { + Prefs.showAllSliceOnly = name.equals("true")?true:false; + macro = false; + return true; + } else if (cmd.equals("centered")) { + restoreCentered = name.equals("true")?true:false; + macro = false; + return true; + } else if (cmd.equals("usenames")) { + Prefs.useNamesAsLabels = name.equals("true")?true:false; + macro = false; + if (labelsCheckbox.getState()) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) imp.draw(); + } + return true; + } + return false; + } + + /** Clears this RoiManager so that it contains no ROIs. */ + public void reset() { + if (IJ.isMacOSX() && IJ.isMacro()) + ignoreInterrupts = true; + listModel.removeAllElements(); + overlayTemplate = null; + rois.clear(); + updateShowAll(); + } + + private void translate() { + GenericDialog gd = new GenericDialog("Translate"); + gd.addNumericField("X offset (pixels): ", translateX, 0); + gd.addNumericField("Y offset (pixels): ", translateY, 0); + gd.showDialog(); + if (gd.wasCanceled()) + return; + translateX = gd.getNextNumber(); + translateY = gd.getNextNumber(); + translate(translateX, translateY); + if (record()) { + if (Recorder.scriptMode()) + Recorder.recordCall("rm.translate("+translateX+", "+translateY+");"); + else + Recorder.record("roiManager", "translate", (int)translateX, (int)translateY); + } + } + + /** Moves the selected ROIs or all the ROIs if none are selected. */ + public void translate(double dx, double dy) { + Roi[] rois = getSelectedRoisAsArray(); + for (int i=0; i=n) return; + boolean mm = list.getSelectionMode() == ListSelectionModel.MULTIPLE_INTERVAL_SELECTION; + if (mm) + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + int delay = 1; + long start = System.currentTimeMillis(); + while (true) { + if (list.isSelectedIndex(index)) + break; + list.clearSelection(); + list.setSelectedIndex(index); + } + if (imp==null) + imp = WindowManager.getCurrentImage(); + if (imp!=null) + restore(imp, index, true); + if (mm) + list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } + + public void selectAndMakeVisible(ImagePlus imp, int index) { + select(imp, index); + list.ensureIndexIsVisible(index); + } + + public void select(int index, boolean shiftKeyDown, boolean altKeyDown) { + if (!(shiftKeyDown||altKeyDown)) + select(index); + ImagePlus imp = IJ.getImage(); + if (imp==null) + return; + Roi previousRoi = imp.getRoi(); + if (previousRoi==null) { + select(index); + return; + } + Roi.setPreviousRoi(previousRoi); + Roi roi = (Roi)rois.get(index); + if (roi!=null) { + roi.setImage(imp); + roi.update(shiftKeyDown, altKeyDown); + } + } + + /** Selects all ROIs of a given group. */ + public void selectGroup(int group) { + ArrayListlistSelected = new ArrayList(); + for (int i=0; i0) rm.selectGroup(groupInt); + } + + public void deselect() { + int n = getCount(); + for (int i=0; i0) { + String label = (String)listModel.getElementAt(indexes[0]); + if (label.equals(roi.getName())) { + deselect(); + repaint(); + } + } + } + + public void setEditMode(ImagePlus imp, boolean editMode) { + showAllCheckbox.setState(editMode); + labelsCheckbox.setState(editMode); + showAll(editMode?LABELS:SHOW_NONE); + } + + /** Overrides PlugInFrame.close(). */ + public void close() { + super.close(); + instance = null; + resetMultiMeasureResults(); + Prefs.saveLocation(LOC_KEY, getLocation()); + if (!showAllCheckbox.getState() || IJ.macroRunning()) + return; + int n = getCount(); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null || (imp.getCanvas()!=null && imp.getCanvas().getShowAllList()==null)) + return; + if (n>0) { + GenericDialog gd = new GenericDialog("ROI Manager"); + gd.addMessage("Save the "+n+" displayed ROIs as an overlay?"); + gd.setOKLabel("Discard"); + gd.setCancelLabel("Save as Overlay"); + gd.showDialog(); + if (gd.wasCanceled()) + moveRoisToOverlay(imp); + else + removeOverlay(imp); + } else + imp.draw(); + } + + /** Moves all the ROIs to the specified image's overlay. */ + public void moveRoisToOverlay(ImagePlus imp) { + if (imp==null) + return; + Roi[] rois = getRoisAsArray(); + int n = rois.length; + Overlay overlay = imp.getOverlay(); + if (overlay==null) + overlay = newOverlay(); + for (int i=0; i1) rot = 1; + index += rot; + if (index<0) index = 0; + if (index>=getCount()) index = getCount(); + //IJ.log(index+" "+rot); + select(index); + if (IJ.isWindows()) + list.requestFocusInWindow(); + if (IJ.isJava18()&&IJ.isMacOSX()) + repaint(); + } + } + + /** Selects multiple ROIs, where 'indexes' is an array of integers, each + * greater than or equal to 0 and less than the value returned by getCount(). + * @see #getSelectedIndexes + * @see #getSelectedRoisAsArray + * @see #getCount + */ + public void setSelectedIndexes(int[] indexes) { + int count = getCount(); + if (count==0) return; + for (int i=0; i=count) indexes[i]=count-1; + } + list.setSelectedIndices(indexes); + } + + /** Returns an array of the selected indexes. */ + public int[] getSelectedIndexes() { + return list.getSelectedIndices(); + } + + /** This is a macro-callable version of getSelectedIndexes(). + * Example: indexes=split(call("ij.plugin.frame.RoiManager.getIndexesAsString")); + */ + public static String getIndexesAsString() { + RoiManager rm = RoiManager.getInstance(); + if (rm==null) return ""; + String str = Arrays.toString(rm.getSelectedIndexes()); + str = str.replaceAll(",",""); + return str.substring(1,str.length()-1); + } + + /** Returns an array of the selected indexes or all indexes if none are selected. */ + public int[] getIndexes() { + int[] indexes = getSelectedIndexes(); + if (indexes.length==0) + indexes = getAllIndexes(); + return indexes; + } + + /** Returns 'true' if the index is valid and the indexed ROI is selected. */ + public boolean isSelected(int index) { + return index>=0 && index iterator() { + + Iterator it = new Iterator() { + private int index = -1; + RoiManager rm = RoiManager.getInstance(); + + /** Returns 'true' if next element exists. */ + @Override + public boolean hasNext() { + if (index+11) + IJ.run("Select None"); + } + + } + + +} diff --git a/src/ij/plugin/frame/SyncWindows.java b/src/ij/plugin/frame/SyncWindows.java new file mode 100644 index 0000000..71b5369 --- /dev/null +++ b/src/ij/plugin/frame/SyncWindows.java @@ -0,0 +1,1197 @@ +package ij.plugin.frame; +import ij.*; +import ij.gui.*; +import ij.measure.Calibration; +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.*; +import java.util.*; + + +/** This class "synchronizes" mouse input in multiple windows. Once + several windows are synchronized, mouse events in any one of the + synchronized windows are propagated to the others. + + Note, the notion of synchronization use by the SyncWindows class + here (i.e. multiple windows that all get the same mouse input) is + somewhat different than the use of the synchronize keyword in the + Java language. (In Java, synchronize has to do w/ critical section + access by multiple threads.) +

+ Optionally passes on change of z-slice of a stack to other stacks; + + Optionally translates positions to different windows via offscreen + coordinates, i.e. correctly translates coordinates to windows with a + different zoom; + + Updates the list of windows by click of a button; + +@author Patrick Kelly ; +Improved GUI, support of image coordinates and z-slices by Joachim Walter + +*/ +public class SyncWindows extends PlugInFrame implements + ActionListener, MouseMotionListener, MouseListener, DisplayChangeListener, + ItemListener, ImageListener, CommandListener { + + /** Indices of synchronized image windows are maintained in this Vector. */ + protected Vector vwins = null; + + /* Manage mouse information. + The mouse coordinates x and y are only changed by the methods of the + MousMotionListener Interface. They are used by the MouseListener methods. + This way, the coordinates that were valid before a MouseListener event + (e.g. a Zoom) happened can be accessed. */ + protected int oldX, oldY; + protected int x=0; + protected int y=0; + + /** List of currently displayed windows retrieved from ImageJ + window manager. */ + protected java.awt.List wList; + + /** Panel for GUI */ + protected java.awt.Panel panel; + + /** Checkboxes for user control. */ + protected Checkbox cCursor, cSlice, cChannel, cFrame, cCoords, cScaling; + + /** Buttons for user control. */ + protected Button bSyncAll, bUnsyncAll; + + /** Hashtable to map list ids to image window ids. */ + protected Vector vListMap; + + /** reference to current instance of ImageJ (to avoid repeated IJ.getInstance() s) */ + protected final ImageJ ijInstance; + + /* Variables to store display values of current window. + * Translation by screenX/Y() and offScreenX/Y() does not work, + * because current window receives events (e.g. zooming) before this plugin. + */ + private double currentMag = 1; + private Rectangle currentSrcRect = new Rectangle(0,0,400,400); + + // Control size of cursor box and clipping region. These could be + // changed to tune performance. + static final int RSZ = 16; + static final int SZ = RSZ/2; + static final int SCALE = 3; + + private static SyncWindows instance; + private static Point location; + + //-------------------------------------------------- + /** Create window sync frame. Frame is shown via call to show() or + by invoking run method. */ + public SyncWindows() { + this("Synchronize Windows"); + } + + public SyncWindows(String s) { + super(s); + ijInstance = IJ.getInstance(); + if (instance!=null) { + WindowManager.toFront(instance); + return; + } + instance = this; + panel = controlPanel(); + add(panel); + GUI.scale(this); + pack(); + setResizable(false); + IJ.register(this.getClass()); + if (location==null) + location = getLocation(); + else + setLocation(location); + updateWindowList(); + WindowManager.addWindow(this); + ImagePlus.addImageListener(this); + Executer.addCommandListener(this); + show(); + } + + public static void setC(ImageWindow source, int channel) { + SyncWindows syncWindows = instance; + if (syncWindows==null || !syncWindows.synced(source)) + return; + DisplayChangeEvent event=new DisplayChangeEvent(source, DisplayChangeEvent.CHANNEL, channel); + syncWindows.displayChanged(event); + } + + public static void setZ(ImageWindow source, int slice) { + SyncWindows syncWindows = instance; + if (syncWindows==null || !syncWindows.synced(source)) + return; + DisplayChangeEvent event=new DisplayChangeEvent(source, DisplayChangeEvent.Z, slice); + syncWindows.displayChanged(event); + } + + public static void setT(ImageWindow source, int frame) { + SyncWindows syncWindows = instance; + if (syncWindows==null || !syncWindows.synced(source)) + return; + DisplayChangeEvent event = new DisplayChangeEvent(source, DisplayChangeEvent.T, frame); + syncWindows.displayChanged(event); + } + + private boolean synced(ImageWindow source) { + if (source==null || vwins==null) + return false; + ImagePlus imp = source.getImagePlus(); + if (imp==null) + return false; + return vwins.contains(new Integer(imp.getID())); + } + + // -------------------------------------------------- + /** + * Method to pass on changes of the z-slice of a stack. + */ + public void displayChanged(DisplayChangeEvent e) { + //if (e!=null) throw new IllegalArgumentException(); + //IJ.log("displayChanged: "+e); + if (vwins == null) return; + + Object source = e.getSource(); + int type = e.getType(); + int value = e.getValue(); + + ImagePlus imp; + ImageWindow iw; + + // Current imagewindow + ImageWindow iwc = WindowManager.getCurrentImage().getWindow(); + + // pass on only if event comes from current window + if (!iwc.equals(source)) return; + + // Change channel in other synchronized windows. + if (cChannel.getState() && type==DisplayChangeEvent.CHANNEL) { + for (int n=0; n1) + imp.setT(value); + else + imp.setZ(value); + } + } + } + } + + // Change frame in other synchronized windows. + if (cFrame.getState() && type==DisplayChangeEvent.T) { + for(int n=0; n 0) + vwins = new Vector(); + + // Add all windows in vector to synchronized window list. + for(int n=0; n=0; i--) { + Roi roi2 = overlay2.get(i); + if (roi2.isCursor()) + overlay2.remove(i); + } + if (cursor==null) { + imp.setOverlay(overlay2); + return; + } + } else + overlay2 = new Overlay(); + if (cursor!=null) { + overlay2.add(cursor); + cursor.setStrokeColor(Color.red); + cursor.setStrokeWidth(2); + cursor.setNonScalable(true); + cursor.setIsCursor(true); + imp.setOverlay(overlay2); + } + } + + /** Store srcRect and Magnification of the currently active ImageCanvas ic */ + private void storeCanvasState(ImageCanvas ic) { + currentMag = ic.getMagnification(); + currentSrcRect = new Rectangle(ic.getSrcRect()); + } + + // -------------------------------------------------- + /** Get ImagePlus from Windows-Vector vwins. */ + public ImagePlus getImageFromVector(int n) { + if (vwins == null || n<0 || vwins.size() < n+1) return null; + + ImagePlus imp; + imp = WindowManager.getImage(((Integer)vwins.elementAt(n)).intValue()); + if (imp.isLocked()) return null; //must not touch locked windows + return imp; + } + + /** Get the title of image n from Windows-Vector vwins. If the image ends with + * .tif, the extension is removed. */ + public String getImageTitleFromVector(int n) { + if (vwins == null || n<0 || vwins.size() < n+1) return ""; + + ImagePlus imp; + imp = WindowManager.getImage(((Integer)vwins.elementAt(n)).intValue()); + String title = imp.getTitle(); + if (title.length()>=4 && (title.substring(title.length()-4)).equalsIgnoreCase(".tif")) { + title = title.substring(0, title.length()-4); + } else if (title.length()>=5 && (title.substring(title.length()-5)).equalsIgnoreCase(".tiff")) { + title = title.substring(0, title.length()-5); + } + return title; + } + + /** Get index of "image" in vector of synchronized windows, if image is in vector. + * Else return -1. + */ + public int getIndexOfImage(ImagePlus image) { + int index = -1; + ImagePlus imp; + if (vwins == null || vwins.size() == 0) + return index; + + for (int n=0; n + * Example how to implement an object, which fires DisplayChangeEvents using the + * IJEventMulticaster: + * + *


+ * public mySpecialWindow extends StackWindow {
+ *
+ *		DisplayChangeListener dclistener = null;
+ *
+ *		public synchronized void addDisplayChangeListener(DisplayChangeListener l) {
+ *			dclistener = IJEventMulticaster.add(dclistener, l);
+ *		}
+ *
+ *		public synchronized void removeDisplayChangeListener(DisplayChangeListener l) {
+ *			dclistener = IJEventMulticaster.remove(dclistener, l);
+ *		}
+ *
+ *		public void myEventFiringMethod(arguments) {
+ *			... code ...
+ *			if (dclistener != null) {
+ *				DisplayChangeEvent dcEvent = new DisplayChangeEvent(this, DisplayChangeEvent.Z, zSlice);
+ *				dclistener.displayChanged(dcEvent);
+ *			}
+ *			... code ...
+ *		}
+ *
+ *		... other methods ...
+ * }
+ * 
+ * + * To put in a new event-listener (by changing this class or extending it): + *

+ * - Add the listener to the "implements" list. + *

+ * - Add the methods of this listener to pass on the events (like displayChanged). + *

+ * - Add the methods "add" and "remove" with the corresponding listener type. + *

+ * + * @author code taken from Sun's AWTEventMulticaster by J. Walter 2002-03-07 + */ + +class IJEventMulticaster extends AWTEventMulticaster implements DisplayChangeListener { + + IJEventMulticaster(EventListener a, EventListener b) { + super(a,b); + } + + /** + * Handles the DisplayChange event by invoking the + * displayChanged methods on listener-a and listener-b. + * @param e the DisplayChange event + */ + public void displayChanged(DisplayChangeEvent e) { + ((DisplayChangeListener)a).displayChanged(e); + ((DisplayChangeListener)b).displayChanged(e); + } + + /** + * Adds DisplayChange-listener-a with DisplayChange-listener-b and + * returns the resulting multicast listener. + * @param a DisplayChange-listener-a + * @param b DisplayChange-listener-b + */ + public static DisplayChangeListener add(DisplayChangeListener a, DisplayChangeListener b) { + return (DisplayChangeListener)addInternal(a, b); + } + + /** + * Removes the old DisplayChange-listener from DisplayChange-listener-l and + * returns the resulting multicast listener. + * @param l DisplayChange-listener-l + * @param oldl the DisplayChange-listener being removed + */ + public static DisplayChangeListener remove(DisplayChangeListener l, DisplayChangeListener oldl) { + return (DisplayChangeListener)removeInternal(l, oldl); + } + +} diff --git a/src/ij/plugin/frame/ThresholdAdjuster.java b/src/ij/plugin/frame/ThresholdAdjuster.java new file mode 100644 index 0000000..56dd856 --- /dev/null +++ b/src/ij/plugin/frame/ThresholdAdjuster.java @@ -0,0 +1,1216 @@ +package ij.plugin.frame; +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; +import ij.*; +import ij.plugin.*; +import ij.process.*; +import ij.gui.*; +import ij.measure.*; +import ij.util.Tools; +import ij.plugin.frame.Recorder; +import ij.plugin.filter.*; +import ij.plugin.ChannelSplitter; +import ij.plugin.Thresholder; + +/** Adjusts the lower and upper threshold levels of the active image. This + class is multi-threaded to provide a more responsive user interface. */ +public class ThresholdAdjuster extends PlugInDialog implements PlugIn, Measurements, Runnable, + ActionListener, AdjustmentListener, ItemListener, FocusListener, KeyListener, MouseWheelListener, ImageListener { + + public static final String LOC_KEY = "threshold.loc"; + public static final String MODE_KEY = "threshold.mode"; + public static final String DARK_BACKGROUND = "threshold.dark"; + public static final String RAW_VALUES = "threshold.raw"; + static final int RED=0, BLACK_AND_WHITE=1, OVER_UNDER=2; + static final String[] modes = {"Red","B&W", "Over/Under"}; + static final double defaultMinThreshold = 0;//85; + static final double defaultMaxThreshold = 255;//170; + static final int DEFAULT = 0; + static boolean fill1 = true; + static boolean fill2 = true; + static boolean useBW = true; + static boolean backgroundToNaN = true; + static ThresholdAdjuster instance; + static int mode = RED; + static String[] methodNames = AutoThresholder.getMethods(); + static String method = methodNames[DEFAULT]; + static AutoThresholder thresholder = new AutoThresholder(); + ThresholdPlot plot = new ThresholdPlot(); + Thread thread; //background thread calculating and applying the threshold + + int minValue = -1; // min slider, 0-255 + int maxValue = -1; + int sliderRange = 256; + boolean doAutoAdjust,doReset,doApplyLut,doStateChange,doSet,doBackground; //actions required from user interface + + Panel panel; + Button autoB, resetB, applyB, setB; + int previousImageID; + int previousImageType; + int previousRoiHashCode; + double previousMin, previousMax; + int previousSlice; + boolean imageWasUpdated; + ImageJ ij; + double minThreshold, maxThreshold; // 0-255 + Scrollbar minSlider, maxSlider; + TextField minLabel, maxLabel; // for current threshold + Label percentiles; + boolean done; + int lutColor; + Choice methodChoice, modeChoice; + Checkbox darkBackground, stackHistogram, rawValues; + boolean firstActivation = true; + boolean setButtonPressed; + boolean noReset = true; + boolean noResetChanged; + boolean enterPressed; + + public ThresholdAdjuster() { + super("Threshold"); + ImagePlus cimp = WindowManager.getCurrentImage(); + if (cimp!=null && cimp.getBitDepth()==24) { + IJ.error("Threshold Adjuster", + "Image>Adjust>Threshold only works with grayscale images.\n \n" + +"You can:\n" + +" Convert to grayscale: Image>Type>8-bit\n" + +" Convert to RGB stack: Image>Type>RGB Stack\n" + +" Convert to HSB stack: Image>Type>HSB Stack\n" + +" Convert to 3 grayscale images: Image>Color>Split Channels\n" + +" Do color thresholding: Image>Adjust>Color Threshold\n"); + return; + } + if (instance!=null) { + instance.firstActivation = true; + instance.toFront(); + instance.setup(cimp, true); + instance.updateScrollBars(); + return; + } + + WindowManager.addWindow(this); + instance = this; + mode = (int)Prefs.get(MODE_KEY, RED); + if (modeOVER_UNDER) mode = RED; + setLutColor(mode); + IJ.register(PasteController.class); + + ij = IJ.getInstance(); + Font font = IJ.font10; + GridBagLayout gridbag = new GridBagLayout(); + GridBagConstraints c = new GridBagConstraints(); + setLayout(gridbag); + + // plot + int y = 0; + c.gridx = 0; + c.gridy = y++; + c.gridwidth = 2; + c.fill = GridBagConstraints.BOTH; + c.anchor = GridBagConstraints.CENTER; + c.insets = new Insets(10, 10, 0, 10); //top left bottom right + add(plot, c); + plot.addKeyListener(ij); + + // percentiles + c.gridx = 0; + c.gridy = y++; + c.insets = new Insets(1, 10, 0, 10); + percentiles = new Label(""); + percentiles.setFont(font); + add(percentiles, c); + + // minThreshold slider + minSlider = new Scrollbar(Scrollbar.HORIZONTAL, sliderRange/3, 1, 0, sliderRange); + GUI.fixScrollbar(minSlider); + c.gridx = 0; + c.gridy = y++; + c.gridwidth = 1; + c.weightx = IJ.isMacintosh()?90:100; + c.fill = GridBagConstraints.HORIZONTAL; + c.insets = new Insets(1, 10, 0, 0); + add(minSlider, c); + minSlider.addAdjustmentListener(this); + minSlider.addMouseWheelListener(this); +// minSlider.addKeyListener(ij); + minSlider.setUnitIncrement(1); + minSlider.setFocusable(false); + + // minThreshold slider label + c.gridx = 1; + c.gridwidth = 1; + c.weightx = IJ.isMacintosh()?10:0; + c.insets = new Insets(5, 0, 0, 10); + String text = "000000"; + int columns = 4; + minLabel = new TextField(text,columns); + minLabel.setFont(font); + add(minLabel, c); + minLabel.addFocusListener(this); + minLabel.addMouseWheelListener(this); + minLabel.addKeyListener(this); + + // maxThreshold slider + maxSlider = new Scrollbar(Scrollbar.HORIZONTAL, sliderRange*2/3, 1, 0, sliderRange); + GUI.fixScrollbar(maxSlider); + c.gridx = 0; + c.gridy = y++; + c.gridwidth = 1; + c.weightx = 100; + c.insets = new Insets(2, 10, 0, 0); + add(maxSlider, c); + maxSlider.addAdjustmentListener(this); + maxSlider.addMouseWheelListener(this); +// maxSlider.addKeyListener(ij); + maxSlider.setUnitIncrement(1); + maxSlider.setFocusable(false); + + // maxThreshold slider label + c.gridx = 1; + c.gridwidth = 1; + c.weightx = 0; + c.insets = new Insets(2, 0, 0, 10); + maxLabel = new TextField(text,columns); + maxLabel.setFont(font); + add(maxLabel, c); + maxLabel.addFocusListener(this); + maxLabel.addMouseWheelListener(this); + maxLabel.addKeyListener(this); + + // choices + panel = new Panel(); + methodChoice = new Choice(); + for (int i=0; imin) { + double scaledThr = ((threshold-min)/(max-min))*255.0; + if (scaledThr < 0.0) scaledThr = 0.0; + if (scaledThr > 255.0) scaledThr = 255.0; + return scaledThr; + } else + return ImageProcessor.NO_THRESHOLD; + } + + /** Scales a threshold level in the range 0-255 to the actual level. */ + double scaleUp(ImageProcessor ip, double threshold) { + double min = ip.getMin(); + double max = ip.getMax(); + if (max>min) + return min + (threshold/255.0)*(max-min); + else + return ImageProcessor.NO_THRESHOLD; + } + + void updatePlot(ImageProcessor ip) { + int min = (int)Math.round(minThreshold); + if (min<0) min=0; + if (min>255) min=255; + if (ip.getMinThreshold()==ImageProcessor.NO_THRESHOLD) + min = -1; + int max = (int)Math.round(maxThreshold); + if (max<0) max=0; + if (max>255) max=255; + plot.setThreshold(min,max); + plot.mode = mode; + plot.repaint(); + } + + void updatePercentiles(ImagePlus imp, ImageProcessor ip) { + if (percentiles==null) + return; + ImageStatistics stats = plot.stats; + int minThresholdInt = (int)Math.round(minThreshold); + if (minThresholdInt<0) minThresholdInt=0; + if (minThresholdInt>255) minThresholdInt=255; + int maxThresholdInt = (int)Math.round(maxThreshold); + if (maxThresholdInt<0) maxThresholdInt=0; + if (maxThresholdInt>255) maxThresholdInt=255; + if (stats!=null && stats.histogram!=null && stats.histogram.length==256 + && ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD) { + int[] histogram = stats.histogram; + int below = 0, inside = 0, above = 0; + int minValue=0, maxValue=255; + if (imp.getBitDepth()==16 && !entireStack(imp)) { //16-bit histogram for better accuracy + ip.setRoi(imp.getRoi()); + histogram = ip.getHistogram(); + minThresholdInt = (int)Math.round(ip.getMinThreshold()); + if (minThresholdInt<0) minThresholdInt=0; + maxThresholdInt = (int)Math.round(ip.getMaxThreshold()); + if (maxThresholdInt>65535) maxThresholdInt=65535; + minValue=0; maxValue=histogram.length-1; + } + for (int i=minValue; i"+maxThresholdInt+":"+above+" sum="+total); + if (mode==OVER_UNDER) + percentiles.setText("below: "+IJ.d2s(100.*below/total)+" %, above: "+IJ.d2s(100.*above/total)+" %"); + else + percentiles.setText(IJ.d2s(100.*inside/total)+" %"); + } else + percentiles.setText(""); + } + + void updateLabels(ImagePlus imp, ImageProcessor ip) { + if (minLabel==null || maxLabel==null || enterPressed) + return; + double min = ip.getMinThreshold(); + double max = ip.getMaxThreshold(); + if (min==ImageProcessor.NO_THRESHOLD) { + minLabel.setText(""); + maxLabel.setText(""); + } else { + Calibration cal = imp.getCalibration(); + boolean calibrated = cal.calibrated() && !rawValues.getState(); + if (calibrated) { + min = cal.getCValue((int)min); + max = cal.getCValue((int)max); + } + if ((((int)min==min && (int)max==max && Math.abs(min)<1e6 && Math.abs(max)<1e6)) || + (ip instanceof ShortProcessor && (cal.isSigned16Bit() || !calibrated))) { + minLabel.setText(ResultsTable.d2s(min,0)); + maxLabel.setText(ResultsTable.d2s(max,0)); + } else { + minLabel.setText(min==-1e30 ? "-1e30" : d2s(min)); + maxLabel.setText(max== 1e30 ? "1e30" : d2s(max)); + } + } + } + + /** Converts a number to a String, such that it should not take much space (for the minLabel, maxLabel TextFields) */ + String d2s(double x) { + return Math.abs(x)>=1e6 ? IJ.d2s(x,-2) : ResultsTable.d2s(x,2); //the latter uses exp notation also for small x + } + + void updateScrollBars() { + minSlider.setValue((int)minThreshold); + maxSlider.setValue((int)maxThreshold); + } + + /** Restore image outside non-rectangular roi. */ + void doMasking(ImagePlus imp, ImageProcessor ip) { + ImageProcessor mask = imp.getMask(); + if (mask!=null) + ip.reset(mask); + } + + void adjustMinThreshold(ImagePlus imp, ImageProcessor ip, double value) { + if (IJ.altKeyDown() || IJ.shiftKeyDown() ) { + double width = maxThreshold-minThreshold; + if (width<1.0) width = 1.0; + minThreshold = value; + maxThreshold = minThreshold+width; + if ((minThreshold+width)>255) { + minThreshold = 255-width; + maxThreshold = minThreshold+width; + minSlider.setValue((int)minThreshold); + } + maxSlider.setValue((int)maxThreshold); + scaleUpAndSet(ip, minThreshold, maxThreshold); + return; + } + minThreshold = value; + if (maxThresholdmaxThreshold) { + minThreshold = maxThreshold; + minSlider.setValue((int)minThreshold); + } + if (minThreshold < 0) { //remove NO_THRESHOLD + minThreshold = 0; + minSlider.setValue((int)minThreshold); + } + scaleUpAndSet(ip, minThreshold, maxThreshold); + IJ.setKeyUp(KeyEvent.VK_ALT); + IJ.setKeyUp(KeyEvent.VK_SHIFT); + } + + void reset(ImagePlus imp, ImageProcessor ip) { + if (noResetChanged) { + noResetChanged = false; + if ((noReset&&mode!=OVER_UNDER) || ip.getBitDepth()==8) + return; + if (!noReset) { + ImageStatistics stats = ip.getStats(); + if (ip.getMin()==stats.min && ip.getMax()==stats.max) + return; // not contrast enhanced; no need to reset + } + } + ip.resetThreshold(); + if (!noReset) + resetMinAndMax(ip); + ImageStatistics stats = plot.setHistogram(imp, entireStack(imp),rawValues.getState()); + if (ip.getBitDepth()!=8 && entireStack(imp)) + ip.setMinAndMax(stats.min, stats.max); + updateScrollBars(); + if (Recorder.record) { + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.resetThreshold(imp);"); + else + Recorder.record("resetThreshold"); + } + } + + /** Numeric input via 'Set' dialog or minLabel, maxLabel TextFields */ + void doSet(ImagePlus imp, ImageProcessor ip) { + double level1 = ip.getMinThreshold(); + double level2 = ip.getMaxThreshold(); + Calibration cal = imp.getCalibration(); + if (level1==ImageProcessor.NO_THRESHOLD) { + level1 = scaleUp(ip, defaultMinThreshold); + level2 = scaleUp(ip, defaultMaxThreshold); + } + boolean calibrated = cal.calibrated() && !rawValues.getState(); + if (calibrated) { + level1 = cal.getCValue(level1); + level2 = cal.getCValue(level2); + } + if (setButtonPressed) { + int digits = (ip instanceof FloatProcessor)||(calibrated && !cal.isSigned16Bit()) ? Math.max(Analyzer.getPrecision(), 4) : 0; + GenericDialog gd = new GenericDialog("Set Threshold Levels"); + gd.addNumericField("Lower threshold level: ", level1, Math.abs(level1)<1e7 ? digits : -4, 10, null); + gd.addNumericField("Upper threshold level: ", level2, Math.abs(level2)<1e7 ? digits : -4, 10, null); + gd.showDialog(); + if (gd.wasCanceled()) { + setButtonPressed = false; + return; + } + level1 = gd.getNextNumber(); + level2 = gd.getNextNumber(); + setButtonPressed = false; + } else { + level1 = Tools.parseDouble(minLabel.getText(), level1); + level2 = Tools.parseDouble(maxLabel.getText(), level2); + } + enterPressed = false; + if (calibrated) { + level1 = cal.getRawValue(level1); + level2 = cal.getRawValue(level2); + } + if (level2maxDisplay)) { + noReset = false; + noResetChanged = true; + //noResetButton.setState(false); + } + resetMinAndMax(ip); + double minValue = ip.getMin(); + double maxValue = ip.getMax(); + if (imp.getStackSize()==1) { + if (level1maxValue) level2 = maxValue; + } + IJ.wait(500); + ip.setThreshold(level1, level2, lutColor); + ip.setSnapshotPixels(null); // disable undo + previousImageID = 0; + setup(imp, false); + updateScrollBars(); + if (Recorder.record) { + if (imp.getBitDepth()==32) { + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.setThreshold(imp, "+IJ.d2s(ip.getMinThreshold(),4)+", "+IJ.d2s(ip.getMaxThreshold(),4)+");"); + else + Recorder.record("setThreshold", ip.getMinThreshold(), ip.getMaxThreshold()); + } else { + int min = (int)ip.getMinThreshold(); + int max = (int)ip.getMaxThreshold(); + if (cal.isSigned16Bit()) { + min = (int)cal.getCValue(level1); + max = (int)cal.getCValue(level2); + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.setThreshold(imp, "+min+", "+max+");"); + else + Recorder.record("setThreshold", min, max); + } + if (Recorder.scriptMode()) + Recorder.recordCall("IJ.setRawThreshold(imp, "+min+", "+max+", null);"); + else { + if (calibrated) + Recorder.record("setThreshold", min, max, "raw"); + else + Recorder.record("setThreshold", min, max); + } + } + } + } + + void changeState(ImagePlus imp, ImageProcessor ip) { + scaleUpAndSet(ip, minThreshold, maxThreshold); + updateScrollBars(); + } + + void autoThreshold(ImagePlus imp, ImageProcessor ip) { + ip.resetThreshold(); + previousImageID = 0; + setup(imp, true); + updateScrollBars(); + } + + /** User has clicked 'Dark background'. + * Switch only if the current thresholds are consistent with + * the previous 'Dark background' state. + */ + void switchBackground(ImagePlus imp, ImageProcessor ip) { + if (minThreshold < 0) { //remove NO_THRESHOLD + autoThreshold(imp, ip); + return; + } + if (thresholdHigh(ip)) { + if (minThreshold == 0) { + minThreshold = maxThreshold+1; + if (minThreshold > 255) minThreshold = 255; + maxThreshold = 255; + } + } else { + if (maxThreshold == 255) { + maxThreshold = minThreshold-1; + if (maxThreshold < 0) maxThreshold = 0; + minThreshold = 0; + } + } + minSlider.setValue((int)minThreshold); + maxSlider.setValue((int)maxThreshold); + scaleUpAndSet(ip, minThreshold, maxThreshold); + } + + void apply(ImagePlus imp) { + if (imp.getProcessor().getMinThreshold()==ImageProcessor.NO_THRESHOLD) { + IJ.error("Thresholder", "Threshold is not set"); + return; + } + try { + if (imp.getBitDepth()==32) { + YesNoCancelDialog d = new YesNoCancelDialog(null, "Thresholder", + "Convert to 8-bit mask or set background pixels to NaN?", "Convert to Mask", "Set to NaN"); + if (d.cancelPressed()) + return; + else if (!d.yesPressed()) { + Recorder.recordInMacros = true; + IJ.run("NaN Background"); + Recorder.recordInMacros = false; + return; + } + } + runThresholdCommand(); + } catch (Exception e) {} + } + + void runThresholdCommand() { + Thresholder.setMethod(method); + Thresholder.setBackground(darkBackground.getState()?"Dark":"Light"); + if (Recorder.record) { + Recorder.setCommand("Convert to Mask"); + (new Thresholder()).run("mask"); + Recorder.saveCommand(); + } else + (new Thresholder()).run("mask"); + } + + static final int RESET=0, AUTO=1, HIST=2, APPLY=3, STATE_CHANGE=4, MIN_THRESHOLD=5, MAX_THRESHOLD=6, SET=7, BACKGROUND=8; + + // Separate thread that does the potentially time-consuming processing + public void run() { + while (!done) { + synchronized(this) { + if (!doAutoAdjust && !doReset && !doApplyLut && !doStateChange && !doSet && !doBackground && minValue<0 && maxValue<0) { + try {wait();} + catch(InterruptedException e) {} + } + } + doUpdate(); + } + } + + /** Triggered by the user interface, with the corresponding boolean, e.g., 'doAutoAdjust' */ + void doUpdate() { + ImagePlus imp; + ImageProcessor ip; + int action; + int min = minValue; + int max = maxValue; + if (doReset) { action = RESET; doReset = false; } + else if (doAutoAdjust) { action = AUTO; doAutoAdjust = false; } + else if (doApplyLut) { action = APPLY; doApplyLut = false; } + else if (doStateChange) { action = STATE_CHANGE; doStateChange = false; } + else if (doSet) { action = SET; doSet = false; } + else if (doBackground) { action = BACKGROUND; doBackground = false; } + else if (minValue>=0) { action = MIN_THRESHOLD; minValue = -1; } + else if (maxValue>=0) { action = MAX_THRESHOLD; maxValue = -1; } + else return; + + imp = WindowManager.getCurrentImage(); + if (imp==null) { + IJ.beep(); + IJ.showStatus("No image"); + return; + } + ip = setup(imp, false); + if (ip==null) { + imp.unlock(); + IJ.beep(); + if (imp.isComposite()) + IJ.showStatus("\"Composite\" mode images cannot be thresholded"); + else + IJ.showStatus("RGB images cannot be thresholded"); + return; + } + switch (action) { + case RESET: reset(imp, ip); break; + case AUTO: autoThreshold(imp, ip); break; + case APPLY: apply(imp); break; + case STATE_CHANGE: changeState(imp, ip); break; + case SET: doSet(imp, ip); break; + case MIN_THRESHOLD: adjustMinThreshold(imp, ip, min); break; + case MAX_THRESHOLD: adjustMaxThreshold(imp, ip, max); break; + case BACKGROUND: switchBackground(imp, ip); break; + } + updatePlot(ip); + updateLabels(imp, ip); + updatePercentiles(imp, ip); + ip.setLutAnimation(true); + imp.updateAndDraw(); + } + + /** Overrides close() in PlugInFrame. */ + public void close() { + super.close(); + instance = null; + done = true; + Prefs.saveLocation(LOC_KEY, getLocation()); + Prefs.set(MODE_KEY, mode); + Prefs.set(DARK_BACKGROUND, darkBackground.getState()); + Prefs.set(RAW_VALUES, rawValues.getState()); + synchronized(this) { + notify(); + } + } + + public void windowActivated(WindowEvent e) { + super.windowActivated(e); + plot.requestFocus(); + ImagePlus imp = WindowManager.getCurrentImage(); + if (!firstActivation && imp!=null) { + setup(imp, false); + updateScrollBars(); + } + } + + // Returns a hashcode for the specified ROI that typically changes + // if it is moved, even though is still the same object. + int roiHashCode(Roi roi) { + return roi!=null?roi.getHashCode():0; + } + + /** Notifies the ThresholdAdjuster that the image has changed. + * If the image has no threshold, it does not autothreshold the image. + */ + public static void update() { + if (instance!=null) { + ThresholdAdjuster ta = ((ThresholdAdjuster)instance); + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null && ta.previousImageID==imp.getID()) { + if ((imp.getCurrentSlice()!=ta.previousSlice) && ta.entireStack(imp)) + return; + ta.previousImageID = 0; + ta.setup(imp, false); + ta.updateScrollBars(); + } + } + } + + public static boolean isDarkBackground() { + return instance!=null?instance.darkBackground.getState():false; + } + + + /** Returns the current thresholding method ("Default", "Huang", etc). */ + public static String getMethod() { + return method; + } + + /** Sets the thresholding method ("Default", "Huang", etc). */ + public static void setMethod(String thresholdingMethod) { + boolean valid = false; + for (int i=0; i maxCount2) && (i != stats.mode)) + maxCount2 = histogram[i]; + hmax = stats.maxCount; + if ((hmax>(maxCount2 * 1.5)) && (maxCount2 != 0)) + hmax = (int)(maxCount2 * 1.2); + os = null; + + if (!(cm instanceof IndexColorModel)) + return null; + IndexColorModel icm = (IndexColorModel)cm; + int mapSize = icm.getMapSize(); + if (mapSize!=256) + return null; + byte[] r = new byte[256]; + byte[] g = new byte[256]; + byte[] b = new byte[256]; + icm.getReds(r); + icm.getGreens(g); + icm.getBlues(b); + hColors = new Color[256]; + final int brightnessLimit = 1800; // 0 ... 2550 scale; brightness is reduced above + for (int i=0; i<256; i++) { //avoid colors that are too bright (invisible) + int sum = 4*(r[i]&255) + 5*(g[i]&255) + (b[i]&255); + if (sum > brightnessLimit) { + r[i] = (byte)(((r[i]&255)*brightnessLimit*2)/(sum+brightnessLimit)); + g[i] = (byte)(((g[i]&255)*brightnessLimit*2)/(sum+brightnessLimit)); + b[i] = (byte)(((b[i]&255)*brightnessLimit*2)/(sum+brightnessLimit)); + } + hColors[i] = new Color(r[i]&255, g[i]&255, b[i]&255); + } + imageID2 = imp.getID(); + entireStack2 = entireStack; + return stats; + } + + public void update(Graphics g) { + paint(g); + } + + public void paint(Graphics g) { + if (g==null) return; + if (histogram!=null) { + if (os==null && hmax>0) { + os = createImage(width,height); + osg = os.getGraphics(); + if (scale>1) + ((Graphics2D)osg).setStroke(new BasicStroke((float)scale)); + osg.setColor(Color.white); + osg.fillRect(0, 0, width, height); + osg.setColor(Color.gray); + double scale2 = width/256.0; + int barWidth = 1; + if (scale>1) barWidth=2; + if (scale>2) barWidth=3; + for (int i = 0; i < 256; i++) { + if (hColors!=null) osg.setColor(hColors[i]); + int x =(int)(i*scale2); + for (int j = 0; j=0||roi.contains(ox,oy)))) { + arrow = new Arrow(sx, sy, imp); + if (imp.okToDeleteRoi()) + imp.setRoi(arrow, false); + e.consume(); + } + } + + public void mouseDragged(ImagePlus imp, MouseEvent e) { + ImageCanvas ic = imp.getCanvas(); + int sx = e.getX(); + int sy = e.getY(); + int ox = ic.offScreenX(sx); + int oy = ic.offScreenY(sy); + Roi roi = imp.getRoi(); + if (roi!=null && (roi instanceof Arrow) && roi.contains(ox,oy)) + roi.mouseDragged(e); + else if (arrow!=null) + arrow.mouseDragged(e); + e.consume(); + } + + public void mouseReleased(ImagePlus imp, MouseEvent e) { + ImageCanvas ic = imp.getCanvas(); + int sx = e.getX(); + int sy = e.getY(); + int ox = ic.offScreenX(sx); + int oy = ic.offScreenY(sy); + Roi roi = imp.getRoi(); + if (arrow!=null && !(roi!=null && (roi instanceof Arrow) && roi.contains(ox,oy))) { + arrow.mouseReleased(e); + e.consume(); + } + } + + public void showOptionsDialog() { + IJ.doCommand("Arrow Tool..."); + } + + public String getToolIcon() { + return "C037L0ff0L74f0Lb8f0L74b8"; + } + + public String getToolName() { + return "Arrow Tool"; + } + +} + + diff --git a/src/ij/plugin/tool/BrushTool.java b/src/ij/plugin/tool/BrushTool.java new file mode 100644 index 0000000..f53ea78 --- /dev/null +++ b/src/ij/plugin/tool/BrushTool.java @@ -0,0 +1,324 @@ +package ij.plugin.tool; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.plugin.Colors; +import java.awt.*; +import java.awt.event.*; +import java.util.Vector; + +// Versions +// 2012-07-22 shift to confine horizontally or vertically, ctrl-shift to resize, ctrl to pick + +/** This class implements the Paintbrush Tool, which allows the user to draw on + an image, or on an Overlay if "Paint on overlay" is enabled. */ +public class BrushTool extends PlugInTool implements Runnable { + + private final static int UNCONSTRAINED=0, HORIZONTAL=1, VERTICAL=2, RESIZING=3, RESIZED=4, IDLE=5; //mode flags + private static String BRUSH_WIDTH_KEY = "brush.width"; + private static String PENCIL_WIDTH_KEY = "pencil.width"; + private static String CIRCLE_NAME = "brush-tool-overlay"; + private static final String LOC_KEY = "brush.loc"; + private static final String OVERLAY_KEY = "brush.overlay"; + + private String widthKey; + private int width; + private ImageProcessor ip; + private int mode; //resizing brush or motion constrained horizontally or vertically + private int xStart, yStart; + private int oldWidth; + private boolean isPencil; + private Overlay overlay; + private Options options; + private GenericDialog gd; + private ImageRoi overlayImage; + private boolean paintOnOverlay; + private static BrushTool brushInstance; + + public void run(String arg) { + isPencil = "pencil".equals(arg); + widthKey = isPencil ? PENCIL_WIDTH_KEY : BRUSH_WIDTH_KEY; + width = (int)Prefs.get(widthKey, isPencil ? 1 : 5); + paintOnOverlay = Prefs.get(OVERLAY_KEY, false); + Toolbar.addPlugInTool(this); + if (!isPencil) + brushInstance = this; + } + + public void mousePressed(ImagePlus imp, MouseEvent e) { + ImageCanvas ic = imp.getCanvas(); + int x = ic.offScreenX(e.getX()); + int y = ic.offScreenY(e.getY()); + xStart = x; + yStart = y; + checkForOverlay(imp); + if (overlayImage!=null) + ip = overlayImage.getProcessor(); + else + ip = imp.getProcessor(); + int ctrlMask = IJ.isMacintosh() ? InputEvent.META_MASK : InputEvent.CTRL_MASK; + int resizeMask = InputEvent.SHIFT_MASK | ctrlMask; + if ((e.getModifiers() & resizeMask) == resizeMask) { + mode = RESIZING; + oldWidth = width; + return; + } else if ((e.getModifiers() & ctrlMask) != 0) { + boolean altKeyDown = (e.getModifiers() & InputEvent.ALT_MASK) != 0; + ic.setDrawingColor(x, y, altKeyDown); //pick color from image (ignore overlay) + if (!altKeyDown) + setColor(Toolbar.getForegroundColor()); + mode = IDLE; + return; + } + mode = UNCONSTRAINED; + ip.snapshot(); + Undo.setup(Undo.FILTER, imp); + ip.setLineWidth(width); + if (e.isAltDown()) { + if (overlayImage!=null) + ip.setColor(0); //erase + else + ip.setColor(Toolbar.getBackgroundColor()); + } else + ip.setColor(Toolbar.getForegroundColor()); + ip.moveTo(x, y); + if (!e.isShiftDown()) { + ip.lineTo(x, y); + if (overlayImage!=null) { + overlayImage.setProcessor(ip); + imp.draw(); + } else + imp.updateAndDraw(); + } + } + + private void checkForOverlay(ImagePlus imp) { + overlayImage = getOverlayImage(imp); + if (overlayImage==null && paintOnOverlay) { + ImageProcessor overlayIP = new ColorProcessor(imp.getWidth(), imp.getHeight()); + ImageRoi imageRoi = new ImageRoi(0, 0, overlayIP); + imageRoi.setZeroTransparent(true); + imageRoi.setName("[Brush]"); + Overlay overlay = imp.getOverlay(); + if (overlay==null) + overlay = new Overlay(); + overlay.add(imageRoi); + overlay.selectable(false); + imp.setOverlay(overlay); + overlayImage = imageRoi; + } + } + + private ImageRoi getOverlayImage(ImagePlus imp) { + if (!paintOnOverlay) + return null; + Overlay overlay = imp.getOverlay(); + if (overlay==null) + return null; + Roi roi = overlay.get("[Brush]"); + if (roi==null||!(roi instanceof ImageRoi)) + return null; + Rectangle bounds = roi.getBounds(); + if (bounds.x!=0||bounds.y!=0||bounds.width!=imp.getWidth()||bounds.height!=imp.getHeight()) + return null; + return (ImageRoi)roi; + } + + public void mouseDragged(ImagePlus imp, MouseEvent e) { + if (mode == IDLE) return; + ImageCanvas ic = imp.getCanvas(); + int x = ic.offScreenX(e.getX()); + int y = ic.offScreenY(e.getY()); + if (mode == RESIZING) { + showToolSize(x-xStart, imp); + return; + } + if ((e.getModifiers() & InputEvent.SHIFT_MASK) != 0) { //shift constrains + if (mode == UNCONSTRAINED) { //first movement with shift down determines direction + if (Math.abs(x-xStart) > Math.abs(y-yStart)) + mode = HORIZONTAL; + else if (Math.abs(x-xStart) < Math.abs(y-yStart)) + mode = VERTICAL; + else return; //constraint direction still unclear + } + if (mode == HORIZONTAL) + y = yStart; + else if (mode == VERTICAL) + x = xStart; + } else { + xStart = x; + yStart = y; + mode = UNCONSTRAINED; + } + ip.setLineWidth(width); + ip.lineTo(x, y); + if (overlayImage!=null) { + overlayImage.setProcessor(ip); + imp.draw(); + } else + imp.updateAndDraw(); + } + + public void mouseReleased(ImagePlus imp, MouseEvent e) { + if (mode==RESIZING) { + if (overlay!=null && overlay.size()>0 && CIRCLE_NAME.equals(overlay.get(overlay.size()-1).getName())) { + overlay.remove(overlay.size()-1); + imp.setOverlay(overlay); + } + overlay = null; + if (e.isShiftDown()) { + setWidth(width); + Prefs.set(widthKey, width); + } + } + } + + private void setWidth(int width) { + if (gd==null) + return; + Vector numericFields = gd.getNumericFields(); + TextField widthField = (TextField)numericFields.elementAt(0); + widthField.setText(""+width); + Vector sliders = gd.getSliders(); + Scrollbar sb = (Scrollbar)sliders.elementAt(0); + sb.setValue(width); + } + + private void setColor(Color c) { + if (gd==null) + return; + String name = Colors.colorToString2(c); + if (name.length()>0) { + Vector choices = gd.getChoices(); + Choice ch = (Choice)choices.elementAt(0); + ch.select(name); + } + } + + + private void showToolSize(int deltaWidth, ImagePlus imp) { + if (deltaWidth !=0) { + width = oldWidth + deltaWidth; + if (width<1) width=1; + Roi circle = new OvalRoi(xStart-width/2, yStart-width/2, width, width); + circle.setName(CIRCLE_NAME); + circle.setStrokeColor(Color.red); + overlay = imp.getOverlay(); + if (overlay==null) + overlay = new Overlay(); + else if (overlay.size()>0 && CIRCLE_NAME.equals(overlay.get(overlay.size()-1).getName())) + overlay.remove(overlay.size()-1); + overlay.add(circle); + imp.setOverlay(overlay); + } + IJ.showStatus((isPencil?"Pencil":"Brush")+" width: "+ width); + } + + public void showOptionsDialog() { + Thread thread = new Thread(this, "Brush Options"); + thread.setPriority(Thread.NORM_PRIORITY); + thread.start(); + } + + public String getToolName() { + if (isPencil) + return "Pencil Tool"; + else + return "Paintbrush Tool"; + } + + public String getToolIcon() { + // C123 is the foreground color + if (isPencil) + return "C037L4990L90b0Lc1c3L82a4Lb58bL7c4fDb4L494fC123L5a5dL6b6cD7b"; + else + return "N02 C123H2i2g3e5c6b9b9e8g6h4i2i0 C037Lc07aLf09b P2i3e5c6b9b9e8g6h4i2i0"; + } + + public void run() { + new Options(); + } + + class Options implements DialogListener { + + Options() { + if (gd != null) { + gd.toFront(); + return; + } + options = this; + showDialog(); + } + + public void showDialog() { + Color color = Toolbar.getForegroundColor(); + String colorName = Colors.colorToString2(color); + String name = isPencil?"Pencil":"Brush"; + gd = GUI.newNonBlockingDialog(name+" Options"); + gd.addSlider(name+" width:", 1, 50, width); + //gd.addSlider("Transparency (%):", 0, 100, transparency); + gd.addChoice("Color:", Colors.getColors(colorName), colorName); + gd.addCheckbox("Paint on overlay", paintOnOverlay); + gd.addDialogListener(this); + gd.addHelp(getHelp()); + Point loc = Prefs.getLocation(LOC_KEY); + if (loc!=null) { + gd.centerDialog(false); + gd.setLocation (loc); + } + gd.showDialog(); + Prefs.saveLocation(LOC_KEY, gd.getLocation()); + gd = null; + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (e!=null && e.toString().contains("Undo")) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp!=null) IJ.run("Undo"); + return true; + } + width = (int)gd.getNextNumber(); + if (gd.invalidNumber() || width<0) + width = (int)Prefs.get(widthKey, 1); + //transparency = (int)gd.getNextNumber(); + //if (gd.invalidNumber() || transparency<0 || transparency>100) + // transparency = 100; + String colorName = gd.getNextChoice(); + paintOnOverlay = gd.getNextBoolean(); + Color color = Colors.decode(colorName, null); + Toolbar.setForegroundColor(color); + Prefs.set(widthKey, width); + Prefs.set(OVERLAY_KEY, paintOnOverlay); + return true; + } + } + + public static void setBrushWidth(int width) { + if (brushInstance!=null) { + Color c = Toolbar.getForegroundColor(); + brushInstance.setWidth(width); + Toolbar.setForegroundColor(c); + } + } + + private String getHelp() { + String ctrlString = IJ.isMacintosh()? "cmd":"ctrl"; + return + "" + +"" + +"Key modifiers" + +"

    " + +"
  • shift to draw horizontal or vertical lines
    " + +"
  • alt to draw in background color (or
    to erase if painting on overlay
    " + +"
  • "+ ctrlString+"-shift-drag to change "+(isPencil ? "pencil" : "brush")+" width
    " + +"
  • "+ ctrlString+"-click to change (\"pick up\") the
    " + +"drawing color, or use the Color
    Picker (shift-k)
    " + +"
" + +"Use Edit>Selection>Create Mask to create
a mask from the painted overlay. " + +"Use
Image>Overlay>Remove Overlay to remove
the painted overlay.
" + +"
" + +"
"; + } + + +} \ No newline at end of file diff --git a/src/ij/plugin/tool/MacroToolRunner.java b/src/ij/plugin/tool/MacroToolRunner.java new file mode 100644 index 0000000..a2cba66 --- /dev/null +++ b/src/ij/plugin/tool/MacroToolRunner.java @@ -0,0 +1,38 @@ +package ij.plugin.tool; +import ij.macro.Program; +import ij.plugin.MacroInstaller; + +public class MacroToolRunner extends PlugInTool { + MacroInstaller installer; + + public MacroToolRunner(MacroInstaller installer) { + this.installer = installer; + } + + public void runMacroTool(String name) { + if (installer!=null) + installer.runMacroTool(name); + } + + public void runMenuTool(String name, String command) { + if (installer!=null) + installer.runMenuTool(name, command); + } + + public Program getMacroProgram() { + if (installer!=null) + return installer.getProgram(); + else + return null; + } + + public int getMacroCount() { + if (installer!=null) + return installer.getMacroCount(); + else + return 0; + } + +} + + diff --git a/src/ij/plugin/tool/OverlayBrushTool.java b/src/ij/plugin/tool/OverlayBrushTool.java new file mode 100644 index 0000000..bec9b2b --- /dev/null +++ b/src/ij/plugin/tool/OverlayBrushTool.java @@ -0,0 +1,255 @@ +package ij.plugin.tool; +import ij.*; +import ij.process.*; +import ij.gui.*; +import ij.plugin.Colors; +import java.awt.*; +import java.awt.event.*; +import java.awt.BasicStroke; +import java.awt.geom.*; +import java.util.Vector; + +//Version history +// 2012-07-14 shift to confine horizontally or vertically, ctrl-shift to resize +// 2012-07-22 options allow width=0; width&transparency range checking, alt for BG, CTRL to pick color + +public class OverlayBrushTool extends PlugInTool implements Runnable { + private final static int UNCONSTRAINED=0, HORIZONTAL=1, VERTICAL=2, DO_RESIZE=3, RESIZED=4, IDLE=5; //mode flags + private static String WIDTH_KEY = "obrush.width"; + private static final String LOC_KEY = "obrush.loc"; + private static float width = (float)Prefs.get(WIDTH_KEY, 5); + private int transparency; + private BasicStroke stroke; + private GeneralPath path; + private int mode; //resizing brush or motion constrained horizontally or vertically + private float xStart, yStart; + private float oldWidth = width; + private boolean newPath; + private Options options; + private GenericDialog gd; + + public void mousePressed(ImagePlus imp, MouseEvent e) { + ImageCanvas ic = imp.getCanvas(); + float x = (float)ic.offScreenXD(e.getX()); + float y = (float)ic.offScreenYD(e.getY()); + xStart = x; + yStart = y; + oldWidth = width; + int ctrlMask = IJ.isMacintosh() ? InputEvent.META_MASK : InputEvent.CTRL_MASK; + int resizeMask = InputEvent.SHIFT_MASK | ctrlMask; + if ((e.getModifiers() & resizeMask) == resizeMask) { + mode = DO_RESIZE; + return; + } else if ((e.getModifiers() & ctrlMask) != 0) { //Pick the color from image or overlay + //Limitiation: no sub-pixel accuracy here. + //Don't use awt.robot to pick the color, it is influenced by screen color calibration + int[] rgbValues = imp.flatten().getPixel((int)x,(int)y); + Color color = new Color(rgbValues[0],rgbValues[1],rgbValues[2]); + boolean altKeyDown = (e.getModifiers() & InputEvent.ALT_MASK) != 0; + if (altKeyDown) + Toolbar.setBackgroundColor(color); + else { + Toolbar.setForegroundColor(color); + if (gd != null) + options.setColor(color); + } + mode = IDLE; + return; + } + mode = UNCONSTRAINED; //prepare drawing + path = new GeneralPath(); + path.moveTo(x, y); + newPath = true; + stroke = new BasicStroke(width, BasicStroke.CAP_ROUND/*CAP_BUTT*/, BasicStroke.JOIN_ROUND); + } + + public void mouseDragged(ImagePlus imp, MouseEvent e) { + if (mode == IDLE) return; + ImageCanvas ic = imp.getCanvas(); + float x = (float)ic.offScreenXD(e.getX()); + float y = (float)ic.offScreenYD(e.getY()); + Overlay overlay = imp.getOverlay(); + if (overlay==null) + overlay = new Overlay(); + if (mode == DO_RESIZE || mode == RESIZED) { + changeBrushSize((float)(x-xStart), imp); + return; + } + if ((e.getModifiers() & InputEvent.SHIFT_MASK) != 0) { //shift constrains + if (mode == UNCONSTRAINED) { //first movement with shift down determines direction + if (Math.abs(x-xStart) > Math.abs(y-yStart)) + mode = HORIZONTAL; + else if (Math.abs(x-xStart) < Math.abs(y-yStart)) + mode = VERTICAL; + else return; //constraint direction still unclear + } + if (mode == HORIZONTAL) + y = yStart; + else if (mode == VERTICAL) + x = xStart; + } else { + xStart = x; + yStart = y; + mode = UNCONSTRAINED; + } + path.lineTo(x, y); + ShapeRoi roi = new ShapeRoi(path); + boolean altKeyDown = (e.getModifiers() & InputEvent.ALT_MASK) != 0; + Color color = altKeyDown ? Toolbar.getBackgroundColor() : Toolbar.getForegroundColor(); + float red = (float)(color.getRed()/255.0); + float green = (float)(color.getGreen()/255.0); + float blue = (float)(color.getBlue()/255.0); + float alpha = (float)((100-transparency)/100.0); + roi.setStrokeColor(new Color(red, green, blue, alpha)); + roi.setStroke(stroke); + if (newPath) { + overlay.add(roi); + newPath = false; + } else { + overlay.remove(overlay.size()-1); + overlay.add(roi); + } + imp.setOverlay(overlay); + } + + public void mouseReleased(ImagePlus imp, MouseEvent e) { + if (mode == RESIZED) { + Overlay overlay = imp.getOverlay(); + overlay.remove(overlay.size()-1); //delete brush resizing circle + imp.setOverlay(overlay); + Prefs.set(WIDTH_KEY, width); + if (gd!=null) + options.setWidth(width); + } else if (newPath) // allow drawing a single dot + mouseDragged(imp, e); + } + + private void changeBrushSize(float deltaWidth, ImagePlus imp) { + if (deltaWidth!=0) { + Overlay overlay = imp.getOverlay(); + width = oldWidth + deltaWidth; + if (width < 0) width = 0; + Roi circle = new OvalRoi(xStart-width/2, yStart-width/2, width, width); + circle.setStrokeColor(Color.red); + overlay = imp.getOverlay(); + if (overlay==null) + overlay = new Overlay(); + if (mode == RESIZED) + overlay.remove(overlay.size()-1); + overlay.add(circle); + imp.setOverlay(overlay); + } + IJ.showStatus("Overlay Brush width: "+IJ.d2s(width)); + mode = RESIZED; + } + + public void showOptionsDialog() { + Thread thread = new Thread(this, "Brush Options"); + thread.setPriority(Thread.NORM_PRIORITY); + thread.start(); + } + + public String getToolName() { + return "Overlay Brush Tool"; + } + + public String getToolIcon() { + return "C037La077Ld098L6859L4a2fL2f4fL3f99L5e9bL9b98L6888L5e8dL888cC123P2f7f9ebdcaf70"; + } + + public void run() { + new Options(); + } + + public static void setWidth(double brushWidth) { + width = (float)brushWidth; + } + + + class Options implements DialogListener { + + Options() { + if (gd != null) { + gd.toFront(); + return; + } + options = this; + if (IJ.debugMode) IJ.log("Options: true"); + showDialog(); + } + + //set 'width' textfield and adjust scrollbar + void setWidth(float width) { + Vector numericFields = gd.getNumericFields(); + TextField widthField = (TextField)numericFields.elementAt(0); + widthField.setText(IJ.d2s(width,1)); + Vector sliders = gd.getSliders(); + Scrollbar sb = (Scrollbar)sliders.elementAt(0); + sb.setValue((int)(width+0.5f)); + } + + void setColor(Color c) { + String name = Colors.getColorName(c, ""); + if (name.length() > 0) { + Vector choices = gd.getChoices(); + Choice ch = (Choice)choices.elementAt(0); + ch.select(name); + } + } + + public void showDialog() { + Color color = Toolbar.getForegroundColor(); + String colorName = Colors.colorToString2(color); + gd = GUI.newNonBlockingDialog("Overlay Brush Options"); + gd.addSlider("Brush width:", 0, 50, width); + gd.addSlider("Transparency:", 0, 100, transparency); + gd.addChoice("Color:", Colors.getColors(colorName), colorName); + gd.setInsets(10, 0, 0); + String ctrlString = IJ.isMacintosh()? "CMD":"CTRL"; + gd.addMessage("SHIFT for horizontal or vertical lines\n"+ + "ALT to draw in background color\n"+ + ctrlString+"-SHIFT-drag to change brush width\n"+ + ctrlString+"-click to change foreground color\n", + null, Color.darkGray); + gd.hideCancelButton(); + gd.addHelp(""); + gd.setHelpLabel("Undo"); + gd.setOKLabel("Close"); + gd.addDialogListener(this); + Point loc = Prefs.getLocation(LOC_KEY); + if (loc!=null) { + gd.centerDialog(false); + gd.setLocation (loc); + } + gd.showDialog(); + Prefs.saveLocation(LOC_KEY, gd.getLocation()); + if (IJ.debugMode) IJ.log("Options: false"); + gd = null; + } + + public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { + if (e!=null && e.toString().contains("Undo")) { + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) return true; + Overlay overlay = imp.getOverlay(); + if (overlay!=null && overlay.size()>0) { + overlay.remove(overlay.size()-1); + imp.draw(); + } + return true; + } + width = (float)gd.getNextNumber(); + if (gd.invalidNumber() || width<0) + width = (float)Prefs.get(WIDTH_KEY, 5); + transparency = (int)gd.getNextNumber(); + if (gd.invalidNumber() || transparency<0 || transparency>100) + transparency = 100; + String colorName = gd.getNextChoice(); + Color color = Colors.decode(colorName, null); + Toolbar.setForegroundColor(color); + Prefs.set(WIDTH_KEY, width); + return true; + } + + } +} \ No newline at end of file diff --git a/src/ij/plugin/tool/PixelInspectionTool.java b/src/ij/plugin/tool/PixelInspectionTool.java new file mode 100644 index 0000000..d175df8 --- /dev/null +++ b/src/ij/plugin/tool/PixelInspectionTool.java @@ -0,0 +1,549 @@ +package ij.plugin.tool; +import ij.*; +import ij.plugin.frame.PlugInFrame; +import ij.process.*; +import ij.measure.*; +import ij.plugin.filter.Analyzer; +import ij.gui.*; +import ij.util.Tools; +import java.awt.*; +import java.awt.event.*; +import java.awt.datatransfer.*; +import java.awt.geom.*; + +/** + * This plugin continuously displays the pixel values of the cursor and + * its surroundings. It is usefule for examining how a filter changes the + * data (also during preview). + * + * If the Pixel Inspector Window is in the foreground, "c" with any modifier + * keys (CTRL-C etc) copies the current data into the clipboard (tab-delimited). + * The arrow keys nudge the position. + * + * Preferences (Press the Prefs button at top left): + * + * Radius determines the size of the window, 3x3 for radius=1, etc. + * The Pixel Inspector window must be closed and opened to get the new + * size. + * Readout for grayscale 8&16 bit images can be raw, calibrated or + * hexadecimal. + * Readout for RGB images can ge R,G,B triples, gray value or hexadecimal. + * For copying the data to clipboard, it can be selected whether the position + * (x,y) is not not written, written in the first line or in the same way + * as the header lines of the Pixel Inspector panel. + * + * Limitations and known problems: + * + * x and y coordinates are always uncalibrated pixel numbers. + * + * Some image operations do not update the display. + * + * Michael Schmid + * Version 2007-Dec-06 - bugs fixed: + * did not always follow cursor + * nudge could make the display hang + * pixel value calibration was sometimes ignored + * Version 2007-Dec-14 - supports exponential format for large/small data values + */ +public class PixelInspectionTool extends PlugInTool { + PixelInspector pi; + + public void mousePressed(ImagePlus imp, MouseEvent e) { + drawOutline(imp, e); + } + + public void mouseDragged(ImagePlus imp, MouseEvent e) { + drawOutline(imp, e); + } + + public void showOptionsDialog() { + if (pi!=null) pi.showDialog(); + } + + void drawOutline(ImagePlus imp, MouseEvent e) { + ImageCanvas ic = imp.getCanvas(); + int x = ic.offScreenX(e.getX()); + int y = ic.offScreenY(e.getY()); + int radius = PixelInspector.radius; + int size = radius*2+1; + Overlay overlay = imp.getOverlay(); + if (overlay==null) + overlay = new Overlay(); + Roi roi = null; + int index = PixelInspector.getIndex(overlay, PixelInspector.TITLE); + if (index>=0) { + roi = overlay.get(index); + Rectangle r = roi.getBounds(); + if (r.width!=size || r.height!=size) { + overlay.remove(index); + roi = null; + } + if (roi!=null) + roi.setLocation(x-radius, y-radius); + } + if (roi==null) { + roi = new Roi(x-radius, y-radius, size, size); + roi.setName(PixelInspector.TITLE); + roi.setStrokeColor(Color.red); + overlay.add(roi); + } + imp.setOverlay(overlay); + if (pi==null) { + if (PixelInspector.instance!=null) + PixelInspector.instance.close(); + pi = new PixelInspector(imp, this); + } + pi.update(imp, PixelInspector.POSITION_UPDATE, x, y); + } + + public String getToolName() { + return "Pixel Inspection Tool"; + } + + public String getToolIcon() { + return "Cb00T3b09PT8b09xC037L2e0cL0c02L0220L20d0Pd0f2fcde2e0BccP125665210"; + } + +} + + +class PixelInspector extends PlugInFrame + implements ImageListener, KeyListener, MouseListener, Runnable { + //ImageListener: listens to changes of image data + //KeyListener: for fix/unfix key + //MouseListener: for "Prefs" label + //Runnable: for background thread + + /* Preferences and related */ + static final String PREFS_KEY="pixelinspector."; //key in IJ_Prefs.txt + static int radius = (int)Prefs.get(PREFS_KEY+"radius", 3); + private static final String LOC_KEY = "inspector.loc"; + final static int MAX_RADIUS = 10;//the largest radius possible (ImageJ can hang if too large) + int grayDisplayType = 0; //how to display 8-bit&16-bit grayscale pixels + final static String[] GRAY_DISPLAY_TYPES = {"Raw","Calibrated","Hex"}; + final static int GRAY_RAW = 0, GRAY_CAL = 1, GRAY_HEX = 2; + int rgbDisplayType = 0; //how to display rgb pixels + final static String[] RGB_DISPLAY_TYPES = {"R,G,B","Gray Value","Hex"}; + final static int RGB_RGB = 0, RGB_GRAY = 1, RGB_HEX = 2; + int copyType = 0; //what to copy to the clipboard + final static String[] COPY_TYPES = {"Data Only","x y and Data","Header and Data"}; + final static int COPY_DATA = 0, COPY_XY = 1, COPY_HEADER = 2; + int colorNumber = 0; //color of the position marker in fixed mode + final static String[] COLOR_STRINGS = {"red","orange","yellow","green","cyan","blue","magenta",}; + final static Color[] COLORS = {Color.RED, Color.ORANGE, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA}; + int fixKey = '!'; //the key (keycode+0x10000 or char) for fixing/unfixing the position + final static int KEYCODE_OFFSET = 0x10000; //we add this to keycodes to separate them from key characters + /* current status */ + private int x0,y0; //the current position + int nextUpdate; //type of next update + final static int POSITION_UPDATE = 1, FULL_UPDATE = 2; + static final String TITLE = "Pixel Inspector"; + static PixelInspector instance; + PixelInspectionTool tool; + + ImageJ ij; + ImagePlus imp; //the ImagePlus that we listen to + int id; //the image ID + int bitDepth; //the image bit depth + int digits; //decimal fraction digits to display + boolean expMode; //whether to display the data in exp format + ImageCanvas canvas; //the canvas of imp + Thread bgThread; //thread for output (in the background) + Label[] labels; //the display fields + //Label prefsLabel = new Label("Prefs\u2026"); + Label prefsLabel = new Label("Prefs"); + + + /* Initialization, preparing the window (panel) **/ + public PixelInspector(ImagePlus imp, PixelInspectionTool tool) { + super("Pixel Values"); + instance = this; + this.imp = imp; + this.tool = tool; + ij = IJ.getInstance(); + if (ij == null) return; //it won't work with the ImageJ applet + if (imp==null) { + IJ.noImage(); return; + } + id = imp.getID(); + bitDepth = imp.getBitDepth(); + //setTitle("Pixels of "+imp.getTitle()); + WindowManager.addWindow(this); + //readPreferences(); + prefsLabel.addMouseListener(this); + addKeyListener(this); + init(); + Point loc = Prefs.getLocation(PREFS_KEY+"loc"); + if (loc!=null) + setLocation(loc); + else + GUI.centerOnImageJScreen(this); + setResizable(false); + show(); + toFront(); + addImageListeners(); + //thread for output in the background + bgThread = new Thread(this, "Pixel Inspector"); + bgThread.start(); + bgThread.setPriority(Math.max(bgThread.getPriority()-3, Thread.MIN_PRIORITY)); + update(FULL_UPDATE); //the first data display + } + + private void init() { + removeAll(); + int size = 2*radius+2; //number of columns and rows + labels = new Label[size*size]; + for (int i=1; i=0) { + overlay.remove(index); + imp.setOverlay(overlay); + } + } + + private void addImageListeners() { + imp.addImageListener(this); + ImageWindow win = imp.getWindow(); + if (win == null) close(); + canvas = win.getCanvas(); + canvas.addKeyListener(this); + } + + private void removeImageListeners() { + imp.removeImageListener(this); + canvas.removeKeyListener(this); + } + + //ImageListener + public void imageUpdated(ImagePlus imp) { update(FULL_UPDATE); } + public void imageOpened(ImagePlus imp) {} + public void imageClosed(ImagePlus imp) {} + + //KeyListener + public void keyPressed(KeyEvent e) { + boolean thisPanel = e.getSource() instanceof PixelInspector; + if (thisPanel && e.getKeyCode()==KeyEvent.VK_C) { + copyToClipboard(); + return; + } + if (e.getKeyCode()==KeyEvent.VK_UP && y0 > 0) { + y0--; update(FULL_UPDATE); + } else if (e.getKeyCode()==KeyEvent.VK_DOWN && y00) { + x0--; update(FULL_UPDATE); + } else if (e.getKeyCode()==KeyEvent.VK_RIGHT && x0=0) { + overlay.remove(index); + Roi roi = new Roi(x0-radius, y0-radius, radius*2+1, radius*2+1); + roi.setName(TITLE); + roi.setStrokeColor(Color.red); + overlay.add(roi); + imp.setOverlay(overlay); + } + } + + public void mousePressed(MouseEvent e) { + showDialog(); + } + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + public void mouseClicked(MouseEvent e) {} + public void mouseReleased(MouseEvent e) {} + + /** In the Overlay class in imageJ 1.46g and later. */ + static int getIndex(Overlay overlay, String name) { + if (name==null) return -1; + Roi[] rois = overlay.toArray(); + for (int i=rois.length-1; i>=0; i--) { + if (name.equals(rois[i].getName())) + return i; + } + return -1; + } + + public void keyReleased(KeyEvent e) {} + public void keyTyped(KeyEvent e) {} + + void update(ImagePlus imp, int whichUpdate, int x, int y) { + if (imp!=this.imp) { + removeImageListeners(); + removeOutline(); + this.imp = imp; + addImageListeners(); + //setTitle("Pixels of "+imp.getTitle()); + } + this.x0 = x; + this.y0 = y; + update(whichUpdate); + } + + synchronized void update(int whichUpdate) { + if (nextUpdate < whichUpdate) + nextUpdate = whichUpdate; + notify(); //wake up the background thread + } + + // the background thread for updating the table + public void run() { + boolean doFullUpdate = false; + while (true) { + if (doFullUpdate) { + setCalibration(); + } + writeNumbers(); + IJ.wait(50); + + synchronized(this) { + if (nextUpdate == 0) { + try {wait();} //notify wakes up the thread + catch(InterruptedException e) { //interrupted tells the thread to exit + return; + } + } else { + doFullUpdate = nextUpdate == FULL_UPDATE; + nextUpdate = 0; + } + } + } //while (true) + } + + /** get the surrounding pixels and display them */ + void writeNumbers() { + if (imp.getID()!=id || imp.getBitDepth()!=bitDepth) { //has the image changed? + removeImageListeners(); + addImageListeners(); + initializeLabels(); + this.pack(); + id = imp.getID(); + bitDepth = imp.getBitDepth(); + nextUpdate = FULL_UPDATE; + return; + } + ImageProcessor ip = imp.getProcessor(); + if (ip == null) return; + int width = ip.getWidth(); + int height = ip.getHeight(); + int x0 = this.x0; //class variables may change asynchronously, fixed values needed here + int y0 = this.y0; + int p = 1; //pointer in labels array + for (int x = x0-radius; x <= x0+radius; x++,p++) + labels[p].setText(x>=0&&x=0&&y=0&&x>16; + int g = (c&0xff00)>>8; + int b = c&0xff; + labels[p].setText(r+","+g+","+b); + } else if (ip instanceof ColorProcessor && rgbDisplayType == RGB_HEX) + labels[p].setText(int2hex(ip.getPixel(x,y),6)); + else if ((ip instanceof ByteProcessor || ip instanceof ShortProcessor) && grayDisplayType == GRAY_RAW) + labels[p].setText(Integer.toString(ip.getPixel(x,y))); + else if ((ip instanceof ByteProcessor || ip instanceof ShortProcessor) && grayDisplayType == GRAY_HEX) + labels[p].setText(int2hex(ip.getPixel(x,y), ip instanceof ByteProcessor ? 2 : 4)); + else + labels[p].setText(stringOf(ip.getPixelValue(x,y), digits, expMode)); + } else + labels[p].setText(" "); + } + } //for y + } + + /** initialize content of the labels to make sure we have enough space */ + void initializeLabels() { + Color bgColor = new Color(0xcccccc); //background for row/column header + String placeHolder = "000000.00"; //how much space to reserve (enough for float, calibrated, rgb) + ImageProcessor ip = imp.getProcessor(); + if (ip instanceof ByteProcessor && grayDisplayType==GRAY_RAW) { + placeHolder = "000"; + } else if (ip instanceof ByteProcessor || ip instanceof ShortProcessor) { + if (grayDisplayType == GRAY_RAW || grayDisplayType == GRAY_HEX) + placeHolder = "00000"; //minimum space, needed for header (max 99k pixels) + } else if (ip instanceof ColorProcessor) { + if (rgbDisplayType == RGB_RGB) + placeHolder = "000,000,000"; + if (rgbDisplayType == RGB_GRAY) + placeHolder = "000.00"; + else if (rgbDisplayType == RGB_HEX) + placeHolder = "CCCCCC"; + } + if (placeHolder.length()<5 && (ip.getWidth()>9999 || ip.getHeight()>9999)) + placeHolder = "00000"; + if (placeHolder.length()<4 && (ip.getWidth()>999 || ip.getHeight()>999)) + placeHolder = "0000"; + int p = 0; //pointer in labels array + int size = 2*radius+1; + for (int y = 0; y 0) //no label in top-left corner + labels[p].setText(placeHolder); + p++; + for (int x = 0; x7; + if (Math.min(minmax[0], minmax[1]) < 0) + digits--; //more space needed for minus sign + } else { + digits = 2; + expMode = false; + } + } + + /** Converts a number to a string in decimal or exp format. + * The number of digits is chosen to make the value fit into + * a cell the size of "000000.00" + */ + String stringOf(float v, int digits, boolean expMode) { + if (expMode) { + int exp = (int)Math.floor(Math.log(Math.abs(v))/Math.log(10)); + double mant = v/Math.pow(10,exp); + digits = (exp > 0 && exp < 10) ? 5 : 4; + if (v<0) digits--; //space needed for minus + return IJ.d2s(mant,digits)+"e"+exp; + } else + return IJ.d2s(v, digits); + } + + void copyToClipboard() { + final char delim = '\t'; + int size = 2*radius+1; + int p = 1; + StringBuffer sb = new StringBuffer(); + if (copyType == COPY_XY) { + sb.append(labels[radius+1].getText()); sb.append(delim); + sb.append(labels[(2*radius+2)*(radius+1)].getText()); sb.append('\n'); + } else if (copyType == COPY_HEADER) { + for (int x=0; x 0) + sb.append(delim); + sb.append(labels[p].getText()); + } + sb.append('\n'); + } + String s = new String(sb); + Clipboard clip = getToolkit().getSystemClipboard(); + if (clip==null) return; + StringSelection contents = new StringSelection(s); + clip.setContents(contents, contents); + IJ.showStatus(size*size+" pixel values copied to clipboard"); + } + + /** Preferences dialog */ + void showDialog() { + GenericDialog gd = new GenericDialog("Pixel Inspector Prefs..."); + gd.addNumericField("Radius:", radius, 0, 6, "(1-"+MAX_RADIUS+")"); + gd.addChoice("Grayscale readout:",GRAY_DISPLAY_TYPES,GRAY_DISPLAY_TYPES[grayDisplayType]); + gd.addChoice("RGB readout:",RGB_DISPLAY_TYPES,RGB_DISPLAY_TYPES[rgbDisplayType]); + gd.addChoice("Copy to clipboard:", COPY_TYPES, COPY_TYPES[copyType]); + gd.addMessage("Use arrow keys to move red outline.\nPress 'c' to copy data to clipboard.", null, Color.darkGray); + Point loc = Prefs.getLocation(LOC_KEY); + if (loc!=null) { + gd.centerDialog(false); + gd.setLocation (loc); + } + gd.showDialog(); + if (gd.wasCanceled()) + return; + radius = (int)gd.getNextNumber(); + if (radius<1) radius=1; + if (radius>MAX_RADIUS) radius=MAX_RADIUS; + grayDisplayType = gd.getNextChoiceIndex(); + rgbDisplayType = gd.getNextChoiceIndex(); + copyType = gd.getNextChoiceIndex(); + boolean keyOK = false; + init(); + update(POSITION_UPDATE); + Prefs.set(PREFS_KEY+"radius", radius); + Prefs.saveLocation(LOC_KEY, gd.getLocation()); + } + + static String int2hex(int i, int digits) { + boolean addHexSign = digits<6; + char[] buf = new char[addHexSign ? digits+1 : digits]; + for (int pos=buf.length-1; pos>=buf.length-digits; pos--) { + buf[pos] = Tools.hexDigits[i&0xf]; + i >>>= 4; + if (addHexSign) buf[0] = 'x'; + } + return new String(buf); + } +} diff --git a/src/ij/plugin/tool/PlugInTool.java b/src/ij/plugin/tool/PlugInTool.java new file mode 100644 index 0000000..0c282a0 --- /dev/null +++ b/src/ij/plugin/tool/PlugInTool.java @@ -0,0 +1,56 @@ +package ij.plugin.tool; +import ij.ImagePlus; +import ij.plugin.PlugIn; +import ij.macro.Program; +import ij.gui.Toolbar; +import java.awt.event.*; + +public abstract class PlugInTool implements PlugIn { + + public void run(String arg) { + Toolbar.addPlugInTool(this); + } + + public void mousePressed(ImagePlus imp, MouseEvent e) {e.consume();} + + public void mouseReleased(ImagePlus imp, MouseEvent e) {e.consume();} + + public void mouseClicked(ImagePlus imp, MouseEvent e) {e.consume();} + + public void mouseDragged(ImagePlus imp, MouseEvent e) {e.consume();} + + public void mouseMoved(ImagePlus imp, MouseEvent e) { } + + public void mouseEntered(ImagePlus imp, MouseEvent e) {e.consume();} + + public void mouseExited(ImagePlus imp, MouseEvent e) {e.consume();} + + public void showPopupMenu(MouseEvent e, Toolbar tb) { } + + /** Return the tool name. */ + public String getToolName() { + return getClass().getName().replace('_', ' '); + } + + /** Return the string encoding of the tool icon. See + http://rsb.info.nih.gov/ij/developer/macro/macros.html#icons + The default icon is the first letter of the tool name. + */ + public String getToolIcon() { + String letter = getToolName(); + if (letter!=null && letter.length()>0) + letter = letter.substring(0,1); + else + letter = "P"; + return "C037T5f16"+letter; + } + + public void showOptionsDialog() { + } + + /** These methods are overridden by MacroToolRunner. */ + public void runMacroTool(String name) { } + public void runMenuTool(String name, String command) { } + public Program getMacroProgram() {return null;} + +} diff --git a/src/ij/plugin/tool/RoiRotationTool.java b/src/ij/plugin/tool/RoiRotationTool.java new file mode 100644 index 0000000..7f00873 --- /dev/null +++ b/src/ij/plugin/tool/RoiRotationTool.java @@ -0,0 +1,146 @@ +/* + * This plugin implements the "Selection Rotator" tool, which + * can be used to interactively rotate selections. + * + * @author: Peter Haub, Oct. 2015, phaub@dipsystems.de + */ + +package ij.plugin.tool; +import ij.*; +import ij.gui.*; +import ij.plugin.RoiRotator; +import ij.plugin.tool.PlugInTool; +import ij.plugin.frame.Recorder; +import java.awt.*; +import java.awt.event.*; + +public class RoiRotationTool extends PlugInTool { + ImageCanvas ic = null; + int startX=0, startY=0; + Roi roi, newRoi; + int centerX, centerY, xNew, yNew, dx1, dy1, dx2, dy2; + double l, l1, l2, dx, dy, phi, phi1, phi2; + boolean isImageRoi; + Rectangle bounds; + ImagePlus imp2; + + static final int UPDOWNROTATION=0, CIRCULARROTATION=1; + int defaultRotationMode = CIRCULARROTATION; + + public void mousePressed(ImagePlus imp, MouseEvent e) { + if (imp == null) return; + imp2 = imp; + ic = imp.getCanvas(); + if (ic == null) return; + roi = imp.getRoi(); + if (roi==null) { + IJ.beep(); + IJ.showStatus("No selection"); + return; + } + + startX = ic.offScreenX(e.getX()); + startY = ic.offScreenY(e.getY()); + + if (defaultRotationMode == UPDOWNROTATION){ + centerX = imp.getWidth()/2; + centerY = imp.getHeight()/2; + } else { + double[] centroid = roi.getContourCentroid(); + centerX = (int)Math.round(centroid[0]); + centerY = (int)Math.round(centroid[1]); + } + } + + public void mouseDragged(ImagePlus imp, MouseEvent e) { + if (imp == null || ic == null) return; + roi = imp.getRoi(); + if (roi == null) return; + isImageRoi = roi instanceof ImageRoi; + if (isImageRoi) + ((ImageRoi)roi).setZeroTransparent(true); + + if ( e.isAltDown() || e.isShiftDown() ) + moveRoi(e.getX(), e.getY()); + else + rotateRoi(e.getX(), e.getY()); + } + + public void mouseReleased(ImagePlus imp, MouseEvent e) { + if (Recorder.record) { + Roi roi = imp.getRoi(); + int n = roi.getPolygon().npoints; + if (n<=20 && roi.getType()!=Roi.LINE) + Recorder.recordRoi(roi); + else if (n>20) + Recorder.recordString("// Selection has "+n+" points, too many to record.\n"); + } + } + + public void showOptionsDialog() { + IJ.log("PlugInTool MouseRoiRotator Peter Haub dipsystems.de 10'2015"); + } + + public String getToolName() { + return "Selection Rotator (press alt or shift to move)"; + } + + public String getToolIcon() { + return "C037D06D15D16D24D25D26D27D28D29D2aD33D34D35D36D37D3bD3cD42D43D44D45D46D47D48D4cD4dDb1Db2Db6Db7Db8Db9DbaDbbDbcDc2Dc3Dc7Dc8Dc9DcaDcbDd4Dd5Dd6Dd7Dd8Dd9DdaDe8De9Df8CabcD05D14D17D18D19D1aD23D2bD2cD32D3dD41D51D52D53D54D55D56D57D58Da6Da7Da8Da9DaaDabDacDadDbdDc1DccDd2Dd3DdbDe4De5De6De7DeaDf9"; + } + + void rotateRoi(int sx, int sy){ + xNew = ic.offScreenX(sx); + yNew = ic.offScreenY(sy); + + dx1 = centerX - xNew; + dy1 = centerY - yNew; + dx2 = centerX - startX; + dy2 = centerY - startY; + + if (defaultRotationMode == UPDOWNROTATION){ + l1 = Math.sqrt(dx1*dx1 + dy1*dy1); + l2 = Math.sqrt(dx2*dx2 + dy2*dy2); + l = (l1 + l2)/2.0; + dy = yNew - startY; + + if (l==0 || dy==0) return; + phi = Math.atan2(dy, l); + } + else{ + phi1 = Math.atan2(dy1, dx1); + phi2 = Math.atan2(dy2, dx2); + phi = phi1 - phi2; + if (phi == 0 || phi == Double.NaN) return; + } + + startX = xNew; + startY = yNew; + + newRoi = RoiRotator.rotate(roi, phi*180/Math.PI); + if (isImageRoi) + imp2.draw(); + else + imp2.setRoi(newRoi); + } + + void moveRoi(int sx, int sy){ + xNew = ic.offScreenX(sx); + yNew = ic.offScreenY(sy); + + dx1 = xNew - startX; + dy1 = yNew - startY; + + if (dx1==0 && dy2==0) return; + startX = xNew; + startY = yNew; + + dx = roi.getXBase() + dx1; + dy = roi.getYBase() + dy1; + roi.setLocation(dx, dy); + + imp2.draw(); + } + +} + diff --git a/src/ij/process/AutoThresholder.java b/src/ij/process/AutoThresholder.java new file mode 100644 index 0000000..55fe946 --- /dev/null +++ b/src/ij/process/AutoThresholder.java @@ -0,0 +1,1246 @@ +package ij.process; +import ij.IJ; +import java.util.Arrays; + +/** Autothresholding methods (limited to 256 bin histograms) from the Auto_Threshold plugin + (http://fiji.sc/Auto_Threshold) by G.Landini at bham dot ac dot uk). */ +public class AutoThresholder { + private static String[] mStrings; + + public enum Method { + Default, + Huang, + Intermodes, + IsoData, + IJ_IsoData, + Li, + MaxEntropy, + Mean, + MinError, + Minimum, + Moments, + Otsu, + Percentile, + RenyiEntropy, + Shanbhag, + Triangle, + Yen + }; + + public static String[] getMethods() { + if (mStrings==null) { + Method[] mVals = Method.values(); + mStrings = new String[mVals.length]; + for (int i=0; i= first_bin; ih-- ) { + if ( data[ih] != 0 ) { + last_bin = ih; + break; + } + } + term = 1.0 / ( double ) ( last_bin - first_bin ); + double [] mu_0 = new double[256]; + sum_pix = num_pix = 0; + for ( ih = first_bin; ih < 256; ih++ ){ + sum_pix += (double)ih * data[ih]; + num_pix += data[ih]; + /* NUM_PIX cannot be zero ! */ + mu_0[ih] = sum_pix / num_pix; + } + + double [] mu_1 = new double[256]; + sum_pix = num_pix = 0; + for ( ih = last_bin; ih > 0; ih-- ){ + sum_pix += (double)ih * data[ih]; + num_pix += data[ih]; + /* NUM_PIX cannot be zero ! */ + mu_1[ih - 1] = sum_pix / ( double ) num_pix; + } + + /* Determine the threshold that minimizes the fuzzy entropy */ + threshold = -1; + min_ent = Double.MAX_VALUE; + for ( it = 0; it < 256; it++ ){ + ent = 0.0; + for ( ih = 0; ih <= it; ih++ ) { + /* Equation (4) in Ref. 1 */ + mu_x = 1.0 / ( 1.0 + term * Math.abs ( ih - mu_0[it] ) ); + if ( !((mu_x < 1e-06 ) || ( mu_x > 0.999999))) { + /* Equation (6) & (8) in Ref. 1 */ + ent += data[ih] * ( -mu_x * Math.log ( mu_x ) - ( 1.0 - mu_x ) * Math.log ( 1.0 - mu_x ) ); + } + } + + for ( ih = it + 1; ih < 256; ih++ ) { + /* Equation (4) in Ref. 1 */ + mu_x = 1.0 / ( 1.0 + term * Math.abs ( ih - mu_1[it] ) ); + if ( !((mu_x < 1e-06 ) || ( mu_x > 0.999999))) { + /* Equation (6) & (8) in Ref. 1 */ + ent += data[ih] * ( -mu_x * Math.log ( mu_x ) - ( 1.0 - mu_x ) * Math.log ( 1.0 - mu_x ) ); + } + } + /* No need to divide by NUM_ROWS * NUM_COLS * LOG(2) ! */ + if ( ent < min_ent ) { + min_ent = ent; + threshold = it; + } + } + return threshold; + } + + boolean bimodalTest(double [] y) { + int len=y.length; + boolean b = false; + int modes = 0; + + for (int k=1;k2) + return false; + } + } + if (modes == 2) + b = true; + return b; + } + + int Intermodes(int[] data ) { + // J. M. S. Prewitt and M. L. Mendelsohn, "The analysis of cell images," in + // Annals of the New York Academy of Sciences, vol. 128, pp. 1035-1053, 1966. + // ported to ImageJ plugin by G.Landini from Antti Niemisto's Matlab code (GPL) + // Original Matlab code Copyright (C) 2004 Antti Niemisto + // See http://www.cs.tut.fi/~ant/histthresh/ for an excellent slide presentation + // and the original Matlab code. + // + // Assumes a bimodal histogram. The histogram needs is smoothed (using a + // running average of size 3, iteratively) until there are only two local maxima. + // j and k + // Threshold t is (j+k)/2. + // Images with histograms having extremely unequal peaks or a broad and + // flat valleys are unsuitable for this method. + + int minbin=-1, maxbin=-1; + for (int i=0; i0) maxbin = i; + for (int i=data.length-1; i>=0; i--) + if (data[i]>0) minbin = i; + int length = (maxbin-minbin)+1; + double [] hist = new double[length]; + for (int i=minbin; i<=maxbin; i++) + hist[i-minbin] = data[i]; + + int iter = 0; + int threshold=-1; + while (!bimodalTest(hist) ) { + //smooth with a 3 point running mean filter + double previous=0, current=0, next=hist[0]; + for (int i=0; i10000) { + threshold = -1; + IJ.log("Intermodes Threshold not found after 10000 iterations."); + return threshold; + } + } + + // The threshold is the mean between the two peaks. + int tt=0; + for (int i=1; i G + // is G = (L + H)/2? + // yes => exit + // no => increment G and repeat + // + int i, l, totl, g=0; + double toth, h; + for (i = 1; i < 256; i++) { + if (data[i] > 0){ + g = i + 1; + break; + } + } + while (true){ + l = 0; + totl = 0; + for (i = 0; i < g; i++) { + totl = totl + data[i]; + l = l + (data[i] * i); + } + h = 0; + toth = 0; + for (i = g + 1; i < 256; i++){ + toth += data[i]; + h += ((double)data[i]*i); + } + if (totl > 0 && toth > 0){ + l /= totl; + h /= toth; + if (g == (int) Math.round((l + h) / 2.0)) + break; + } + g++; + if (g > 254) + return -1; + } + return g; + } + + int defaultIsoData(int[] data) { + // This is the modified IsoData method used by the "Threshold" widget in "Default" mode + int n = data.length; + int[] data2 = new int[n]; + int mode=0, maxCount=0; + for (int i=0; imaxCount) { + maxCount = data2[i]; + mode = i; + } + } + int maxCount2 = 0; + for (int i = 0; imaxCount2) && (i!=mode)) + maxCount2 = data2[i]; + } + int hmax = maxCount; + if ((hmax>(maxCount2*2)) && (maxCount2!=0)) { + hmax = (int)(maxCount2 * 1.5); + data2[mode] = hmax; + } + return IJIsoData(data2); + } + + int IJIsoData(int[] data) { + // This is the original ImageJ IsoData implementation, here for backward compatibility. + int level; + int maxValue = data.length - 1; + double result, sum1, sum2, sum3, sum4; + int count0 = data[0]; + data[0] = 0; //set to zero so erased areas aren't included + int countMax = data[maxValue]; + data[maxValue] = 0; + int min = 0; + while ((data[min]==0) && (min0)) + max--; + if (min>=max) { + data[0]= count0; data[maxValue]=countMax; + level = data.length/2; + return level; + } + int movingIndex = min; + int inc = Math.max(max/40, 1); + do { + sum1=sum2=sum3=sum4=0.0; + for (int i=min; i<=movingIndex; i++) { + sum1 += (double)i*data[i]; + sum2 += data[i]; + } + for (int i=(movingIndex+1); i<=max; i++) { + sum3 += (double)i*data[i]; + sum4 += data[i]; + } + result = (sum1/sum2 + sum3/sum4)/2.0; + movingIndex++; + } while ((movingIndex+1)<=result && movingIndex tolerance ); + return threshold; + } + + int MaxEntropy(int [] data ) { + // Implements Kapur-Sahoo-Wong (Maximum Entropy) thresholding method + // Kapur J.N., Sahoo P.K., and Wong A.K.C. (1985) "A New Method for + // Gray-Level Picture Thresholding Using the Entropy of the Histogram" + // Graphical Models and Image Processing, 29(3): 273-285 + // M. Emre Celebi + // 06.15.2007 + // Ported to ImageJ plugin by G.Landini from E Celebi's fourier_0.8 routines + int threshold=-1; + int ih, it; + int first_bin; + int last_bin; + double tot_ent; /* total entropy */ + double max_ent; /* max entropy */ + double ent_back; /* entropy of the background pixels at a given threshold */ + double ent_obj; /* entropy of the object pixels at a given threshold */ + double [] norm_histo = new double[256]; /* normalized histogram */ + double [] P1 = new double[256]; /* cumulative normalized histogram */ + double [] P2 = new double[256]; + + double total =0; + for (ih = 0; ih < 256; ih++ ) + total+=data[ih]; + + for (ih = 0; ih < 256; ih++ ) + norm_histo[ih] = data[ih]/total; + + P1[0]=norm_histo[0]; + P2[0]=1.0-P1[0]; + for (ih = 1; ih < 256; ih++ ){ + P1[ih]= P1[ih-1] + norm_histo[ih]; + P2[ih]= 1.0 - P1[ih]; + } + + /* Determine the first non-zero bin */ + first_bin=0; + for (ih = 0; ih < 256; ih++ ) { + if ( !(Math.abs(P1[ih])<2.220446049250313E-16)) { + first_bin = ih; + break; + } + } + + /* Determine the last non-zero bin */ + last_bin=255; + for (ih = 255; ih >= first_bin; ih-- ) { + if ( !(Math.abs(P2[ih])<2.220446049250313E-16)) { + last_bin = ih; + break; + } + } + + // Calculate the total entropy each gray-level + // and find the threshold that maximizes it + max_ent = Double.MIN_VALUE; + + for ( it = first_bin; it <= last_bin; it++ ) { + /* Entropy of the background pixels */ + ent_back = 0.0; + for ( ih = 0; ih <= it; ih++ ) { + if ( data[ih] !=0 ) { + ent_back -= ( norm_histo[ih] / P1[it] ) * Math.log ( norm_histo[ih] / P1[it] ); + } + } + + /* Entropy of the object pixels */ + ent_obj = 0.0; + for ( ih = it + 1; ih < 256; ih++ ){ + if (data[ih]!=0){ + ent_obj -= ( norm_histo[ih] / P2[it] ) * Math.log ( norm_histo[ih] / P2[it] ); + } + } + + /* Total entropy */ + tot_ent = ent_back + ent_obj; + + // IJ.log(""+max_ent+" "+tot_ent); + if ( max_ent < tot_ent ) { + max_ent = tot_ent; + threshold = it; + } + } + return threshold; + } + + int Mean(int [] data ) { + // C. A. Glasbey, "An analysis of histogram-based thresholding algorithms," + // CVGIP: Graphical Models and Image Processing, vol. 55, pp. 532-537, 1993. + // + // The threshold is the mean of the greyscale data + int threshold = -1; + double tot=0, sum=0; + for (int i=0; i<256; i++){ + tot+= data[i]; + sum+=((double)i*data[i]); + } + threshold =(int) Math.floor(sum/tot); + return threshold; + } + + int MinErrorI(int [] data ) { + // Kittler and J. Illingworth, "Minimum error thresholding," Pattern Recognition, vol. 19, pp. 41-47, 1986. + // C. A. Glasbey, "An analysis of histogram-based thresholding algorithms," CVGIP: Graphical Models and Image Processing, vol. 55, pp. 532-537, 1993. + // Ported to ImageJ plugin by G.Landini from Antti Niemisto's Matlab code (GPL) + // Original Matlab code Copyright (C) 2004 Antti Niemisto + // See http://www.cs.tut.fi/~ant/histthresh/ for an excellent slide presentation + // and the original Matlab code. + + int threshold = Mean(data); //Initial estimate for the threshold is found with the MEAN algorithm. + int Tprev =-2; + double mu, nu, p, q, sigma2, tau2, w0, w1, w2, sqterm, temp; + //int counter=1; + while (threshold!=Tprev){ + //Calculate some statistics. + mu = B(data, threshold)/A(data, threshold); + nu = (B(data, data.length - 1)-B(data, threshold))/(A(data, data.length - 1)-A(data, threshold)); + p = A(data, threshold)/A(data, data.length - 1); + q = (A(data, data.length - 1)-A(data, threshold)) / A(data, data.length - 1); + sigma2 = C(data, threshold)/A(data, threshold)-(mu*mu); + tau2 = (C(data, data.length - 1)-C(data, threshold)) / (A(data, data.length - 1)-A(data, threshold)) - (nu*nu); + + //The terms of the quadratic equation to be solved. + w0 = 1.0/sigma2-1.0/tau2; + w1 = mu/sigma2-nu/tau2; + w2 = (mu*mu)/sigma2 - (nu*nu)/tau2 + Math.log10((sigma2*(q*q))/(tau2*(p*p))); + + //If the next threshold would be imaginary, return with the current one. + sqterm = (w1*w1)-w0*w2; + if (sqterm < 0) { + IJ.log("MinError(I): not converging."); + return threshold; + } + + //The updated threshold is the integer part of the solution of the quadratic equation. + Tprev = threshold; + temp = (w1+Math.sqrt(sqterm))/w0; + + if (Double.isNaN(temp)) + threshold = Tprev; + else + threshold =(int) Math.floor(temp); + } + return threshold; + } + + private double A(int[] y, int j) { + if (j>=y.length) j=y.length-1; + double x = 0; + for (int i=0;i<=j;i++) + x+=y[i]; + return x; + } + + private double B(int[] y, int j) { + if (j>=y.length) j=y.length-1; + double x = 0; + for (int i=0;i<=j;i++) + x+=i*y[i]; + return x; + } + + private double C(int[] y, int j) { + if (j>=y.length) j=y.length-1; + double x = 0; + for (int i=0;i<=j;i++) + x+=i*i*y[i]; + return x; + } + + int Minimum(int [] data ) { + // J. M. S. Prewitt and M. L. Mendelsohn, "The analysis of cell images," in + // Annals of the New York Academy of Sciences, vol. 128, pp. 1035-1053, 1966. + // ported to ImageJ plugin by G.Landini from Antti Niemisto's Matlab code (GPL) + // Original Matlab code Copyright (C) 2004 Antti Niemisto + // See http://www.cs.tut.fi/~ant/histthresh/ for an excellent slide presentation + // and the original Matlab code. + // + // Assumes a bimodal histogram. The histogram needs is smoothed (using a + // running average of size 3, iteratively) until there are only two local maxima. + // Threshold t is such that yt-1 > yt <= yt+1. + // Images with histograms having extremely unequal peaks or a broad and + // flat valleys are unsuitable for this method. + int iter =0; + int threshold = -1; + double [] iHisto = new double [256]; + for (int i=0; i<256; i++) + iHisto[i]=(double) data[i]; + double [] tHisto = new double[iHisto.length] ; + + while (!bimodalTest(iHisto) ) { + //smooth with a 3 point running mean filter + for (int i=1; i<255; i++) + tHisto[i]= (iHisto[i-1] + iHisto[i] +iHisto[i+1])/3; + tHisto[0] = (iHisto[0]+iHisto[1])/3; //0 outside + tHisto[255] = (iHisto[254]+iHisto[255])/3; //0 outside + System.arraycopy(tHisto, 0, iHisto, 0, iHisto.length) ; + iter++; + if (iter>10000) { + threshold = -1; + IJ.log("Minimum: threshold not found after 10000 iterations."); + return threshold; + } + } + // The threshold is the minimum between the two peaks. + for (int i=1; i<255; i++) { + if (iHisto[i-1] > iHisto[i] && iHisto[i+1] >= iHisto[i]) { + threshold = i; + break; + } + } + return threshold; + } + + int Moments(int [] data ) { + // W. Tsai, "Moment-preserving thresholding: a new approach," Computer Vision, + // Graphics, and Image Processing, vol. 29, pp. 377-393, 1985. + // Ported to ImageJ plugin by G.Landini from the the open source project FOURIER 0.8 + // by M. Emre Celebi , Department of Computer Science, Louisiana State University in Shreveport + // Shreveport, LA 71115, USA + // http://sourceforge.net/projects/fourier-ipal + // http://www.lsus.edu/faculty/~ecelebi/fourier.htm + double total =0; + double m0=1.0, m1=0.0, m2 =0.0, m3 =0.0, sum =0.0, p0=0.0; + double cd, c0, c1, z0, z1; /* auxiliary variables */ + int threshold = -1; + + double [] histo = new double [256]; + + for (int i=0; i<256; i++) + total+=data[i]; + + for (int i=0; i<256; i++) + histo[i]=(double)(data[i]/total); //normalised histogram + + /* Calculate the first, second, and third order moments */ + for ( int i = 0; i < 256; i++ ) { + double di = i; + m1 += di * histo[i]; + m2 += di * di * histo[i]; + m3 += di * di * di * histo[i]; + } + /* + First 4 moments of the gray-level image should match the first 4 moments + of the target binary image. This leads to 4 equalities whose solutions + are given in the Appendix of Ref. 1 + */ + cd = m0 * m2 - m1 * m1; + c0 = ( -m2 * m2 + m1 * m3 ) / cd; + c1 = ( m0 * -m3 + m2 * m1 ) / cd; + z0 = 0.5 * ( -c1 - Math.sqrt ( c1 * c1 - 4.0 * c0 ) ); + z1 = 0.5 * ( -c1 + Math.sqrt ( c1 * c1 - 4.0 * c0 ) ); + p0 = ( z1 - m1 ) / ( z1 - z0 ); /* Fraction of the object pixels in the target binary image */ + + // The threshold is the gray-level closest + // to the p0-tile of the normalized histogram + sum=0; + for (int i=0; i<256; i++){ + sum+=histo[i]; + if (sum>p0) { + threshold = i; + break; + } + } + return threshold; + } + + int Otsu(int [] data ) { + // Otsu's threshold algorithm + // C++ code by Jordan Bevik + // ported to ImageJ plugin by G.Landini + int k,kStar; // k = the current threshold; kStar = optimal threshold + double N1, N; // N1 = # points with intensity <=k; N = total number of points + double BCV, BCVmax; // The current Between Class Variance and maximum BCV + double num, denom; // temporary bookeeping + double Sk; // The total intensity for all histogram points <=k + double S, L=256; // The total intensity of the image + + // Initialize values: + S = N = 0; + for (k=0; k= BCVmax){ // Assign the best threshold found so far + BCVmax = BCV; + kStar = k; + } + } + // kStar += 1; // Use QTI convention that intensity -> 1 if intensity >= k + // (the algorithm was developed for I-> 1 if I <= k.) + return kStar; + } + + + int Percentile(int [] data ) { + // W. Doyle, "Operation useful for similarity-invariant pattern recognition," + // Journal of the Association for Computing Machinery, vol. 9,pp. 259-267, 1962. + // ported to ImageJ plugin by G.Landini from Antti Niemisto's Matlab code (GPL) + // Original Matlab code Copyright (C) 2004 Antti Niemisto + // See http://www.cs.tut.fi/~ant/histthresh/ for an excellent slide presentation + // and the original Matlab code. + + int iter =0; + int threshold = -1; + double ptile= 0.5; // default fraction of foreground pixels + double [] avec = new double [256]; + + for (int i=0; i<256; i++) + avec[i]=0.0; + + double total =partialSum(data, 255); + double temp = 1.0; + for (int i=0; i<256; i++){ + avec[i]=Math.abs((partialSum(data, i)/total)-ptile); + //IJ.log("Ptile["+i+"]:"+ avec[i]); + if (avec[i]= first_bin; ih-- ) { + if ( !(Math.abs(P2[ih])<2.220446049250313E-16)) { + last_bin = ih; + break; + } + } + + /* Maximum Entropy Thresholding - BEGIN */ + /* ALPHA = 1.0 */ + /* Calculate the total entropy each gray-level + and find the threshold that maximizes it + */ + threshold =0; // was MIN_INT in original code, but if an empty image is processed it gives an error later on. + max_ent = 0.0; + + for ( it = first_bin; it <= last_bin; it++ ) { + /* Entropy of the background pixels */ + ent_back = 0.0; + for ( ih = 0; ih <= it; ih++ ) { + if ( data[ih] !=0 ) { + ent_back -= ( norm_histo[ih] / P1[it] ) * Math.log ( norm_histo[ih] / P1[it] ); + } + } + + /* Entropy of the object pixels */ + ent_obj = 0.0; + for ( ih = it + 1; ih < 256; ih++ ){ + if (data[ih]!=0){ + ent_obj -= ( norm_histo[ih] / P2[it] ) * Math.log ( norm_histo[ih] / P2[it] ); + } + } + + /* Total entropy */ + tot_ent = ent_back + ent_obj; + + // IJ.log(""+max_ent+" "+tot_ent); + + if ( max_ent < tot_ent ) { + max_ent = tot_ent; + threshold = it; + } + } + t_star2 = threshold; + + /* Maximum Entropy Thresholding - END */ + threshold =0; //was MIN_INT in original code, but if an empty image is processed it gives an error later on. + max_ent = 0.0; + alpha = 0.5; + term = 1.0 / ( 1.0 - alpha ); + for ( it = first_bin; it <= last_bin; it++ ) { + /* Entropy of the background pixels */ + ent_back = 0.0; + for ( ih = 0; ih <= it; ih++ ) + ent_back += Math.sqrt ( norm_histo[ih] / P1[it] ); + + /* Entropy of the object pixels */ + ent_obj = 0.0; + for ( ih = it + 1; ih < 256; ih++ ) + ent_obj += Math.sqrt ( norm_histo[ih] / P2[it] ); + + /* Total entropy */ + tot_ent = term * ( ( ent_back * ent_obj ) > 0.0 ? Math.log ( ent_back * ent_obj ) : 0.0); + + if ( tot_ent > max_ent ){ + max_ent = tot_ent; + threshold = it; + } + } + + t_star1 = threshold; + + threshold = 0; //was MIN_INT in original code, but if an empty image is processed it gives an error later on. + max_ent = 0.0; + alpha = 2.0; + term = 1.0 / ( 1.0 - alpha ); + for ( it = first_bin; it <= last_bin; it++ ) { + /* Entropy of the background pixels */ + ent_back = 0.0; + for ( ih = 0; ih <= it; ih++ ) + ent_back += ( norm_histo[ih] * norm_histo[ih] ) / ( P1[it] * P1[it] ); + + /* Entropy of the object pixels */ + ent_obj = 0.0; + for ( ih = it + 1; ih < 256; ih++ ) + ent_obj += ( norm_histo[ih] * norm_histo[ih] ) / ( P2[it] * P2[it] ); + + /* Total entropy */ + tot_ent = term *( ( ent_back * ent_obj ) > 0.0 ? Math.log(ent_back * ent_obj ): 0.0 ); + + if ( tot_ent > max_ent ){ + max_ent = tot_ent; + threshold = it; + } + } + + t_star3 = threshold; + + /* Sort t_star values */ + if ( t_star2 < t_star1 ){ + tmp_var = t_star1; + t_star1 = t_star2; + t_star2 = tmp_var; + } + if ( t_star3 < t_star2 ){ + tmp_var = t_star2; + t_star2 = t_star3; + t_star3 = tmp_var; + } + if ( t_star2 < t_star1 ) { + tmp_var = t_star1; + t_star1 = t_star2; + t_star2 = tmp_var; + } + + /* Adjust beta values */ + if ( Math.abs ( t_star1 - t_star2 ) <= 5 ) { + if ( Math.abs ( t_star2 - t_star3 ) <= 5 ) { + beta1 = 1; + beta2 = 2; + beta3 = 1; + } + else { + beta1 = 0; + beta2 = 1; + beta3 = 3; + } + } + else { + if ( Math.abs ( t_star2 - t_star3 ) <= 5 ) { + beta1 = 3; + beta2 = 1; + beta3 = 0; + } + else { + beta1 = 1; + beta2 = 2; + beta3 = 1; + } + } + //IJ.log(""+t_star1+" "+t_star2+" "+t_star3); + /* Determine the optimal threshold value */ + omega = P1[t_star3] - P1[t_star1]; + opt_threshold = (int) (t_star1 * ( P1[t_star1] + 0.25 * omega * beta1 ) + 0.25 * t_star2 * omega * beta2 + t_star3 * ( P2[t_star3] + 0.25 * omega * beta3 )); + + return opt_threshold; + } + + + int Shanbhag(int [] data ) { + // Shanhbag A.G. (1994) "Utilization of Information Measure as a Means of + // Image Thresholding" Graphical Models and Image Processing, 56(5): 414-419 + // Ported to ImageJ plugin by G.Landini from E Celebi's fourier_0.8 routines + int threshold; + int ih, it; + int first_bin; + int last_bin; + double term; + double tot_ent; /* total entropy */ + double min_ent; /* max entropy */ + double ent_back; /* entropy of the background pixels at a given threshold */ + double ent_obj; /* entropy of the object pixels at a given threshold */ + double [] norm_histo = new double[256]; /* normalized histogram */ + double [] P1 = new double[256]; /* cumulative normalized histogram */ + double [] P2 = new double[256]; + + double total =0; + for (ih = 0; ih < 256; ih++ ) + total+=data[ih]; + + for (ih = 0; ih < 256; ih++ ) + norm_histo[ih] = data[ih]/total; + + P1[0]=norm_histo[0]; + P2[0]=1.0-P1[0]; + for (ih = 1; ih < 256; ih++ ){ + P1[ih]= P1[ih-1] + norm_histo[ih]; + P2[ih]= 1.0 - P1[ih]; + } + + /* Determine the first non-zero bin */ + first_bin=0; + for (ih = 0; ih < 256; ih++ ) { + if ( !(Math.abs(P1[ih])<2.220446049250313E-16)) { + first_bin = ih; + break; + } + } + + /* Determine the last non-zero bin */ + last_bin=255; + for (ih = 255; ih >= first_bin; ih-- ) { + if ( !(Math.abs(P2[ih])<2.220446049250313E-16)) { + last_bin = ih; + break; + } + } + + // Calculate the total entropy each gray-level + // and find the threshold that maximizes it + threshold =-1; + min_ent = Double.MAX_VALUE; + + for ( it = first_bin; it <= last_bin; it++ ) { + /* Entropy of the background pixels */ + ent_back = 0.0; + term = 0.5 / P1[it]; + for ( ih = 1; ih <= it; ih++ ) { //0+1? + ent_back -= norm_histo[ih] * Math.log ( 1.0 - term * P1[ih - 1] ); + } + ent_back *= term; + + /* Entropy of the object pixels */ + ent_obj = 0.0; + term = 0.5 / P2[it]; + for ( ih = it + 1; ih < 256; ih++ ){ + ent_obj -= norm_histo[ih] * Math.log ( 1.0 - term * P2[ih] ); + } + ent_obj *= term; + + /* Total entropy */ + tot_ent = Math.abs ( ent_back - ent_obj ); + + if ( tot_ent < min_ent ) { + min_ent = tot_ent; + threshold = it; + } + } + return threshold; + } + + + int Triangle(int [] data ) { + // Zack, G. W., Rogers, W. E. and Latt, S. A., 1977, + // Automatic Measurement of Sister Chromatid Exchange Frequency, + // Journal of Histochemistry and Cytochemistry 25 (7), pp. 741-753 + // + // modified from Johannes Schindelin plugin + // + // find min and max + int min = 0, dmax=0, max = 0, min2=0; + for (int i = 0; i < data.length; i++) { + if (data[i]>0){ + min=i; + break; + } + } + if (min>0) min--; // line to the (p==0) point, not to data[min] + + // The Triangle algorithm cannot tell whether the data is skewed to one side or another. + // This causes a problem as there are 2 possible thresholds between the max and the 2 extremes + // of the histogram. + // Here I propose to find out to which side of the max point the data is furthest, and use that as + // the other extreme. + for (int i = 255; i >0; i-- ) { + if (data[i]>0){ + min2=i; + break; + } + } + if (min2<255) min2++; // line to the (p==0) point, not to data[min] + + for (int i =0; i < 256; i++) { + if (data[i] >dmax) { + max=i; + dmax=data[i]; + } + } + // find which is the furthest side + //IJ.log(""+min+" "+max+" "+min2); + boolean inverted = false; + if ((max-min)<(min2-max)){ + // reverse the histogram + //IJ.log("Reversing histogram."); + inverted = true; + int left = 0; // index of leftmost element + int right = 255; // index of rightmost element + while (left < right) { + // exchange the left and right elements + int temp = data[left]; + data[left] = data[right]; + data[right] = temp; + // move the bounds toward the center + left++; + right--; + } + min=255-min2; + max=255-max; + } + + if (min == max){ + //IJ.log("Triangle: min == max."); + return min; + } + + // describe line by nx * x + ny * y - d = 0 + double nx, ny, d; + // nx is just the max frequency as the other point has freq=0 + nx = data[max]; //-min; // data[min]; // lowest value bmin = (p=0)% in the image + ny = min - max; + d = Math.sqrt(nx * nx + ny * ny); + nx /= d; + ny /= d; + d = nx * min + ny * data[min]; + + // find split point + int split = min; + double splitDistance = 0; + for (int i = min + 1; i <= max; i++) { + double newDistance = nx * i + ny * data[i] - d; + if (newDistance > splitDistance) { + split = i; + splitDistance = newDistance; + } + } + split--; + + if (inverted) { + // The histogram might be used for something else, so let's reverse it back + int left = 0; + int right = 255; + while (left < right) { + int temp = data[left]; + data[left] = data[right]; + data[right] = temp; + left++; + right--; + } + return (255-split); + } + else + return split; + } + + + int Yen(int [] data ) { + // Implements Yen thresholding method + // 1) Yen J.C., Chang F.J., and Chang S. (1995) "A New Criterion + // for Automatic Multilevel Thresholding" IEEE Trans. on Image + // Processing, 4(3): 370-378 + // 2) Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding + // Techniques and Quantitative Performance Evaluation" Journal of + // Electronic Imaging, 13(1): 146-165 + // http://citeseer.ist.psu.edu/sezgin04survey.html + // + // M. Emre Celebi + // 06.15.2007 + // Ported to ImageJ plugin by G.Landini from E Celebi's fourier_0.8 routines + int threshold; + int ih, it; + double crit; + double max_crit; + double [] norm_histo = new double[256]; /* normalized histogram */ + double [] P1 = new double[256]; /* cumulative normalized histogram */ + double [] P1_sq = new double[256]; + double [] P2_sq = new double[256]; + + double total =0; + for (ih = 0; ih < 256; ih++ ) + total+=data[ih]; + + for (ih = 0; ih < 256; ih++ ) + norm_histo[ih] = data[ih]/total; + + P1[0]=norm_histo[0]; + for (ih = 1; ih < 256; ih++ ) + P1[ih]= P1[ih-1] + norm_histo[ih]; + + P1_sq[0]=norm_histo[0]*norm_histo[0]; + for (ih = 1; ih < 256; ih++ ) + P1_sq[ih]= P1_sq[ih-1] + norm_histo[ih] * norm_histo[ih]; + + P2_sq[255] = 0.0; + for ( ih = 254; ih >= 0; ih-- ) + P2_sq[ih] = P2_sq[ih + 1] + norm_histo[ih + 1] * norm_histo[ih + 1]; + + /* Find the threshold that maximizes the criterion */ + threshold = -1; + max_crit = Double.MIN_VALUE; + for ( it = 0; it < 256; it++ ) { + crit = -1.0 * (( P1_sq[it] * P2_sq[it] )> 0.0? Math.log( P1_sq[it] * P2_sq[it]):0.0) + 2 * ( ( P1[it] * ( 1.0 - P1[it] ) )>0.0? Math.log( P1[it] * ( 1.0 - P1[it] ) ): 0.0); + if ( crit > max_crit ) { + max_crit = crit; + threshold = it; + } + } + return threshold; + } + +} + diff --git a/src/ij/process/BinaryInterpolator.java b/src/ij/process/BinaryInterpolator.java new file mode 100644 index 0000000..6df28df --- /dev/null +++ b/src/ij/process/BinaryInterpolator.java @@ -0,0 +1,240 @@ +package ij.process; +import ij.IJ; +import ij.ImagePlus; +import ij.ImageStack; +import ij.gui.Roi; +import ij.plugin.filter.ThresholdToSelection; + +/* + * This plugin takes a binary stack as input, where some slices are + * labeled (i.e. contain white regions), and some are not. The unlabaled + * regions are interpolated by weighting the signed integer distance + * transformed labeled slices. + * + * from: + * http://fiji.sc/cgi-bin/gitweb.cgi?p=fiji.git;a=blob_plain;f=src-plugins/VIB-lib/vib/BinaryInterpolator.java;h=f6a610659ad624d13f94639bc5c0149712071f9f;hb=refs/heads/master + */ + +public class BinaryInterpolator { + int[][] idt; + int w, h; + + public void run(ImagePlus image, Roi[] rois) { + w = image.getWidth(); + h = image.getHeight(); + ImageStack stack = new ImageStack(w, h); + int firstIndex = -1, lastIndex = -1; + for(int i = 1; i < rois.length; i++) { + if(rois[i] != null) { + firstIndex = (firstIndex == -1) ? i : firstIndex; + lastIndex = i; + } + } + if (firstIndex == -1) { + IJ.error("There must be at least one selection in order to interpolate."); + return; + } + + for(int i = firstIndex; i <= lastIndex; i++) { + ByteProcessor bp = new ByteProcessor(w, h); + if(rois[i] != null) { + bp.copyBits(rois[i].getMask(), + rois[i].getBounds().x, + rois[i].getBounds().y, + ij.process.Blitter.ADD); + } + stack.addSlice("", bp); + } + run(stack); + ImagePlus roiImage = new ImagePlus("bla", stack); + + ThresholdToSelection ts = new ThresholdToSelection(); + ts.setup("", roiImage); + for(int i = firstIndex; i <= lastIndex; i++) { + ImageProcessor bp = stack.getProcessor(1); + stack.deleteSlice(1); + int threshold = 255; + bp.setThreshold(threshold, threshold, ImageProcessor.NO_LUT_UPDATE); + ts.run(bp); + rois[i] = roiImage.getRoi(); + } + } + + public void run(ImageStack stack) { + int sliceCount = stack.size(); + if (sliceCount < 3) { + IJ.error("Too few slices to interpolate!"); + return; + } + + IJ.showStatus("getting signed integer distance transform"); + w = stack.getWidth(); + h = stack.getHeight(); + idt = new int[sliceCount][]; + int first = sliceCount, last = -1; + + for (int z = 0; z < sliceCount; z++) { + idt[z] = getIDT(stack.getProcessor(z + 1).getPixels()); + if (idt[z] != null) { + if (z < first) + first = z; + last = z; + } + } + + if (first == last || last < 0) { + IJ.error("Not enough to interpolate"); + return; + } + + IJ.showStatus("calculating weights"); + int current = 0, next = first; + for (int z = first; z < last; z++) { + if (z == next) { + current = z; + for (next = z + 1; idt[next] == null; next++); + continue; + } + + byte[] p = + (byte[])stack.getProcessor(z + 1).getPixels(); + for (int i = 0; i < w * h; i++) + if (0 <= idt[current][i] * (next - z) + + idt[next][i] * (z - current)) + p[i] = (byte)255; + IJ.showProgress(z - first + 1, last - z); + } + } + + /* + * The following calculates the signed integer distance transform. + * Distance transform means that each pixel is assigned the distance + * to the boundary. + * IDT means that the distance is not the Euclidean, but the minimal + * sum of neighbour distances with 3 for horizontal and neighbours, + * and 4 for diagonal neighbours (in 3d, the 3d diagonal neighbour + * would be 5). + * Signed means that the outside pixels have a negative sign. + */ + class IDT { + int[] result; + + IDT() { + result = new int[w * h]; + int infinity = (w + h) * 9; + + for (int i = 0; i < result.length; i++) + result[i] = infinity; + } + + int init(byte[] p) { + int count = 0; + + for (int j = 0; j < h; j++) + for (int i = 0; i < w; i++) { + int idx = i + w * j; + if (isBoundary(p, i, j)) { + result[idx] = 0; + count++; + } else if (isJustOutside(p, i, j)) + result[idx] = -1; + } + return count; + } + + final void idt(int x, int y, int dx, int dy) { + if (x + dx < 0 || y + dy < 0 || + x + dx >= w || y + dy >= h) + return; + int value = result[x + dx + w * (y + dy)]; + int distance = (dx == 0 || dy == 0 ? 3 : 4); + value += distance * (value < 0 ? -1 : 1); + if (Math.abs(result[x + w * y]) > Math.abs(value)) + result[x + w * y] = value; + } + + void propagate() { + for (int j = 0; j < h; j++) + for (int i = 0; i < w; i++) { + idt(i, j, -1, 0); + idt(i, j, -1, -1); + idt(i, j, 0, -1); + } + + for (int j = h - 1; j >= 0; j--) + for (int i = w - 1; i >= 0; i--) { + idt(i, j, +1, 0); + idt(i, j, +1, +1); + idt(i, j, 0, +1); + } + + for (int i = w - 1; i >= 0; i--) + for (int j = h - 1; j >= 0; j--) { + idt(i, j, +1, 0); + idt(i, j, +1, +1); + idt(i, j, 0, +1); + } + + for (int i = 0; i < w; i++) + for (int j = 0; j < h; j++) { + idt(i, j, -1, 0); + idt(i, j, -1, -1); + idt(i, j, 0, -1); + } + } + } + + int[] getIDT(Object pixels) { + IDT idt = new IDT(); + if (idt.init((byte[])pixels) == 0) + return null; + idt.propagate(); + return idt.result; + } + + final boolean isBoundary(byte[] pixels, int x, int y) { + if (pixels[x + w * y] == 0) + return false; + if (x <= 0 || pixels[x - 1 + w * y] == 0) + return true; + if (x >= w - 1 || pixels[x + 1 + w * y] == 0) + return true; + if (y <= 0 || pixels[x + w * (y - 1)] == 0) + return true; + if (y >= h - 1 || pixels[x + w * (y + 1)] == 0) + return true; + if (x <= 0 || y <= 0 || pixels[x - 1 + w * (y - 1)] == 0) + return true; + if (x <= 0 || y >= h - 1 || pixels[x - 1 + w * (y + 1)] == 0) + return true; + if (x >= w - 1 || y <= 0 || pixels[x + 1 + w * (y - 1)] == 0) + return true; + if (x >= w - 1 || y >= h - 1 || + pixels[x + 1 + w * (y + 1)] == 0) + return true; + return false; + } + + final boolean isJustOutside(byte[] pixels, int x, int y) { + if (pixels[x + w * y] != 0) + return false; + if (x > 0 && pixels[x - 1 + w * y] != 0) + return true; + if (x < w - 1 && pixels[x + 1 + w * y] != 0) + return true; + if (y > 0 && pixels[x + w * (y - 1)] != 0) + return true; + if (y < h - 1 && pixels[x + w * (y + 1)] != 0) + return true; + if (x > 0 && y > 0 && pixels[x - 1 + w * (y - 1)] != 0) + return true; + if (x > 0 && y < h - 1 && pixels[x - 1 + w * (y + 1)] != 0) + return true; + if (x < w - 1 && y > 0 && pixels[x + 1 + w * (y - 1)] != 0) + return true; + if (x < w - 1 && y < h - 1 && + pixels[x + 1 + w * (y + 1)] != 0) + return true; + return false; + } +} diff --git a/src/ij/process/BinaryProcessor.java b/src/ij/process/BinaryProcessor.java new file mode 100644 index 0000000..3277096 --- /dev/null +++ b/src/ij/process/BinaryProcessor.java @@ -0,0 +1,256 @@ +package ij.process; +import java.awt.*; + +/** This class processes binary images. */ +public class BinaryProcessor extends ByteProcessor { + + private ByteProcessor parent; + private int foreground; + + /** Creates a BinaryProcessor from a ByteProcessor. The ByteProcessor + must contain a binary image (pixels values are either 0 or 255). + Backgound is assumed to be white. */ + public BinaryProcessor(ByteProcessor ip) { + super(ip.getWidth(), ip.getHeight(), (byte[])ip.getPixels(), ip.getColorModel()); + setRoi(ip.getRoi()); + parent = ip; + } + + static final int OUTLINE=0; + + void process(int type, int count) { + int p1, p2, p3, p4, p5, p6, p7, p8, p9; + int bgColor = 255; + if (parent.isInvertedLut()) + bgColor = 0; + + byte[] pixels2 = (byte[])parent.getPixelsCopy(); + int offset, v=0, sum; + int rowOffset = width; + for (int y=yMin; y<=yMax; y++) { + offset = xMin + y * width; + p2 = pixels2[offset-rowOffset-1]&0xff; + p3 = pixels2[offset-rowOffset]&0xff; + p5 = pixels2[offset-1]&0xff; + p6 = pixels2[offset]&0xff; + p8 = pixels2[offset+rowOffset-1]&0xff; + p9 = pixels2[offset+rowOffset]&0xff; + + for (int x=xMin; x<=xMax; x++) { + p1 = p2; p2 = p3; + p3 = pixels2[offset-rowOffset+1]&0xff; + p4 = p5; p5 = p6; + p6 = pixels2[offset+1]&0xff; + p7 = p8; p8 = p9; + p9 = pixels2[offset+rowOffset+1]&0xff; + + switch (type) { + case OUTLINE: + v = p5; + if (v!=bgColor) { + if (!(p1==bgColor || p2==bgColor || p3==bgColor || p4==bgColor + || p6==bgColor || p7==bgColor || p8==bgColor || p9==bgColor)) + v = bgColor; + } + break; + } + + pixels[offset++] = (byte)v; + } + } + } + + // 2012/09/16: 3,0 1->0 + // 2012/09/16: 24,0 2->0 + private static int[] table = + //0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1 + {0,0,0,0,0,0,1,3,0,0,3,1,1,0,1,3,0,0,0,0,0,0,0,0,0,0,2,0,3,0,3,3, + 0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,3,0,2,2, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 2,0,0,0,0,0,0,0,2,0,0,0,2,0,0,0,3,0,0,0,0,0,0,0,3,0,0,0,3,0,2,0, + 0,0,3,1,0,0,1,3,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, + 3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 2,3,1,3,0,0,1,3,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 2,3,0,1,0,0,0,1,0,0,0,0,0,0,0,0,3,3,0,1,0,0,0,0,2,2,0,0,2,0,0,0}; + + // 2013/12/02: 16,6 2->0 + // 2013/12/02: 24,5 0->2 + private static int[] table2 = + //0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1 + {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,2,2,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; + + + /** Converts objects in a binary image with pixel values of + 'forground' (255 or 0) to single pixel skeletons. + Uses a lookup table to repeatably removes pixels from the + edges of objects in a binary image, reducing them to single + pixel wide skeletons. There is an entry in the table for each + of the 256 possible 3x3 neighborhood configurations. An entry + of '1' means delete pixel on first pass, '2' means delete pixel on + second pass, and '3' means delete on either pass. Pixels are + removed from the right and bottom edges of objects on the first + pass and from the left and top edges on the second pass. A + graphical representation of the 256 neighborhoods indexed by + the table is available at + "http://imagej.nih.gov/ij/images/skeletonize-table.gif". + */ + public void skeletonize(int foreground) { + if (!(foreground==255||foreground==0)) + throw new IllegalArgumentException("Skeletonize: foreground must be 255 or 0"); + this.foreground = foreground; + boolean edgePixels = hasEdgePixels(); + BinaryProcessor ip2 = expand(edgePixels); + ip2.skeletonize2(foreground); + shrink(ip2, edgePixels); + } + + /** Converts black objects in a binary image to single pixel skeletons. */ + public void skeletonize() { + int fg = parent.isInvertedLut()?255:0; + skeletonize(fg); + } + + private void skeletonize2(int foreground) { + this.foreground = foreground; + int pass = 0; + int pixelsRemoved; + resetRoi(); + int background = 255 - foreground; + setColor(background); + moveTo(0,0); lineTo(0,height-1); + moveTo(0,0); lineTo(width-1,0); + moveTo(width-1,0); lineTo(width-1,height-1); + moveTo(0,height-1); lineTo(width/*-1*/,height-1); + ij.ImageStack movie=null; + boolean debug = ij.IJ.debugMode; + if (debug) movie = new ij.ImageStack(width, height); + if (debug) movie.addSlice("-", duplicate()); + do { + snapshot(); + pixelsRemoved = thin(pass++, table); + if (debug) movie.addSlice(""+(pass-1), duplicate()); + snapshot(); + pixelsRemoved += thin(pass++, table); + if (debug) movie.addSlice(""+(pass-1), duplicate()); + } while (pixelsRemoved>0); + do { // use a second table to remove "stuck" pixels + snapshot(); + pixelsRemoved = thin(pass++, table2); + if (debug) movie.addSlice("2-"+(pass-1), duplicate()); + snapshot(); + pixelsRemoved += thin(pass++, table2); + if (debug) movie.addSlice("2-"+(pass-1), duplicate()); + } while (pixelsRemoved>0); + if (debug) new ij.ImagePlus("Skel Movie", movie).show(); + } + + private boolean hasEdgePixels() { + int width = getWidth(); + int height = getHeight(); + boolean edgePixels = false; + for (int x=0; x=0;) + pixels[dstIndex++] = srcPixels[srcIndex++]; + break; + case COPY_INVERTED: + for (int i=r1.width; --i>=0;) + pixels[dstIndex++] = (byte)(255-srcPixels[srcIndex++]&255); + break; + case COPY_TRANSPARENT: + for (int i=r1.width; --i>=0;) { + src = srcPixels[srcIndex++]&255; + if (src==transparent) + dst = pixels[dstIndex]; + else + dst = src; + pixels[dstIndex++] = (byte)dst; + } + break; + case COPY_ZERO_TRANSPARENT: + for (int i=r1.width; --i>=0;) { + src = srcPixels[srcIndex++]&255; + if (src==0) + dst = pixels[dstIndex]; + else + dst = src; + pixels[dstIndex++] = (byte)dst; + } + break; + case ADD: + for (int i=r1.width; --i>=0;) { + dst = (srcPixels[srcIndex++]&255)+(pixels[dstIndex]&255); + if (dst>255) dst = 255; + pixels[dstIndex++] = (byte)dst; + } + break; + case AVERAGE: + for (int i=r1.width; --i>=0;) { + dst = ((srcPixels[srcIndex++]&255)+(pixels[dstIndex]&255))/2; + pixels[dstIndex++] = (byte)dst; + } + break; + case SUBTRACT: + for (int i=r1.width; --i>=0;) { + dst = (pixels[dstIndex]&255)-(srcPixels[srcIndex++]&255); + if (dst<0) dst = 0; + pixels[dstIndex++] = (byte)dst; + } + break; + case DIFFERENCE: + for (int i=r1.width; --i>=0;) { + dst = (pixels[dstIndex]&255)-(srcPixels[srcIndex++]&255); + if (dst<0) dst = -dst; + pixels[dstIndex++] = (byte)dst; + } + break; + case MULTIPLY: + for (int i=r1.width; --i>=0;) { + dst = (srcPixels[srcIndex++]&255)*(pixels[dstIndex]&255); + if (dst>255) dst = 255; + pixels[dstIndex++] = (byte)dst; + } + break; + case DIVIDE: + for (int i=r1.width; --i>=0;) { + src = srcPixels[srcIndex++]&255; + if (src==0) + dst = 255; + else + dst = (pixels[dstIndex]&255)/src; + pixels[dstIndex++] = (byte)dst; + } + break; + case AND: + for (int i=r1.width; --i>=0;) { + dst = srcPixels[srcIndex++]&pixels[dstIndex]; + pixels[dstIndex++] = (byte)dst; + } + break; + case OR: + for (int i=r1.width; --i>=0;) { + dst = srcPixels[srcIndex++]|pixels[dstIndex]; + pixels[dstIndex++] = (byte)dst; + } + break; + case XOR: + for (int i=r1.width; --i>=0;) { + dst = srcPixels[srcIndex++]^pixels[dstIndex]; + pixels[dstIndex++] = (byte)dst; + } + break; + case MIN: + for (int i=r1.width; --i>=0;) { + src = srcPixels[srcIndex++]&255; + dst = pixels[dstIndex]&255; + if (src=0;) { + src = srcPixels[srcIndex++]&255; + dst = pixels[dstIndex]&255; + if (src>dst) dst = src; + pixels[dstIndex++] = (byte)dst; + } + break; + } + } + } +} diff --git a/src/ij/process/ByteProcessor.java b/src/ij/process/ByteProcessor.java new file mode 100644 index 0000000..54b75b0 --- /dev/null +++ b/src/ij/process/ByteProcessor.java @@ -0,0 +1,1302 @@ +package ij.process; + +import java.util.*; +import java.awt.*; +import java.awt.image.*; +import ij.gui.*; +import ij.Prefs; + +/** +This is an 8-bit image and methods that operate on that image. Based on the ImageProcessor class +from "KickAss Java Programming" by Tonny Espeset. +*/ +public class ByteProcessor extends ImageProcessor { + + static final int ERODE=10, DILATE=11; + protected byte[] pixels; + protected byte[] snapshotPixels; + private int bgColor = 255; //white + private boolean bgColorSet; + private int min=0, max=255; + private int binaryCount, binaryBackground; + + /**Creates a ByteProcessor from an AWT Image. */ + public ByteProcessor(Image img) { + width = img.getWidth(null); + height = img.getHeight(null); + resetRoi(); + pixels = new byte[width * height]; + PixelGrabber pg = new PixelGrabber(img, 0, 0, width, height, false); + try { + pg.grabPixels(); + } catch (InterruptedException e) { + System.err.println(e); + }; + cm = pg.getColorModel(); + if (cm instanceof IndexColorModel) + pixels = (byte[])(pg.getPixels()); + if ((cm instanceof IndexColorModel) && ((IndexColorModel)cm).getTransparentPixel()!=-1) { + IndexColorModel icm = (IndexColorModel)cm; + int mapSize = icm.getMapSize(); + byte[] reds = new byte[mapSize]; + byte[] greens = new byte[mapSize]; + byte[] blues = new byte[mapSize]; + icm.getReds(reds); + icm.getGreens(greens); + icm.getBlues(blues); + cm = new IndexColorModel(8, mapSize, reds, greens, blues); + } + } + + /**Creates a blank ByteProcessor of the specified dimensions. */ + public ByteProcessor(int width, int height) { + this(width, height, new byte[width*height], null); + } + + /**Creates a ByteProcessor from a byte array. */ + public ByteProcessor(int width, int height, byte[] pixels) { + this(width, height, pixels, null); + } + + /**Creates a ByteProcessor from a pixel array and IndexColorModel. */ + public ByteProcessor(int width, int height, byte[] pixels, ColorModel cm) { + if (pixels!=null && width*height!=pixels.length) + throw new IllegalArgumentException(WRONG_LENGTH); + this.width = width; + this.height = height; + resetRoi(); + this.pixels = pixels; + this.cm = cm; + } + + /** Creates a ByteProcessor from a TYPE_BYTE_GRAY BufferedImage. */ + public ByteProcessor(BufferedImage bi) { + if (bi.getType()!=BufferedImage.TYPE_BYTE_GRAY) + throw new IllegalArgumentException("Type!=TYPE_BYTE_GRAYY"); + WritableRaster raster = bi.getRaster(); + DataBuffer buffer = raster.getDataBuffer(); + pixels = ((DataBufferByte) buffer).getData(); + width = raster.getWidth(); + height = raster.getHeight(); + resetRoi(); + } + + /** Creates a ByteProcessor from an ImageProcessor. 16-bit and 32-bit + * pixel data are scaled from min-max to 0-255 if 'scale' is true. + * @see ImageProcessor#convertToByteProcessor + * @see ImageProcessor#convertToShortProcessor + * @see ImageProcessor#convertToFloatProcessor + * @see ImageProcessor#convertToColorProcessor + */ + public ByteProcessor(ImageProcessor ip, boolean scale) { + ImageProcessor bp; + if (ip instanceof ByteProcessor) + bp = ip.duplicate(); + else + bp = ip.convertToByte(scale); + this.width = bp.getWidth(); + this.height = bp.getHeight(); + resetRoi(); + this.pixels = (byte[])bp.getPixels(); + this.cm = bp.getCurrentColorModel(); + } + + public Image createImage() { + if (cm==null) + cm = getDefaultColorModel(); + return createBufferedImage(); + } + + Image createBufferedImage() { + if (raster==null) { + SampleModel sm = getIndexSampleModel(); + DataBuffer db = new DataBufferByte(pixels, width*height, 0); + raster = Raster.createWritableRaster(sm, db, null); + } + if (image==null || cm!=cm2) { + if (cm==null) cm=getDefaultColorModel(); + image = new BufferedImage(cm, raster, false, null); + cm2 = cm; + } + return image; + } + + /** Returns this image as a BufferedImage. */ + public BufferedImage getBufferedImage() { + if (isDefaultLut()) { + BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + Graphics g = bi.createGraphics(); + g.drawImage(createImage(), 0, 0, null); + return bi; + } else + return (BufferedImage)createBufferedImage(); + } + + /** Returns a new, blank ByteProcessor with the specified width and height. */ + public ImageProcessor createProcessor(int width, int height) { + ImageProcessor ip2; + ip2 = new ByteProcessor(width, height, new byte[width*height], getColorModel()); + if (baseCM!=null) + ip2.setMinAndMax(min, max); + ip2.setInterpolationMethod(interpolationMethod); + return ip2; + } + + public ImageProcessor crop() { + ImageProcessor ip2 = createProcessor(roiWidth, roiHeight); + byte[] pixels2 = (byte[])ip2.getPixels(); + for (int ys=roiY; ys=0 && x=0 && y=width-1.0) x = width-1.001; + if (y<0.0) y = 0.0; + if (y>=height-1.0) y = height-1.001; + return getInterpolatedPixel(x, y, pixels); + } + } + + final public int getPixelInterpolated(double x, double y) { + if (interpolationMethod==BILINEAR) { + if (x<0.0 || y<0.0 || x>=width-1 || y>=height-1) + return 0; + else + return (int)Math.round(getInterpolatedPixel(x, y, pixels)); + } else if (interpolationMethod==BICUBIC) { + int value = (int)(getBicubicInterpolatedPixel(x, y, this)+0.5); + if (value<0) value = 0; + if (value>255) value = 255; + return value; + } else + return getPixel((int)(x+0.5), (int)(y+0.5)); + } + + public float getPixelValue(int x, int y) { + if (x>=0 && x=0 && y255) fgColor = 255; + fillValueSet = true; + } + + /** Returns the foreground fill/draw value. */ + public double getForegroundValue() { + return fgColor; + } + + /** Sets the background fill value, where 0<=value<=255. */ + public void setBackgroundValue(double value) { + bgColor = (int)value; + if (bgColor<0) bgColor = 0; + if (bgColor>255) bgColor = 255; + bgColorSet = true; + } + + /** Returns the background fill value. */ + public double getBackgroundValue() { + return bgColor; + } + + /** Stores the specified real value at (x,y). Does + nothing if (x,y) is outside the image boundary. + Values outside the range 0-255 are clamped. */ + public void putPixelValue(int x, int y, double value) { + if (x>=0 && x=0 && y255.0) + value = 255.0; + else if (value<0.0) + value = 0.0; + pixels[y*width + x] = (byte)(value+0.5); + } + } + + /** Stores the specified value at (x,y). Does + nothing if (x,y) is outside the image boundary. + Values outside the range 0-255 are clamped. */ + public final void putPixel(int x, int y, int value) { + if (x>=0 && x=0 && y255) value = 255; + if (value<0) value = 0; + pixels[y*width + x] = (byte)value; + } + } + + /** Draws a pixel in the current foreground color. */ + public void drawPixel(int x, int y) { + if (x>=clipXMin && x<=clipXMax && y>=clipYMin && y<=clipYMax) + pixels[y*width + x] = (byte)fgColor; + } + + /** Returns a reference to the byte array containing this image's + pixel data. To avoid sign extension, the pixel values must be + accessed using a mask (e.g. int i = pixels[j]&0xff). */ + public Object getPixels() { + return (Object)pixels; + } + + /** Returns a copy of the pixel data. Or returns a reference to the + snapshot buffer if it is not null and 'snapshotCopyMode' is true. + @see ImageProcessor#snapshot + @see ImageProcessor#setSnapshotCopyMode + */ + public Object getPixelsCopy() { + if (snapshotPixels!=null && snapshotCopyMode) { + snapshotCopyMode = false; + return snapshotPixels; + } else { + byte[] pixels2 = new byte[width*height]; + System.arraycopy(pixels, 0, pixels2, 0, width*height); + return pixels2; + } + } + + public void setPixels(Object pixels) { + if (pixels!=null && this.pixels!=null && (((byte[])pixels).length!=this.pixels.length)) + throw new IllegalArgumentException(""); + this.pixels = (byte[])pixels; + resetPixels(pixels); + if (pixels==null) snapshotPixels = null; + raster = null; + image = null; + } + + /** Returns the smallest displayed pixel value. */ + public double getMin() { + return min; + } + + /** Returns the largest displayed pixel value. */ + public double getMax() { + return max; + } + + /** Maps the entries in this image's LUT from min-max to 0-255. */ + public void setMinAndMax(double min, double max) { + if (maxmax) { + rLUT2[i] = rLUT1[255]; + gLUT2[i] = gLUT1[255]; + bLUT2[i] = bLUT1[255]; + } else { + index = i-this.min; + index = (int)(256.0*index/(max-min)); + if (index < 0) + index = 0; + if (index > 255) + index = 255; + rLUT2[i] = rLUT1[index]; + gLUT2[i] = gLUT1[index]; + bLUT2[i] = bLUT1[index]; + } + } + cm = new IndexColorModel(8, 256, rLUT2, gLUT2, bLUT2); + minThreshold = NO_THRESHOLD; + } + + /** Resets this image's LUT. */ + public void resetMinAndMax() { + setMinAndMax(0, 255); + } + + public void setThreshold(double minThreshold, double maxThreshold, int lutUpdate) { + super.setThreshold(minThreshold, maxThreshold, lutUpdate); + if (this.minThreshold<0.0 && this.minThreshold!=NO_THRESHOLD) + this.minThreshold = 0.0; + if (this.maxThreshold>255.0) + this.maxThreshold = 255.0; + } + + /** Copies the image contained in 'ip' to (xloc, yloc) using one of + the transfer modes defined in the Blitter interface. */ + public void copyBits(ImageProcessor ip, int xloc, int yloc, int mode) { + boolean temporaryFloat = ip.getBitDepth()==32 && (mode==Blitter.MULTIPLY || mode==Blitter.DIVIDE); + if (temporaryFloat) { + FloatProcessor ipFloat = this.convertToFloatProcessor(); + new FloatBlitter(ipFloat).copyBits(ip, xloc, yloc, mode); + setPixels(1, ipFloat); + } else { + ip = ip.convertToByte(true); + new ByteBlitter(this).copyBits(ip, xloc, yloc, mode); + } + } + + /* Filters start here */ + + public void applyTable(int[] lut) { + int lineStart, lineEnd; + for (int y=roiY; y<(roiY+roiHeight); y++) { + lineStart = y * width + roiX; + lineEnd = lineStart + roiWidth; + for (int i=lineEnd; --i>=lineStart;) + pixels[i] = (byte)lut[pixels[i]&0xff]; + } + } + + public void convolve3x3(int[] kernel) { + int v1, v2, v3; //input pixel values around the current pixel + int v4, v5, v6; + int v7, v8, v9; + int scale = 0; + int k1=kernel[0], k2=kernel[1], k3=kernel[2], + k4=kernel[3], k5=kernel[4], k6=kernel[5], + k7=kernel[6], k8=kernel[7], k9=kernel[8]; + for (int i=0; i0 ? 1 : 0); //will point to v6, currently lower + int p3 = p6 - (y>0 ? width : 0); //will point to v3, currently lower + int p9 = p6 + (y0) { p3++; p6++; p9++; } + v3 = pixels2[p3]&0xff; + v6 = pixels2[p6]&0xff; + v9 = pixels2[p9]&0xff; + for (int x=roiX; x255) sum = 255; + if (sum<0) sum = 0; + pixels[p] = (byte)sum; + } + } + } + + /** Filters using a 3x3 neighborhood. The p1, p2, etc variables, which + contain the values of the pixels in the neighborhood, are arranged + as follows: +
+		    p1 p2 p3
+		    p4 p5 p6
+		    p7 p8 p9
+		
+ */ + public void filter(int type) { + int p1, p2, p3, p4, p5, p6, p7, p8, p9; + byte[] pixels2 = (byte[])getPixelsCopy(); + if (width==1) { + filterEdge(type, pixels2, roiHeight, roiX, roiY, 0, 1); + return; + } + int offset, sum1, sum2=0, sum=0; + int[] values = new int[10]; + if (type==MEDIAN_FILTER) values = new int[10]; + int rowOffset = width; + int count; + int binaryForeground = 255 - binaryBackground; + for (int y=yMin; y<=yMax; y++) { + offset = xMin + y * width; + p2 = pixels2[offset-rowOffset-1]&0xff; + p3 = pixels2[offset-rowOffset]&0xff; + p5 = pixels2[offset-1]&0xff; + p6 = pixels2[offset]&0xff; + p8 = pixels2[offset+rowOffset-1]&0xff; + p9 = pixels2[offset+rowOffset]&0xff; + + for (int x=xMin; x<=xMax; x++) { + p1 = p2; p2 = p3; + p3 = pixels2[offset-rowOffset+1]&0xff; + p4 = p5; p5 = p6; + p6 = pixels2[offset+1]&0xff; + p7 = p8; p8 = p9; + p9 = pixels2[offset+rowOffset+1]&0xff; + + switch (type) { + case BLUR_MORE: + sum = (p1+p2+p3+p4+p5+p6+p7+p8+p9+4)/9; + break; + case FIND_EDGES: // 3x3 Sobel filter + sum1 = p1 + 2*p2 + p3 - p7 - 2*p8 - p9; + sum2 = p1 + 2*p4 + p7 - p3 - 2*p6 - p9; + sum = (int)Math.sqrt(sum1*sum1 + sum2*sum2); + if (sum> 255) sum = 255; + break; + case MEDIAN_FILTER: + values[1]=p1; values[2]=p2; values[3]=p3; values[4]=p4; values[5]=p5; + values[6]=p6; values[7]=p7; values[8]=p8; values[9]=p9; + sum = findMedian(values); + break; + case MIN: + sum = p5; + if (p1sum) sum = p1; + if (p2>sum) sum = p2; + if (p3>sum) sum = p3; + if (p4>sum) sum = p4; + if (p6>sum) sum = p6; + if (p7>sum) sum = p7; + if (p8>sum) sum = p8; + if (p9>sum) sum = p9; + break; + case ERODE: + if (p5==binaryBackground) + sum = binaryBackground; + else { + count = 0; + if (p1==binaryBackground) count++; + if (p2==binaryBackground) count++; + if (p3==binaryBackground) count++; + if (p4==binaryBackground) count++; + if (p6==binaryBackground) count++; + if (p7==binaryBackground) count++; + if (p8==binaryBackground) count++; + if (p9==binaryBackground) count++; + if (count>=binaryCount) + sum = binaryBackground; + else + sum = binaryForeground; + } + break; + case DILATE: + if (p5==binaryForeground) + sum = binaryForeground; + else { + count = 0; + if (p1==binaryForeground) count++; + if (p2==binaryForeground) count++; + if (p3==binaryForeground) count++; + if (p4==binaryForeground) count++; + if (p6==binaryForeground) count++; + if (p7==binaryForeground) count++; + if (p8==binaryForeground) count++; + if (p9==binaryForeground) count++; + if (count>=binaryCount) + sum = binaryForeground; + else + sum = binaryBackground; + } + break; + } + + pixels[offset++] = (byte)sum; + } + } + if (xMin==1) filterEdge(type, pixels2, roiHeight, roiX, roiY, 0, 1); + if (yMin==1) filterEdge(type, pixels2, roiWidth, roiX, roiY, 1, 0); + if (xMax==width-2) filterEdge(type, pixels2, roiHeight, width-1, roiY, 0, 1); + if (yMax==height-2) filterEdge(type, pixels2, roiWidth, roiX, height-1, 1, 0); + } + + void filterEdge(int type, byte[] pixels2, int n, int x, int y, int xinc, int yinc) { + int p1, p2, p3, p4, p5, p6, p7, p8, p9; + int sum=0, sum1, sum2; + int count; + int binaryForeground = 255 - binaryBackground; + int bg = binaryBackground; + int fg = binaryForeground; + + for (int i=0; i 255) sum = 255; + break; + case MIN: + sum = p5; + if (p1sum) sum = p1; + if (p2>sum) sum = p2; + if (p3>sum) sum = p3; + if (p4>sum) sum = p4; + if (p6>sum) sum = p6; + if (p7>sum) sum = p7; + if (p8>sum) sum = p8; + if (p9>sum) sum = p9; + break; + case ERODE: + if (p5==binaryBackground) + sum = binaryBackground; + else { + count = 0; + if (p1==binaryBackground) count++; + if (p2==binaryBackground) count++; + if (p3==binaryBackground) count++; + if (p4==binaryBackground) count++; + if (p6==binaryBackground) count++; + if (p7==binaryBackground) count++; + if (p8==binaryBackground) count++; + if (p9==binaryBackground) count++; + if (count>=binaryCount) + sum = binaryBackground; + else + sum = binaryForeground; + } + break; + case DILATE: + if (p5==binaryForeground) + sum = binaryForeground; + else { + count = 0; + if (p1==binaryForeground) count++; + if (p2==binaryForeground) count++; + if (p3==binaryForeground) count++; + if (p4==binaryForeground) count++; + if (p6==binaryForeground) count++; + if (p7==binaryForeground) count++; + if (p8==binaryForeground) count++; + if (p9==binaryForeground) count++; + if (count>=binaryCount) + sum = binaryForeground; + else + sum = binaryBackground; + } + break; + } + pixels[x+y*width] = (byte)sum; + x+=xinc; y+=yinc; + } + } + + final int getEdgePixel(byte[] pixels2, int x, int y) { + if (x<=0) x = 0; + if (x>=width) x = width-1; + if (y<=0) y = 0; + if (y>=height) y = height-1; + return pixels2[x+y*width]&255; + } + + final int getEdgePixel1(byte[] pixels2, int foreground, int x, int y) { + if (x<0 || x>width-1 || y<0 || y>height-1) + return foreground; + else + return pixels2[x+y*width]&255; + } + + final int getEdgePixel0(byte[] pixels2, int background, int x, int y) { + if (x<0 || x>width-1 || y<0 || y>height-1) + return background; + else + return pixels2[x+y*width]&255; + } + + public void erode() { + if (isInvertedLut()) + filter(MIN); + else + filter(MAX); + } + + public void dilate() { + if (isInvertedLut()) + filter(MAX); + else + filter(MIN); + } + + public void erode(int count, int background) { + binaryCount = count; + binaryBackground = background; + filter(ERODE); + } + + public void dilate(int count, int background) { + binaryCount = count; + binaryBackground = background; + filter(DILATE); + } + + public void outline() { + new BinaryProcessor(this).outline(); + } + + /** Converts black objects in a binary image to single pixel skeletons. */ + public void skeletonize() { + new BinaryProcessor(this).skeletonize(); + } + + /** Converts objects with pixel values of 'forground' (255 or 0) + in a binary imager to single pixel skeletons. + */ + public void skeletonize(int foreground) { + new BinaryProcessor(this).skeletonize(foreground); + } + + private final int findMedian (int[] values) { + //Finds the 5th largest of 9 values + for (int i = 1; i <= 4; i++) { + int max = 0; + int mj = 1; + for (int j = 1; j <= 9; j++) + if (values[j] > max) { + max = values[j]; + mj = j; + } + values[mj] = 0; + } + int max = 0; + for (int j = 1; j <= 9; j++) + if (values[j] > max) + max = values[j]; + return max; + } + + public void medianFilter() { + filter(MEDIAN_FILTER); + } + + + /** Adds pseudorandom, Gaussian ("normally") distributed values, with + mean 0.0 and the specified standard deviation, to this image or ROI. */ + public void noise(double standardDeviation) { + if (rnd==null) + rnd = new Random(); + if (!Double.isNaN(seed)) + rnd.setSeed((int)seed); + seed = Double.NaN; + int v, ran; + boolean inRange; + for (int y=roiY; y<(roiY+roiHeight); y++) { + int i = y * width + roiX; + for (int x=roiX; x<(roiX+roiWidth); x++) { + inRange = false; + do { + ran = (int)Math.round(rnd.nextGaussian()*standardDeviation); + v = (pixels[i] & 0xff) + ran; + inRange = v>=0 && v<=255; + if (inRange) pixels[i] = (byte)v; + } while (!inRange); + i++; + } + } + } + + + /** Scales the image or selection using the specified scale factors. + @see ImageProcessor#setInterpolate + */ + public void scale(double xScale, double yScale) { + double xCenter = roiX + roiWidth/2.0; + double yCenter = roiY + roiHeight/2.0; + int xmin, xmax, ymin, ymax; + if (!bgColorSet && isInvertedLut()) bgColor = 0; + + if ((xScale>1.0) && (yScale>1.0)) { + //expand roi + xmin = (int)(xCenter-(xCenter-roiX)*xScale); + if (xmin<0) xmin = 0; + xmax = xmin + (int)(roiWidth*xScale) - 1; + if (xmax>=width) xmax = width - 1; + ymin = (int)(yCenter-(yCenter-roiY)*yScale); + if (ymin<0) ymin = 0; + ymax = ymin + (int)(roiHeight*yScale) - 1; + if (ymax>=height) ymax = height - 1; + } else { + xmin = roiX; + xmax = roiX + roiWidth - 1; + ymin = roiY; + ymax = roiY + roiHeight - 1; + } + byte[] pixels2 = (byte[])getPixelsCopy(); + ImageProcessor ip2 = null; + if (interpolationMethod==BICUBIC) { + ip2 = new ByteProcessor(getWidth(), getHeight(), pixels2, null); + ip2.setBackgroundValue(getBackgroundValue()); + } + boolean checkCoordinates = (xScale < 1.0) || (yScale < 1.0); + int index1, index2, xsi, ysi; + double ys, xs; + if (interpolationMethod==BICUBIC) { + for (int y=ymin; y<=ymax; y++) { + ys = (y-yCenter)/yScale + yCenter; + index1 = y*width + xmin; + index2 = width*(int)ys; + for (int x=xmin; x<=xmax; x++) { + xs = (x-xCenter)/xScale + xCenter; + int value = (int)(getBicubicInterpolatedPixel(xs, ys, ip2)+0.5); + if (value<0) value = 0; + if (value>255) value = 255; + pixels[index1++] = (byte)value; + } + } + } else { + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + for (int y=ymin; y<=ymax; y++) { + ys = (y-yCenter)/yScale + yCenter; + ysi = (int)ys; + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + index1 = y*width + xmin; + index2 = width*(int)ys; + for (int x=xmin; x<=xmax; x++) { + xs = (x-xCenter)/xScale + xCenter; + xsi = (int)xs; + if (checkCoordinates && ((xsixmax) || (ysiymax))) + pixels[index1++] = (byte)bgColor; + else { + if (interpolationMethod==BILINEAR) { + if (xs<0.0) xs = 0.0; + if (xs>=xlimit) xs = xlimit2; + pixels[index1++] =(byte)((int)(getInterpolatedPixel(xs, ys, pixels2)+0.5)&255); + } else + pixels[index1++] = pixels2[index2+xsi]; + } + } + } + } + } + + /** Uses bilinear interpolation to find the pixel value at real coordinates (x,y). */ + private final double getInterpolatedPixel(double x, double y, byte[] pixels) { + int xbase = (int)x; + int ybase = (int)y; + double xFraction = x - xbase; + double yFraction = y - ybase; + int offset = ybase * width + xbase; + int lowerLeft = pixels[offset]&255; + int lowerRight = pixels[offset + 1]&255; + int upperRight = pixels[offset + width + 1]&255; + int upperLeft = pixels[offset + width]&255; + double upperAverage = upperLeft + xFraction * (upperRight - upperLeft); + double lowerAverage = lowerLeft + xFraction * (lowerRight - lowerLeft); + return lowerAverage + yFraction * (upperAverage - lowerAverage); + } + + /** Creates a new ByteProcessor containing a scaled copy of this image or selection. + @see ij.process.ImageProcessor#setInterpolate + */ + public ImageProcessor resize(int dstWidth, int dstHeight) { + if (roiWidth==dstWidth && roiHeight==dstHeight) + return crop(); + if ((width==1||height==1) && interpolationMethod!=NONE) + return resizeLinearly(dstWidth, dstHeight); + double srcCenterX = roiX + roiWidth/2.0; + double srcCenterY = roiY + roiHeight/2.0; + double dstCenterX = dstWidth/2.0; + double dstCenterY = dstHeight/2.0; + double xScale = (double)dstWidth/roiWidth; + double yScale = (double)dstHeight/roiHeight; + if (interpolationMethod!=NONE) { + if (dstWidth!=width) dstCenterX+=xScale/4.0; + if (dstHeight!=height) dstCenterY+=yScale/4.0; + } + ImageProcessor ip2 = createProcessor(dstWidth, dstHeight); + byte[] pixels2 = (byte[])ip2.getPixels(); + int inc = getProgressIncrement(dstWidth,dstHeight); + double xs, ys; + int index1, index2; + if (interpolationMethod==BICUBIC) { + for (int y=0; y<=dstHeight-1; y++) { + if (inc!=0&&y%inc==0) showProgress((double)y/dstHeight); + ys = (y-dstCenterY)/yScale + srcCenterY; + index1 = width*(int)ys; + index2 = y*dstWidth; + for (int x=0; x<=dstWidth-1; x++) { + xs = (x-dstCenterX)/xScale + srcCenterX; + int value = (int)(getBicubicInterpolatedPixel(xs, ys, this)+0.5); + if (value<0) value = 0; + if (value>255) value = 255; + pixels2[index2++] = (byte)value; + } + } + } else { + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + for (int y=0; y<=dstHeight-1; y++) { + if (inc!=0&&y%inc==0) showProgress((double)y/dstHeight); + ys = (y-dstCenterY)/yScale + srcCenterY; + if (interpolationMethod==BILINEAR) { + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + } + index1 = width*(int)ys; + index2 = y*dstWidth; + for (int x=0; x<=dstWidth-1; x++) { + xs = (x-dstCenterX)/xScale + srcCenterX; + if (interpolationMethod==BILINEAR) { + if (xs<0.0) xs = 0.0; + if (xs>=xlimit) xs = xlimit2; + pixels2[index2++] = (byte)((int)(getInterpolatedPixel(xs, ys, pixels)+0.5)&255); + } else + pixels2[index2++] = pixels[index1+(int)xs]; + } + } + } + if (inc!=0) showProgress(1.0); + return ip2; + } + + /** Rotates the image or ROI 'angle' degrees clockwise. + @see ImageProcessor#setInterpolationMethod + */ + public void rotate(double angle) { + if (angle%360==0) + return; + byte[] pixels2 = (byte[])getPixelsCopy(); + ImageProcessor ip2 = null; + if (interpolationMethod==BICUBIC) { + ip2 = new ByteProcessor(getWidth(), getHeight(), pixels2, null); + ip2.setBackgroundValue(getBackgroundValue()); + } + double centerX = roiX + (roiWidth-1)/2.0; + double centerY = roiY + (roiHeight-1)/2.0; + int xMax = roiX + this.roiWidth - 1; + if (!bgColorSet && isInvertedLut()) bgColor = 0; + + double angleRadians = -angle/(180.0/Math.PI); + double ca = Math.cos(angleRadians); + double sa = Math.sin(angleRadians); + double tmp1 = centerY*sa-centerX*ca; + double tmp2 = -centerX*sa-centerY*ca; + double tmp3, tmp4, xs, ys; + int index, ixs, iys; + double dwidth=width, dheight=height; + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + + if (interpolationMethod==BICUBIC) { + for (int y=roiY; y<(roiY + roiHeight); y++) { + index = y*width + roiX; + tmp3 = tmp1 - y*sa + centerX; + tmp4 = tmp2 + y*ca + centerY; + for (int x=roiX; x<=xMax; x++) { + xs = x*ca + tmp3; + ys = x*sa + tmp4; + int value = (int)(getBicubicInterpolatedPixel(xs, ys, ip2)+0.5); + if (value<0) value = 0; + if (value>255) value = 255; + pixels[index++] = (byte)value; + } + } + } else { + for (int y=roiY; y<(roiY + roiHeight); y++) { + index = y*width + roiX; + tmp3 = tmp1 - y*sa + centerX; + tmp4 = tmp2 + y*ca + centerY; + for (int x=roiX; x<=xMax; x++) { + xs = x*ca + tmp3; + ys = x*sa + tmp4; + if ((xs>=-0.01) && (xs=-0.01) && (ys=xlimit) xs = xlimit2; + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + pixels[index++] = (byte)(getInterpolatedPixel(xs, ys, pixels2)+0.5); + } else { + ixs = (int)(xs+0.5); + iys = (int)(ys+0.5); + if (ixs>=width) ixs = width - 1; + if (iys>=height) iys = height -1; + pixels[index++] = pixels2[width*iys+ixs]; + } + } else + pixels[index++] = (byte)bgColor; + } + } + } + } + + public void flipVertical() { + int index1,index2; + byte tmp; + for (int y=0; y255f) value = 255f; + pixels[i] = (byte)value; + } + setMinAndMax(fp.getMin(), fp.getMax()); + } + + /** Returns 'true' if this is a binary image (8-bit-image with only 0 and 255). */ + public boolean isBinary() { + for (int i=0; i=minThreshold && value<=maxThreshold) + mpixels[i] = (byte)255; + } + return mask; + } + + byte[] create8BitImage() { + return pixels; + } + +} + diff --git a/src/ij/process/ByteStatistics.java b/src/ij/process/ByteStatistics.java new file mode 100644 index 0000000..fe033cf --- /dev/null +++ b/src/ij/process/ByteStatistics.java @@ -0,0 +1,177 @@ +package ij.process; +import ij.measure.Calibration; + +/** 8-bit image statistics, including histogram. */ +public class ByteStatistics extends ImageStatistics { + + /** Construct an ImageStatistics object from a ByteProcessor + using the standard measurement options (area, mean, + mode, min and max) and no calibration. */ + public ByteStatistics(ImageProcessor ip) { + this(ip, AREA+MEAN+MODE+MIN_MAX, null); + } + + /** Constructs a ByteStatistics object from a ByteProcessor using + the specified measurement and calibration. */ + public ByteStatistics(ImageProcessor ip, int mOptions, Calibration cal) { + ByteProcessor bp = (ByteProcessor)ip; + histogram = bp.getHistogram(); + setup(ip, cal); + double minT = ip.getMinThreshold(); + int minThreshold,maxThreshold; + boolean limitToThreshold = (mOptions&LIMIT)!=0; + if (!limitToThreshold || minT==ImageProcessor.NO_THRESHOLD) { + minThreshold=0; + maxThreshold=255; + } else { + minThreshold = (int)minT; + maxThreshold = (int)ip.getMaxThreshold(); + } + if (limitToThreshold) + saveThreshold(minThreshold, maxThreshold, cal); + float[] cTable = cal!=null?cal.getCTable():null; + if (cTable!=null) + getCalibratedStatistics(minThreshold,maxThreshold,cTable); + else + getRawStatistics(minThreshold,maxThreshold); + if ((mOptions&MIN_MAX)!=0) { + if (pixelCount==0) + min = max = Double.NaN; + else if (cTable!=null) + getCalibratedMinAndMax(minThreshold, maxThreshold, cTable); + else + getRawMinAndMax(minThreshold, maxThreshold); + } + if ((mOptions&ELLIPSE)!=0 || (mOptions&SHAPE_DESCRIPTORS)!=0) + fitEllipse(ip, mOptions); + else if ((mOptions&CENTROID)!=0) + getCentroid(ip, minThreshold, maxThreshold); + if ((mOptions&(CENTER_OF_MASS|SKEWNESS|KURTOSIS))!=0) + calculateMoments(ip, minThreshold, maxThreshold, cTable); + if ((mOptions&MEDIAN)!=0) + calculateMedian(histogram, minThreshold, maxThreshold, cal); + if ((mOptions&AREA_FRACTION)!=0) + calculateAreaFraction(ip, histogram); + } + + void getCalibratedStatistics(int minThreshold, int maxThreshold, float[] cTable) { + int count; + double value; + double sum = 0; + double sum2 = 0.0; + double isum = 0.0; + + for (int i=minThreshold; i<=maxThreshold; i++) { + count = histogram[i]; + value = cTable[i]; + if (count>0 && !Double.isNaN(value)) { + pixelCount += count; + sum += value*count; + isum += i*count; + sum2 += (value*value)*count; + if (count>maxCount) { + maxCount = count; + mode = i; + } + } + } + area = pixelCount*pw*ph; + mean = sum/pixelCount; + umean = isum/pixelCount; + dmode = cTable[mode]; + calculateStdDev(pixelCount,sum,sum2); + histMin = 0.0; + histMax = 255.0; + } + + void getCentroid(ImageProcessor ip, int minThreshold, int maxThreshold) { + byte[] pixels = (byte[])ip.getPixels(); + byte[] mask = ip.getMaskArray(); + boolean limit = minThreshold>0 || maxThreshold<255; + double xsum=0, ysum=0; + int count=0,i,mi,v; + for (int y=ry,my=0; y<(ry+rh); y++,my++) { + i = y*width + rx; + mi = my*rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null||mask[mi++]!=0) { + if (limit) { + v = pixels[i]&255; + if (v>=minThreshold&&v<=maxThreshold) { + count++; + xsum+=x; + ysum+=y; + } + } else { + count++; + xsum+=x; + ysum+=y; + } + } + i++; + } + } + xCentroid = xsum/count+0.5; + yCentroid = ysum/count+0.5; + if (cal!=null) { + xCentroid = cal.getX(xCentroid); + yCentroid = cal.getY(yCentroid, height); + } + } + + void calculateMoments(ImageProcessor ip, int minThreshold, int maxThreshold, float[] cTable) { + byte[] pixels = (byte[])ip.getPixels(); + byte[] mask = ip.getMaskArray(); + int v, i, mi; + double dv, dv2, sum1=0.0, sum2=0.0, sum3=0.0, sum4=0.0, xsum=0.0, ysum=0.0; + for (int y=ry,my=0; y<(ry+rh); y++,my++) { + i = y*width + rx; + mi = my*rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null || mask[mi++]!=0) { + v = pixels[i]&255; + if (v>=minThreshold&&v<=maxThreshold) { + dv = ((cTable!=null)?cTable[v]:v)+Double.MIN_VALUE; + dv2 = dv*dv; + sum1 += dv; + sum2 += dv2; + sum3 += dv*dv2; + sum4 += dv2*dv2; + xsum += x*dv; + ysum += y*dv; + } + } + i++; + } + } + double mean2 = mean*mean; + double variance = sum2/pixelCount - mean2; + double sDeviation = Math.sqrt(variance); + skewness = ((sum3 - 3.0*mean*sum2)/pixelCount + 2.0*mean*mean2)/(variance*sDeviation); + kurtosis = (((sum4 - 4.0*mean*sum3 + 6.0*mean2*sum2)/pixelCount - 3.0*mean2*mean2)/(variance*variance)-3.0); + xCenterOfMass = xsum/sum1+0.5; + yCenterOfMass = ysum/sum1+0.5; + if (cal!=null) { + xCenterOfMass = cal.getX(xCenterOfMass); + yCenterOfMass = cal.getY(yCenterOfMass, height); + } + } + + void getCalibratedMinAndMax(int minThreshold, int maxThreshold, float[] cTable) { + if (pixelCount==0) + {min=0.0; max=0.0; return;} + min = Double.MAX_VALUE; + max = -Double.MAX_VALUE; + double v = 0.0; + for (int i=minThreshold; i<=maxThreshold; i++) { + if (histogram[i]>0) { + v = cTable[i]; + if (vmax) + max = v; + } + } + } + +} diff --git a/src/ij/process/ColorBlitter.java b/src/ij/process/ColorBlitter.java new file mode 100644 index 0000000..16b3a4a --- /dev/null +++ b/src/ij/process/ColorBlitter.java @@ -0,0 +1,139 @@ +package ij.process; +import java.awt.*; +import java.awt.image.*; + +/** This class does bit blitting of RGB images. */ +public class ColorBlitter implements Blitter { + + private ColorProcessor ip; + private int width, height; + private int[] pixels; + private int transparent = 0xffffff; + + /** Constructs a ColorBlitter from a ColorProcessor. */ + public ColorBlitter(ColorProcessor ip) { + this.ip = ip; + width = ip.getWidth(); + height = ip.getHeight(); + pixels = (int[])ip.getPixels(); + } + + public void setTransparentColor(Color c) { + transparent = c.getRGB()&0xffffff; + } + + /** Copies the RGB image in 'ip' to (x,y) using the specified mode. */ + public void copyBits(ImageProcessor ip, int xloc, int yloc, int mode) { + int srcIndex, dstIndex; + int xSrcBase, ySrcBase; + int[] srcPixels; + + int srcWidth = ip.getWidth(); + int srcHeight = ip.getHeight(); + Rectangle rect1 = new Rectangle(srcWidth, srcHeight); + rect1.setLocation(xloc, yloc); + Rectangle rect2 = new Rectangle(width, height); + if (!rect1.intersects(rect2)) + return; + if (ip instanceof ByteProcessor) { + byte[] pixels8 = (byte[])ip.getPixels(); + ColorModel cm = ip.getColorModel(); + if (ip.isInvertedLut()) + cm = ip.getDefaultColorModel(); + int size = ip.getWidth()*ip.getHeight(); + srcPixels = new int[size]; + int v; + for (int i=0; i=0;) + pixels[dstIndex++] = srcPixels[srcIndex++]; + } else { + for (int i=rect1.width; --i>=0;) { + src = srcPixels[srcIndex++]; + dst = pixels[dstIndex]; + pixels[dstIndex++] = (src&0xffffff)==trancolor?dst:src; + } + } + } + return; + } + + for (int y=rect1.y; y<(rect1.y+rect1.height); y++) { + srcIndex = (y-yloc)*srcWidth + (rect1.x-xloc); + dstIndex = y * width + rect1.x; + for (int i=rect1.width; --i>=0;) { + c1 = srcPixels[srcIndex++]; + r1 = (c1&0xff0000)>>16; + g1 = (c1&0xff00)>>8; + b1 = c1&0xff; + c2 = pixels[dstIndex]; + r2 = (c2&0xff0000)>>16; + g2 = (c2&0xff00)>>8; + b2 = c2&0xff; + switch (mode) { + case COPY_INVERTED: + break; + case ADD: + r2=r1+r2; g2=g1+g2; b2=b1+b2; + if (r2>255) r2=255; if (g2>255) g2=255; if (b2>255) b2=255; + break; + case AVERAGE: + r2=(r1+r2)/2; g2=(g1+g2)/2; b2=(b1+b2)/2; + break; + case SUBTRACT: + r2=r2-r1; g2=g2-g1; b2=b2-b1; + if (r2<0) r2=0; if (g2<0) g2=0; if (b2<0) b2=0; + break; + case DIFFERENCE: + r2=r2-r1; if (r2<0) r2=-r2; + g2=g2-g1; if (g2<0) g2=-g2; + b2=b2-b1; if (b2<0) b2=-b2; + break; + case MULTIPLY: + r2=r1*r2; g2=g1*g2; b2=b1*b2; + if (r2>255) r2=255; if (g2>255) g2=255; if (b2>255) b2=255; + break; + case DIVIDE: + if (r1==0) r2=255; else r2=r2/r1; + if (g1==0) g2=255; else g2=g2/g1; + if (b1==0) b2=255; else b2=b2/b1; + break; + case AND: + r2=r1&r2; g2=g1&g2; b2=b1&b2; + break; + case OR: + r2=r1|r2; g2=g1|g2; b2=b1|b2; + break; + case XOR: + r2=r1^r2; g2=g1^g2; b2=b1^b2; + break; + case MIN: + if (r1r2) r2 = r1; + if (g1>g2) g2 = g1; + if (b1>b2) b2 = b1; + break; + } + pixels[dstIndex++] = 0xff000000 + (r2<<16) + (g2<<8) + b2; + } + } + } +} diff --git a/src/ij/process/ColorProcessor.java b/src/ij/process/ColorProcessor.java new file mode 100644 index 0000000..efbafb9 --- /dev/null +++ b/src/ij/process/ColorProcessor.java @@ -0,0 +1,1459 @@ +package ij.process; + +import java.util.*; +import java.awt.*; +import java.awt.image.*; +import ij.gui.*; +import ij.ImageStack; + +/** +This is an 32-bit RGB image and methods that operate on that image.. Based on the ImageProcessor class from +"KickAss Java Programming" by Tonny Espeset (1996). +*/ +public class ColorProcessor extends ImageProcessor { + + protected int[] pixels; + protected int[] snapshotPixels = null; + private int bgColor = 0xffffffff; //white + protected int min=0, max=255; + private WritableRaster rgbRaster; + private SampleModel rgbSampleModel; + private boolean caSnapshot; + + // Weighting factors used by getPixelValue(), getHistogram() and convertToByte(). + // Enable "Weighted RGB Conversion" in Edit/Options/Conversions + // to use 0.299, 0.587 and 0.114. + private static double rWeight=1d/3d, gWeight=1d/3d, bWeight=1d/3d; + private double[] weights; // Overrides rWeight, etc. when set by setWeights() + + /**Creates a ColorProcessor from an AWT Image or BufferedImage. */ + public ColorProcessor(Image img) { + width = img.getWidth(null); + height = img.getHeight(null); + pixels = new int[width * height]; + PixelGrabber pg = new PixelGrabber(img, 0, 0, width, height, pixels, 0, width); + try { + pg.grabPixels(); + } catch (InterruptedException e){}; + createColorModel(); + fgColor = 0xff000000; //black + resetRoi(); + } + + /**Creates a blank ColorProcessor of the specified dimensions. */ + public ColorProcessor(int width, int height) { + this(width, height, new int[width*height]); + } + + /**Creates a ColorProcessor from a pixel array. */ + public ColorProcessor(int width, int height, int[] pixels) { + if (pixels!=null && width*height!=pixels.length) + throw new IllegalArgumentException(WRONG_LENGTH); + this.width = width; + this.height = height; + createColorModel(); + fgColor = 0xff000000; //black + resetRoi(); + this.pixels = pixels; + } + + void createColorModel() { + cm = new DirectColorModel(24, 0xff0000, 0xff00, 0xff); + } + + public Image createImage() { + return createBufferedImage(); + } + + Image createBufferedImage() { + if (rgbSampleModel==null) + rgbSampleModel = getRGBSampleModel(); + if (rgbRaster==null) { + DataBuffer dataBuffer = new DataBufferInt(pixels, width*height, 0); + rgbRaster = Raster.createWritableRaster(rgbSampleModel, dataBuffer, null); + } + if (image==null) { + image = new BufferedImage(cm, rgbRaster, false, null); + } + return image; + } + + SampleModel getRGBSampleModel() { + WritableRaster wr = cm.createCompatibleWritableRaster(1, 1); + SampleModel sampleModel = wr.getSampleModel(); + sampleModel = sampleModel.createCompatibleSampleModel(width, height); + return sampleModel; + } + + public void setColorModel(ColorModel cm) { + if (cm!=null && (cm instanceof IndexColorModel)) + throw new IllegalArgumentException("RGB images do not support IndexColorModels"); + this.cm = cm; + rgbSampleModel = null; + rgbRaster = null; + } + + /** Returns a new, blank ColorProcessor with the specified width and height. */ + public ImageProcessor createProcessor(int width, int height) { + ImageProcessor ip2 = new ColorProcessor(width, height); + ip2.setInterpolationMethod(interpolationMethod); + return ip2; + } + + public Color getColor(int x, int y) { + int c = pixels[y*width+x]; + int r = (c&0xff0000)>>16; + int g = (c&0xff00)>>8; + int b = c&0xff; + return new Color(r,g,b); + } + + /** Sets the foreground color. */ + public void setColor(Color color) { + fgColor = color.getRGB(); + drawingColor = color; + fillValueSet = true; + } + + /** Sets the fill/draw color, where color is an RGB int. */ + public void setColor(int color) { + fgColor = color; + fillValueSet = true; + } + + /** Sets the default fill/draw value, where value is interpreted as an RGB int. */ + public void setValue(double value) { + fgColor = (int)value; + fillValueSet = true; + } + + /** Returns the foreground fill/draw value. */ + public double getForegroundValue() { + return fgColor; + } + + /** Sets the background fill value, where value is interpreted as an RGB int. */ + public void setBackgroundValue(double value) { + bgColor = (int)value; + } + + /** Returns the background fill value. */ + public double getBackgroundValue() { + return bgColor; + } + + /** Returns the smallest displayed pixel value. */ + public double getMin() { + return min; + } + + + /** Returns the largest displayed pixel value. */ + public double getMax() { + return max; + } + + + /** Uses a table look-up to map the pixels in this image from min-max to 0-255. */ + public void setMinAndMax(double min, double max) { + setMinAndMax(min, max, 7); + } + + public void setMinAndMax(double min, double max, int channels) { + if (max 255) + v = 255; + lut[i] = v; + } + reset(); + if (channels==7) + applyTable(lut); + else + applyTable(lut, channels); + } + + public void snapshot() { + snapshotWidth = width; + snapshotHeight = height; + if (snapshotPixels==null || (snapshotPixels!=null && snapshotPixels.length!=pixels.length)) + snapshotPixels = new int[width * height]; + System.arraycopy(pixels, 0, snapshotPixels, 0, width*height); + caSnapshot = false; + } + + + public void reset() { + if (snapshotPixels!=null) + System.arraycopy(snapshotPixels, 0, pixels, 0, width*height); + } + + + public void reset(ImageProcessor mask) { + if (mask==null || snapshotPixels==null) + return; + if (mask.getWidth()!=roiWidth||mask.getHeight()!=roiHeight) + throw new IllegalArgumentException(maskSizeError(mask)); + byte[] mpixels = (byte[])mask.getPixels(); + for (int y=roiY, my=0; y<(roiY+roiHeight); y++, my++) { + int i = y * width + roiX; + int mi = my * roiWidth; + for (int x=roiX; x<(roiX+roiWidth); x++) { + if (mpixels[mi++]==0) + pixels[i] = snapshotPixels[i]; + i++; + } + } + } + + /** Used by the ContrastAdjuster */ + public void caSnapshot(boolean set) { + caSnapshot = set; + } + + /** Used by the ContrastAdjuster */ + public boolean caSnapshot() { + return caSnapshot; + } + + /** Swaps the pixel and snapshot (undo) arrays. */ + public void swapPixelArrays() { + if (snapshotPixels==null) + return; + int pixel; + for (int i=0; i=0 && x=0 && y>16; + iArray[1] = (c&0xff00)>>8; + iArray[2] = c&0xff; + return iArray; + } + + /** Sets a pixel in the image using a 3 element (R, G and B) + int array of samples. */ + public final void putPixel(int x, int y, int[] iArray) { + int r=iArray[0], g=iArray[1], b=iArray[2]; + putPixel(x, y, (r<<16)+(g<<8)+b); + } + + /** Calls getPixelValue(x,y). */ + public double getInterpolatedPixel(double x, double y) { + int ix = (int)(x+0.5); + int iy = (int)(y+0.5); + if (ix<0) ix = 0; + if (ix>=width) ix = width-1; + if (iy<0) iy = 0; + if (iy>=height) iy = height-1; + return getPixelValue(ix, iy); + } + + final public int getPixelInterpolated(double x,double y) { + if (x<0.0 || y<0.0 || x>=width-1 || y>=height-1) + return 0; + else + return getInterpolatedPixel(x, y, pixels); + } + + /** Stores the specified value at (x,y). */ + public final void putPixel(int x, int y, int value) { + if (x>=0 && x=0 && y=0 && x=0 && y255.0) + value = 255; + else if (value<0.0) + value = 0.0; + int gray = (int)(value+0.5); + pixels[y*width + x] = 0xff000000 + (gray<<16) + (gray<<8) + gray; + + } + } + + /** Converts the specified pixel to grayscale using the + formula g=(r+g+b)/3 and returns it as a float. + Call setWeightingFactors() to specify different conversion + factors. */ + public float getPixelValue(int x, int y) { + if (x>=0 && x=0 && y>16; + int g = (c&0xff00)>>8; + int b = c&0xff; + if (weights!=null) + return (float)(r*weights[0] + g*weights[1] + b*weights[2]); + else + return (float)(r*rWeight + g*gWeight + b*bWeight); + } + else + return Float.NaN; + } + + + /** Draws a pixel in the current foreground color. */ + public void drawPixel(int x, int y) { + if (x>=clipXMin && x<=clipXMax && y>=clipYMin && y<=clipYMax) + pixels[y*width + x] = fgColor; + } + + + /** Returns a reference to the int array containing + this image's pixel data. */ + public Object getPixels() { + return (Object)pixels; + } + + + public void setPixels(Object pixels) { + this.pixels = (int[])pixels; + resetPixels(pixels); + if (pixels==null) + snapshotPixels = null; + rgbRaster = null; + image = null; + caSnapshot = false; + } + + + /** Returns hue, saturation and brightness in 3 byte arrays. */ + public void getHSB(byte[] H, byte[] S, byte[] B) { + int c, r, g, b; + float[] hsb = new float[3]; + for (int i=0; i < width*height; i++) { + c = pixels[i]; + r = (c&0xff0000)>>16; + g = (c&0xff00)>>8; + b = c&0xff; + hsb = Color.RGBtoHSB(r, g, b, hsb); + H[i] = (byte)((int)(hsb[0]*255.0)); + S[i] = (byte)((int)(hsb[1]*255.0)); + B[i] = (byte)((int)(hsb[2]*255.0)); + } + } + + /** Returns hue, saturation and brightness in 3 float arrays. */ + public void getHSB(float[] H, float[] S, float[] B) { + int c, r, g, b; + float[] hsb = new float[3]; + for (int i=0; i < width*height; i++) { + c = pixels[i]; + r = (c&0xff0000)>>16; + g = (c&0xff00)>>8; + b = c&0xff; + hsb = Color.RGBtoHSB(r, g, b, hsb); + H[i] = hsb[0]; + S[i] = hsb[1]; + B[i] = hsb[2]; + } + } + + /** Returns an ImageStack with three 8-bit slices, + representing hue, saturation and brightness */ + public ImageStack getHSBStack() { + int width = getWidth(); + int height = getHeight(); + byte[] H = new byte[width*height]; + byte[] S = new byte[width*height]; + byte[] B = new byte[width*height]; + getHSB(H, S, B); + ColorModel cm = getDefaultColorModel(); + ImageStack stack = new ImageStack(width, height, cm); + stack.addSlice("Hue", H); + stack.addSlice("Saturation", S); + stack.addSlice("Brightness", B); + return stack; + } + + /** Returns an ImageStack with three 32-bit slices, + representing hue, saturation and brightness */ + public ImageStack getHSB32Stack() { + int width = getWidth(); + int height = getHeight(); + float[] H = new float[width*height]; + float[] S = new float[width*height]; + float[] B = new float[width*height]; + getHSB(H, S, B); + ColorModel cm = getDefaultColorModel(); + ImageStack stack = new ImageStack(width, height, cm); + stack.addSlice("Hue", H); + stack.addSlice("Saturation", S); + stack.addSlice("Brightness", B); + return stack; + } + + /** Returns brightness as a FloatProcessor. */ + public FloatProcessor getBrightness() { + int c, r, g, b; + int size = width*height; + float[] brightness = new float[size]; + float[] hsb = new float[3]; + for (int i=0; i>16; + g = (c&0xff00)>>8; + b = c&0xff; + hsb = Color.RGBtoHSB(r, g, b, hsb); + brightness[i] = hsb[2]; + } + return new FloatProcessor(width, height, brightness, null); + } + + /** Returns the red, green and blue planes as 3 byte arrays. */ + public void getRGB(byte[] R, byte[] G, byte[] B) { + int c, r, g, b; + for (int i=0; i < width*height; i++) { + c = pixels[i]; + r = (c&0xff0000)>>16; + g = (c&0xff00)>>8; + b = c&0xff; + R[i] = (byte)r; + G[i] = (byte)g; + B[i] = (byte)b; + } + } + + /** Returns the specified plane (1=red, 2=green, 3=blue, 4=alpha) as a byte array. */ + public byte[] getChannel(int channel) { + ByteProcessor bp = getChannel(channel, null); + return (byte[])bp.getPixels(); + } + + /** Returns the specified plane (1=red, 2=green, 3=blue, 4=alpha) as a ByteProcessor. */ + public ByteProcessor getChannel(int channel, ByteProcessor bp) { + int size = width*height; + if (bp == null || bp.getWidth()!=width || bp.getHeight()!=height) + bp = new ByteProcessor(width, height); + byte[] bPixels = (byte[])bp.getPixels(); + int shift = 16 - 8*(channel-1); + if (channel==4) shift=24; + for (int i=0; i>shift); + return bp; + } + + /** Sets the pixels of one color channel from a ByteProcessor. + * @param channel Determines the color channel, 1=red, 2=green, 3=blue, 4=alpha + * @param bp The ByteProcessor where the image data are read from. + */ + public void setChannel(int channel, ByteProcessor bp) { + byte[] bPixels = (byte[])bp.getPixels(); + int value; + int size = width*height; + int shift = 16 - 8*(channel-1); + if (channel==4) shift=24; + int resetMask = 0xffffffff^(255<>16; + g = (c&0xff00)>>8; + b = c&0xff; + hsb = Color.RGBtoHSB(r, g, b, hsb); + float bvalue = brightness[i]; + if (bvalue<0f) bvalue = 0f; + if (bvalue>1.0f) bvalue = 1.0f; + pixels[i] = Color.HSBtoRGB(hsb[0], hsb[1], bvalue); + } + } + + /** Copies the image contained in 'ip' to (xloc, yloc) using one of + the transfer modes defined in the Blitter interface. */ + public void copyBits(ImageProcessor ip, int xloc, int yloc, int mode) { + ip = ip.convertToRGB(); + new ColorBlitter(this).copyBits(ip, xloc, yloc, mode); + } + + /* Filters start here */ + + public void applyTable(int[] lut) { + int c, r, g, b; + for (int y=roiY; y<(roiY+roiHeight); y++) { + int i = y * width + roiX; + for (int x=roiX; x<(roiX+roiWidth); x++) { + c = pixels[i]; + r = lut[(c&0xff0000)>>16]; + g = lut[(c&0xff00)>>8]; + b = lut[c&0xff]; + pixels[i] = 0xff000000 + (r<<16) + (g<<8) + b; + i++; + } + } + } + + public void applyTable(int[] lut, int channels) { + int c, r=0, g=0, b=0; + for (int y=roiY; y<(roiY+roiHeight); y++) { + int i = y * width + roiX; + for (int x=roiX; x<(roiX+roiWidth); x++) { + c = pixels[i]; + if (channels==4) { + r = lut[(c&0xff0000)>>16]; + g = (c&0xff00)>>8; + b = c&0xff; + } else if (channels==2) { + r = (c&0xff0000)>>16; + g = lut[(c&0xff00)>>8]; + b = c&0xff; + } else if (channels==1) { + r = (c&0xff0000)>>16; + g = (c&0xff00)>>8; + b = lut[c&0xff]; + } else if ((channels&6)==6) { + r = lut[(c&0xff0000)>>16]; + g = lut[(c&0xff00)>>8]; + b = c&0xff; + } else if ((channels&5)==5) { + r = lut[(c&0xff0000)>>16]; + g = (c&0xff00)>>8; + b = lut[c&0xff]; + } else if ((channels&3)==3) { + r = (c&0xff0000)>>16; + g = lut[(c&0xff00)>>8]; + b = lut[c&0xff]; + } + pixels[i] = 0xff000000 + (r<<16) + (g<<8) + b; + i++; + } + } + } + + /** Fills the current rectangular ROI. */ + public void fill() { + for (int y=roiY; y<(roiY+roiHeight); y++) { + int i = y * width + roiX; + for (int x=roiX; x<(roiX+roiWidth); x++) + pixels[i++] = fgColor; + } + } + + public static final int RGB_NOISE=0, RGB_MEDIAN=1, RGB_FIND_EDGES=2, + RGB_ERODE=3, RGB_DILATE=4, RGB_THRESHOLD=5, RGB_ROTATE=6, + RGB_SCALE=7, RGB_RESIZE=8, RGB_TRANSLATE=9, RGB_MIN=10, RGB_MAX=11; + + /** Performs the specified filter on the red, green and blue planes of this image. */ + public void filterRGB(int type, double arg) { + filterRGB(type, arg, 0.0); + } + + final ImageProcessor filterRGB(int type, double arg, double arg2) { + showProgress(0.01); + byte[] R = new byte[width*height]; + byte[] G = new byte[width*height]; + byte[] B = new byte[width*height]; + getRGB(R, G, B); + Rectangle roi = new Rectangle(roiX, roiY, roiWidth, roiHeight); + + ByteProcessor r = new ByteProcessor(width, height, R, null); + r.setRoi(roi); + ByteProcessor g = new ByteProcessor(width, height, G, null); + g.setRoi(roi); + ByteProcessor b = new ByteProcessor(width, height, B, null); + b.setRoi(roi); + r.setBackgroundValue((bgColor&0xff0000)>>16); + g.setBackgroundValue((bgColor&0xff00)>>8); + b.setBackgroundValue(bgColor&0xff); + r.setInterpolationMethod(interpolationMethod); + g.setInterpolationMethod(interpolationMethod); + b.setInterpolationMethod(interpolationMethod); + + showProgress(0.15); + switch (type) { + case RGB_NOISE: + r.noise(arg); showProgress(0.40); + g.noise(arg); showProgress(0.65); + b.noise(arg); showProgress(0.90); + break; + case RGB_MEDIAN: + r.medianFilter(); showProgress(0.40); + g.medianFilter(); showProgress(0.65); + b.medianFilter(); showProgress(0.90); + break; + case RGB_FIND_EDGES: + r.findEdges(); showProgress(0.40); + g.findEdges(); showProgress(0.65); + b.findEdges(); showProgress(0.90); + break; + case RGB_ERODE: + r.erode(); showProgress(0.40); + g.erode(); showProgress(0.65); + b.erode(); showProgress(0.90); + break; + case RGB_DILATE: + r.dilate(); showProgress(0.40); + g.dilate(); showProgress(0.65); + b.dilate(); showProgress(0.90); + break; + case RGB_THRESHOLD: + r.autoThreshold(); showProgress(0.40); + g.autoThreshold(); showProgress(0.65); + b.autoThreshold(); showProgress(0.90); + break; + case RGB_ROTATE: + ij.IJ.showStatus("Rotating red"); + r.rotate(arg); showProgress(0.40); + ij.IJ.showStatus("Rotating green"); + g.rotate(arg); showProgress(0.65); + ij.IJ.showStatus("Rotating blue"); + b.rotate(arg); showProgress(0.90); + break; + case RGB_SCALE: + ij.IJ.showStatus("Scaling red"); + r.scale(arg, arg2); showProgress(0.40); + ij.IJ.showStatus("Scaling green"); + g.scale(arg, arg2); showProgress(0.65); + ij.IJ.showStatus("Scaling blue"); + b.scale(arg, arg2); showProgress(0.90); + break; + case RGB_RESIZE: + int w=(int)arg, h=(int)arg2; + ij.IJ.showStatus("Resizing red"); + ImageProcessor r2 = r.resize(w, h); showProgress(0.40); + ij.IJ.showStatus("Resizing green"); + ImageProcessor g2 = g.resize(w, h); showProgress(0.65); + ij.IJ.showStatus("Resizing blue"); + ImageProcessor b2 = b.resize(w, h); showProgress(0.90); + R = (byte[])r2.getPixels(); + G = (byte[])g2.getPixels(); + B = (byte[])b2.getPixels(); + ColorProcessor ip2 = new ColorProcessor(w, h); + ip2.setRGB(R, G, B); + showProgress(1.0); + return ip2; + case RGB_TRANSLATE: + ij.IJ.showStatus("Translating red"); + r.translate(arg, arg2); showProgress(0.40); + ij.IJ.showStatus("Translating green"); + g.translate(arg, arg2); showProgress(0.65); + ij.IJ.showStatus("Translating blue"); + b.translate(arg, arg2); showProgress(0.90); + break; + case RGB_MIN: + r.filter(MIN); showProgress(0.40); + g.filter(MIN); showProgress(0.65); + b.filter(MIN); showProgress(0.90); + break; + case RGB_MAX: + r.filter(MAX); showProgress(0.40); + g.filter(MAX); showProgress(0.65); + b.filter(MAX); showProgress(0.90); + break; + } + + R = (byte[])r.getPixels(); + G = (byte[])g.getPixels(); + B = (byte[])b.getPixels(); + + setRGB(R, G, B); + showProgress(1.0); + return null; + } + + public void noise(double range) { + filterRGB(RGB_NOISE, range); + } + + public void medianFilter() { + filterRGB(RGB_MEDIAN, 0.0); + } + + public void findEdges() { + filterRGB(RGB_FIND_EDGES, 0.0); + } + + public void erode() { + filterRGB(RGB_ERODE, 0.0); + } + + public void dilate() { + filterRGB(RGB_DILATE, 0.0); + + } + + public void autoThreshold() { + filterRGB(RGB_THRESHOLD, 0.0); + } + + /** Scales the image or selection using the specified scale factors. + @see ImageProcessor#setInterpolate + */ + public void scale(double xScale, double yScale) { + if (interpolationMethod==BICUBIC) { + filterRGB(RGB_SCALE, xScale, yScale); + return; + } + double xCenter = roiX + roiWidth/2.0; + double yCenter = roiY + roiHeight/2.0; + int xmin, xmax, ymin, ymax; + + if ((xScale>1.0) && (yScale>1.0)) { + //expand roi + xmin = (int)(xCenter-(xCenter-roiX)*xScale); + if (xmin<0) xmin = 0; + xmax = xmin + (int)(roiWidth*xScale) - 1; + if (xmax>=width) xmax = width - 1; + ymin = (int)(yCenter-(yCenter-roiY)*yScale); + if (ymin<0) ymin = 0; + ymax = ymin + (int)(roiHeight*yScale) - 1; + if (ymax>=height) ymax = height - 1; + } else { + xmin = roiX; + xmax = roiX + roiWidth - 1; + ymin = roiY; + ymax = roiY + roiHeight - 1; + } + int[] pixels2 = (int[])getPixelsCopy(); + boolean checkCoordinates = (xScale < 1.0) || (yScale < 1.0); + int index1, index2, xsi, ysi; + double ys, xs; + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + for (int y=ymin; y<=ymax; y++) { + ys = (y-yCenter)/yScale + yCenter; + ysi = (int)ys; + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + index1 = y*width + xmin; + index2 = width*(int)ys; + for (int x=xmin; x<=xmax; x++) { + xs = (x-xCenter)/xScale + xCenter; + xsi = (int)xs; + if (checkCoordinates && ((xsixmax) || (ysiymax))) + pixels[index1++] = bgColor; + else { + if (interpolationMethod==BILINEAR) { + if (xs<0.0) xs = 0.0; + if (xs>=xlimit) xs = xlimit2; + pixels[index1++] = getInterpolatedPixel(xs, ys, pixels2); + } else + pixels[index1++] = pixels2[index2+xsi]; + } + } + if (y%20==0) + showProgress((double)(y-ymin)/height); + } + showProgress(1.0); + } + + public ImageProcessor crop() { + int[] pixels2 = new int[roiWidth*roiHeight]; + for (int ys=roiY; ys=width-1.0) + x = width-1.001; + if (y<0.0) y = 0.0; + if (y>=height-1.0) y = height-1.001; + return getInterpolatedPixel(x, y, pixels); + } + + /** Uses bilinear interpolation to find the pixel value at real coordinates (x,y). */ + private final int getInterpolatedPixel(double x, double y, int[] pixels) { + int xbase = (int)x; + int ybase = (int)y; + double xFraction = x - xbase; + double yFraction = y - ybase; + int offset = ybase * width + xbase; + + int lowerLeft = pixels[offset]; + int rll = (lowerLeft&0xff0000)>>16; + int gll = (lowerLeft&0xff00)>>8; + int bll = lowerLeft&0xff; + + int lowerRight = pixels[offset + 1]; + int rlr = (lowerRight&0xff0000)>>16; + int glr = (lowerRight&0xff00)>>8; + int blr = lowerRight&0xff; + + int upperRight = pixels[offset + width + 1]; + int rur = (upperRight&0xff0000)>>16; + int gur = (upperRight&0xff00)>>8; + int bur = upperRight&0xff; + + int upperLeft = pixels[offset + width]; + int rul = (upperLeft&0xff0000)>>16; + int gul = (upperLeft&0xff00)>>8; + int bul = upperLeft&0xff; + + int r, g, b; + double upperAverage, lowerAverage; + upperAverage = rul + xFraction * (rur - rul); + lowerAverage = rll + xFraction * (rlr - rll); + r = (int)(lowerAverage + yFraction * (upperAverage - lowerAverage)+0.5); + upperAverage = gul + xFraction * (gur - gul); + lowerAverage = gll + xFraction * (glr - gll); + g = (int)(lowerAverage + yFraction * (upperAverage - lowerAverage)+0.5); + upperAverage = bul + xFraction * (bur - bul); + lowerAverage = bll + xFraction * (blr - bll); + b = (int)(lowerAverage + yFraction * (upperAverage - lowerAverage)+0.5); + + return 0xff000000 | ((r&0xff)<<16) | ((g&0xff)<<8) | b&0xff; + } + + /** Creates a new ColorProcessor containing a scaled copy of this image or selection. + @see ImageProcessor#setInterpolate + */ + public ImageProcessor resize(int dstWidth, int dstHeight) { + if (roiWidth==dstWidth && roiHeight==dstHeight) + return crop(); + if (interpolationMethod!=NONE && (width==1||height==1)) { + ByteProcessor r2 = (ByteProcessor)getChannel(1,null).resizeLinearly(dstWidth, dstHeight); + ByteProcessor g2 = (ByteProcessor)getChannel(2,null).resizeLinearly(dstWidth, dstHeight); + ByteProcessor b2 = (ByteProcessor)getChannel(3,null).resizeLinearly(dstWidth, dstHeight); + ColorProcessor ip2 = new ColorProcessor(dstWidth, dstHeight); + ip2.setChannel(1, r2); ip2.setChannel(2, g2); ip2.setChannel(3, b2); + return ip2; + } + if (interpolationMethod==BICUBIC) + return filterRGB(RGB_RESIZE, dstWidth, dstHeight); + double srcCenterX = roiX + roiWidth/2.0; + double srcCenterY = roiY + roiHeight/2.0; + double dstCenterX = dstWidth/2.0; + double dstCenterY = dstHeight/2.0; + double xScale = (double)dstWidth/roiWidth; + double yScale = (double)dstHeight/roiHeight; + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + if (interpolationMethod==BILINEAR) { + if (dstWidth!=width) dstCenterX+=xScale/4.0; + if (dstHeight!=height) dstCenterY+=yScale/4.0; + } + ImageProcessor ip2 = createProcessor(dstWidth, dstHeight); + int[] pixels2 = (int[])ip2.getPixels(); + double xs, ys; + int index1, index2; + for (int y=0; y<=dstHeight-1; y++) { + ys = (y-dstCenterY)/yScale + srcCenterY; + if (interpolationMethod==BILINEAR) { + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + } + index1 = width*(int)ys; + index2 = y*dstWidth; + for (int x=0; x<=dstWidth-1; x++) { + xs = (x-dstCenterX)/xScale + srcCenterX; + if (interpolationMethod==BILINEAR) { + if (xs<0.0) xs = 0.0; + if (xs>=xlimit) xs = xlimit2; + pixels2[index2++] = getInterpolatedPixel(xs, ys, pixels); + } else + pixels2[index2++] = pixels[index1+(int)xs]; + } + if (y%20==0) + showProgress((double)y/dstHeight); + } + showProgress(1.0); + return ip2; + } + + /** Uses averaging to creates a new ColorProcessor containing + a downsized copy of this image or selection. */ + public ImageProcessor makeThumbnail(int width2, int height2, double smoothFactor) { + return resize(width2, height2, true); + } + + /** Rotates the image or ROI 'angle' degrees clockwise. + @see ImageProcessor#setInterpolationMethod + */ + public void rotate(double angle) { + if (angle%360==0) + return; + if (interpolationMethod==BICUBIC) { + filterRGB(RGB_ROTATE, angle); + return; + } + int[] pixels2 = (int[])getPixelsCopy(); + double centerX = roiX + (roiWidth-1)/2.0; + double centerY = roiY + (roiHeight-1)/2.0; + int xMax = roiX + this.roiWidth - 1; + + double angleRadians = -angle/(180.0/Math.PI); + double ca = Math.cos(angleRadians); + double sa = Math.sin(angleRadians); + double tmp1 = centerY*sa-centerX*ca; + double tmp2 = -centerX*sa-centerY*ca; + double tmp3, tmp4, xs, ys; + int index, ixs, iys; + double dwidth = width, dheight=height; + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + + for (int y=roiY; y<(roiY + roiHeight); y++) { + index = y*width + roiX; + tmp3 = tmp1 - y*sa + centerX; + tmp4 = tmp2 + y*ca + centerY; + for (int x=roiX; x<=xMax; x++) { + xs = x*ca + tmp3; + ys = x*sa + tmp4; + if ((xs>=-0.01) && (xs=-0.01) && (ys=xlimit) xs = xlimit2; + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + pixels[index++] = getInterpolatedPixel(xs, ys, pixels2); + } else { + ixs = (int)(xs+0.5); + iys = (int)(ys+0.5); + if (ixs>=width) ixs = width - 1; + if (iys>=height) iys = height -1; + pixels[index++] = pixels2[width*iys+ixs]; + } + } else + pixels[index++] = bgColor; + } + if (y%30==0) + showProgress((double)(y-roiY)/roiHeight); + } + showProgress(1.0); + } + + public void flipVertical() { + int index1,index2; + int tmp; + for (int y=0; y> 16) + + k2*((p2 & 0xff0000) >> 16) + + k3*((p3 & 0xff0000) >> 16) + + k4*((p4 & 0xff0000) >> 16) + + k5*((p5 & 0xff0000) >> 16) + + k6*((p6 & 0xff0000) >> 16) + + k7*((p7 & 0xff0000) >> 16) + + k8*((p8 & 0xff0000) >> 16) + + k9*((p9 & 0xff0000) >> 16); + rsum /= scale; + if(rsum>255) rsum = 255; + if(rsum<0) rsum = 0; + + gsum = k1*((p1 & 0xff00) >> 8) + + k2*((p2 & 0xff00) >> 8) + + k3*((p3 & 0xff00) >> 8) + + k4*((p4 & 0xff00) >> 8) + + k5*((p5 & 0xff00) >> 8) + + k6*((p6 & 0xff00) >> 8) + + k7*((p7 & 0xff00) >> 8) + + k8*((p8 & 0xff00) >> 8) + + k9*((p9 & 0xff00) >> 8); + gsum /= scale; + if(gsum>255) gsum = 255; + else if(gsum<0) gsum = 0; + + bsum = k1*(p1 & 0xff) + + k2*(p2 & 0xff) + + k3*(p3 & 0xff) + + k4*(p4 & 0xff) + + k5*(p5 & 0xff) + + k6*(p6 & 0xff) + + k7*(p7 & 0xff) + + k8*(p8 & 0xff) + + k9*(p9 & 0xff); + bsum /= scale; + if (bsum>255) bsum = 255; + if (bsum<0) bsum = 0; + + pixels[offset++] = 0xff000000 + | ((rsum << 16) & 0xff0000) + | ((gsum << 8 ) & 0xff00) + | (bsum & 0xff); + } + if (y%inc==0) + showProgress((double)(y-roiY)/roiHeight); + } + showProgress(1.0); + } + + /** A 3x3 filter operation, where the argument (ImageProcessor.BLUR_MORE, FIND_EDGES, + MEDIAN_FILTER, MIN or MAX) determines the filter type. */ + public void filter(int type) { + if (type == FIND_EDGES) + filterRGB(RGB_FIND_EDGES, 0, 0); + else if (type == MEDIAN_FILTER) + filterRGB(RGB_MEDIAN, 0, 0); + else if (type == MIN) + filterRGB(RGB_MIN, 0, 0); + else if (type == MAX) + filterRGB(RGB_MAX, 0, 0); + else + blurMore(); + } + + /** BLUR MORE: 3x3 unweighted smoothing is implemented directly, does not convert the image to three ByteProcessors. */ + private void blurMore() { + int p1 = 0, p2, p3, p4 = 0, p5, p6, p7 = 0, p8, p9; + + int[] prevRow = new int[width]; + int[] thisRow = new int[width]; + int[] nextRow = new int[width]; + System.arraycopy(pixels, Math.max(roiY-1,0)*width, thisRow, 0, width); + System.arraycopy(pixels, roiY*width, nextRow, 0, width); + for (int y=roiY; y1000000 && (y&0xff)==0) + showProgress((double)(y-roiY)/roiHeight); + } + showProgress(1.0); + } + + public int[] getHistogram() { + if (mask!=null) + return getHistogram(mask); + double rw=rWeight, gw=gWeight, bw=bWeight; + if (weights!=null) + {rw=weights[0]; gw=weights[1]; bw=weights[2];} + int c, r, g, b, v; + int[] histogram = new int[256]; + for (int y=roiY; y<(roiY+roiHeight); y++) { + int i = y * width + roiX; + for (int x=roiX; x<(roiX+roiWidth); x++) { + c = pixels[i++]; + r = (c&0xff0000)>>16; + g = (c&0xff00)>>8; + b = c&0xff; + v = (int)(r*rw + g*gw + b*bw + 0.5); + histogram[v]++; + } + } + return histogram; + } + + + public int[] getHistogram(ImageProcessor mask) { + if (mask.getWidth()!=roiWidth||mask.getHeight()!=roiHeight) + throw new IllegalArgumentException(maskSizeError(mask)); + double rw=rWeight, gw=gWeight, bw=bWeight; + if (weights!=null) + {rw=weights[0]; gw=weights[1]; bw=weights[2];} + byte[] mpixels = (byte[])mask.getPixels(); + int c, r, g, b, v; + int[] histogram = new int[256]; + for (int y=roiY, my=0; y<(roiY+roiHeight); y++, my++) { + int i = y * width + roiX; + int mi = my * roiWidth; + for (int x=roiX; x<(roiX+roiWidth); x++) { + if (mpixels[mi++]!=0) { + c = pixels[i]; + r = (c&0xff0000)>>16; + g = (c&0xff00)>>8; + b = c&0xff; + v = (int)(r*rw + g*gw + b*bw + 0.5); + histogram[v]++; + } + i++; + } + } + return histogram; + } + + public synchronized boolean weightedHistogram() { + if (weights!=null && (weights[0]!=1d/3d||weights[1]!=1d/3d||weights[2]!=1d/3d)) + return true; + if (rWeight!=1d/3d || gWeight!=1d/3d || bWeight!=1d/3d) + return true; + return false; + } + + /** Performs a convolution operation using the specified kernel. */ + public void convolve(float[] kernel, int kernelWidth, int kernelHeight) { + int size = width*height; + byte[] r = new byte[size]; + byte[] g = new byte[size]; + byte[] b = new byte[size]; + getRGB(r,g,b); + ImageProcessor rip = new ByteProcessor(width, height, r, null); + ImageProcessor gip = new ByteProcessor(width, height, g, null); + ImageProcessor bip = new ByteProcessor(width, height, b, null); + ImageProcessor ip2 = rip.convertToFloat(); + Rectangle roi = getRoi(); + ip2.setRoi(roi); + ip2.convolve(kernel, kernelWidth, kernelHeight); + ImageProcessor r2 = ip2.convertToByte(false); + ip2 = gip.convertToFloat(); + ip2.setRoi(roi); + ip2.convolve(kernel, kernelWidth, kernelHeight); + ImageProcessor g2 = ip2.convertToByte(false); + ip2 = bip.convertToFloat(); + ip2.setRoi(roi); + ip2.convolve(kernel, kernelWidth, kernelHeight); + ImageProcessor b2 = ip2.convertToByte(false); + setRGB((byte[])r2.getPixels(), (byte[])g2.getPixels(), (byte[])b2.getPixels()); + } + + /** Sets the weighting factors used by getPixelValue(), getHistogram() + and convertToByte() to do color conversions. The default values are + 1/3, 1/3 and 1/3. Check "Weighted RGB Conversions" in + Edit/Options/Conversions to use 0.299, 0.587 and 0.114. + @see #getWeightingFactors + @see #setRGBWeights + */ + public static void setWeightingFactors(double rFactor, double gFactor, double bFactor) { + rWeight = rFactor; + gWeight = gFactor; + bWeight = bFactor; + } + + /** Returns the three weighting factors used by getPixelValue(), + getHistogram() and convertToByte() to do color conversions. + @see #setWeightingFactors + @see #getRGBWeights + */ + public static double[] getWeightingFactors() { + double[] weights = new double[3]; + weights[0] = rWeight; + weights[1] = gWeight; + weights[2] = bWeight; + return weights; + } + + /** This is a thread-safe (non-static) version of setWeightingFactors(). */ + public void setRGBWeights(double rweight, double gweight, double bweight) { + weights = new double[3]; + weights[0] = rweight; + weights[1] = gweight; + weights[2] = bweight; + } + + /** This is a thread-safe (non-static) version of setWeightingFactors(). */ + public void setRGBWeights(double[] weights) { + this.weights = weights; + } + + /** Returns the values set by setRGBWeights(), or null if setRGBWeights() has not been called. */ + public double[] getRGBWeights() { + return weights; + } + + /** Always returns false since RGB images do not use LUTs. */ + public boolean isInvertedLut() { + return false; + } + + /** Returns 'true' if this is a grayscale image. */ + public final boolean isGrayscale() { + int c, r, g, b; + for (int i=0; i>16; + g = (c&0xff00)>>8; + b = c&0xff; + if (r!=g || r!=b) return false; + } + return true; + } + + /** Always returns 0 since RGB images do not use LUTs. */ + public int getBestIndex(Color c) { + return 0; + } + + /** Does nothing since RGB images do not use LUTs. */ + public void invertLut() { + } + + public void updateComposite(int[] rgbPixels, int channel) { + } + + /** Not implemented. */ + public void threshold(int level) {} + + /** Returns the number of color channels (3). */ + public int getNChannels() { + return 3; + } + + /** Returns a FloatProcessor with one color channel of the image. + * The roi and mask are also set for the FloatProcessor. + * @param channelNumber Determines the color channel, 0=red, 1=green, 2=blue + * @param fp Here a FloatProcessor can be supplied, or null. The FloatProcessor + * is overwritten by this method (re-using its pixels array + * improves performance). + * @return A FloatProcessor with the converted image data of the color channel selected + */ + public FloatProcessor toFloat(int channelNumber, FloatProcessor fp) { + int size = width*height; + if (fp == null || fp.getWidth()!=width || fp.getHeight()!=height) + fp = new FloatProcessor(width, height, new float[size], null); + float[] fPixels = (float[])fp.getPixels(); + int shift = 16 - 8*channelNumber; + int byteMask = 255<>shift; + fp.setRoi(getRoi()); + fp.setMask(mask); + fp.setMinAndMax(0, 255); + return fp; + } + + /** Sets the pixels of one color channel from a FloatProcessor. + * @param channelNumber Determines the color channel, 0=red, 1=green, 2=blue + * @param fp The FloatProcessor where the image data are read from. + */ + public void setPixels(int channelNumber, FloatProcessor fp) { + float[] fPixels = (float[])fp.getPixels(); + float value; + int size = width*height; + int shift = 16 - 8*channelNumber; + int resetMask = 0xffffffff^(255<255f) value = 255f; + pixels[i] = (pixels[i]&resetMask) | ((int)value<> 16) & 0xff; + result[1] = (rgb >> 8) & 0xff; + result[2] = (rgb >> 0) & 0xff; + return result; + } + + public int[] HSBtoRGB(double[] HSB) { + return HSBtoRGB(HSB[0], HSB[1], HSB[2]); + } + + /** + * Convert LAB to RGB. + * @param L + * @param a + * @param b + * @return RGB values + */ + public int[] LABtoRGB(double L, double a, double b) { + return XYZtoRGB(LABtoXYZ(L, a, b)); + } + + /** + * @param Lab + * @return RGB values + */ + public int[] LABtoRGB(double[] Lab) { + return XYZtoRGB(LABtoXYZ(Lab)); + } + + /** + * Convert LAB to XYZ. + * @param L + * @param a + * @param b + * @return XYZ values + */ + public double[] LABtoXYZ(double L, double a, double b) { + double[] result = new double[3]; + + double y = (L + 16.0) / 116.0; + double y3 = Math.pow(y, 3.0); + double x = (a / 500.0) + y; + double x3 = Math.pow(x, 3.0); + double z = y - (b / 200.0); + double z3 = Math.pow(z, 3.0); + + if (y3 > 0.008856) { + y = y3; + } + else { + y = (y - (16.0 / 116.0)) / 7.787; + } + if (x3 > 0.008856) { + x = x3; + } + else { + x = (x - (16.0 / 116.0)) / 7.787; + } + if (z3 > 0.008856) { + z = z3; + } + else { + z = (z - (16.0 / 116.0)) / 7.787; + } + + result[0] = x * whitePoint[0]; + result[1] = y * whitePoint[1]; + result[2] = z * whitePoint[2]; + + return result; + } + + /** + * Convert LAB to XYZ. + * @param Lab + * @return XYZ values + */ + public double[] LABtoXYZ(double[] Lab) { + return LABtoXYZ(Lab[0], Lab[1], Lab[2]); + } + + /** + * @param R Red in range 0..255 + * @param G Green in range 0..255 + * @param B Blue in range 0..255 + * @return HSB values: H is 0..360 degrees / 360 (0..1), S is 0..1, B is 0..1 + */ + public double[] RGBtoHSB(int R, int G, int B) { + double[] result = new double[3]; + float[] hsb = new float[3]; + Color.RGBtoHSB(R, G, B, hsb); + result[0] = hsb[0]; + result[1] = hsb[1]; + result[2] = hsb[2]; + return result; + } + + public double[] RGBtoHSB(int[] RGB) { + return RGBtoHSB(RGB[0], RGB[1], RGB[2]); + } + + /** + * @param rgb RGB value + * @return Lab values + */ + public double[] RGBtoLAB(int rgb) { + int r = (rgb&0xff0000)>>16; + int g = (rgb&0xff00)>>8; + int b = rgb&0xff; + return XYZtoLAB(RGBtoXYZ(r,g,b)); + } + + /** + * @param RGB + * @return Lab values + */ + public double[] RGBtoLAB(int[] RGB) { + return XYZtoLAB(RGBtoXYZ(RGB)); + } + + /** + * Convert RGB to XYZ + * @param R + * @param G + * @param B + * @return XYZ in double array. + */ + public double[] RGBtoXYZ(int R, int G, int B) { + double[] result = new double[3]; + + // convert 0..255 into 0..1 + double r = R / 255.0; + double g = G / 255.0; + double b = B / 255.0; + + // assume sRGB + if (r <= 0.04045) { + r = r / 12.92; + } + else { + r = Math.pow(((r + 0.055) / 1.055), 2.4); + } + if (g <= 0.04045) { + g = g / 12.92; + } + else { + g = Math.pow(((g + 0.055) / 1.055), 2.4); + } + if (b <= 0.04045) { + b = b / 12.92; + } + else { + b = Math.pow(((b + 0.055) / 1.055), 2.4); + } + + r *= 100.0; + g *= 100.0; + b *= 100.0; + + // [X Y Z] = [r g b][M] + result[0] = (r * M[0][0]) + (g * M[0][1]) + (b * M[0][2]); + result[1] = (r * M[1][0]) + (g * M[1][1]) + (b * M[1][2]); + result[2] = (r * M[2][0]) + (g * M[2][1]) + (b * M[2][2]); + + return result; + } + + /** + * Convert RGB to XYZ + * @param RGB + * @return XYZ in double array. + */ + public double[] RGBtoXYZ(int[] RGB) { + return RGBtoXYZ(RGB[0], RGB[1], RGB[2]); + } + + /** + * @param x + * @param y + * @param Y + * @return XYZ values + */ + public double[] xyYtoXYZ(double x, double y, double Y) { + double[] result = new double[3]; + if (y == 0) { + result[0] = 0; + result[1] = 0; + result[2] = 0; + } + else { + result[0] = (x * Y) / y; + result[1] = Y; + result[2] = ((1 - x - y) * Y) / y; + } + return result; + } + + /** + * @param xyY + * @return XYZ values + */ + public double[] xyYtoXYZ(double[] xyY) { + return xyYtoXYZ(xyY[0], xyY[1], xyY[2]); + } + + /** + * Convert XYZ to LAB. + * @param X + * @param Y + * @param Z + * @return Lab values + */ + public double[] XYZtoLAB(double X, double Y, double Z) { + + double x = X / whitePoint[0]; + double y = Y / whitePoint[1]; + double z = Z / whitePoint[2]; + + if (x > 0.008856) { + x = Math.pow(x, 1.0 / 3.0); + } + else { + x = (7.787 * x) + (16.0 / 116.0); + } + if (y > 0.008856) { + y = Math.pow(y, 1.0 / 3.0); + } + else { + y = (7.787 * y) + (16.0 / 116.0); + } + if (z > 0.008856) { + z = Math.pow(z, 1.0 / 3.0); + } + else { + z = (7.787 * z) + (16.0 / 116.0); + } + + double[] result = new double[3]; + + result[0] = (116.0 * y) - 16.0; + result[1] = 500.0 * (x - y); + result[2] = 200.0 * (y - z); + + return result; + } + + /** + * Convert XYZ to LAB. + * @param XYZ + * @return Lab values + */ + public double[] XYZtoLAB(double[] XYZ) { + return XYZtoLAB(XYZ[0], XYZ[1], XYZ[2]); + } + + /** + * Convert XYZ to RGB. + * @param X + * @param Y + * @param Z + * @return RGB in int array. + */ + public int[] XYZtoRGB(double X, double Y, double Z) { + int[] result = new int[3]; + + double x = X / 100.0; + double y = Y / 100.0; + double z = Z / 100.0; + + // [r g b] = [X Y Z][Mi] + double r = (x * Mi[0][0]) + (y * Mi[0][1]) + (z * Mi[0][2]); + double g = (x * Mi[1][0]) + (y * Mi[1][1]) + (z * Mi[1][2]); + double b = (x * Mi[2][0]) + (y * Mi[2][1]) + (z * Mi[2][2]); + + // assume sRGB + if (r > 0.0031308) { + r = ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055); + } + else { + r = (r * 12.92); + } + if (g > 0.0031308) { + g = ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055); + } + else { + g = (g * 12.92); + } + if (b > 0.0031308) { + b = ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055); + } + else { + b = (b * 12.92); + } + + r = (r < 0) ? 0 : r; + g = (g < 0) ? 0 : g; + b = (b < 0) ? 0 : b; + + // convert 0..1 into 0..255 + result[0] = (int) Math.round(r * 255); + result[1] = (int) Math.round(g * 255); + result[2] = (int) Math.round(b * 255); + + return result; + } + + /** + * Convert XYZ to RGB + * @param XYZ in a double array. + * @return RGB in int array. + */ + public int[] XYZtoRGB(double[] XYZ) { + return XYZtoRGB(XYZ[0], XYZ[1], XYZ[2]); + } + + /** + * @param X + * @param Y + * @param Z + * @return xyY values + */ + public double[] XYZtoxyY(double X, double Y, double Z) { + double[] result = new double[3]; + if ((X + Y + Z) == 0) { + result[0] = chromaWhitePoint[0]; + result[1] = chromaWhitePoint[1]; + result[2] = chromaWhitePoint[2]; + } else { + result[0] = X / (X + Y + Z); + result[1] = Y / (X + Y + Z); + result[2] = Y; + } + return result; + } + + /** + * @param XYZ + * @return xyY values + */ + public double[] XYZtoxyY(double[] XYZ) { + return XYZtoxyY(XYZ[0], XYZ[1], XYZ[2]); + } + + /** Converts an RGB image into a Lab stack. */ + public ImagePlus RGBToLab(ImagePlus img) { + ColorProcessor cp = (ColorProcessor)img.getProcessor(); + ColorSpaceConverter converter = new ColorSpaceConverter(); + int[] pixels = (int[])cp.getPixels(); + int w = cp.getWidth(); + int h = cp.getHeight(); + ImageStack stack = new ImageStack(w,h); + FloatProcessor L = new FloatProcessor(w,h); + FloatProcessor a = new FloatProcessor(w,h); + FloatProcessor b = new FloatProcessor(w,h); + stack.addSlice("L*",L); + stack.addSlice("a*",a); + stack.addSlice("b*",b); + for (int i=0; iroiMax) + roiMax = v; + } + i++; + } + } + min = roiMin; max = roiMax; + binSize = (max-min)/nBins; + histMin = min; + histMax = max; + + // Generate histogram + double scale = nBins/(max-min); + int index; + pixelCount = 0; + for (int y=ry, my=0; y<(ry+rh); y++, my++) { + int i = y * width + rx; + int mi = my * rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null || mask[mi++]!=0) { + v = pixels[i]; + pixelCount++; + sum += v; + sum2 += v*v; + index = (int)(scale*(v-min)); + if (index>=nBins) + index = nBins-1; + histogram[index]++; + } + i++; + } + } + area = pixelCount*pw*ph; + mean = sum/pixelCount; + umean = mean; + calculateStdDev(pixelCount, sum, sum2); + + // calculate mode + int count; + maxCount = 0; + for (int i = 0; i < nBins; i++) { + count = histogram[i]; + if (count > maxCount) { + maxCount = count; + mode = i; + } + } + dmode = histMin+mode*binSize; + if (binSize!=1.0) + dmode += binSize/2.0; + } + +} diff --git a/src/ij/process/DownsizeTable.java b/src/ij/process/DownsizeTable.java new file mode 100644 index 0000000..0f73d18 --- /dev/null +++ b/src/ij/process/DownsizeTable.java @@ -0,0 +1,130 @@ +package ij.process; +import java.util.Arrays; + +/** A table for easier downsizing by convolution with a kernel. + * Supports the interpolation methods of ImageProcessor: none, bilinear, bicubic + * Convention used: The left edges of the first pixel are the same for source and destination. + * E.g. when downsizing by a factor of 2, pixel 0 of the destination + * takes the space of pixels 0 and 1 of the source. + * + * Example for use: Downsizing row 0 of 'pixels' from 'roi.width' to 'destinationWidth'. + * The input range is given by the roi rectangle. + * Output is written to row 0 of 'pixels2' (width: 'destinationWidth') + + DownSizeTable dt = new DownSizeTable(width, roi.x, roi.width, destinationWidth, ImageProcessor.BICUBIC); + int tablePointer = 0; + for (int srcPoint=dt.srcStart, srcPoint<=dt.srcEnd; srcPoint++) { + float v = pixels[srcPoint]; + for (int i=0; i + */ +public class DownsizeTable { + /** Number of kernel points per source data point */ + public final int kernelSize; + /** index of the first point of the source data that should be accessed */ + public final int srcStart; + /** index of the last point of the source data that should be accessed */ + public final int srcEnd; + /** For each source point between srcStart and srcEnd, indices of destination + * points where the data should be added. + * Arranged in blocks of 'kernelSize' points. E.g. for kernelSize=2, array + * elements 0,1 are for point srcStart, 2,3 for point srcStart+1, etc. */ + public final int[] indices; + /** For each source point, weights for adding it to the destination point + * given in the corresponding element of 'indices' */ + public final float[] weights; + /** Kernel sizes corresponding to the interpolation methods NONE, BILINEAR, BICUBIC */ + private final static int[] kernelSizes = new int[] {1, 2, 4}; + private final int srcOrigin, srcLength; + private final double scale; //source/destination pixel numbers + private final int interpolationMethod; + private final static int UNUSED=-1; //marks unused entries in 'indices' array + + + /** Create a table for 1-dimensional downscaling interpolation. + * Interpolation is done by + * @param srcSize Size of source data, i.e., width or height of input image + * @param srcOrigin Index of first pixel of source data that corresponds to an ouput pixel, + * 0 or origin of source rectangle if only a roi is scaled + * @param srcLength Number of pixels of source data that should correspond to output, i.e., + * width or height of source roi + * @param dstSize Number of destination pixels. + * @param interpolationMethod One of the methods defined in ImageProcessor: NONE, BILINEAR, BICUBIC + */ + DownsizeTable(int srcSize, int srcOrigin, int srcLength, int dstSize, int interpolationMethod) { + this.srcOrigin = srcOrigin; + this.srcLength = srcLength; + this.interpolationMethod = interpolationMethod; + this.scale = srcLength / (double)dstSize; + this.kernelSize = kernelSizes[interpolationMethod]; + int srcStartUncorr = (int)(Math.ceil(1e-8+srcIndex(-0.5*kernelSize))); //may be <0 + srcStart = srcStartUncorr < 0 ? 0 : srcStartUncorr; //corrected value, avoids pointing out of array + int srcEndUncorr = (int)(Math.floor(1e-8+srcIndex(dstSize-1 + 0.5*kernelSize))); + srcEnd = srcEndUncorr >=srcSize ? srcSize-1 : srcEndUncorr; + int arraySize = (srcEnd - srcStart + 1) * kernelSize; + indices = new int[arraySize]; + weights = new float[arraySize]; + Arrays.fill(indices, UNUSED); + //IJ.log("src size="+srcSize+" range="+srcStart+"-"+srcEnd+" array:"+arraySize+" scale="+(float)scale); + + for (int dst=0; dst= srcSize ? srcSize-1 : src); + int p = (s-srcStart)*kernelSize;// points to first value in 'indices' and 'weights' + // arrays reserved for this source pixel + while(indices[p]!=UNUSED && indices[p]!=dst) + p++; //position used for other destination pixel, try the next one + //if(p-(s-srcStart)*kernelSize>=kernelSize)IJ.log(srcSize+">"+dstSize+": too long: src="+src+" dst="+dst); + indices[p] = dst; + float weight = kernel(dst - dstIndex(src)); + sum += weight; + weights[p] += weight; + //IJ.log("src="+src+"("+s+") to "+dst+" w="+weight+" p="+p); + } + //normalize: sum of weights contributing to this destination pixel should be 1 + int iStart = (lowestS-srcStart)*kernelSize; + if (iStart < 0) iStart = 0; + int iStop = (highestS-srcStart)*kernelSize+(kernelSize-1); + if (iStop>=indices.length) iStop = indices.length-1; + //IJ.log("normalize "+iStart+"-"+iStop+" sum="+sum); + for (int i=iStart; i<=iStop; i++) + if (indices[i] == dst) + weights[i] = (float)(weights[i]/sum); + } + for (int i=0; i= array size. + private double srcIndex(double dstIndex) { + return srcOrigin-0.5 + (dstIndex+0.5)*scale; + } + // Converts the coordinate (index) of a source pixel to the destination pixel + private double dstIndex(int srcIndex) { + return (srcIndex-srcOrigin+0.5)/scale - 0.5; + } + // Calculates the kernel value. Only valid within +/- 0.5*kernelSize + protected float kernel(double x) { + switch (interpolationMethod) { + case ImageProcessor.NONE: + return 1f; + case ImageProcessor.BILINEAR: + return 1f - (float)Math.abs(x); + case ImageProcessor.BICUBIC: + return (float)ImageProcessor.cubic(x); + } + return Float.NaN; + } + +} diff --git a/src/ij/process/EllipseFitter.java b/src/ij/process/EllipseFitter.java new file mode 100644 index 0000000..517df38 --- /dev/null +++ b/src/ij/process/EllipseFitter.java @@ -0,0 +1,346 @@ +package ij.process; +import ij.*; +import ij.gui.*; +import java.awt.*; +import ij.plugin.filter.*; + +/* +Best-fitting ellipse routines by: + + Bob Rodieck + Department of Ophthalmology, RJ-10 + University of Washington, + Seattle, WA, 98195 + +Notes on best-fitting ellipse: + + Consider some arbitrarily shaped closed profile, which we wish to + characterize in a quantitative manner by a series of terms, each + term providing a better approximation to the shape of the profile. + Assume also that we wish to include the orientation of the profile + (i.e. which way is up) in our characterization. + + One approach is to view the profile as formed by a series harmonic + components, much in the same way that one can decompose a waveform + over a fixed interval into a series of Fourier harmonics over that + interval. From this perspective the first term is the mean radius, + or some related value (i.e. the area). The second term is the + magnitude and phase of the first harmonic, which is equivalent to the + best-fitting ellipse. + + What constitutes the best-fitting ellipse? First, it should have the + same area. In statistics, the measure that attempts to characterize some + two-dimensional distribution of data points is the 'ellipse of + concentration' (see Cramer, Mathematical Methods of Statistics, + Princeton Univ. Press, 945, page 283). This measure equates the second + order central moments of the ellipse to those of the distribution, + and thereby effectively defines both the shape and size of the ellipse. + + This technique can be applied to a profile by assuming that it constitutes + a uniform distribution of points bounded by the perimeter of the profile. + For most 'blob-like' shapes the area of the ellipse is close to that + of the profile, differing by no more than about 4%. We can then make + a small adjustment to the size of the ellipse, so as to give it the + same area as that of the profile. This is what is done here, and + therefore this is what we mean by 'best-fitting'. + + For a real pathologic case, consider a dumbell shape formed by two small + circles separated by a thin line. Changing the distance between the + circles alters the second order moments, and thus the size of the ellipse + of concentration, without altering the area of the profile. + +public class Ellipse_Fitter implements PlugInFilter { + public int setup(String arg, ImagePlus imp) { + return DOES_ALL; + } + public void run(ImageProcessor ip) { + EllipseFitter ef = new EllipseFitter(); + ef.fit(ip); + IJ.log(IJ.d2s(ef.major)+" "+IJ.d2s(ef.minor)+" "+IJ.d2s(ef.angle)+" "+IJ.d2s(ef.xCenter)+" "+IJ.d2s(ef.yCenter)); + ef.drawEllipse(ip); + } +} +*/ + + +/** This class fits an ellipse to an ROI. */ +public class EllipseFitter { + + static final double HALFPI = 1.5707963267949; + + /** X centroid */ + public double xCenter; + + /** X centroid */ + public double yCenter; + + /** Length of major axis */ + public double major; + + /** Length of minor axis */ + public double minor; + + /** Angle in degrees */ + public double angle; + + /** Angle in radians */ + public double theta; + + /** Initialized by makeRoi() */ + public int[] xCoordinates; + /** Initialized by makeRoi() */ + public int[] yCoordinates; + /** Initialized by makeRoi() */ + public int nCoordinates = 0; + + + private int bitCount; + private double xsum, ysum, x2sum, y2sum, xysum; + private byte[] mask; + private int left, top, width, height; + private double n; + private double xm, ym; //mean values + private double u20, u02, u11; //central moments + private ImageProcessor ip; + //private double pw, ph; + private boolean record; + + /** Fits an ellipse to the current ROI. The 'stats' argument, currently not used, + can be null. The fit parameters are returned in public fields. */ + public void fit(ImageProcessor ip, ImageStatistics stats) { + this.ip = ip; + mask = ip.getMaskArray(); + Rectangle r = ip.getRoi(); + left = r.x; + top = r.y; + width = r.width; + height = r.height; + getEllipseParam(); + } + + void getEllipseParam() { + double sqrtPi = 1.772453851; + double a11, a12, a22, m4, z, scale, tmp, xoffset, yoffset; + double RealAngle; + + if (mask==null) { + major = (width*2) / sqrtPi; + minor = (height*2) / sqrtPi; // * Info->PixelAspectRatio; + angle = 0.0; + theta = 0.0; + if (major < minor) { + tmp = major; + major = minor; + minor = tmp; + angle = 90.0; + theta = Math.PI/2.0; + } + xCenter = left + width / 2.0; + yCenter = top + height / 2.0; + return; + } + + computeSums(); + getMoments(); + m4 = 4.0 * Math.abs(u02 * u20 - u11 * u11); + if (m4 < 0.000001) + m4 = 0.000001; + a11 = u02 / m4; + a12 = u11 / m4; + a22 = u20 / m4; + xoffset = xm; + yoffset = ym; + + tmp = a11 - a22; + if (tmp == 0.0) + tmp = 0.000001; + theta = 0.5 * Math.atan(2.0 * a12 / tmp); + if (theta < 0.0) + theta += HALFPI; + if (a12 > 0.0) + theta += HALFPI; + else if (a12 == 0.0) { + if (a22 > a11) { + theta = 0.0; + tmp = a22; + a22 = a11; + a11 = tmp; + } else if (a11 != a22) + theta = HALFPI; + } + tmp = Math.sin(theta); + if (tmp == 0.0) + tmp = 0.000001; + z = a12 * Math.cos(theta) / tmp; + major = Math.sqrt (1.0 / Math.abs(a22 + z)); + minor = Math.sqrt (1.0 / Math.abs(a11 - z)); + scale = Math.sqrt (bitCount / (Math.PI * major * minor)); //equalize areas + major = major*scale*2.0; + minor = minor*scale*2.0; + angle = 180.0 * theta / Math.PI; + if (angle == 180.0) + angle = 0.0; + if (major < minor) { + tmp = major; + major = minor; + minor = tmp; + } + xCenter = left + xoffset + 0.5; + yCenter = top + yoffset + 0.5; + } + + void computeSums () { + xsum = 0.0; + ysum = 0.0; + x2sum = 0.0; + y2sum = 0.0; + xysum = 0.0; + int bitcountOfLine; + double xe, ye; + int xSumOfLine; + for (int y=0; ymaxY) + ymax = maxY; + if (ymax<1) + ymax = 1; + ymin = -ymax; + // Precalculation and use of symmetry speed things up + for (int y=0; y<=ymax; y++) { + //GetMinMax(y, aMinMax); + j2 = Math.sqrt(k2 * sqr(y) + k3); + j1 = k1 * y; + txmin[y] = (int)Math.round(j1 - j2); + txmax[y] = (int)Math.round(j1 + j2); + } + if (record) { + xCoordinates[nCoordinates] = xc + txmin[ymax - 1]; + yCoordinates[nCoordinates] = yc + ymin; + nCoordinates++; + } else + ip.moveTo(xc + txmin[ymax - 1], yc + ymin); + for (int y=ymin; yymin; y--) { + x = y<0?txmin[-y]:-txmax[y]; + if (record) { + xCoordinates[nCoordinates] = xc + x; + yCoordinates[nCoordinates] = yc + y; + nCoordinates++; + } else + ip.lineTo(xc + x, yc + y); + } + } + + /** Generates the xCoordinates, yCoordinates public arrays + that can be used to create an ROI. */ + public void makeRoi(ImageProcessor ip) { + record = true; + int size = ip.getHeight()*3; + xCoordinates = new int[size]; + yCoordinates = new int[size]; + nCoordinates = 0; + drawEllipse(ip); + record = false; + } + + private double sqr(double x) { + return x*x; + } + +} diff --git a/src/ij/process/FHT.java b/src/ij/process/FHT.java new file mode 100644 index 0000000..13d36bb --- /dev/null +++ b/src/ij/process/FHT.java @@ -0,0 +1,662 @@ +package ij.process; +import ij.*; +import ij.plugin.FFT; +import ij.plugin.ContrastEnhancer; +import java.awt.image.ColorModel; + +/** +This class contains a Java implementation of the Fast Hartley +Transform. It is based on Pascal code in NIH Image contributed +by Arlo Reeves (http://imagej.nih.gov/ij/docs/ImageFFT/). +The Fast Hartley Transform was restricted by U.S. Patent No. 4,646,256, +but was placed in the public domain by Stanford University in 1995 +and is now freely available. +*/ +public class FHT extends FloatProcessor { + private boolean isFrequencyDomain; + private int maxN; + private float[] C; + private float[] S; + private int[] bitrev; + private float[] tempArr; + private boolean showProgress; + + + /** Used by the FFT class. */ + public boolean quadrantSwapNeeded; + /** Used by the FFT class. */ + public ColorProcessor rgb; + /** Used by the FFT class. */ + public int originalWidth; + /** Used by the FFT class. */ + public int originalHeight; + /** Used by the FFT class. */ + public int originalBitDepth; + /** Used by the FFT class. */ + public ColorModel originalColorModel; + /** Used by the FFT class. */ + public double powerSpectrumMean; + + /** Constructs a FHT object from an ImageProcessor. Byte, short and RGB images + are converted to float. Float images are duplicated. */ + public FHT(ImageProcessor ip) { + this(ip, false); + } + + public FHT(ImageProcessor ip, boolean isFrequencyDomain) { + super(ip.getWidth(), ip.getHeight(), (float[])((ip instanceof FloatProcessor)?ip.duplicate().getPixels():ip.convertToFloat().getPixels()), null); + this.isFrequencyDomain = isFrequencyDomain; + maxN = getWidth(); + resetRoi(); + } + + public FHT() { + super(8,8); //create dummy FloatProcessor + } + + /** Returns true of this FHT contains a square image with a width that is a power of two. */ + public boolean powerOf2Size() { + int i=2; + while(i0, use (x[i]+x[maxN-i])/(2*maxN). + * The imaginary part of the complex FFT, with i>0, is given by (x[i]-x[maxN-i])/(2*maxN) + * The coefficients of cosine and sine are like the real and imaginary values above, + * but you have to divide by maxN instead of 2*maxN. + */ + public void transform1D(float[] x) { + int n = x.length; + if (S==null || n!=maxN) { + if (!isPowerOf2(n)) + throw new IllegalArgumentException("Not power of 2 length: "+n); + initializeTables(n); + } + dfht3(x, 0, false, n); + } + + /** Performs an inverse 1D Fast Hartley Transform (FHT) of an array */ + public void inverseTransform1D(float[] fht) { + int n = fht.length; + if (S==null || n!=maxN) { + if (!isPowerOf2(n)) + throw new IllegalArgumentException("Not power of 2 length: "+n); + initializeTables(n); + } + dfht3(fht, 0, true, n); + } + + void transform(boolean inverse) { + if (!powerOf2Size()) + throw new IllegalArgumentException("Image not power of 2 size or not square: "+width+"x"+height); + setShowProgress(true); + maxN = width; + if (S==null) + initializeTables(maxN); + float[] fht = (float[])getPixels(); + rc2DFHT(fht, inverse, maxN); + isFrequencyDomain = !inverse; + } + + void initializeTables(int maxN) { + if (maxN>0x40000000) + throw new IllegalArgumentException("Too large for FHT: "+maxN+" >2^30"); + makeSinCosTables(maxN); + makeBitReverseTable(maxN); + tempArr = new float[maxN]; + } + + void makeSinCosTables(int maxN) { + int n = maxN/4; + C = new float[n]; + S = new float[n]; + double theta = 0.0; + double dTheta = 2.0 * Math.PI/maxN; + for (int i=0; i 2) { + // third + stages computed here + gpSize = 4; + numBfs = 2; + numGps = numGps / 2; + for (stage=2; stage 2 */ + + if (inverse) { + for (i=0; imax) + max = r; + } + } + + max = (float)Math.log(max); + min = (float)Math.log(min); + if (Float.isNaN(min) || max-min>50) + min = max - 50; //display range not more than approx e^50 + scale = (float)(253.999/(max-min)); + + //long t0 = System.currentTimeMillis(); + for (int row=0; row + 2 1 + 3 4 +
+ */ + public void swapQuadrants(ImageProcessor ip) { + FFT.swapQuadrants(ip); + } + + /** Swap quadrants 1 and 3 and 2 and 4 of the image + contained in this FHT. */ + public void swapQuadrants () { + swapQuadrants(this); + } + + void changeValues(ImageProcessor ip, int v1, int v2, int v3) { + byte[] pixels = (byte[])ip.getPixels(); + int v; + for (int i=0; i=v1 && v<=v2) + pixels[i] = (byte)v3; + } + } + + /** Returns the image resulting from the point by point Hartley multiplication + of this image and the specified image. Both images are assumed to be in + the frequency domain. Multiplication in the frequency domain is equivalent + to convolution in the space domain. */ + public FHT multiply(FHT fht) { + return multiply(fht, false); + } + + /** Returns the image resulting from the point by point Hartley conjugate + multiplication of this image and the specified image. Both images are + assumed to be in the frequency domain. Conjugate multiplication in + the frequency domain is equivalent to correlation in the space domain. */ + public FHT conjugateMultiply(FHT fht) { + return multiply(fht, true); + } + + FHT multiply(FHT fht, boolean conjugate) { + int rowMod, cMod, colMod; + double h2e, h2o; + float[] h1 = (float[])getPixels(); + float[] h2 = (float[])fht.getPixels(); + float[] tmp = new float[maxN*maxN]; + for (int r =0; r=y)!=(ypoints[j]>=y)) && + (x>((double)xpoints[j]-xpoints[i])*((double)y-ypoints[i])/((double)ypoints[j]-ypoints[i])+(double)xpoints[i])) + inside = !inside; + } + return inside; + } + + /** A version of contains() that accepts float arguments. */ + public boolean contains(float x, float y) { + return contains((double)x, (double)y); + } + + public Rectangle getBounds() { + if (npoints==0) + return new Rectangle(); + if (bounds==null) + calculateBounds(xpoints, ypoints, npoints); + return bounds.getBounds(); + } + + public Rectangle2D.Double getFloatBounds() { + if (npoints==0) + return new Rectangle2D.Double(); + if (bounds==null) + calculateBounds(xpoints, ypoints, npoints); + return new Rectangle2D.Double(minX, minY, maxX-minX, maxY-minY); + } + + void calculateBounds(float[] xpoints, float[] ypoints, int npoints) { + minX = Float.MAX_VALUE; + minY = Float.MAX_VALUE; + maxX = Float.MIN_VALUE; + maxY = Float.MIN_VALUE; + for (int i=0; i d2sqr; + } + if (determinate > 0 || collinearAndFurther) { + x2=x3; y2=y3; p2=p3; // p2 is not on the convex hull, p3 becomes the new candidate + } + p3 ++; if (p3==npoints) p3 = 0; + } while (p3 != p1); // all points have been checked whether they are the next one on the convex hull + + xx[n2] = (float)x1; // save p1 as a point on the convex hull + yy[n2] = (float)y1; + n2++; + + if (p2 == p1) break; // happens only if there was only one unique point + p1 = p2; + if (n2 > 1 && xpoints[p1]==xx[0] && ypoints[p1]==yy[0]) break; //all done but pstart was missed because of duplicate points + } while (p1!=pstart); + return new FloatPolygon(xx, yy, n2); + } + + public synchronized void translate(double x, double y) { + float fx = (float)x; + float fy = (float)y; + for (int i=0; imax) + max = value; + } + } + this.min = min; this.max = max; + minMaxSet = true; + } + + /** Sets the min and max variables that control how real + * pixel values are mapped to 0-255 screen values. Use + * resetMinAndMax() to enable auto-scaling; + * @see ij.plugin.frame.ContrastAdjuster + */ + public void setMinAndMax(double minimum, double maximum) { + if (minimum==0.0 && maximum==0.0) { + resetMinAndMax(); + return; + } + min = (float)minimum; + max = (float)maximum; + fixedScale = true; + minMaxSet = true; + resetThreshold(); + } + + /** Recalculates the min and max values used to scale pixel + values to 0-255 for display. This ensures that this + FloatProcessor is set up to correctly display the image. */ + public void resetMinAndMax() { + fixedScale = false; + findMinAndMax(); + resetThreshold(); + } + + /** Returns the smallest displayed pixel value. */ + public double getMin() { + if (!minMaxSet) findMinAndMax(); + return min; + } + + /** Returns the largest displayed pixel value. */ + public double getMax() { + if (!minMaxSet) findMinAndMax(); + return max; + } + + /** Create an 8-bit AWT image by scaling pixels in the range min-max to 0-255. */ + public Image createImage() { + if (!minMaxSet) + findMinAndMax(); + boolean firstTime = pixels8==null; + boolean thresholding = minThreshold!=NO_THRESHOLD && lutUpdateMode=minThreshold && value<=maxThreshold) + pixels8[i] = (byte)255; + else + pixels8[i] = (byte)0; + } + } else { // threshold red + for (int i=0; i=minThreshold && value<=maxThreshold) + pixels8[i] = (byte)255; + } + } + } + return createBufferedImage(); + } + + // creates 8-bit image by linearly scaling from float to 8-bits + private byte[] create8BitImage(boolean thresholding) { + int size = width*height; + if (pixels8==null) + pixels8 = new byte[size]; + double value; + int ivalue; + double min2 = getMin(); + double max2 = getMax(); + double scale = 255.0/(max2-min2); + int maxValue = thresholding?254:255; + for (int i=0; imaxValue) ivalue = maxValue; + pixels8[i] = (byte)ivalue; + } + return pixels8; + } + + @Override + byte[] create8BitImage() { + return create8BitImage(false); + } + + Image createBufferedImage() { + if (raster==null) { + SampleModel sm = getIndexSampleModel(); + DataBuffer db = new DataBufferByte(pixels8, width*height, 0); + raster = Raster.createWritableRaster(sm, db, null); + } + if (image==null || cm!=cm2) { + if (cm==null) cm = getDefaultColorModel(); + image = new BufferedImage(cm, raster, false, null); + cm2 = cm; + } + lutAnimation = false; + return image; + } + + /** Returns this image as an 8-bit BufferedImage. */ + public BufferedImage getBufferedImage() { + return convertToByte(true).getBufferedImage(); + } + + /** Returns a new, blank FloatProcessor with the specified width and height. */ + public ImageProcessor createProcessor(int width, int height) { + ImageProcessor ip2 = new FloatProcessor(width, height, new float[width*height], getColorModel()); + ip2.setMinAndMax(getMin(), getMax()); + ip2.setInterpolationMethod(interpolationMethod); + return ip2; + } + + public void snapshot() { + snapshotWidth=width; + snapshotHeight=height; + snapshotMin=(float)getMin(); + snapshotMax=(float)getMax(); + if (snapshotPixels==null || (snapshotPixels!=null && snapshotPixels.length!=pixels.length)) + snapshotPixels = new float[width * height]; + System.arraycopy(pixels, 0, snapshotPixels, 0, width*height); + } + + public void reset() { + if (snapshotPixels==null) + return; + min=snapshotMin; + max=snapshotMax; + minMaxSet = true; + System.arraycopy(snapshotPixels,0,pixels,0,width*height); + } + + public void reset(ImageProcessor mask) { + if (mask==null || snapshotPixels==null) + return; + if (mask.getWidth()!=roiWidth||mask.getHeight()!=roiHeight) + throw new IllegalArgumentException(maskSizeError(mask)); + byte[] mpixels = (byte[])mask.getPixels(); + for (int y=roiY, my=0; y<(roiY+roiHeight); y++, my++) { + int i = y * width + roiX; + int mi = my * roiWidth; + for (int x=roiX; x<(roiX+roiWidth); x++) { + if (mpixels[mi++]==0) + pixels[i] = snapshotPixels[i]; + i++; + } + } + } + + /** Swaps the pixel and snapshot (undo) arrays. */ + public void swapPixelArrays() { + if (snapshotPixels==null) return; + float pixel; + for (int i=0; i=0 && x=0 && y=width-1.0) x = width-1.001; + if (y<0.0) y = 0.0; + if (y>=height-1.0) y = height-1.001; + return getInterpolatedPixel(x, y, pixels); + } + } + + final public int getPixelInterpolated(double x, double y) { + if (interpolationMethod==BILINEAR) { + if (x<0.0 || y<0.0 || x>=width-1 || y>=height-1) + return 0; + else + return Float.floatToIntBits((float)getInterpolatedPixel(x, y, pixels)); + } else if (interpolationMethod==BICUBIC) + return Float.floatToIntBits((float)getBicubicInterpolatedPixel(x, y, this)); + else + return getPixel((int)(x+0.5), (int)(y+0.5)); + } + + /** Stores the specified value at (x,y). The value is expected to be a + float that has been converted to an int using Float.floatToIntBits(). */ + public final void putPixel(int x, int y, int value) { + if (x>=0 && x=0 && y=0 && x=0 && y=0 && x=0 && y=clipXMin && x<=clipXMax && y>=clipYMin && y<=clipYMax) + putPixel(x, y, Float.floatToIntBits(fillColor)); + } + + /** Returns a reference to the float array containing + this image's pixel data. */ + public Object getPixels() { + return (Object)pixels; + } + + /** Returns a copy of the pixel data. Or returns a reference to the + snapshot buffer if it is not null and 'snapshotCopyMode' is true. + @see ImageProcessor#snapshot + @see ImageProcessor#setSnapshotCopyMode + */ + public Object getPixelsCopy() { + if (snapshotCopyMode && snapshotPixels!=null) { + snapshotCopyMode = false; + return snapshotPixels; + } else { + float[] pixels2 = new float[width*height]; + System.arraycopy(pixels, 0, pixels2, 0, width*height); + return pixels2; + } + } + + public void setPixels(Object pixels) { + this.pixels = (float[])pixels; + resetPixels(pixels); + if (pixels==null) snapshotPixels = null; + if (pixels==null) pixels8 = null; + } + + /** Copies the image contained in 'ip' to (xloc, yloc) using one of + the transfer modes defined in the Blitter interface. */ + public void copyBits(ImageProcessor ip, int xloc, int yloc, int mode) { + ip = ip.convertToFloat(); + new FloatBlitter(this).copyBits(ip, xloc, yloc, mode); + } + + public void applyTable(int[] lut) {} + + private void process(int op, double value) { + float c, v1, v2; + //boolean resetMinMax = roiWidth==width && roiHeight==height && !(op==FILL); + c = (float)value; + float min2=0f, max2=0f; + if (op==INVERT) + {min2=(float)getMin(); max2=(float)getMax();} + for (int y=roiY; y<(roiY+roiHeight); y++) { + int i = y * width + roiX; + for (int x=roiX; x<(roiX+roiWidth); x++) { + v1 = pixels[i]; + switch(op) { + case INVERT: + v2 = max2 - (v1 - min2); + break; + case FILL: + v2 = fillColor; + break; + case SET: + v2 = c; + break; + case ADD: + v2 = v1 + c; + break; + case MULT: + v2 = v1 * c; + break; + case GAMMA: + if (v1<=0f) + v2 = 0f; + else + v2 = (float)Math.exp(c*Math.log(v1)); + break; + case LOG: + v2 = (float)Math.log(v1); + break; + case EXP: + v2 = (float)Math.exp(v1); + break; + case SQR: + v2 = v1*v1; + break; + case SQRT: + if (v1<=0f) + v2 = 0f; + else + v2 = (float)Math.sqrt(v1); + break; + case ABS: + v2 = (float)Math.abs(v1); + break; + case MINIMUM: + if (v1value) + v2 = (float)value; + else + v2 = v1; + break; + default: + v2 = v1; + } + pixels[i++] = v2; + } + } + } + + /** Each pixel in the image is inverted using p=max-(p-min), where 'min' + and 'max' are the display range limits set using setMinAndMax(). */ + public void invert() { + process(INVERT, 0.0); + } + + public void add(int value) {process(ADD, value);} + public void add(double value) {process(ADD, value);} + public void set(double value) {process(SET, value);} + public void multiply(double value) {process(MULT, value);} + public void and(int value) {} + public void or(int value) {} + public void xor(int value) {} + public void gamma(double value) {process(GAMMA, value);} + public void log() {process(LOG, 0.0);} + public void exp() {process(EXP, 0.0);} + public void sqr() {process(SQR, 0.0);} + public void sqrt() {process(SQRT, 0.0);} + public void abs() {process(ABS, 0.0);} + public void min(double value) {process(MINIMUM, value);} + public void max(double value) {process(MAXIMUM, value);} + + + /** Fills the current rectangular ROI. */ + public void fill() {process(FILL, 0.0);} + + /** Fills pixels that are within roi and part of the mask. + Does nothing if the mask is not the same as the the ROI. */ + public void fill(ImageProcessor mask) { + if (mask==null) + {fill(); return;} + int roiWidth=this.roiWidth, roiHeight=this.roiHeight; + int roiX=this.roiX, roiY=this.roiY; + if (mask.getWidth()!=roiWidth||mask.getHeight()!=roiHeight) + return; + byte[] mpixels = (byte[])mask.getPixels(); + for (int y=roiY, my=0; y<(roiY+roiHeight); y++, my++) { + int i = y * width + roiX; + int mi = my * roiWidth; + for (int x=roiX; x<(roiX+roiWidth); x++) { + if (mpixels[mi++]!=0) + pixels[i] = fillColor; + i++; + } + } + } + + /** Does 3x3 convolution. */ + public void convolve3x3(int[] kernel) { + filter3x3(CONVOLVE, kernel); + } + + /** Filters using a 3x3 neighborhood. */ + public void filter(int type) { + filter3x3(type, null); + } + + /** 3x3 filter operations, code partly based on 3x3 convolution code + * contributed by Glynne Casteel. */ + void filter3x3(int type, int[] kernel) { + float v1, v2, v3; //input pixel values around the current pixel + float v4, v5, v6; + float v7, v8, v9; + float k1=0f, k2=0f, k3=0f; //kernel values (used for CONVOLVE only) + float k4=0f, k5=0f, k6=0f; + float k7=0f, k8=0f, k9=0f; + float scale = 0f; + if (type==CONVOLVE) { + k1=kernel[0]; k2=kernel[1]; k3=kernel[2]; + k4=kernel[3]; k5=kernel[4]; k6=kernel[5]; + k7=kernel[6]; k8=kernel[7]; k9=kernel[8]; + for (int i=0; i0 ? 1 : 0); //will point to v6, currently lower + int p3 = p6 - (y>0 ? width : 0); //will point to v3, currently lower + int p9 = p6 + (y0) { p3++; p6++; p9++; } + v3 = pixels2[p3]; + v6 = pixels2[p6]; + v9 = pixels2[p9]; + + switch (type) { + case BLUR_MORE: + for (int x=roiX; x=-0.01) && (xs=-0.01) && (ys=xlimit) xs = xlimit2; + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + pixels[index++] = (float)getInterpolatedPixel(xs, ys, pixels2); + } else { + ixs = (int)(xs+0.5); + iys = (int)(ys+0.5); + if (ixs>=width) ixs = width - 1; + if (iys>=height) iys = height -1; + pixels[index++] = pixels2[width*iys+ixs]; + } + } else + pixels[index++] = bgValue; + } + } + } + } + + public void flipVertical() { + int index1,index2; + float tmp; + for (int y=0; y1.0) && (yScale>1.0)) { + //expand roi + xmin = (int)(xCenter-(xCenter-roiX)*xScale); + if (xmin<0) xmin = 0; + xmax = xmin + (int)(roiWidth*xScale) - 1; + if (xmax>=width) xmax = width - 1; + ymin = (int)(yCenter-(yCenter-roiY)*yScale); + if (ymin<0) ymin = 0; + ymax = ymin + (int)(roiHeight*yScale) - 1; + if (ymax>=height) ymax = height - 1; + } else { + xmin = roiX; + xmax = roiX + roiWidth - 1; + ymin = roiY; + ymax = roiY + roiHeight - 1; + } + float[] pixels2 = (float[])getPixelsCopy(); + ImageProcessor ip2 = null; + if (interpolationMethod==BICUBIC) + ip2 = new FloatProcessor(getWidth(), getHeight(), pixels2, null); + boolean checkCoordinates = (xScale < 1.0) || (yScale < 1.0); + int index1, index2, xsi, ysi; + double ys, xs; + if (interpolationMethod==BICUBIC) { + for (int y=ymin; y<=ymax; y++) { + ys = (y-yCenter)/yScale + yCenter; + index1 = y*width + xmin; + for (int x=xmin; x<=xmax; x++) { + xs = (x-xCenter)/xScale + xCenter; + pixels[index1++] = (float)getBicubicInterpolatedPixel(xs, ys, ip2); + } + } + } else { + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + for (int y=ymin; y<=ymax; y++) { + ys = (y-yCenter)/yScale + yCenter; + ysi = (int)ys; + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + index1 = y*width + xmin; + index2 = width*(int)ys; + for (int x=xmin; x<=xmax; x++) { + xs = (x-xCenter)/xScale + xCenter; + xsi = (int)xs; + if (checkCoordinates && ((xsixmax) || (ysiymax))) + pixels[index1++] = (float)getMin(); + else { + if (interpolationMethod==BILINEAR) { + if (xs<0.0) xs = 0.0; + if (xs>=xlimit) xs = xlimit2; + pixels[index1++] = (float)getInterpolatedPixel(xs, ys, pixels2); + } else + pixels[index1++] = pixels2[index2+xsi]; + } + } + } + } + } + + /** Uses bilinear interpolation to find the pixel value at real coordinates (x,y). */ + private final double getInterpolatedPixel(double x, double y, float[] pixels) { + int xbase = (int)x; + int ybase = (int)y; + double xFraction = x - xbase; + double yFraction = y - ybase; + int offset = ybase * width + xbase; + double lowerLeft = pixels[offset]; + double lowerRight = pixels[offset + 1]; + double upperRight = pixels[offset + width + 1]; + double upperLeft = pixels[offset + width]; + double upperAverage; + if (Double.isNaN(upperLeft ) && xFraction>=0.5) + upperAverage = upperRight; + else if (Double.isNaN(upperRight) && xFraction<0.5 ) + upperAverage = upperLeft; + else + upperAverage = upperLeft + xFraction * (upperRight-upperLeft); + double lowerAverage; + if (Double.isNaN(lowerLeft) && xFraction>=0.5) + lowerAverage = lowerRight; + else if (Double.isNaN(lowerRight) && xFraction<0.5 ) + lowerAverage = lowerLeft; + else + lowerAverage = lowerLeft + xFraction * (lowerRight-lowerLeft); + if (Double.isNaN(lowerAverage) && yFraction>=0.5) + return upperAverage; + else if (Double.isNaN(upperAverage) && yFraction<0.5 ) + return lowerAverage; + else + return lowerAverage + yFraction * (upperAverage-lowerAverage); + } + + /* + private final double getInterpolatedPixel(double x, double y, float[] pixels) { + int xbase = (int)x; + int ybase = (int)y; + double xFraction = x - xbase; + double yFraction = y - ybase; + int offset = ybase * width + xbase; + double lowerLeft = pixels[offset]; + double lowerRight = pixels[offset + 1]; + double upperRight = pixels[offset + width + 1]; + double upperLeft = pixels[offset + width]; + double upperAverage = upperLeft + xFraction * (upperRight - upperLeft); + double lowerAverage = lowerLeft + xFraction * (lowerRight - lowerLeft); + return lowerAverage + yFraction * (upperAverage - lowerAverage); + } + */ + + /** Creates a new FloatProcessor containing a scaled copy of this image or selection. */ + public ImageProcessor resize(int dstWidth, int dstHeight) { + if (roiWidth==dstWidth && roiHeight==dstHeight) + return crop(); + if ((width==1||height==1) && interpolationMethod!=NONE) + return resizeLinearly(dstWidth, dstHeight); + double srcCenterX = roiX + roiWidth/2.0; + double srcCenterY = roiY + roiHeight/2.0; + double dstCenterX = dstWidth/2.0; + double dstCenterY = dstHeight/2.0; + double xScale = (double)dstWidth/roiWidth; + double yScale = (double)dstHeight/roiHeight; + if (interpolationMethod!=NONE) { + if (dstWidth!=width) dstCenterX+=xScale/4.0; + if (dstHeight!=height) dstCenterY+=yScale/4.0; + } + int inc = getProgressIncrement(dstWidth,dstHeight); + ImageProcessor ip2 = createProcessor(dstWidth, dstHeight); + float[] pixels2 = (float[])ip2.getPixels(); + double xs, ys; + if (interpolationMethod==BICUBIC) { + for (int y=0; y<=dstHeight-1; y++) { + if (inc>0&&y%inc==0) showProgress((double)y/dstHeight); + ys = (y-dstCenterY)/yScale + srcCenterY; + int index = y*dstWidth; + for (int x=0; x<=dstWidth-1; x++) { + xs = (x-dstCenterX)/xScale + srcCenterX; + pixels2[index++] = (float)getBicubicInterpolatedPixel(xs, ys, this); + } + } + } else { + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + int index1, index2; + for (int y=0; y<=dstHeight-1; y++) { + if (inc>0&&y%inc==0) showProgress((double)y/dstHeight); + ys = (y-dstCenterY)/yScale + srcCenterY; + if (interpolationMethod==BILINEAR) { + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + } + index1 = width*(int)ys; + index2 = y*dstWidth; + for (int x=0; x<=dstWidth-1; x++) { + xs = (x-dstCenterX)/xScale + srcCenterX; + if (interpolationMethod==BILINEAR) { + if (xs<0.0) xs = 0.0; + if (xs>=xlimit) xs = xlimit2; + pixels2[index2++] = (float)getInterpolatedPixel(xs, ys, pixels); + } else + pixels2[index2++] = pixels[index1+(int)xs]; + } + } + } + if (inc>0) showProgress(1.0); + return ip2; + } + + FloatProcessor downsize(int dstWidth, int dstHeight, String msg) { + FloatProcessor ip2 = this; + if (msg!=null) + ij.IJ.showStatus("downsizing in x"+msg); + if (dstWidth=width-2 || v0<=0 || v0>=height-2) + return ip2.getBilinearInterpolatedPixel(x0, y0); + double q = 0; + for (int j = 0; j <= 3; j++) { + int v = v0 - 1 + j; + double p = 0; + for (int i = 0; i <= 3; i++) { + int u = u0 - 1 + i; + p = p + ip2.getf(u,v) * cubic(x0 - u); + } + q = q + p * cubic(y0 - v); + } + return q; + } + + /** Sets the foreground fill/draw color. */ + public void setColor(Color color) { + drawingColor = color; + int bestIndex = getBestIndex(color); + if (bestIndex>0 && getMin()==0.0 && getMax()==0.0) { + fillColor = bestIndex; + setMinAndMax(0.0,255.0); + } else if (bestIndex==0 && getMin()>0.0 && (color.getRGB()&0xffffff)==0) + fillColor = 0f; + else + fillColor = (float)(getMin() + (getMax()-getMin())*(bestIndex/255.0)); + fillValueSet = true; + } + + /** Sets the default fill/draw value. */ + public void setValue(double value) { + fillColor = (float)value; + fillValueSet = true; + } + + /** Returns the foreground fill/draw value. */ + public double getForegroundValue() { + return fillColor; + } + + public void setBackgroundValue(double value) { + bgValue = (float)value; + } + + public double getBackgroundValue() { + return bgValue; + } + + public void setLutAnimation(boolean lutAnimation) { + this.lutAnimation = false; + } + + public void setThreshold(double minThreshold, double maxThreshold, int lutUpdate) { + if (minThreshold==NO_THRESHOLD) { + resetThreshold(); + return; + } + if (getMax()>getMin()) { + if (lutUpdate==OVER_UNDER_LUT) { + double minT = ((minThreshold-getMin())/(getMax()-getMin())*255.0); + double maxT = ((maxThreshold-getMin())/(getMax()-getMin())*255.0); + super.setThreshold(minT, maxT, lutUpdate); // update LUT + } else { + lutUpdateMode = lutUpdate; + if (rLUT1==null) { + if (cm==null) + makeDefaultColorModel(); + baseCM = cm; + IndexColorModel m = (IndexColorModel)cm; + rLUT1 = new byte[256]; gLUT1 = new byte[256]; bLUT1 = new byte[256]; + m.getReds(rLUT1); m.getGreens(gLUT1); m.getBlues(bLUT1); + rLUT2 = new byte[256]; gLUT2 = new byte[256]; bLUT2 = new byte[256]; + } + if (lutUpdateMode==RED_LUT) + cm = getThresholdColorModel(); + else + cm = getDefaultColorModel(); + } + } else + super.resetThreshold(); + this.minThreshold = minThreshold; + this.maxThreshold = maxThreshold; + } + + /** Performs a convolution operation using the specified kernel. */ + public void convolve(float[] kernel, int kernelWidth, int kernelHeight) { + snapshot(); + new ij.plugin.filter.Convolver().convolve(this, kernel, kernelWidth, kernelHeight); + } + + /** Returns a 256 bin histogram of the current ROI or of the entire image if there is no ROI. */ + public int[] getHistogram() { + return getStatistics().histogram; + } + + /** Not implemented. */ + public void threshold(int level) {} + /** Not implemented. */ + public void autoThreshold() {} + /** Not implemented. */ + public void medianFilter() {} + /** Not implemented. */ + public void erode() {} + /** Not implemented. */ + public void dilate() {} + + /** Returns this FloatProcessor. + * @param channelNumber Ignored (needed for compatibility with ColorProcessor.toFloat) + * @param fp Ignored (needed for compatibility with the other ImageProcessor types). + * @return This FloatProcessor + */ + public FloatProcessor toFloat(int channelNumber, FloatProcessor fp) { + return this; + } + + /** Sets the pixels, and min&max values from a FloatProcessor. + * Also the values are taken from the FloatProcessor. + * @param channelNumber Ignored (needed for compatibility with ColorProcessor.toFloat) + * @param fp The FloatProcessor where the image data are read from. + */ + public void setPixels(int channelNumber, FloatProcessor fp) { + if (fp.getPixels() != getPixels()) + setPixels(fp.getPixels()); + setMinAndMax(fp.getMin(), fp.getMax()); + } + + /** Returns the smallest possible positive nonzero pixel value. */ + public double minValue() { + return Float.MIN_VALUE; + } + + /** Returns the largest possible positive finite pixel value. */ + public double maxValue() { + return Float.MAX_VALUE; + } + + public int getBitDepth() { + return 32; + } + + /** Returns a binary mask, or null if a threshold is not set. */ + public ByteProcessor createMask() { + if (getMinThreshold()==NO_THRESHOLD) + return null; + float minThreshold = (float)getMinThreshold(); + float maxThreshold = (float)getMaxThreshold(); + ByteProcessor mask = new ByteProcessor(width, height); + byte[] mpixels = (byte[])mask.getPixels(); + for (int i=0; i=minThreshold && pixels[i]<=maxThreshold) + mpixels[i] = (byte)255; + } + return mask; + } + +} + diff --git a/src/ij/process/FloatStatistics.java b/src/ij/process/FloatStatistics.java new file mode 100644 index 0000000..3609278 --- /dev/null +++ b/src/ij/process/FloatStatistics.java @@ -0,0 +1,264 @@ +package ij.process; +import ij.measure.Calibration; +import java.util.Arrays; + +/** 32-bit (float) image statistics, including histogram. */ +public class FloatStatistics extends ImageStatistics { + + /** Constructs an ImageStatistics object from a FloatProcessor + using the standard measurement options (area, mean, + mode, min and max). */ + public FloatStatistics(ImageProcessor ip) { + this(ip, AREA+MEAN+MODE+MIN_MAX, null); + } + + /** Constructs a FloatStatistics object from a FloatProcessor + using the specified measurement options. + */ + public FloatStatistics(ImageProcessor ip, int mOptions, Calibration cal) { + this.width = ip.getWidth(); + this.height = ip.getHeight(); + setup(ip, cal); + double minT = ip.getMinThreshold(); + double minThreshold,maxThreshold; + boolean limitToThreshold = (mOptions&LIMIT)!=0; + if (!limitToThreshold || minT==ImageProcessor.NO_THRESHOLD) { + minThreshold=-Float.MAX_VALUE; + maxThreshold=Float.MAX_VALUE; + } else { + minThreshold=minT; + maxThreshold=ip.getMaxThreshold(); + } + if (limitToThreshold) + saveThreshold(minThreshold, maxThreshold, cal); + getStatistics(ip, minThreshold, maxThreshold); + if ((mOptions&MODE)!=0) + getMode(); + if ((mOptions&ELLIPSE)!=0 || (mOptions&SHAPE_DESCRIPTORS)!=0) + fitEllipse(ip, mOptions); + else if ((mOptions&CENTROID)!=0) + getCentroid(ip, minThreshold, maxThreshold); + if ((mOptions&(CENTER_OF_MASS|SKEWNESS|KURTOSIS))!=0) + calculateMoments(ip, minThreshold, maxThreshold); + if ((mOptions&MEDIAN)!=0) + getMedian(ip, minThreshold, maxThreshold); + if ((mOptions&AREA_FRACTION)!=0) + calculateAreaFraction(ip); + } + + void getStatistics(ImageProcessor ip, double minThreshold, double maxThreshold) { + double v; + float[] pixels = (float[])ip.getPixels(); + nBins = ip.getHistogramSize(); + histMin = ip.getHistogramMin(); + histMax = ip.getHistogramMax(); + histogram = new int[nBins]; + double sum = 0; + double sum2 = 0; + byte[] mask = ip.getMaskArray(); + + // Find image min and max + double roiMin = Double.MAX_VALUE; + double roiMax = -Double.MAX_VALUE; + for (int y=ry, my=0; y<(ry+rh); y++, my++) { + int i = y * width + rx; + int mi = my * rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null || mask[mi++]!=0) { + v = pixels[i]; + if (v>=minThreshold && v<=maxThreshold) { + if (vroiMax) + roiMax = v; + } + } + i++; + } + } + min = roiMin; max = roiMax; + if (histMin==0.0 && histMax==0.0) { + histMin = min; + histMax = max; + } else { + if (minhistMax) + max = histMax; + } + binSize = (histMax-histMin)/nBins; + + // Generate histogram + double scale = nBins/(histMax-histMin); + int index; + pixelCount = 0; + for (int y=ry, my=0; y<(ry+rh); y++, my++) { + int i = y * width + rx; + int mi = my * rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null || mask[mi++]!=0) { + v = pixels[i]; + if (v>=minThreshold && v<=maxThreshold && v>=histMin && v<=histMax) { + pixelCount++; + sum += v; + sum2 += v*v; + index = (int)(scale*(v-histMin)); + if (index>=nBins) + index = nBins-1; + histogram[index]++; + } + } + i++; + } + } + area = pixelCount*pw*ph; + mean = sum/pixelCount; + umean = mean; + calculateStdDev(pixelCount, sum, sum2); + } + + void getMode() { + int count; + maxCount = 0; + for (int i = 0; i < nBins; i++) { + count = histogram[i]; + if (count > maxCount) { + maxCount = count; + mode = i; + } + } + dmode = histMin+mode*binSize; + if (binSize!=1.0) + dmode += binSize/2.0; + } + + void calculateMoments(ImageProcessor ip, double minThreshold, double maxThreshold) { + float[] pixels = (float[])ip.getPixels(); + byte[] mask = ip.getMaskArray(); + int i, mi; + double v, v2, sum1=0.0, sum2=0.0, sum3=0.0, sum4=0.0, xsum=0.0, ysum=0.0; + for (int y=ry,my=0; y<(ry+rh); y++,my++) { + i = y*width + rx; + mi = my*rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null || mask[mi++]!=0) { + v = pixels[i]+Double.MIN_VALUE; + if (v>=minThreshold && v<=maxThreshold) { + v2 = v*v; + sum1 += v; + sum2 += v2; + sum3 += v*v2; + sum4 += v2*v2; + xsum += x*v; + ysum += y*v; + } + } + i++; + } + } + double mean2 = mean*mean; + double variance = sum2/pixelCount - mean2; + double sDeviation = Math.sqrt(variance); + skewness = ((sum3 - 3.0*mean*sum2)/pixelCount + 2.0*mean*mean2)/(variance*sDeviation); + kurtosis = (((sum4 - 4.0*mean*sum3 + 6.0*mean2*sum2)/pixelCount - 3.0*mean2*mean2)/(variance*variance)-3.0); + xCenterOfMass = xsum/sum1+0.5; + yCenterOfMass = ysum/sum1+0.5; + if (cal!=null) { + xCenterOfMass = cal.getX(xCenterOfMass); + yCenterOfMass = cal.getY(yCenterOfMass, height); + } + } + + void getCentroid(ImageProcessor ip, double minThreshold, double maxThreshold) { + float[] pixels = (float[])ip.getPixels(); + byte[] mask = ip.getMaskArray(); + double count=0.0, xsum=0.0, ysum=0.0, v; + int i, mi; + for (int y=ry,my=0; y<(ry+rh); y++,my++) { + i = y*width + rx; + mi = my*rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null||mask[mi++]!=0) { + v = pixels[i]; + if (v>=minThreshold && v<=maxThreshold) { + count++; + xsum+=x; + ysum+=y; + } + } + i++; + } + } + xCentroid = xsum/count+0.5; + yCentroid = ysum/count+0.5; + if (cal!=null) { + xCentroid = cal.getX(xCentroid); + yCentroid = cal.getY(yCentroid, height); + } + } + + void calculateAreaFraction(ImageProcessor ip) { + int sum = 0; + int total = 0; + float t1 = (float)ip.getMinThreshold(); + float t2 = (float)ip.getMaxThreshold(); + float v; + float[] pixels = (float[])ip.getPixels(); + boolean noThresh = t1==ImageProcessor.NO_THRESHOLD; + byte[] mask = ip.getMaskArray(); + int i, mi; + for (int y=ry,my=0; y<(ry+rh); y++,my++) { + i = y*width + rx; + mi = my*rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null||mask[mi++]!=0) { + v = pixels[i]; + total++; + if (noThresh) { + if (v!=0f) sum++; + } else if (v>=t1 && v<=t2) + sum++; + } + i++; + } + } + areaFraction = sum*100.0/total; + } + + void getMedian(ImageProcessor ip, double minThreshold, double maxThreshold) { + if (pixelCount==0) { + median = Double.NaN; + return; + } + float[] pixels = (float[])ip.getPixels(); + float[] pixels2 = new float[pixelCount]; + byte[] mask = ip.getMaskArray(); + int i, mi; + float v; + int count = 0; + for (int y=ry,my=0; y<(ry+rh); y++,my++) { + i = y*width + rx; + mi = my*rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null||mask[mi++]!=0) { + v = pixels[i]; + if (v>=minThreshold && v<=maxThreshold) { + if (count==pixels2.length) { + median = Double.NaN; + return; + } + pixels2[count++] = v; + } + } + i++; + } + } + Arrays.sort(pixels2); + int middle = pixels2.length/2; + if ((pixels2.length&1)==0) //even + median = (pixels2[middle-1] + pixels2[middle])/2f; + else + median = pixels2[middle]; + } + +} diff --git a/src/ij/process/FloodFiller.java b/src/ij/process/FloodFiller.java new file mode 100644 index 0000000..d8f13fe --- /dev/null +++ b/src/ij/process/FloodFiller.java @@ -0,0 +1,222 @@ +package ij.process; +import ij.*; +import ij.gui.Toolbar; +import java.awt.Rectangle; + + +/** This class, which does flood filling, is used by the floodFill() macro function and + by the particle analyzer + The Wikipedia at "http://en.wikipedia.org/wiki/Flood_fill" has a good + description of the algorithm used here as well as examples in C and Java. +*/ +public class FloodFiller { + int maxStackSize = 500; // will be increased as needed + int[] xstack = new int[maxStackSize]; + int[] ystack = new int[maxStackSize]; + int stackSize; + ImageProcessor ip; + int max; + boolean isFloat; + + public FloodFiller(ImageProcessor ip) { + this.ip = ip; + isFloat = ip instanceof FloatProcessor; + } + + /** Does a 4-connected flood fill using the current fill/draw + value, which is defined by ImageProcessor.setValue(). */ + public boolean fill(int x, int y) { + int width = ip.getWidth(); + int height = ip.getHeight(); + int color = ip.getPixel(x, y); + fillLine(ip, x, x, y); + int newColor = ip.getPixel(x, y); + ip.putPixel(x, y, color); + if (color==newColor) return false; + stackSize = 0; + push(x, y); + while(true) { + x = popx(); + if (x ==-1) return true; + y = popy(); + if (ip.getPixel(x,y)!=color) continue; + int x1 = x; int x2 = x; + while (ip.getPixel(x1,y)==color && x1>=0) x1--; // find start of scan-line + x1++; + while (ip.getPixel(x2,y)==color && x20 && ip.getPixel(i,y-1)==color) + {push(i, y-1); inScanLine = true;} + else if (inScanLine && y>0 && ip.getPixel(i,y-1)!=color) + inScanLine = false; + } + inScanLine = false; + for (int i=x1; i<=x2; i++) { // find scan-lines below this one + if (!inScanLine && y=0) x1--; // find start of scan-line + x1++; + while (ip.getPixel(x2,y)==color && x20){ + if (x1>0){ + if (ip.getPixel(x1-1,y-1)==color){ + push(x1-1,y-1); + } + } + if (x20){ + if (ip.getPixel(x1-1,y+1)==color){ + push(x1-1,y+1); + } + } + if (x20 && ip.getPixel(i,y-1)==color) + {push(i, y-1); inScanLine = true;} + else if (inScanLine && y>0 && ip.getPixel(i,y-1)!=color) + inScanLine = false; + } + inScanLine = false; + for (int i=x1; i<=x2; i++) {// find scan-lines below this one + if (!inScanLine && y=0) x1--; // find start of scan-line + x1++; + while (inParticle(x2,y,level1,level2) && x20) x1--; if (x20 && inParticle(i,y-1,level1,level2)) + {push(i, y-1); inScanLine = true;} + else if (inScanLine && y>0 && !inParticle(i,y-1,level1,level2)) + inScanLine = false; + } + inScanLine = false; + for (int i=x1; i<=x2; i++) { // find scan-lines below this one + if (!inScanLine && y=level1 && ip.getPixelValue(x,y)<=level2; + else { + int v = ip.getPixel(x,y); + return v>=level1 && v<=level2; + } + } + + final void push(int x, int y) { + stackSize++; + if (stackSize==maxStackSize) { + int[] newXStack = new int[maxStackSize*2]; + int[] newYStack = new int[maxStackSize*2]; + System.arraycopy(xstack, 0, newXStack, 0, maxStackSize); + System.arraycopy(ystack, 0, newYStack, 0, maxStackSize); + xstack = newXStack; + ystack = newYStack; + maxStackSize *= 2; + } + xstack[stackSize-1] = x; + ystack[stackSize-1] = y; + } + + final int popx() { + if (stackSize==0) + return -1; + else + return xstack[stackSize-1]; + } + + final int popy() { + int value = ystack[stackSize-1]; + stackSize--; + return value; + } + + final void fillLine(ImageProcessor ip, int x1, int x2, int y) { + if (x1>x2) {int t = x1; x1=x2; x2=t;} + for (int x=x1; x<=x2; x++) + ip.drawPixel(x, y); + } + +} diff --git a/src/ij/process/ImageConverter.java b/src/ij/process/ImageConverter.java new file mode 100644 index 0000000..c3b76e9 --- /dev/null +++ b/src/ij/process/ImageConverter.java @@ -0,0 +1,303 @@ +package ij.process; + +import java.awt.*; +import java.awt.image.*; +import ij.*; +import ij.gui.*; +import ij.measure.*; +import ij.plugin.frame.Recorder; + +/** This class converts an ImagePlus object to a different type. */ +public class ImageConverter { + private ImagePlus imp; + private int type; + //private static boolean doScaling = Prefs.getBoolean(Prefs.SCALE_CONVERSIONS,true); + private static boolean doScaling = true; + + /** Constructs an ImageConverter based on an ImagePlus object. */ + public ImageConverter(ImagePlus imp) { + this.imp = imp; + type = imp.getType(); + } + + /** Converts this ImagePlus to 8-bit grayscale. */ + public synchronized void convertToGray8() { + if (imp.getStackSize()>1) { + new StackConverter(imp).convertToGray8(); + return; + } + ImageProcessor ip = imp.getProcessor(); + if (type==ImagePlus.GRAY16 || type==ImagePlus.GRAY32) { + imp.setProcessor(null, ip.convertToByte(doScaling)); + imp.setCalibration(imp.getCalibration()); //update calibration + record(); + } else if (type==ImagePlus.COLOR_RGB) + imp.setProcessor(null, ip.convertToByte(doScaling)); + else if (ip.isPseudoColorLut()) { + boolean invertedLut = ip.isInvertedLut(); + ip.setColorModel(LookUpTable.createGrayscaleColorModel(invertedLut)); + imp.updateAndDraw(); + } else { + ip = new ColorProcessor(imp.getImage()); + imp.setProcessor(null, ip.convertToByte(doScaling)); + } + ImageProcessor ip2 = imp.getProcessor(); + if (Prefs.useInvertingLut && ip2 instanceof ByteProcessor && !ip2.isInvertedLut()&& !ip2.isColorLut()) { + ip2.invertLut(); + ip2.invert(); + } + } + + /** Converts this ImagePlus to 16-bit grayscale. */ + public void convertToGray16() { + if (type==ImagePlus.GRAY16) + return; + if (!(type==ImagePlus.GRAY8||type==ImagePlus.GRAY32||type==ImagePlus.COLOR_RGB)) + throw new IllegalArgumentException("Unsupported conversion"); + if (imp.getStackSize()>1) { + new StackConverter(imp).convertToGray16(); + return; + } + ImageProcessor ip = imp.getProcessor(); + if (type==ImagePlus.GRAY32) + record(); + imp.trimProcessor(); + imp.setProcessor(null, ip.convertToShort(doScaling)); + imp.setCalibration(imp.getCalibration()); //update calibration + } + + private void record() { + if (Recorder.record) { + Boolean state = ImageConverter.getDoScaling(); + if (Recorder.scriptMode()) + Recorder.recordCall("ImageConverter.setDoScaling("+state+");", true); + else + Recorder. recordString("setOption(\"ScaleConversions\", "+state+");\n"); + } + } + + /** Converts this ImagePlus to 32-bit grayscale. */ + public void convertToGray32() { + if (type==ImagePlus.GRAY32) + return; + if (!(type==ImagePlus.GRAY8||type==ImagePlus.GRAY16||type==ImagePlus.COLOR_RGB)) + throw new IllegalArgumentException("Unsupported conversion"); + Calibration cal = imp.getCalibration(); + double min = cal.getCValue(imp.getDisplayRangeMin()); + double max = cal.getCValue(imp.getDisplayRangeMax()); + if (imp.getStackSize()>1) { + new StackConverter(imp).convertToGray32(); + IJ.setMinAndMax(imp, min, max); + return; + } + ImageProcessor ip = imp.getProcessor(); + imp.trimProcessor(); + imp.setProcessor(null, ip.convertToFloat()); + imp.setCalibration(cal); //update calibration + IJ.setMinAndMax(imp, min, max); + } + + /** Converts this ImagePlus to RGB. */ + public void convertToRGB() { + if (imp.getBitDepth()==24) + return; + if (imp.getStackSize()>1) { + new StackConverter(imp).convertToRGB(); + return; + } + ImageProcessor ip = imp.getProcessor(); + imp.setProcessor(null, ip.convertToRGB()); + imp.setCalibration(imp.getCalibration()); //update calibration + } + + /** Converts an RGB image to an RGB (red, green and blue) stack. */ + public void convertToRGBStack() { + if (type!=ImagePlus.COLOR_RGB) + throw new IllegalArgumentException("Image must be RGB"); + + //convert to RGB Stack + ColorProcessor cp; + if (imp.getType()==ImagePlus.COLOR_RGB) + cp = (ColorProcessor)imp.getProcessor(); + else + cp = new ColorProcessor(imp.getImage()); + int width = imp.getWidth(); + int height = imp.getHeight(); + byte[] R = new byte[width*height]; + byte[] G = new byte[width*height]; + byte[] B = new byte[width*height]; + cp.getRGB(R, G, B); + imp.trimProcessor(); + + // Create stack and select Red channel + ColorModel cm = LookUpTable.createGrayscaleColorModel(false); + ImageStack stack = new ImageStack(width, height, cm); + stack.addSlice("Red", R); + stack.addSlice("Green", G); + stack.addSlice("Blue", B); + imp.setStack(null, stack); + imp.setDimensions(3, 1, 1); + if (imp.isComposite()) + ((CompositeImage)imp).setMode(IJ.GRAYSCALE); + } + + /** Converts an RGB image to a HSB (hue, saturation and brightness) stack. */ + public void convertToHSB() { + if (type!=ImagePlus.COLOR_RGB) + throw new IllegalArgumentException("Image must be RGB");; + ColorProcessor cp; + if (imp.getType()==ImagePlus.COLOR_RGB) + cp = (ColorProcessor)imp.getProcessor(); + else + cp = new ColorProcessor(imp.getImage()); + ImageStack stack = cp.getHSBStack(); + imp.trimProcessor(); + imp.setStack(null, stack); + imp.setDimensions(3, 1, 1); + } + + /** Converts an RGB image to a 32-bit HSB (hue, saturation and brightness) stack. */ + public void convertToHSB32() { + if (type!=ImagePlus.COLOR_RGB) + throw new IllegalArgumentException("Image must be RGB");; + ColorProcessor cp; + if (imp.getType()==ImagePlus.COLOR_RGB) + cp = (ColorProcessor)imp.getProcessor(); + else + cp = new ColorProcessor(imp.getImage()); + ImageStack stack = cp.getHSB32Stack(); + imp.trimProcessor(); + imp.setStack(null, stack); + imp.setDimensions(3, 1, 1); + } + + /** Converts an RGB image to a Lab stack. */ + public void convertToLab() { + if (type!=ImagePlus.COLOR_RGB) + throw new IllegalArgumentException("Image must be RGB"); + ColorSpaceConverter converter = new ColorSpaceConverter(); + ImagePlus imp2 = converter.RGBToLab(imp); + Point loc = null; + ImageWindow win = imp.getWindow(); + if (win!=null) + loc = win.getLocation(); + ImageWindow.setNextLocation(loc); + imp2.show(); + imp.hide(); + imp2.copyAttributes(imp); + imp.changes = false; + imp.close(); + } + + /** Converts a 2 or 3 slice 8-bit stack to RGB. */ + public void convertRGBStackToRGB() { + int stackSize = imp.getStackSize(); + if (stackSize<2 || stackSize>3 || type!=ImagePlus.GRAY8) + throw new IllegalArgumentException("2 or 3 slice 8-bit stack required"); + int width = imp.getWidth(); + int height = imp.getHeight(); + ImageStack stack = imp.getStack(); + byte[] R = (byte[])stack.getPixels(1); + byte[] G = (byte[])stack.getPixels(2); + byte[] B; + if (stackSize>2) + B = (byte[])stack.getPixels(3); + else + B = new byte[width*height]; + imp.trimProcessor(); + ColorProcessor cp = new ColorProcessor(width, height); + cp.setRGB(R, G, B); + if (imp.isInvertedLut()) + cp.invert(); + imp.setImage(cp.createImage()); + imp.killStack(); + if (IJ.isLinux()) + imp.setTitle(imp.getTitle()); + } + + /** Converts a 3-slice (hue, saturation, brightness) 8-bit stack to RGB. */ + public void convertHSBToRGB() { + if (imp.getStackSize()!=3) + throw new IllegalArgumentException("3-slice 8-bit stack required"); + ImageStack stack = imp.getStack(); + byte[] H = (byte[])stack.getPixels(1); + byte[] S = (byte[])stack.getPixels(2); + byte[] B = (byte[])stack.getPixels(3); + int width = imp.getWidth(); + int height = imp.getHeight(); + imp.trimProcessor(); + ColorProcessor cp = new ColorProcessor(width, height); + cp.setHSB(H, S, B); + imp.setImage(cp.createImage()); + imp.killStack(); + if (IJ.isLinux()) + imp.setTitle(imp.getTitle()); + } + + /** Converts a 3-slice (hue, saturation, brightness) 32-bit stack to RGB. */ + public void convertHSB32ToRGB() { + if (imp.getStackSize()!=3) + throw new IllegalArgumentException("3-slice 8-bit stack required"); + ImageStack stack = imp.getStack(); + float[] H = (float[])stack.getPixels(1); + float[] S = (float[])stack.getPixels(2); + float[] B = (float[])stack.getPixels(3); + int width = imp.getWidth(); + int height = imp.getHeight(); + imp.trimProcessor(); + ColorProcessor cp = new ColorProcessor(width, height); + cp.setHSB(H, S, B); + imp.setImage(cp.createImage()); + imp.killStack(); + if (IJ.isLinux()) + imp.setTitle(imp.getTitle()); + } + + /** Converts a Lab stack to RGB. */ + public void convertLabToRGB() { + if (imp.getStackSize()!=3) + throw new IllegalArgumentException("3-slice 32-bit stack required"); + ColorSpaceConverter converter = new ColorSpaceConverter(); + ImagePlus imp2 = converter.LabToRGB(imp); + imp2.setCalibration(imp.getCalibration()); + imp.setImage(imp2); + } + + /** Converts an RGB image to 8-bits indexed color. 'nColors' must + be greater than 1 and less than or equal to 256. */ + public void convertRGBtoIndexedColor(int nColors) { + if (type!=ImagePlus.COLOR_RGB) + throw new IllegalArgumentException("Image must be RGB"); + if (nColors<2) nColors = 2; + if (nColors>256) nColors = 256; + + // get RGB pixels + IJ.showProgress(0.1); + IJ.showStatus("Grabbing pixels"); + int width = imp.getWidth(); + int height = imp.getHeight(); + ImageProcessor ip = imp.getProcessor(); + ip.snapshot(); + int[] pixels = (int[])ip.getPixels(); + imp.trimProcessor(); + + // convert to 8-bits + long start = System.currentTimeMillis(); + MedianCut mc = new MedianCut(pixels, width, height); + ImageProcessor ip2 = mc.convertToByte(nColors); + imp.setProcessor(null, ip2); + imp.setTypeToColor256(); + } + + /** Set true to scale to 0-255 when converting short to byte or float + to byte and to 0-65535 when converting float to short. */ + public static void setDoScaling(boolean scaleConversions) { + doScaling = scaleConversions; + IJ.register(ImageConverter.class); + } + + /** Returns true if scaling is enabled. */ + public static boolean getDoScaling() { + return doScaling; + } +} diff --git a/src/ij/process/ImageProcessor.java b/src/ij/process/ImageProcessor.java new file mode 100644 index 0000000..a6f54be --- /dev/null +++ b/src/ij/process/ImageProcessor.java @@ -0,0 +1,2835 @@ +package ij.process; +import java.util.*; +import java.awt.*; +import java.awt.image.*; +import java.awt.geom.Rectangle2D; +import java.awt.font.GlyphVector; +import ij.gui.*; +import ij.util.*; +import ij.plugin.filter.GaussianBlur; +import ij.plugin.Binner; +import ij.process.AutoThresholder.Method; +import ij.Prefs; +import ij.measure.Measurements; + +/** +This abstract class is the superclass for classes that process +the four data types (byte, short, float and RGB) supported by ImageJ. +An ImageProcessor contains the pixel data of a 2D image and +some basic methods to manipulate it. +@see ByteProcessor +@see ShortProcessor +@see FloatProcessor +@see ColorProcessor +@see ij.ImagePlus +@see ij.ImageStack +*/ +public abstract class ImageProcessor implements Cloneable { + + /** Value of pixels included in masks. */ + public static final int BLACK = 0xFF000000; + + /** Value returned by getMinThreshold() when thresholding is not enabled. */ + public static final double NO_THRESHOLD = -808080.0; + + /** Left justify text. */ + public static final int LEFT_JUSTIFY = 0; + /** Center justify text. */ + public static final int CENTER_JUSTIFY = 1; + /** Right justify text. */ + public static final int RIGHT_JUSTIFY = 2; + + /** Isodata thresholding method */ + public static final int ISODATA = 0; + + /** Modified isodata method used in Image/Adjust/Threshold tool */ + public static final int ISODATA2 = 1; + + /** Interpolation methods */ + public static final int NEAREST_NEIGHBOR=0, NONE=0, BILINEAR=1, BICUBIC=2; + + public static final int BLUR_MORE=0, FIND_EDGES=1, MEDIAN_FILTER=2, MIN=3, MAX=4, CONVOLVE=5; + static public final int RED_LUT=0, BLACK_AND_WHITE_LUT=1, NO_LUT_UPDATE=2, OVER_UNDER_LUT=3; + static final int INVERT=0, FILL=1, ADD=2, MULT=3, AND=4, OR=5, + XOR=6, GAMMA=7, LOG=8, MINIMUM=9, MAXIMUM=10, SQR=11, SQRT=12, EXP=13, ABS=14, SET=15; + static final String WRONG_LENGTH = "width*height!=pixels.length"; + + int fgColor = 0; + protected int lineWidth = 1; + protected int cx, cy; //current drawing coordinates + protected Font font = ij.IJ.font12; + protected FontMetrics fontMetrics; + protected boolean antialiasedText; + protected boolean boldFont; + private static String[] interpolationMethods; + // Over/Under tresholding colors + private static int overRed, overGreen=255, overBlue; + private static int underRed, underGreen, underBlue=255; + private static boolean useBicubic; + private int sliceNumber; + private Overlay overlay; + private boolean noReset; + + ProgressBar progressBar; + protected int width, snapshotWidth; + protected int height, snapshotHeight; + protected int roiX, roiY, roiWidth, roiHeight; + protected int xMin, xMax, yMin, yMax; + boolean snapshotCopyMode; + ImageProcessor mask; + protected ColorModel baseCM; // base color model + protected ColorModel cm; + protected byte[] rLUT1, gLUT1, bLUT1; // base LUT + protected byte[] rLUT2, gLUT2, bLUT2; // LUT as modified by setMinAndMax and setThreshold + protected boolean interpolate; // replaced by interpolationMethod + protected int interpolationMethod = NONE; + protected double minThreshold=NO_THRESHOLD, maxThreshold=NO_THRESHOLD; + protected int histogramSize = 256; + protected double histogramMin, histogramMax; + protected float[] cTable; + protected boolean lutAnimation; + protected MemoryImageSource source; //unused + protected Image img; + protected boolean newPixels; // unused + protected Color drawingColor = Color.black; + protected int clipXMin, clipXMax, clipYMin, clipYMax; // clip rect used by drawTo, drawLine, drawDot and drawPixel + protected int justification = LEFT_JUSTIFY; + protected int lutUpdateMode; + protected WritableRaster raster; + protected BufferedImage image; + protected BufferedImage fmImage; + protected Graphics2D fmGraphics; + protected ColorModel cm2; + protected SampleModel sampleModel; + protected static IndexColorModel defaultColorModel; + protected boolean minMaxSet; + protected static double seed = Double.NaN; + protected static Random rnd; + protected boolean fillValueSet; + + protected void showProgress(double percentDone) { + if (progressBar!=null) + progressBar.show(percentDone); + } + + /** @deprecated */ + protected void hideProgress() { + showProgress(1.0); + } + + /** Returns the width of this image in pixels. */ + public int getWidth() { + return width; + } + + /** Returns the height of this image in pixels. */ + public int getHeight() { + return height; + } + + /** Returns the bit depth, 8, 16, 24 (RGB) or 32. RGB images actually use 32 bits per pixel. */ + public int getBitDepth() { + Object pixels = getPixels(); + if (pixels==null) + return 0; + else if (pixels instanceof byte[]) + return 8; + else if (pixels instanceof short[]) + return 16; + else if (pixels instanceof int[]) + return 24; + else if (pixels instanceof float[]) + return 32; + else + return 0; + } + + /** Returns this processor's color model. For non-RGB processors, + this is the base lookup table (LUT), not the one that may have + been modified by setMinAndMax() or setThreshold(). */ + public ColorModel getColorModel() { + if (cm==null) + makeDefaultColorModel(); + if (baseCM!=null) + return baseCM; + else + return cm; + } + + private IndexColorModel getIndexColorModel() { + ColorModel cm2 = baseCM; + if (cm2==null) + cm2 = cm; + if (cm2!=null && (cm2 instanceof IndexColorModel)) + return (IndexColorModel)cm2; + else + return null; + } + + /** Returns the current color model, which may have + been modified by setMinAndMax() or setThreshold(). */ + public ColorModel getCurrentColorModel() { + if (cm==null) makeDefaultColorModel(); + return cm; + } + + /** Sets the color model. Must be an IndexColorModel (aka LUT) + for all processors except the ColorProcessor. */ + public void setColorModel(ColorModel cm) { + if (cm!=null && !(cm instanceof IndexColorModel)) + throw new IllegalArgumentException("IndexColorModel required"); + if (cm!=null && cm instanceof LUT) + cm = ((LUT)cm).getColorModel(); + this.cm = cm; + baseCM = null; + rLUT1 = rLUT2 = null; + inversionTested = false; + minThreshold = NO_THRESHOLD; + } + + public LUT getLut() { + ColorModel cm2 = getColorModel(); + if (cm2!=null && (cm2 instanceof IndexColorModel)) + return new LUT((IndexColorModel)cm2, getMin(), getMax()); + else + return null; + } + + public void setLut(LUT lut) { + if (lut==null) + setColorModel(null); + else { + setColorModel(lut.getColorModel()); + if (lut.min!=0.0 || lut.max!=0.0) + setMinAndMax(lut.min, lut.max); + } + } + + + protected void makeDefaultColorModel() { + cm = getDefaultColorModel(); + } + + /** Inverts the values in this image's LUT (indexed color model). + Does nothing if this is a ColorProcessor. */ + public void invertLut() { + IndexColorModel icm = (IndexColorModel)getColorModel(); + int mapSize = icm.getMapSize(); + byte[] reds = new byte[mapSize]; + byte[] greens = new byte[mapSize]; + byte[] blues = new byte[mapSize]; + byte[] reds2 = new byte[mapSize]; + byte[] greens2 = new byte[mapSize]; + byte[] blues2 = new byte[mapSize]; + icm.getReds(reds); + icm.getGreens(greens); + icm.getBlues(blues); + for (int i=0; i0.0) + stdDev = Math.sqrt(stdDev/(767.0)); + else + stdDev = 0.0; + boolean isPseudoColor = stdDev<20.0; + if ((int)stdDev==67) isPseudoColor = true; // "3-3-2 RGB" LUT + return isPseudoColor; + } + + /** Returns true if the image is using the default grayscale LUT. */ + public boolean isDefaultLut() { + if (cm==null) + makeDefaultColorModel(); + IndexColorModel icm = getIndexColorModel(); + if (icm==null) + return false; + int mapSize = icm.getMapSize(); + if (mapSize!=256) + return false; + byte[] reds = new byte[mapSize]; + byte[] greens = new byte[mapSize]; + byte[] blues = new byte[mapSize]; + icm.getReds(reds); + icm.getGreens(greens); + icm.getBlues(blues); + boolean isDefault = true; + for (int i=0; i=t1 && i<=t2) { + rLUT2[i] = (byte)255; + gLUT2[i] = (byte)0; + bLUT2[i] = (byte)0; + } else { + rLUT2[i] = rLUT1[i]; + gLUT2[i] = gLUT1[i]; + bLUT2[i] = bLUT1[i]; + } + } + else if (lutUpdate==BLACK_AND_WHITE_LUT) { + // updated in v1.43i by Gabriel Lindini to use blackBackground setting + byte foreground = Prefs.blackBackground?(byte)255:(byte)0; + byte background = (byte)(255 - foreground); + for (int i=0; i<256; i++) { + if (i>=t1 && i<=t2) { + rLUT2[i] = foreground; + gLUT2[i] = foreground; + bLUT2[i] = foreground; + } else { + rLUT2[i] = background; + gLUT2[i] =background; + bLUT2[i] =background; + } + } + } else { + for (int i=0; i<256; i++) { + if (i>=t1 && i<=t2) { + rLUT2[i] = rLUT1[i]; + gLUT2[i] = gLUT1[i]; + bLUT2[i] = bLUT1[i]; + } else if (i>t2) { + rLUT2[i] = (byte)overRed; + gLUT2[i] = (byte)overGreen; + bLUT2[i] = (byte)overBlue; + } else { + rLUT2[i] = (byte)underRed; + gLUT2[i] = (byte)underGreen; + bLUT2[i] = (byte)underBlue; + } + } + } + cm = new IndexColorModel(8, 256, rLUT2, gLUT2, bLUT2); + } + + /** Automatically sets the lower and upper threshold levels, where 'method' + * must be "Default", "Huang", "Intermodes", "IsoData", "IJ_IsoData", "Li", + * "MaxEntropy", "Mean", "MinError", "Minimum", "Moments", "Otsu", + * "Percentile", "RenyiEntropy", "Shanbhag", "Triangle" or "Yen". The + * 'method' string may also include the keywords 'dark' (dark background) + * 'red' (red LUT, the default), 'b&w' (black and white LUT), 'over/under' (over/under LUT) or + * 'no-lut' (no LUT changes), for example "Huang dark b&w". The display range + * of 16-bit and 32-bit images is not reset if the 'method' string contains 'no-reset'. + * @see ImageProcessor#resetThreshold + * @see ImageProcessor#setThreshold + * @see ImageProcessor#createMask + */ + public void setAutoThreshold(String method) { + if (method==null) + throw new IllegalArgumentException("Null method"); + boolean darkBackground = method.contains("dark"); + noReset = method.contains("no-reset"); + int lut = RED_LUT; + if (method.contains("b&w")) + lut = BLACK_AND_WHITE_LUT; + if (method.contains("over")) + lut = OVER_UNDER_LUT; + if (method.contains("no-lut")) + lut = NO_LUT_UPDATE; + int index = method.indexOf(" "); + if (index!=-1) + method = method.substring(0, index); + setAutoThreshold(method, darkBackground, lut); + noReset = false; + } + + public void setAutoThreshold(String mString, boolean darkBackground, int lutUpdate) { + Method m = null; + try { + m = Method.valueOf(Method.class, mString); + } catch(Exception e) { + m = null; + } + if (m==null) + throw new IllegalArgumentException("Invalid method (\""+mString+"\")"); + setAutoThreshold(m, darkBackground, lutUpdate); + } + + public void setAutoThreshold(Method method, boolean darkBackground) { + setAutoThreshold(method, darkBackground, RED_LUT); + } + + public void setAutoThreshold(Method method, boolean darkBackground, int lutUpdate) { + if (method==null || (this instanceof ColorProcessor)) + return; + double min=0.0, max=0.0; + boolean notByteData = !(this instanceof ByteProcessor); + ImageProcessor ip2 = this; + if (notByteData) { + ImageProcessor mask = ip2.getMask(); + Rectangle rect = ip2.getRoi(); + if (!noReset || lutUpdate==OVER_UNDER_LUT) + ip2.resetMinAndMax(); + noReset = false; + min = ip2.getMin(); max = ip2.getMax(); + ip2 = ip2.convertToByte(true); + ip2.setMask(mask); + ip2.setRoi(rect); + } + ImageStatistics stats = ip2.getStats(); + AutoThresholder thresholder = new AutoThresholder(); + int threshold = thresholder.getThreshold(method, stats.histogram); + double lower, upper; + if (darkBackground) { + if (isInvertedLut()) + {lower=0.0; upper=threshold;} + else + {lower=threshold+1; upper=255.0;} + } else { + if (isInvertedLut()) + {lower=threshold+1; upper=255.0;} + else + {lower=0.0; upper=threshold;} + } + if (lower>255) lower = 255; + scaleAndSetThreshold(lower, upper, lutUpdate); + } + + /** Automatically sets the lower and upper threshold levels, where 'method' + must be ISODATA or ISODATA2 and 'lutUpdate' must be RED_LUT, + BLACK_AND_WHITE_LUT, OVER_UNDER_LUT or NO_LUT_UPDATE. + */ + public void setAutoThreshold(int method, int lutUpdate) { + if (method<0 || method>ISODATA2) + throw new IllegalArgumentException("Invalid thresholding method"); + if (this instanceof ColorProcessor) + return; + boolean notByteData = !(this instanceof ByteProcessor); + ImageProcessor ip2 = this; + if (notByteData) { + ImageProcessor mask = ip2.getMask(); + Rectangle rect = ip2.getRoi(); + resetMinAndMax(); + ip2 = convertToByte(true); + ip2.setMask(mask); + ip2.setRoi(rect); + } + ImageStatistics stats = ip2.getStats(); + int[] histogram = stats.histogram; + int originalModeCount = histogram[stats.mode]; + if (method==ISODATA2) { + int maxCount2 = 0; + for (int i = 0; i maxCount2) && (i!=stats.mode)) + maxCount2 = histogram[i]; + } + int hmax = stats.maxCount; + if ((hmax>(maxCount2 * 2)) && (maxCount2 != 0)) { + hmax = (int)(maxCount2 * 1.5); + histogram[stats.mode] = hmax; + } + } + int threshold = ip2.getAutoThreshold(stats.histogram); + histogram[stats.mode] = originalModeCount; + float[] hist = new float[256]; + for (int i=0; i<256; i++) + hist[i] = stats.histogram[i]; + FloatProcessor fp = new FloatProcessor(256, 1, hist, null); + GaussianBlur gb = new GaussianBlur(); + gb.blur1Direction(fp, 2.0, 0.01, true, 0); + float maxCount=0f, sum=0f, mean, count; + int mode = 0; + for (int i=0; i<256; i++) { + count = hist[i]; + sum += count; + if (count>maxCount) { + maxCount = count; + mode = i; + } + } + double avg = sum/256.0; + double lower, upper; + if (maxCount/avg>1.5) { + if ((stats.max-mode)>(mode-stats.min)) + {lower=threshold; upper=255.0;} + else + {lower=0.0; upper=threshold;} + } else { + if (isInvertedLut()) + {lower=threshold; upper=255.0;} + else + {lower=0.0; upper=threshold;} + } + scaleAndSetThreshold(lower, upper, lutUpdate); + + } + + /** Set the threshold using a 0-255 range. For 16-bit and 32-bit images, + * this range is taken as relative with respect to the range between the + * current display min and max, but lower=0 and upper=255 are set to the + * full-range limits of 16-bit images and -/+1e30 for float images. + */ + public void scaleAndSetThreshold(double lower, double upper, int lutUpdate) { + int bitDepth = getBitDepth(); + if (bitDepth!=8 && lower!=NO_THRESHOLD) { + double min = getMin(); + double max = getMax(); + if (max>min) { + if (lower==0.0) { + if (bitDepth==32) + lower = Math.min(min, -1e30); // can't set to -Float.MAX_VALUE; causes FloodFiller.particleAnalyzerFill to hang; + } else + lower = min + (lower/255.0)*(max-min); + if (upper==255.0) { + if (bitDepth==16) + upper = 65535; + else if (bitDepth==32) + upper = Math.max(max, 1e30); + } else + upper = min + (upper/255.0)*(max-min); + } else + lower = upper = min; + } + setThreshold(lower, upper, lutUpdate); + } + + /** Disables thresholding. */ + public void resetThreshold() { + minThreshold = NO_THRESHOLD; + if (baseCM!=null) { + cm = baseCM; + baseCM = null; + } + rLUT1 = rLUT2 = null; + inversionTested = false; + } + + /** Returns the lower threshold level. Returns NO_THRESHOLD + if thresholding is not enabled. */ + public double getMinThreshold() { + return minThreshold; + } + + /** Returns the upper threshold level. */ + public double getMaxThreshold() { + return maxThreshold; + } + + /** Returns the LUT update mode, which can be RED_LUT, BLACK_AND_WHITE_LUT, + OVER_UNDER_LUT or NO_LUT_UPDATE. */ + public int getLutUpdateMode() { + return lutUpdateMode; + } + + /* Sets the threshold levels (non-visible) of an 8-bit mask based on + the state of Prefs.blackBackground and isInvertedLut(). + @see ImageProcessor#resetBinaryThreshold + */ + public void setBinaryThreshold() { + //ij.IJ.log("setMaskThreshold1"); + if (!(this instanceof ByteProcessor)) return; + double t1=255.0, t2=255.0; + boolean invertedLut = isInvertedLut(); + if ((invertedLut&&ij.Prefs.blackBackground) || (!invertedLut&&!ij.Prefs.blackBackground)) { + t1 = 0.0; + t2 = 0.0; + } + //ij.IJ.log("setMaskThreshold2 "+t1+" "+t2); + setThreshold(t1, t2, ImageProcessor.NO_LUT_UPDATE); + } + + /** Resets the threshold if minThreshold=maxThreshold and lutUpdateMode=NO_LUT_UPDATE. + This removes the invisible threshold set by the MakeBinary and Convert to Mask commands. + @see ImageProcessor#setBinaryThreshold + */ + public void resetBinaryThreshold() { + if (minThreshold==maxThreshold && lutUpdateMode==NO_LUT_UPDATE) + resetThreshold(); + } + + /** Defines a rectangular region of interest and sets the mask + to null if this ROI is not the same size as the previous one. + @see ImageProcessor#resetRoi + */ + public void setRoi(Rectangle roi) { + if (roi==null) + resetRoi(); + else + setRoi(roi.x, roi.y, roi.width, roi.height); + } + + /** Defines a rectangular region of interest and sets the mask to + null if this ROI is not the same size as the previous one. + @see ImageProcessor#resetRoi + */ + public void setRoi(int x, int y, int rwidth, int rheight) { + if (x<0 || y<0 || x+rwidth>width || y+rheight>height) { + //find intersection of roi and this image + Rectangle r1 = new Rectangle(x, y, rwidth, rheight); + Rectangle r2 = r1.intersection(new Rectangle(0, 0, width, height)); + if (r2.width<=0 || r2.height<=0) { + roiX=0; roiY=0; roiWidth=0; roiHeight=0; + xMin=0; xMax=0; yMin=0; yMax=0; + mask=null; + return; + } + if (mask!=null && mask.getWidth()==rwidth && mask.getHeight()==rheight) { + Rectangle r3 = new Rectangle(0, 0, r2.width, r2.height); + if (x<0) r3.x = -x; + if (y<0) r3.y = -y; + mask.setRoi(r3); + if (mask!=null) + mask = mask.crop(); + } + roiX=r2.x; roiY=r2.y; roiWidth=r2.width; roiHeight=r2.height; + } else { + roiX=x; roiY=y; roiWidth=rwidth; roiHeight=rheight; + } + if (mask!=null && (mask.getWidth()!=roiWidth||mask.getHeight()!=roiHeight)) + mask = null; + //setup limits for 3x3 filters + xMin = Math.max(roiX, 1); + xMax = Math.min(roiX + roiWidth - 1, width - 2); + yMin = Math.max(roiY, 1); + yMax = Math.min(roiY + roiHeight - 1, height - 2); + } + + /** Defines a non-rectangular region of interest that will consist of a + rectangular ROI and a mask. After processing, call reset(mask) + to restore non-masked pixels. Here is an example: +
+		ip.setRoi(new OvalRoi(50, 50, 100, 50));
+		ip.fill();
+		ip.reset(ip.getMask());
+		
+ The example assumes snapshot() has been called, which is the case + for code executed in the run() method of plugins that implement the + PlugInFilter interface. + @see ij.ImagePlus#getRoi + */ + public void setRoi(Roi roi) { + if (roi==null) + resetRoi(); + else { + if ((roi instanceof PointRoi) && roi.size()==1) { + setMask(null); + FloatPolygon p = roi.getFloatPolygon(); + setRoi((int)p.xpoints[0], (int)p.ypoints[0], 1, 1); + } else { + setMask(roi.getMask()); + setRoi(roi.getBounds()); + } + } + } + + /** Defines a polygonal region of interest that will consist of a + rectangular ROI and a mask. After processing, call reset(mask) + to restore non-masked pixels. Here is an example: +
+		Polygon p = new Polygon();
+		p.addPoint(50, 0); p.addPoint(100, 100); p.addPoint(0, 100);
+		ip.setRoi(triangle);
+		ip.invert();
+		ip.reset(ip.getMask());
+		
+ The example assumes snapshot() has been called, which is the case + for code executed in the run() method of plugins that implement the + PlugInFilter interface. + @see ij.gui.Roi#getPolygon + @see ImageProcessor#drawPolygon + @see ImageProcessor#fillPolygon + */ + public void setRoi(Polygon roi) { + if (roi==null) + {resetRoi(); return;} + Rectangle bounds = roi.getBounds(); + for (int i=0; iBICUBIC) + throw new IllegalArgumentException("Invalid interpolation method"); + interpolationMethod = method; + interpolate = method!=NONE?true:false; + } + + /** Returns the current interpolation method (NONE, BILINEAR or BICUBIC). */ + public int getInterpolationMethod() { + return interpolationMethod; + } + + public static String[] getInterpolationMethods() { + if (interpolationMethods==null) + interpolationMethods = new String[] {"None", "Bilinear", "Bicubic"}; + return interpolationMethods; + } + + /** Returns the value of the interpolate field. */ + public boolean getInterpolate() { + return interpolate; + } + + /** @deprecated */ + public boolean isKillable() { + return false; + } + + private void process(int op, double value) { + double SCALE = 255.0/Math.log(255.0); + int v; + + int[] lut = new int[256]; + for (int i=0; i<256; i++) { + switch(op) { + case INVERT: + v = 255 - i; + break; + case FILL: + v = fgColor; + break; + case SET: + v = (int)value; + break; + case ADD: + v = i + (int)value; + break; + case MULT: + v = (int)Math.round(i * value); + break; + case AND: + v = i & (int)value; + break; + case OR: + v = i | (int)value; + break; + case XOR: + v = i ^ (int)value; + break; + case GAMMA: + v = (int)(Math.exp(Math.log(i/255.0)*value)*255.0); + break; + case LOG: + if (i==0) + v = 0; + else + v = (int)(Math.log(i) * SCALE); + break; + case EXP: + v = (int)(Math.exp(i/SCALE)); + break; + case SQR: + v = i*i; + break; + case SQRT: + v = (int)Math.sqrt(i); + break; + case MINIMUM: + if (ivalue) + v = (int)value; + else + v = i; + break; + default: + v = i; + } + if (v < 0) + v = 0; + if (v > 255) + v = 255; + lut[i] = v; + } + applyTable(lut); + } + + /** + * Returns an array containing the pixel values along the + * line starting at (x1,y1) and ending at (x2,y2). Pixel + * values are sampled using getInterpolatedValue(double,double) + * if interpolation is enabled or getPixelValue(int,int) if it is not. + * For byte and short images, returns calibrated values if a + * calibration table has been set using setCalibrationTable(). + * The length of the returned array, minus one, is approximately + * equal to the length of the line. + * @see ImageProcessor#setInterpolate + * @see ImageProcessor#getPixelValue + * @see ImageProcessor#getInterpolatedValue + */ + public double[] getLine(double x1, double y1, double x2, double y2) { + double dx = x2-x1; + double dy = y2-y1; + int n = (int)Math.round(Math.sqrt(dx*dx + dy*dy)); + double xinc = n>0?dx/n:0; + double yinc = n>0?dy/n:0; + if (!((xinc==0&&n==height) || (yinc==0&&n==width))) + n++; + double[] data = new double[n]; + double rx = x1; + double ry = y1; + if (interpolate) { + for (int i=0; i=0 && x=0 && (y+length)<=height) + // ((ShortProcessor)this).putColumn2(x, y, data, length); + //else + for (int i=0; i=0?dx:-dx; + int absdy = dy>=0?dy:-dy; + int n = absdy>absdx?absdy:absdx; + double xinc = dx!=0 ? (double)dx/n : 0; //single point (dx=dy=n=0): avoid division by zero + double yinc = dy!=0 ? (double)dy/n : 0; + double x = cx; + double y = cy; + cx = x2; cy = y2; //keep end point as starting for the next lineTo + int i1 = 0; + if (dx>0) + i1 = Math.max(i1, (int)((xMin-x)/xinc)); + else if (dx<0) + i1 = Math.max(i1, (int)((xMax-x)/xinc)); + else if (xxMax) + return; // vertical line outside y range + if (dy>0) + i1 = Math.max(i1, (int)((yMin-y)/yinc)); + else if (dy<0) + i1 = Math.max(i1, (int)((yMax-y)/yinc)); + else if (yyMax) + return; // horizontal line outside y range + int i2 = n; + if (dx>0) + i2 = Math.min(i2, (int)((xMax-x)/xinc)); + else if (dx<0) + i2 = Math.min(i2, (int)((xMin-x)/xinc)); + if (dy>0) + i2 = Math.min(i2, (int)((yMax-y)/yinc)); + else if (dy<0) + i2 = Math.min(i2, (int)((yMin-y)/yinc)); + x += i1*xinc; + y += i1*yinc; + for (int i=i1; i<=i2; i++) { + if (lineWidth==1) + drawPixel((int)Math.round(x), (int)Math.round(y)); + else if (lineWidth==2) + drawDot2((int)Math.round(x), (int)Math.round(y)); + else + drawDot((int)Math.round(x), (int)Math.round(y)); + x += xinc; + y += yinc; + } + } + + /** Draws a line from (x1,y1) to (x2,y2). */ + public void drawLine(int x1, int y1, int x2, int y2) { + moveTo(x1, y1); + lineTo(x2, y2); + } + + /* Draws a line using the Bresenham's algorithm that is 4-connected instead of 8-connected.
+ Based on code from http://stackoverflow.com/questions/5186939/algorithm-for-drawing-a-4-connected-line
+ Author: Gabriel Landini (G.Landini at bham.ac.uk) + */ + public void drawLine4(int x1, int y1, int x2, int y2) { + int dx = Math.abs(x2 - x1); + int dy = Math.abs(y2 - y1); + int sgnX = x1 < x2 ? 1 : -1; + int sgnY = y1 < y2 ? 1 : -1; + int e = 0; + for (int i=0; i < dx+dy; i++) { + putPixel(x1, y1, fgColor); + int e1 = e + dy; + int e2 = e - dx; + if (Math.abs(e1) < Math.abs(e2)) { + x1 += sgnX; + e = e1; + } else { + y1 += sgnY; + e = e2; + } + } + putPixel(x2, y2, fgColor); + } + + /** Draws a rectangle. */ + public void drawRect(int x, int y, int width, int height) { + if (width<1 || height<1) + return; + if (lineWidth==1) { + moveTo(x, y); + lineTo(x+width-1, y); + lineTo(x+width-1, y+height-1); + lineTo(x, y+height-1); + lineTo(x, y); + } else { + moveTo(x, y); + lineTo(x+width, y); + lineTo(x+width, y+height); + lineTo(x, y+height); + lineTo(x, y); + } + } + + /** Fills a rectangle. */ + public void fillRect(int x, int y, int width, int height) { + setRoi(x, y, width, height); + fill(); + resetRoi(); + } + + /** Draws an elliptical shape. */ + public void drawOval(int x, int y, int width, int height) { + if ((long)width*height>4L*this.width*this.height) + return; + OvalRoi oval = new OvalRoi(x, y, width, height); + drawPolygon(oval.getPolygon()); + } + + /** Fills an elliptical shape. */ + public void fillOval(int x, int y, int width, int height) { + if ((long)width*height>4L*this.width*this.height) + return; + OvalRoi oval = new OvalRoi(x, y, width, height); + fillPolygon(oval.getPolygon()); + } + + /** Draws a polygon. */ + public void drawPolygon(Polygon p) { + moveTo(p.xpoints[0], p.ypoints[0]); + for (int i=0; iclipXMax || ycenter>clipYMax ) { + if (xminclipXMax || ymax>clipYMax ) { + // draw edge dot + double r2 = r*r; + r -= 0.5; + double xoffset=xmin+r, yoffset=ymin+r; + double xx, yy; + for (int y=ymin; y=0 && cy-h>=0) { + Java2.setAntialiasedText(g, true); + setRoi(cxx, cy-h, w, h); + ImageProcessor ip = crop(); + resetRoi(); + if (ip.getWidth()==0||ip.getHeight()==0) + return; + g.drawImage(ip.createImage(), 0, 0, null); + g.setColor(drawingColor); + g.drawString(s, 0, h-descent); + g.dispose(); + ip = new ColorProcessor(bi); + if (this instanceof ByteProcessor) { + ip = ip.convertToByte(false); + if (isInvertedLut()) ip.invert(); + } + //new ij.ImagePlus("ip",ip).show(); + insert(ip, cxx, cy-h); + cy += h; + return; + } + + Java2.setAntialiasedText(g, false); + g.setColor(Color.white); + g.fillRect(0, 0, w, h); + g.setColor(Color.black); + g.drawString(s, 0, h-descent); + g.dispose(); + ImageProcessor ip = new ColorProcessor(bi); + ImageProcessor textMask = ip.convertToByte(false); + byte[] mpixels = (byte[])textMask.getPixels(); + //new ij.ImagePlus("textmask",textMask).show(); + textMask.invert(); + if (cxxw) w = w2; + } + int h2 = metrics.getHeight(); + y2 += h2*(s2.length-1); + h += h2*(s2.length-1); + } else + w = getStringWidth(s); + int x2 = x; + if (justification==CENTER_JUSTIFY) + x2 -= w/2; + else if (justification==RIGHT_JUSTIFY) + x2 -= w; + setColor(background); + setRoi(x2, y2-h, w, h); + fill(); + resetRoi(); + setColor(foreground); + drawString(s, x, y); + } + + /** Sets the justification used by drawString(), where justification + is CENTER_JUSTIFY, RIGHT_JUSTIFY or LEFT_JUSTIFY. The default is LEFT_JUSTIFY. */ + public void setJustification(int justification) { + if (justificationRIGHT_JUSTIFY) + justification = LEFT_JUSTIFY; + this.justification = justification; + } + + /** Sets the font used by drawString(). */ + public void setFont(Font font) { + this.font = font; + boldFont = font.isBold(); + setupFontMetrics(); + fmGraphics.setFont(font); + Java2.setAntialiasedText(fmGraphics, antialiasedText); + fontMetrics = fmGraphics.getFontMetrics(font); + } + + /** Sets the size of the font used by drawString(). */ + public void setFontSize(int size) { + setFont(font.deriveFont(font.getStyle(), size)); + if (size>15) + setAntialiasedText(true); + } + + + /** Specifies whether or not text is drawn using antialiasing. Antialiased + test requires an 8 bit or RGB image. Antialiasing does not + work with 8-bit images that are not using 0-255 display range. */ + public void setAntialiasedText(boolean antialiasedText) { + setupFontMetrics(); + if (antialiasedText && (((this instanceof ByteProcessor)&&getMin()==0.0&&getMax()==255.0) || (this instanceof ColorProcessor))) + this.antialiasedText = true; + else + this.antialiasedText = false; + Java2.setAntialiasedText(fmGraphics, this.antialiasedText); + fontMetrics = fmGraphics.getFontMetrics(font); + } + + /** Returns the width in pixels of the specified string, including any background + * space (whitespace) between the x drawing coordinate and the string, not necessarily + * including all whitespace at the right. */ + public int getStringWidth(String s) { + // Note that fontMetrics.getStringBounds often underestimates the width (worst for italic fonts on Macs) + // On the other hand, GlyphVector.getPixelBounds (returned by this.getStringBounds) + // does not include the full character width of e.g. the '1' character, which would make + // lists of right-justified numbers such as the y axis of plots look ugly. + // Thus, the maximum of both methods is returned. + Rectangle2D rect = getStringBounds(s); + return (int)Math.max(fontMetrics.getStringBounds(s, fmGraphics).getWidth(), rect.getX()+rect.getWidth()); + } + + /** Returns a rectangle enclosing the pixels affected by drawString + * assuming it is drawn at (x=0, y=0). As drawString draws above the drawing location, + * the y coordinate of the rectangle is negative. */ + public Rectangle getStringBounds(String s) { + setupFontMetrics(); + GlyphVector gv = font.createGlyphVector(fmGraphics.getFontRenderContext(), s); + Rectangle2D rect = gv.getPixelBounds(null, 0.f, -fontMetrics.getDescent()); + return new Rectangle((int)rect.getX(), (int)rect.getY(), (int)rect.getWidth(), (int)rect.getHeight()); + } + + /** Returns the current font. */ + public Font getFont() { + return font; + } + + /** Returns the current FontMetrics. */ + public FontMetrics getFontMetrics() { + setupFontMetrics(); + return fontMetrics; + } + + /** Replaces each pixel with the 3x3 neighborhood mean. */ + public void smooth() { + if (width>1) + filter(BLUR_MORE); + } + + /** Sharpens the image or ROI using a 3x3 convolution kernel. */ + public void sharpen() { + if (width>1) { + int[] kernel = {-1, -1, -1, + -1, 12, -1, + -1, -1, -1}; + convolve3x3(kernel); + } + } + + /** Finds edges in the image or ROI using a Sobel operator. */ + public void findEdges() { + if (width>1) + filter(FIND_EDGES); + } + + /** Flips the image or ROI vertically. */ + public abstract void flipVertical(); + + /** Flips the image or ROI horizontally. */ + public void flipHorizontal() { + int[] col1 = new int[roiHeight]; + int[] col2 = new int[roiHeight]; + for (int x=0; x1 && !(roi instanceof Arrow)) + fillPolygon(roi.getPolygon()); + else + roi.drawPixels(this); + return; + } + ImageProcessor m = getMask(); + Rectangle r = getRoi(); + setRoi(roi); + fill(getMask()); + setMask(m); + setRoi(r); + } + + /** Fills outside an Roi. */ + public void fillOutside(Roi roi) { + if (roi==null || !roi.isArea()) return; + ImageProcessor m = getMask(); + Rectangle r = getRoi(); + ShapeRoi s1, s2; + if (roi instanceof ShapeRoi) + s1 = (ShapeRoi)roi; + else + s1 = new ShapeRoi(roi); + s2 = new ShapeRoi(new Roi(0,0, width, height)); + setRoi(s2.xor(s1)); + fill(getMask()); + setMask(m); + setRoi(r); + } + + /** Draws the specified ROI on this image using the line + width and color defined by ip.setLineWidth() and ip.setColor(). + @see ImageProcessor#drawRoi + */ + public void draw(Roi roi) { + roi.drawPixels(this); + } + + /** Draws the specified ROI on this image using the stroke + width, stroke color and fill color defined by roi.setStrokeWidth, + roi.setStrokeColor() and roi.setFillColor(). Works with RGB + images. Does not work with 16-bit and float images. + @see ImageProcessor#draw + @see ImageProcessor#drawOverlay + */ + public void drawRoi(Roi roi) { + Image img = createImage(); + Graphics g = img.getGraphics(); + ij.ImagePlus imp = roi.getImage(); + if (imp!=null) { + roi.setImage(null); + roi.drawOverlay(g); + roi.setImage(imp); + } else + roi.drawOverlay(g); + } + + /** Draws the specified Overlay on this image. Works best + with RGB images. Does not work with 16-bit and float + images. Requires Java 1.6. + @see ImageProcessor#drawRoi + */ + public void drawOverlay(Overlay overlay) { + Roi[] rois = overlay.toArray(); + for (int i=0; ihistMin
and histMax are zero. */ + public void setHistogramRange(double histMin, double histMax) { + if (histMin>histMax) { + histMin = 0.0; + histMax = 0.0; + } + histogramMin = histMin; + histogramMax = histMax; + } + + /** Returns the minimum histogram value used for histograms of float images. */ + public double getHistogramMin() { + return histogramMin; + } + + /** Returns the maximum histogram value used for histograms of float images. */ + public double getHistogramMax() { + return histogramMax; + } + + /** Returns a reference to this image's pixel array. The + array type (byte[], short[], float[] or int[]) varies + depending on the image type. */ + public abstract Object getPixels(); + + /** Returns a copy of the pixel data. Or returns a reference to the + snapshot buffer if it is not null and 'snapshotCopyMode' is true. + @see ImageProcessor#snapshot + @see ImageProcessor#setSnapshotCopyMode + */ + public abstract Object getPixelsCopy(); + + /** Returns the value of the pixel at (x,y). For RGB images, the + * argb values are packed in an int. For float images, the + * the value must be converted using Float.intBitsToFloat(). + * Returns zero if either the x or y coodinate is out of range. + * Use getValue(x,y) to get calibrated values from + * 8-bit and 16-bit images, to get intensity values from RGB + * images and to get float values from 32-bit images. + * @see ImageProcessor#getValue + */ + public abstract int getPixel(int x, int y); + + /** This is a faster version of getPixel() that does not do bounds checking. */ + public abstract int get(int x, int y); + + public abstract int get(int index); + + /** This is a faster version of putPixel() that does not clip + out of range values and does not do bounds checking. */ + public abstract void set(int x, int y, int value); + + public abstract void set(int index, int value); + + /** Returns the value of the pixel at (x,y) as a float. Faster + than getPixelValue() because no bounds checking is done. */ + public abstract float getf(int x, int y); + + public abstract float getf(int index); + + /** Sets the value of the pixel at (x,y) to 'value'. Does no bounds + checking or clamping, making it faster than putPixel(). Due to the lack + of bounds checking, (x,y) coordinates outside the image may cause + an exception. Due to the lack of clamping, values outside the 0-255 + range (for byte) or 0-65535 range (for short) are not handled correctly. + */ + public abstract void setf(int x, int y, float value); + + public abstract void setf(int index, float value); + + public int getPixelCount() { + return width*height; + } + + /** Returns a copy of the pixel data as a 2D int array with + dimensions [x=0..width-1][y=0..height-1]. With RGB + images, the returned values are in packed ARGB format. + With float images, the returned values must be converted + to float using Float.intBitsToFloat(). */ + public int[][] getIntArray() { + int[][] a = new int [width][height]; + for(int y=0; y=nx2 && y>=ny2 && x=width-1.0 || y<0.0 || y>=height-1.0) { + if (x<-1.0 || x>=width || y<-1.0 || y>=height) + return 0.0; + else + return getInterpolatedEdgeValue(x, y); + } + int xbase = (int)x; + int ybase = (int)y; + double xFraction = x - xbase; + double yFraction = y - ybase; + if (xFraction<0.0) xFraction = 0.0; + if (yFraction<0.0) yFraction = 0.0; + double lowerLeft = getPixelValue(xbase, ybase); + double lowerRight = getPixelValue(xbase+1, ybase); + double upperRight = getPixelValue(xbase+1, ybase+1); + double upperLeft = getPixelValue(xbase, ybase+1); + double upperAverage = upperLeft + xFraction * (upperRight - upperLeft); + double lowerAverage = lowerLeft + xFraction * (lowerRight - lowerLeft); + return lowerAverage + yFraction * (upperAverage - lowerAverage); + } + + /** This method is from Chapter 16 of "Digital Image Processing: + An Algorithmic Introduction Using Java" by Burger and Burge + (http://www.imagingbook.com/). */ + public double getBicubicInterpolatedPixel(double x0, double y0, ImageProcessor ip2) { + int u0 = (int) Math.floor(x0); //use floor to handle negative coordinates too + int v0 = (int) Math.floor(y0); + if (u0<=0 || u0>=width-2 || v0<=0 || v0>=height-2) + return ip2.getBilinearInterpolatedPixel(x0, y0); + double q = 0; + for (int j = 0; j <= 3; j++) { + int v = v0 - 1 + j; + double p = 0; + for (int i = 0; i <= 3; i++) { + int u = u0 - 1 + i; + p = p + ip2.get(u,v) * cubic(x0 - u); + } + q = q + p * cubic(y0 - v); + } + return q; + } + + final double getBilinearInterpolatedPixel(double x, double y) { + if (x>=-1 && x=-1 && y=width) x = width-1; + if (y<=0) y = 0; + if (y>=height) y = height-1; + return getPixelValue(x, y); + } + + /** Stores the specified value at (x,y). Does + nothing if (x,y) is outside the image boundary. + For 8-bit and 16-bit images, out of range values + are clamped. For RGB images, the + argb values are packed in 'value'. For float images, + 'value' is expected to be a float converted to an int + using Float.floatToIntBits(). */ + public abstract void putPixel(int x, int y, int value); + + + /** Returns the value of the pixel at (x,y), a calibrated + * value from 8-bit and 16-bit images, an intensity value + * from RGB images and a double value from 32-bit images. + * This is an alias for getPixelValue(x,y). + * @see ImageProcessor#getPixel + * @see ImageProcessor#getPixelValue + */ + public double getValue(int x, int y) { + return getPixelValue(x,y); + } + + /** Returns the value of the pixel at (x,y). For byte and short + * images, returns a calibrated value if a calibration table + * has been set using setCalibraionTable(). For RGB images, + * returns the luminance value. + */ + public abstract float getPixelValue(int x, int y); + + /** Stores the specified value at (x,y). */ + public abstract void putPixelValue(int x, int y, double value); + + /** Sets the pixel at (x,y) to the current fill/draw value. */ + public abstract void drawPixel(int x, int y); + + /** Sets a new pixel array for the image. The length of the array must be equal to width*height. + Use setSnapshotPixels(null) to clear the snapshot buffer. */ + public abstract void setPixels(Object pixels); + + /** Copies the image contained in 'ip' to (xloc, yloc) using one of + the transfer modes defined in the Blitter interface. */ + public abstract void copyBits(ImageProcessor ip, int xloc, int yloc, int mode); + + /** Transforms the image or ROI using a lookup table. The + length of the table must be 256 for byte images and + 65536 for short images. RGB and float images are not + supported. */ + public abstract void applyTable(int[] lut); + + /** Inverts the image or ROI. */ + public void invert() {process(INVERT, 0.0);} + + /** Adds 'value' to each pixel in the image or ROI. */ + public void add(int value) {process(ADD, value);} + + /** Adds 'value' to each pixel in the image or ROI. */ + public void add(double value) {process(ADD, value);} + + /** Subtracts 'value' from each pixel in the image or ROI. */ + public void subtract(double value) { + add(-value); + } + + /** Multiplies each pixel in the image or ROI by 'value'. */ + public void multiply(double value) {process(MULT, value);} + + /** Assigns 'value' to each pixel in the image or ROI. */ + public void set(double value) {process(SET, value);} + + /** Binary AND of each pixel in the image or ROI with 'value'. */ + public void and(int value) {process(AND, value);} + + /** Binary OR of each pixel in the image or ROI with 'value'. */ + public void or(int value) {process(OR, value);} + + /** Binary exclusive OR of each pixel in the image or ROI with 'value'. */ + public void xor(int value) {process(XOR, value);} + + /** Performs gamma correction of the image or ROI. */ + public void gamma(double value) {process(GAMMA, value);} + + /** Does a natural logarithmic (base e) transform of the image or ROI. */ + public void log() {process(LOG, 0.0);} + + /** Does a natural logarithmic (base e) transform of the image or ROI. */ + public void ln() {log();} + + /** Performs a exponential transform on the image or ROI. */ + public void exp() {process(EXP, 0.0);} + + /** Performs a square transform on the image or ROI. */ + public void sqr() {process(SQR, 0.0);} + + /** Performs a square root transform on the image or ROI. */ + public void sqrt() {process(SQRT, 0.0);} + + /** If this is a 32-bit or signed 16-bit image, performs an + absolute value transform, otherwise does nothing. */ + public void abs() {} + + /** Pixels less than 'value' are set to 'value'. */ + public void min(double value) {process(MINIMUM, value);} + + /** Pixels greater than 'value' are set to 'value'. */ + public void max(double value) {process(MAXIMUM, value);} + + /** Returns a copy of this image is the form of an AWT Image. */ + public abstract Image createImage(); + + /** Returns this image as a BufferedImage. */ + public BufferedImage getBufferedImage() { + BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g = (Graphics2D)bi.getGraphics(); + g.drawImage(createImage(), 0, 0, null); + return bi; + } + + /** Returns a new, blank processor with the specified width and height. */ + public abstract ImageProcessor createProcessor(int width, int height); + + /** Makes a copy of this image's pixel data that can be + later restored using reset() or reset(mask). + @see ImageProcessor#reset + @see ImageProcessor#reset(ImageProcessor) + */ + public abstract void snapshot(); + + /** Restores the pixel data from the snapshot (undo) buffer. */ + public abstract void reset(); + + /** Swaps the pixel and snapshot (undo) buffers. */ + public abstract void swapPixelArrays(); + + /** Restores pixels from the snapshot buffer that are + within the rectangular roi but not part of the mask. */ + public abstract void reset(ImageProcessor mask); + + /** Sets a new pixel array for the snapshot (undo) buffer. */ + public abstract void setSnapshotPixels(Object pixels); + + /** Returns a reference to the snapshot (undo) buffer, or null. */ + public abstract Object getSnapshotPixels(); + + /** Convolves the image or ROI with the specified + 3x3 integer convolution kernel. */ + public abstract void convolve3x3(int[] kernel); + + /** A 3x3 filter operation, where the argument (BLUR_MORE, FIND_EDGES, + MEDIAN_FILTER, MIN or MAX) determines the filter type. */ + public abstract void filter(int type); + + /** A 3x3 median filter. Requires 8-bit or RGB image. */ + public abstract void medianFilter(); + + /** Adds pseudorandom, Gaussian ("normally") distributed values, with + mean 0.0 and the specified standard deviation, to this image or ROI. */ + public abstract void noise(double standardDeviation); + + /** Returns a new processor containing an image + that corresponds to the current ROI. */ + public abstract ImageProcessor crop(); + + /** Sets pixels less than or equal to level to 0 and all other + pixels to 255. Only works with 8-bit and 16-bit images. */ + public abstract void threshold(int level); + + /** Returns a duplicate of this image. */ + public abstract ImageProcessor duplicate(); + + /** Scales the image by the specified factors. Does not + change the image size. + @see ImageProcessor#setInterpolate + @see ImageProcessor#resize + */ + public abstract void scale(double xScale, double yScale); + + /** Returns a new ImageProcessor containing a scaled copy of this image or ROI. + @see ij.process.ImageProcessor#setInterpolate + */ + public abstract ImageProcessor resize(int dstWidth, int dstHeight); + + /** Returns a new ImageProcessor containing a scaled copy + of this image or ROI, with the aspect ratio maintained. */ + public ImageProcessor resize(int dstWidth) { + return resize(dstWidth, (int)(dstWidth*((double)roiHeight/roiWidth))); + } + + /** Returns a new ImageProcessor containing a scaled copy of this image or ROI. + @param dstWidth Image width of the resulting ImageProcessor + @param dstHeight Image height of the resulting ImageProcessor + @param useAverging True means that the averaging occurs to avoid + aliasing artifacts; the kernel shape for averaging is determined by + the interpolationMethod. False if subsampling without any averaging + should be used on downsizing. Has no effect on upsizing. + @see ImageProcessor#setInterpolationMethod + Author: Michael Schmid + */ + public ImageProcessor resize(int dstWidth, int dstHeight, boolean useAverging) { + Rectangle r = getRoi(); + int rWidth = r.width; + int rHeight = r.height; + if ((dstWidth>=rWidth && dstHeight>=rHeight) || !useAverging) + return resize(dstWidth, dstHeight); //upsizing or downsizing without averaging + else { //downsizing with averaging in at least one direction: convert to float + ImageProcessor ip2 = createProcessor(dstWidth, dstHeight); + FloatProcessor fp = null; + int channels = getNChannels(); + boolean showStatus = getProgressIncrement(width,height)>0; + boolean showProgress = showStatus && channels>1; + if (showProgress) showProgress(0.15); + for (int channelNumber=0; channelNumber + For 8-bit and 16-bit images, returns an array with one entry for each possible + value that a pixel can have, from 0 to 255 (8-bit image) or 0-65535 (16-bit image). + Thus, the array size is 256 or 65536, and the bin width in uncalibrated units is 1. +

+ For RGB images, the brightness is evaluated using the color weights (which would result in a + float value) and rounded to an int. This gives 256 bins. FloatProcessor.getHistogram is not + implemented (returns null). + */ + public abstract int[] getHistogram(); + + /** Returns the histogram of the image or ROI, using the specified number of bins. */ + public int[] getHistogram(int nBins) { + ImageProcessor ip; + if (((this instanceof ByteProcessor)||(this instanceof ColorProcessor)) && nBins!=256) + ip = convertToShort(false); + else + ip = this; + ip.setHistogramSize(nBins); + ip.setHistogramRange(0.0, 0.0); + ImageStatistics stats = ImageStatistics.getStatistics(ip); + ip.setHistogramSize(256); + return stats.histogram; + } + + /** Erodes the image or ROI using a 3x3 maximum filter. Requires 8-bit or RGB image. */ + public abstract void erode(); + + /** Dilates the image or ROI using a 3x3 minimum filter. Requires 8-bit or RGB image. */ + public abstract void dilate(); + + /** For 16 and 32 bit processors, set 'lutAnimation' true + to have createImage() use the cached 8-bit version + of the image. */ + public void setLutAnimation(boolean lutAnimation) { + this.lutAnimation = lutAnimation; + } + + void resetPixels(Object pixels) { + if (pixels==null) { + if (img!=null) { + img.flush(); + img = null; + } + } + } + + /** Returns an 8-bit version of this image as a ByteProcessor. */ + public ImageProcessor convertToByte(boolean doScaling) { + TypeConverter tc = new TypeConverter(this, doScaling); + return tc.convertToByte(); + } + + /** Returns a 16-bit version of this image as a ShortProcessor. */ + public ImageProcessor convertToShort(boolean doScaling) { + TypeConverter tc = new TypeConverter(this, doScaling); + return tc.convertToShort(); + } + + /** Returns a 32-bit float version of this image as a FloatProcessor. + For byte and short images, converts using a calibration function + if a calibration table has been set using setCalibrationTable(). */ + public ImageProcessor convertToFloat() { + TypeConverter tc = new TypeConverter(this, false); + return tc.convertToFloat(cTable); + } + + /** Returns an RGB version of this image as a ColorProcessor. */ + public ImageProcessor convertToRGB() { + TypeConverter tc = new TypeConverter(this, true); + return tc.convertToRGB(); + } + + /** Returns an 8-bit version of this image as a ByteProcessor. 16-bit and 32-bit + * pixel data are scaled from min-max to 0-255. + */ + public ByteProcessor convertToByteProcessor() { + return convertToByteProcessor(true); + } + + /** Returns an 8-bit version of this image as a ByteProcessor. 16-bit and 32-bit + * pixel data are scaled from min-max to 0-255 if 'scale' is true. + */ + public ByteProcessor convertToByteProcessor(boolean scale) { + ByteProcessor bp; + if (this instanceof ByteProcessor) + bp = (ByteProcessor)this.duplicate(); + else + bp = (ByteProcessor)this.convertToByte(scale); + return bp; + } + + /** Returns a 16-bit version of this image as a ShortProcessor. 32-bit + * pixel data are scaled from min-max to 0-255. + */ + public ShortProcessor convertToShortProcessor() { + return convertToShortProcessor(true); + } + + /** Returns a 16-bit version of this image as a ShortProcessor. 32-bit + * pixel data are scaled from min-max to 0-255 if 'scale' is true. + */ + public ShortProcessor convertToShortProcessor(boolean scale) { + ShortProcessor sp; + if (this instanceof ShortProcessor) + sp = (ShortProcessor)this.duplicate(); + else + sp = (ShortProcessor)this.convertToShort(scale); + return sp; + } + + /** Returns a 32-bit float version of this image as a FloatProcessor. + For byte and short images, converts using a calibration function + if a calibration table has been set using setCalibrationTable(). */ + public FloatProcessor convertToFloatProcessor() { + FloatProcessor fp; + if (this instanceof FloatProcessor) + fp = (FloatProcessor)this.duplicate(); + else + fp = (FloatProcessor)this.convertToFloat(); + return fp; + } + + /** Returns an RGB version of this image as a ColorProcessor. */ + public ColorProcessor convertToColorProcessor() { + ColorProcessor cp; + if (this instanceof ColorProcessor) + cp = (ColorProcessor)this.duplicate(); + else + cp = (ColorProcessor)this.convertToRGB(); + return cp; + } + + /** Performs a convolution operation using the specified kernel. + KernelWidth and kernelHeight must be odd. */ + public abstract void convolve(float[] kernel, int kernelWidth, int kernelHeight); + + /** Converts the image to binary using an automatically determined threshold. + For byte and short images, converts to binary using an automatically determined + threshold. For RGB images, converts each channel to binary. For + float images, does nothing. + */ + public void autoThreshold() { + threshold(getAutoThreshold()); + } + + /** Returns a pixel value (threshold) that can be used to divide the image into objects + and background. It does this by taking a test threshold and computing the average + of the pixels at or below the threshold and pixels above. It then computes the average + of those two, increments the threshold, and repeats the process. Incrementing stops + when the threshold is larger than the composite average. That is, threshold = (average + background + average objects)/2. This description was posted to the ImageJ mailing + list by Jordan Bevic. */ + public int getAutoThreshold() { + return getAutoThreshold(getHistogram()); + } + + /** This is a version of getAutoThreshold() that uses a histogram passed as an argument. */ + public int getAutoThreshold(int[] histogram) { + int level; + int maxValue = histogram.length - 1; + double result, sum1, sum2, sum3, sum4; + + int count0 = histogram[0]; + histogram[0] = 0; //set to zero so erased areas aren't included + int countMax = histogram[maxValue]; + histogram[maxValue] = 0; + int min = 0; + while ((histogram[min]==0) && (min0)) + max--; + if (min>=max) { + histogram[0]= count0; histogram[maxValue]=countMax; + level = histogram.length/2; + return level; + } + + int movingIndex = min; + int inc = Math.max(max/40, 1); + do { + sum1=sum2=sum3=sum4=0.0; + for (int i=min; i<=movingIndex; i++) { + sum1 += (double)i*histogram[i]; + sum2 += histogram[i]; + } + for (int i=(movingIndex+1); i<=max; i++) { + sum3 += (double)i*histogram[i]; + sum4 += histogram[i]; + } + result = (sum1/sum2 + sum3/sum4)/2.0; + movingIndex++; + } while ((movingIndex+1)<=result && movingIndex=width) clipXMax = width-1; + if (clipYMin<0) clipYMin = 0; + if (clipYMax>=height) clipYMax = height-1; + } + } + + protected String maskSizeError(ImageProcessor mask) { + return "Mask size ("+mask.getWidth()+"x"+mask.getHeight()+") != ROI size ("+ + roiWidth+"x"+roiHeight+")"; + } + + protected SampleModel getIndexSampleModel() { + if (sampleModel==null) { + IndexColorModel icm = getDefaultColorModel(); + WritableRaster wr = icm.createCompatibleWritableRaster(1, 1); + sampleModel = wr.getSampleModel(); + sampleModel = sampleModel.createCompatibleSampleModel(width, height); + } + return sampleModel; + } + + /** Returns the default grayscale IndexColorModel. */ + public IndexColorModel getDefaultColorModel() { + if (defaultColorModel==null) { + byte[] r = new byte[256]; + byte[] g = new byte[256]; + byte[] b = new byte[256]; + for(int i=0; i<256; i++) { + r[i]=(byte)i; + g[i]=(byte)i; + b[i]=(byte)i; + } + defaultColorModel = new IndexColorModel(8, 256, r, g, b); + } + return defaultColorModel; + } + + /** The getPixelsCopy() method returns a reference to the + snapshot buffer if it is not null and 'snapshotCopyMode' is true. + @see ImageProcessor#getPixelsCopy + @see ImageProcessor#snapshot + */ + public void setSnapshotCopyMode(boolean b) { + snapshotCopyMode = b; + } + + /** Returns the number of color channels in the image. The color channels can be + * accessed by toFloat(channelNumber, fp) and written by setPixels(channelNumber, fp). + * @return 1 for grayscale images, 3 for RGB images + */ + public int getNChannels() { + return 1; /* superseded by ColorProcessor */ + } + + /** Returns a FloatProcessor with the image or one color channel thereof. + * The roi and mask are also set for the FloatProcessor. + * @param channelNumber Determines the color channel, 0=red, 1=green, 2=blue. Ignored for + * grayscale images. + * @param fp Here a FloatProcessor can be supplied, or null. The FloatProcessor + * is overwritten when converting data (re-using its pixels array + * improves performance). + * @return A FloatProcessor with the converted image data of the color channel selected + */ + public abstract FloatProcessor toFloat(int channelNumber, FloatProcessor fp); + + /** Sets the pixels (of one color channel for RGB images) from a FloatProcessor. + * @param channelNumber Determines the color channel, 0=red, 1=green, 2=blue.Ignored for + * grayscale images. + * @param fp The FloatProcessor where the image data are read from. + */ + public abstract void setPixels(int channelNumber, FloatProcessor fp); + + /** Returns the minimum possible pixel value. */ + public double minValue() { + return 0.0; + } + + /** Returns the maximum possible pixel value. */ + public double maxValue() { + return 255.0; + } + + /** CompositeImage calls this method to generate an updated color image. */ + public void updateComposite(int[] rgbPixels, int channel) { + int redValue, greenValue, blueValue; + int size = width*height; + if (bytes==null || !lutAnimation) + bytes = create8BitImage(); + if (cm==null) + makeDefaultColorModel(); + if (reds==null || cm!=cm2) + updateLutBytes(); + switch (channel) { + case 1: // update red channel + for (int i=0; i16711680) redValue = 16711680; + if (greenValue>65280) greenValue = 65280; + if (blueValue>255) blueValue = 255; + rgbPixels[i] = redValue | greenValue | blueValue; + } + break; + } + lutAnimation = false; + } + + // method and variables used by updateComposite() + byte[] create8BitImage() {return null;} + private byte[] bytes; + private int[] reds, greens, blues; + + void updateLutBytes() { + IndexColorModel icm = (IndexColorModel)cm; + int mapSize = icm.getMapSize(); + if (reds==null || reds.length!=mapSize) { + reds = new int[mapSize]; + greens = new int[mapSize]; + blues = new int[mapSize]; + } + byte[] tmp = new byte[mapSize]; + icm.getReds(tmp); + for (int i=0; ithreshold; + if (isBig) { + inc = h/30; + if (inc<1) inc=1; + } + return inc; + } + + public static void setRandomSeed(double randomSeed) { + seed = randomSeed; + } + + /** Returns a binary mask, or null if a threshold is not set or this is an RGB image. + * @see ij.ImagePlus#createThresholdMask + * @see ij.ImagePlus#createRoiMask + */ + public ByteProcessor createMask() { + return null; + } + + protected IndexColorModel getThresholdColorModel() { + byte[] r = new byte[256]; + byte[] g = new byte[256]; + byte[] b = new byte[256]; + for(int i=0; i<255; i++) { + r[i]=(byte)i; + g[i]=(byte)i; + b[i]=(byte)i; + } + r[255] = (byte)255; + g[255] = (byte)0; + b[255] = (byte)0; + return new IndexColorModel(8, 256, r, g, b); + } + +} diff --git a/src/ij/process/ImageStatistics.java b/src/ij/process/ImageStatistics.java new file mode 100644 index 0000000..d03db2d --- /dev/null +++ b/src/ij/process/ImageStatistics.java @@ -0,0 +1,329 @@ +package ij.process; +import ij.measure.*; +import java.awt.*; + +/** Statistics, including the histogram, of an image or selection. */ +public class ImageStatistics implements Measurements { + + /** Use the hIstogram() method to get the histogram as a double array. */ + public int[] histogram; + + /** Int pixel count (limited to 2^31-1) */ + public int pixelCount; + /** Long pixel count */ + public long longPixelCount; + + /** Mode */ + public double dmode; + /** Mode of 256 bin histogram (counts limited to 2^31-1) */ + public int mode; + + public double area; + public double min; + public double max; + public double mean; + public double median; + public double stdDev; + public double skewness; + public double kurtosis; + public double xCentroid; + public double yCentroid; + public double xCenterOfMass; + public double yCenterOfMass; + public double roiX, roiY, roiWidth, roiHeight; + /** Uncalibrated mean */ + public double umean; + /** Length of major axis of fitted ellipse */ + public double major; + /** Length of minor axis of fitted ellipse */ + public double minor; + /** Angle in degrees of fitted ellipse */ + public double angle; + /** Bin width 1 histogram of 16-bit images */ + public int[] histogram16; + /** Long histogram; use getHIstogram() to retrieve. */ + protected long[] longHistogram; + public double areaFraction; + /** Used internally by AnalyzeParticles */ + public int xstart, ystart; + /** Used by HistogramWindow */ + public boolean stackStatistics; + /** Minimum threshold when "Limit to threshold" enabled */ + public double lowerThreshold = Double.NaN; + /** Maximum threshold when "Limit to threshold" enabled */ + public double upperThreshold = Double.NaN; + public double histMin; + public double histMax; + public int histYMax; + public int maxCount; + public int nBins = 256; + public double binSize = 1.0; + + protected int width, height; + protected int rx, ry, rw, rh; + protected double pw, ph; + protected Calibration cal; + + EllipseFitter ef; + + + /** Calculates and returns uncalibrated (raw) statistics for the + * specified image, including histogram, area, mean, min and + * max, standard deviation and mode. Use ImageProcessor.setRoi(x,y,width,height) + * to limit statistics to a rectangular area and ImageProcessor.setRoi(Roi) + * to limit to a non-rectangular area. + * @see ij.process.ImageProcessor#setRoi(int,int,int,int) + * @see ij.process.ImageProcessor#setRoi(Roi) + * @see ij.process.ImageProcessor#getStats + */ + public static ImageStatistics getStatistics(ImageProcessor ip) { + return getStatistics(ip, AREA+MEAN+STD_DEV+MODE+MIN_MAX+RECT, null); + } + + /** Calculates and returns statistics for the specified + * image using the specified measurent options + * and calibration. Use ImageProcessor.setRoi(x,y,width,height) + * to limit statistics to a rectangular area and ImageProcessor.setRoi(Roi) + * to limit to a non-rectangular area. + * @see ij.process.ImageProcessor#setRoi(int,int,int,int) + * @see ij.process.ImageProcessor#setRoi(Roi) + * @see ij.measure.Measurements + */ + public static ImageStatistics getStatistics(ImageProcessor ip, int mOptions, Calibration cal) { + Object pixels = ip.getPixels(); + if (pixels instanceof byte[]) + return new ByteStatistics(ip, mOptions, cal); + else if (pixels instanceof short[]) + return new ShortStatistics(ip, mOptions, cal); + else if (pixels instanceof int[]) + return new ColorStatistics(ip, mOptions, cal); + else if (pixels instanceof float[]) + return new FloatStatistics(ip, mOptions, cal); + else + throw new IllegalArgumentException("Pixels are not byte, short, int or float"); + } + + void getRawMinAndMax(int minThreshold, int maxThreshold) { + int min = minThreshold; + while ((histogram[min] == 0) && (min < 255)) + min++; + this.min = min; + int max = maxThreshold; + while ((histogram[max] == 0) && (max > 0)) + max--; + this.max = max; + } + + void getRawStatistics(int minThreshold, int maxThreshold) { + int count; + double value; + double sum = 0.0; + double sum2 = 0.0; + + for (int i=minThreshold; i<=maxThreshold; i++) { + count = histogram[i]; + longPixelCount += count; + sum += (double)i*count; + value = i; + sum2 += (value*value)*count; + if (count>maxCount) { + maxCount = count; + mode = i; + } + } + pixelCount = (int)longPixelCount; + area = longPixelCount*pw*ph; + mean = sum/longPixelCount; + umean = mean; + dmode = mode; + calculateStdDev(longPixelCount, sum, sum2); + histMin = 0.0; + histMax = 255.0; + } + + void calculateStdDev(double n, double sum, double sum2) { + if (n>0.0) { + stdDev = (n*sum2-sum*sum)/n; + if (stdDev>0.0) + stdDev = Math.sqrt(stdDev/(n-1.0)); + else + stdDev = 0.0; + } else + stdDev = 0.0; + } + + void setup(ImageProcessor ip, Calibration cal) { + width = ip.getWidth(); + height = ip.getHeight(); + this.cal = cal; + Rectangle roi = ip.getRoi(); + if (roi != null) { + rx = roi.x; + ry = roi.y; + rw = roi.width; + rh = roi.height; + } + else { + rx = 0; + ry = 0; + rw = width; + rh = height; + } + + if (cal!=null) { + pw = cal.pixelWidth; + ph = cal.pixelHeight; + } else { + pw = 1.0; + ph = 1.0; + } + + roiX = cal!=null?cal.getX(rx):rx; + roiY = cal!=null?cal.getY(ry, height):ry; + roiWidth = rw*pw; + roiHeight = rh*ph; + } + + void getCentroid(ImageProcessor ip) { + byte[] mask = ip.getMaskArray(); + int count=0, mi; + double xsum=0.0, ysum=0.0; + for (int y=ry,my=0; y<(ry+rh); y++,my++) { + mi = my*rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null||mask[mi++]!=0) { + count++; + xsum += x; + ysum += y; + } + } + } + xCentroid = xsum/count+0.5; + yCentroid = ysum/count+0.5; + if (cal!=null) { + xCentroid = cal.getX(xCentroid); + yCentroid = cal.getY(yCentroid, height); + } + } + + void fitEllipse(ImageProcessor ip, int mOptions) { + ImageProcessor originalMask = null; + boolean limitToThreshold = (mOptions&LIMIT)!=0 && ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD; + if (limitToThreshold) { + ImageProcessor mask = ip.getMask(); + Rectangle r = ip.getRoi(); + if (mask==null) { + mask = new ByteProcessor(r.width, r.height); + mask.invert(); + } else { + originalMask = mask; + mask = mask.duplicate(); + } + int n = r.width*r.height; + double t1 = ip.getMinThreshold(); + double t2 = ip.getMaxThreshold(); + double value; + for (int y=0; yt2) + mask.setf(x, y, 0f); + } + } + ip.setMask(mask); + } + if (ef==null) + ef = new EllipseFitter(); + ef.fit(ip, this); + if (limitToThreshold) { + if (originalMask==null) + ip.setMask(null); + else + ip.setMask(originalMask); + } + double psize = (Math.abs(pw-ph)/pw)<.01?pw:0.0; + major = ef.major*psize; + minor = ef.minor*psize; + angle = ef.angle; + xCentroid = ef.xCenter; + yCentroid = ef.yCenter; + if (cal!=null) { + xCentroid = cal.getX(xCentroid); + yCentroid = cal.getY(yCentroid, height); + } + } + + public void drawEllipse(ImageProcessor ip) { + if (ef!=null) + ef.drawEllipse(ip); + } + + void calculateMedian(int[] hist, int first, int last, Calibration cal) { + //ij.IJ.log("calculateMedian: "+first+" "+last+" "+hist.length+" "+pixelCount); + if (pixelCount==0 || first<0 || last>hist.length) { + median = Double.NaN; + return; + } + double sum = 0; + int i = first-1; + double halfCount = pixelCount/2.0; + do { + sum += hist[++i]; + } while (sum<=halfCount && i=t1 && i<=t2) + sum += hist[i]; + total += hist[i]; + } + } + areaFraction = sum*100.0/total; + } + + /** Returns the histogram as an array of doubles. */ + public double[] histogram() { + double[] hist = new double[histogram.length]; + for (int i=0; i=t1 && value<=t2) + pixels8[i] = (byte)255; + else + pixels8[i] = (byte)0; + } + } else { // threshold red + for (int i=0; i=t1 && value<=t2) + pixels8[i] = (byte)255; + } + } + } + return createBufferedImage(); + } + + // creates 8-bit image by linearly scaling from float to 8-bits + private byte[] create8BitImage(boolean thresholding) { + int size = width*height; + if (pixels8==null) + pixels8 = new byte[size]; + double value; + int ivalue; + double min2 = getMin(); + double max2 = getMax(); + double scale = 255.0/(max2-min2); + int maxValue = thresholding?254:255; + for (int i=0; imaxValue) ivalue = maxValue; + pixels8[i] = (byte)ivalue; + } + return pixels8; + } + + @Override + byte[] create8BitImage() { + return create8BitImage(false); + } + + Image createBufferedImage() { + if (raster==null) { + SampleModel sm = getIndexSampleModel(); + DataBuffer db = new DataBufferByte(pixels8, width*height, 0); + raster = Raster.createWritableRaster(sm, db, null); + } + if (image==null || cm!=cm2) { + if (cm==null) cm = getDefaultColorModel(); + image = new BufferedImage(cm, raster, false, null); + cm2 = cm; + } + lutAnimation = false; + return image; + } + + /** Returns this image as an 8-bit BufferedImage . */ + public BufferedImage getBufferedImage() { + return convertToByte(true).getBufferedImage(); + } + + @Override + public void setColorModel(ColorModel cm) { + if (cm!=null && !(cm instanceof IndexColorModel)) + throw new IllegalArgumentException("IndexColorModel required"); + if (cm!=null && cm instanceof LUT) + cm = ((LUT)cm).getColorModel(); + this.cm = cm; + baseCM = null; + rLUT1 = rLUT2 = null; + inversionTested = false; + minThreshold = NO_THRESHOLD; + } + + @Override + public float getPixelValue(int x, int y) { + if (x>=0 && x=0 && ymax) + max = value; + } + this.min = min; + this.max = max; + minMaxSet = true; + } + + @Override + public void resetMinAndMax() { + findMinAndMax(); + resetThreshold(); + } + + @Override + public void setMinAndMax(double minimum, double maximum, int channels) { + min = (int)minimum; + max = (int)maximum; + minMaxSet = true; + resetThreshold(); + } + +} + + diff --git a/src/ij/process/LUT.java b/src/ij/process/LUT.java new file mode 100644 index 0000000..179aa5f --- /dev/null +++ b/src/ij/process/LUT.java @@ -0,0 +1,111 @@ +package ij.process; +import ij.IJ; +import ij.plugin.Colors; +import java.awt.image.*; +import java.awt.Color; + + /** This is an indexed color model that allows an + lower and upper bound to be specified. */ + public class LUT extends IndexColorModel implements Cloneable { + public static final String nameKey = "CurrentLUT"; + public double min, max; + private IndexColorModel cm; + + /** Constructs a LUT from red, green and blue byte arrays, which must have a length of 256. */ + public LUT(byte r[], byte g[], byte b[]) { + this(8, 256, r, g, b); + } + + /** Constructs a LUT from red, green and blue byte arrays, where 'bits' + must be 8 and 'size' must be less than or equal to 256. */ + public LUT(int bits, int size, byte r[], byte g[], byte b[]) { + super(bits, size, r, g, b); + } + + public LUT(IndexColorModel cm, double min, double max) { + super(8, cm.getMapSize(), getReds(cm), getGreens(cm), getBlues(cm)); + this.min = min; + this.max = max; + } + + static byte[] getReds(IndexColorModel cm) { + byte[] reds=new byte[256]; cm.getReds(reds); return reds; + } + + static byte[] getGreens(IndexColorModel cm) { + byte[] greens=new byte[256]; cm.getGreens(greens); return greens; + } + + static byte[] getBlues(IndexColorModel cm) { + byte[] blues=new byte[256]; cm.getBlues(blues); return blues; + } + + public IndexColorModel getColorModel() { + if (cm==null) { + byte[] reds=new byte[256]; getReds(reds); + byte[] greens=new byte[256]; getGreens(greens); + byte[] blues=new byte[256]; getBlues(blues); + cm = new IndexColorModel(8, getMapSize(), reds, greens, blues); + } + return cm; + } + + public byte[] getBytes() { + int size = getMapSize(); + if (size!=256) return null; + byte[] bytes = new byte[256*3]; + for (int i=0; i<256; i++) bytes[i] = (byte)getRed(i); + for (int i=0; i<256; i++) bytes[256+i] = (byte)getGreen(i); + for (int i=0; i<256; i++) bytes[512+i] = (byte)getBlue(i); + return bytes; + } + + public LUT createInvertedLut() { + int mapSize = getMapSize(); + byte[] reds = new byte[mapSize]; + byte[] greens = new byte[mapSize]; + byte[] blues = new byte[mapSize]; + byte[] reds2 = new byte[mapSize]; + byte[] greens2 = new byte[mapSize]; + byte[] blues2 = new byte[mapSize]; + getReds(reds); + getGreens(greens); + getBlues(blues); + for (int i=0; i0) count++; + return count; + } + + + Color getModalColor() { + int max=0; + int c = 0; + for (int i=0; imax) { + max = hist[i]; + c = i; + } + return new Color(red(c), green(c), blue(c)); + } + + + // Convert from 24-bit to 15-bit color + private final int rgb(int c) { + int r = (c&0xf80000)>>19; + int g = (c&0xf800)>>6; + int b = (c&0xf8)<<7; + return b | g | r; + } + + // Get red component of a 15-bit color + private final int red(int x) { + return (x&31)<<3; + } + + // Get green component of a 15-bit color + private final int green(int x) { + return (x>>2)&0xf8; + } + + // Get blue component of a 15-bit color + private final int blue(int x) { + return (x>>7)&0xf8; + } + + + /** Uses Heckbert's median-cut algorithm to divide the color space defined by + "hist" into "maxcubes" cubes. The centroids (average value) of each cube + are are used to create a color table. "hist" is then updated to function + as an inverse color map that is used to generate an 8-bit image. */ + public Image convert(int maxcubes) { + ImageProcessor ip = convertToByte(maxcubes); + return ip.createImage(); + } + + /** This is a version of convert that returns a ByteProcessor. */ + public ImageProcessor convertToByte(int maxcubes) { + int lr, lg, lb; + int i, median, color; + int count; + int k, level, ncubes, splitpos; + int num, width; + int longdim=0; //longest dimension of cube + Cube cube, cubeA, cubeB; + + // Create initial cube + IJ.showStatus("Median cut"); + list = new Cube[MAXCOLORS]; + histPtr = new int[HSIZE]; + ncubes = 0; + cube = new Cube(); + for (i=0,color=0; i<=HSIZE-1; i++) { + if (hist[i] != 0) { + histPtr[color++] = i; + cube.count = cube.count + hist[i]; + } + } + cube.lower = 0; cube.upper = color-1; + cube.level = 0; + Shrink(cube); + list[ncubes++] = cube; + + //Main loop + while (ncubes < maxcubes) { + + // Search the list of cubes for next cube to split, the lowest level cube + level = 255; splitpos = -1; + for (k=0; k<=ncubes-1; k++) { + if (list[k].lower == list[k].upper) + ; // single color; cannot be split + else if (list[k].level < level) { + level = list[k].level; + splitpos = k; + } + } + if (splitpos == -1) // no more cubes to split + break; + + // Find longest dimension of this cube + cube = list[splitpos]; + lr = cube.rmax - cube.rmin; + lg = cube.gmax - cube.gmin; + lb = cube.bmax - cube.bmin; + if (lr >= lg && lr >= lb) longdim = 0; + if (lg >= lr && lg >= lb) longdim = 1; + if (lb >= lr && lb >= lg) longdim = 2; + + // Sort along "longdim" + reorderColors(histPtr, cube.lower, cube.upper, longdim); + quickSort(histPtr, cube.lower, cube.upper); + restoreColorOrder(histPtr, cube.lower, cube.upper, longdim); + + // Find median + count = 0; + for (i=cube.lower;i<=cube.upper-1;i++) { + if (count >= cube.count/2) break; + color = histPtr[i]; + count = count + hist[color]; + } + median = i; + + // Now split "cube" at the median and add the two new + // cubes to the list of cubes. + cubeA = new Cube(); + cubeA.lower = cube.lower; + cubeA.upper = median-1; + cubeA.count = count; + cubeA.level = cube.level + 1; + Shrink(cubeA); + list[splitpos] = cubeA; // add in old slot + + cubeB = new Cube(); + cubeB.lower = median; + cubeB.upper = cube.upper; + cubeB.count = cube.count - count; + cubeB.level = cube.level + 1; + Shrink(cubeB); + list[ncubes++] = cubeB; // add in new slot */ + if (ncubes%15==0) + IJ.showProgress(0.3 + (0.6*ncubes)/maxcubes); + } + + // We have enough cubes, or we have split all we can. Now + // compute the color map, the inverse color map, and return + // an 8-bit image. + IJ.showProgress(0.9); + makeInverseMap(hist, ncubes); + IJ.showProgress(0.95); + return makeImage(); + } + + void Shrink(Cube cube) { + // Encloses "cube" with a tight-fitting cube by updating the + // (rmin,gmin,bmin) and (rmax,gmax,bmax) members of "cube". + + int r, g, b; + int color; + int rmin, rmax, gmin, gmax, bmin, bmax; + + rmin = 255; rmax = 0; + gmin = 255; gmax = 0; + bmin = 255; bmax = 0; + for (int i=cube.lower; i<=cube.upper; i++) { + color = histPtr[i]; + r = red(color); + g = green(color); + b = blue(color); + if (r > rmax) rmax = r; + if (r < rmin) rmin = r; + if (g > gmax) gmax = g; + if (g < gmin) gmin = g; + if (b > bmax) bmax = b; + if (b < bmin) bmin = b; + } + cube.rmin = rmin; cube.rmax = rmax; + cube.gmin = gmin; cube.gmax = gmax; + cube.bmin = bmin; cube.bmax = bmax; + } + + + void makeInverseMap(int[] hist, int ncubes) { + // For each cube in the list of cubes, computes the centroid + // (average value) of the colors enclosed by that cube, and + // then loads the centroids in the color map. Next loads + // "hist" with indices into the color map + + int r, g, b; + int color; + float rsum, gsum, bsum; + Cube cube; + byte[] rLUT = new byte[256]; + byte[] gLUT = new byte[256]; + byte[] bLUT = new byte[256]; + + IJ.showStatus("Making inverse map"); + for (int k=0; k<=ncubes-1; k++) { + cube = list[k]; + rsum = gsum = bsum = (float)0.0; + for (int i=cube.lower; i<=cube.upper; i++) { + color = histPtr[i]; + r = red(color); + rsum += (float)r*(float)hist[color]; + g = green(color); + gsum += (float)g*(float)hist[color]; + b = blue(color); + bsum += (float)b*(float)hist[color]; + } + + // Update the color map + r = (int)(rsum/(float)cube.count); + g = (int)(gsum/(float)cube.count); + b = (int)(bsum/(float)cube.count); + if (r==248 && g==248 && b==248) + r=g=b=255; // Restore white (255,255,255) + rLUT[k] = (byte)r; + gLUT[k] = (byte)g; + bLUT[k] = (byte)b; + } + cm = new IndexColorModel(8, ncubes, rLUT, gLUT, bLUT); + + // For each color in each cube, load the corre- + // sponding slot in "hist" with the centroid of the cube. + for (int k=0; k<=ncubes-1; k++) { + cube = list[k]; + for (int i=cube.lower; i<=cube.upper; i++) { + color = histPtr[i]; + hist[color] = k; + } + } + } + + + void reorderColors(int[] a, int lo, int hi, int longDim) { + // Change the ordering of the 5-bit colors in each word of int[] + // so we can sort on the 'longDim' color + + int c, r, g, b; + switch (longDim) { + case 0: //red + for (int i=lo; i<=hi; i++) { + c = a[i]; + r = c & 31; + a[i] = (r<<10) | (c>>5); + } + break; + case 1: //green + for (int i=lo; i<=hi; i++) { + c = a[i]; + r = c & 31; + g = (c>>5) & 31; + b = c>>10; + a[i] = (g<<10) | (b<<5) | r; + } + break; + case 2: //blue; already in the needed order + break; + } + } + + + void restoreColorOrder(int[] a, int lo, int hi, int longDim) { + // Restore the 5-bit colors to the original order + + int c, r, g, b; + switch (longDim){ + case 0: //red + for (int i=lo; i<=hi; i++) { + c = a[i]; + r = c >> 10; + a[i] = ((c&1023)<<5) | r; + } + break; + case 1: //green + for (int i=lo; i<=hi; i++) { + c = a[i]; + r = c & 31; + g = c>>10; + b = (c>>5) & 31; + a[i] = (b<<10) | (g<<5) | r; + } + break; + case 2: //blue + break; + } + } + + + void quickSort(int a[], int lo0, int hi0) { + // Based on the QuickSort method by James Gosling from Sun's SortDemo applet + + int lo = lo0; + int hi = hi0; + int mid, t; + + if ( hi0 > lo0) { + mid = a[ ( lo0 + hi0 ) / 2 ]; + while( lo <= hi ) { + while( ( lo < hi0 ) && ( a[lo] < mid ) ) + ++lo; + while( ( hi > lo0 ) && ( a[hi] > mid ) ) + --hi; + if( lo <= hi ) { + t = a[lo]; + a[lo] = a[hi]; + a[hi] = t; + ++lo; + --hi; + } + } + if( lo0 < hi ) + quickSort( a, lo0, hi ); + if( lo < hi0 ) + quickSort( a, lo, hi0 ); + + } + } + + + ImageProcessor makeImage() { + // Generate 8-bit image + + Image img8; + byte[] pixels8; + int color16; + + IJ.showStatus("Creating 8-bit image"); + pixels8 = new byte[width*height]; + for (int i=0; iex.length) { + ex = new double[n]; + ey1 = new int[n]; + ey2 = new int[n]; + sedge = new int[n]; + aedge = new int[n]; + eslope = new double[n]; + } + } + + /** Generates the edge table for all non-horizontal lines: + * ey1, ey2: min & max y value + * eslope: inverse slope dx/dy + * ex: x value at ey1, corrected for half-pixel shift between outline&pixel coordinates + * sedge: list of sorted edges is prepared (not sorted yet) */ + void buildEdgeTable() { + yMin = Integer.MAX_VALUE; + yMax = Integer.MIN_VALUE; + edges = 0; + int polyStart = 0; //index where the polygon has started (i.e., 0 unless we have multiple ploygons separated by NaN) + for (int i=0; iy2) { //swap ends to ensure y1 yMax) yMax = y2; + } else { //using float arrays + if (Float.isNaN(xf[iplus1])) //after the last point, close the polygon + iplus1 = polyStart; + if (Float.isNaN(xf[i])) { //when a new polygon follows, remember the start point for closing it + polyStart = i + 1; + continue; + } + double y1f = yf[i] + yOffset; double y2f = yf[iplus1] + yOffset; + double x1f = xf[i] + xOffset; double x2f = xf[iplus1] + xOffset; + int y1 = (int)Math.round(y1f); + int y2 = (int)Math.round(y2f); + //IJ.log("x, y="+xf[i]+","+yf[i]+"+ offs="+xOffset+","+yOffset+"->"+x1f+","+y1f+" int="+y1); + if (y1==y2 || (y1<=0 && y2<=0)) + continue; //ignore horizontal lines or lines that don't reach the first row of pixels + if (y1>y2) { //swap ends to ensure y1 yMax) yMax = y2; + } + edges++; + } + for (int i=0; i0 ? yMin : 0; + if (yMin != 0) + shiftXValuesAndActivate(yStart); + //IJ.log("yMin="+yMin+" yStart="+yStart+" nActive="+activeEdges); + for (int y=yStart; ywidth) x1 = width; + x2 = (int)(ex[aedge[i+1]]+0.5); + if (x2<0) x2=0; + if (x2>width) x2 = width; + for (int x=x1; x= yStart) { + ex[index] += eslope[index] * (yStart - ey1[index]); + aedge[activeEdges++] = index; + } + } + sortActiveEdges(); + } + + /** Updates the x coordinates in the active edges list and sorts the list if necessary. */ + void updateXCoordinates() { + int index; + double x1=-Double.MAX_VALUE, x2; + boolean sorted = true; + for (int i=0; i=ey2[index]) { + for (int j=i; jex[aedge[index]]) + index++; + for (int j=activeEdges-1; j>=index; j--) + aedge[j+1] = aedge[j]; + aedge[index] = edge; + activeEdges++; + } + } + } + + /** Display the contents of the edge table*/ + void printEdges() { + for (int i=0; i=0;) + pixels[dstIndex++] = srcPixels[srcIndex++]; + break; + case COPY_ZERO_TRANSPARENT: + for (int i=r1.width; --i>=0;) { + src = srcPixels[srcIndex++]&0xffff; + if (src==0) + dst = pixels[dstIndex]; + else + dst = src; + pixels[dstIndex++] = (short)dst; + } + break; + case ADD: + for (int i=r1.width; --i>=0;) { + dst = (srcPixels[srcIndex++]&0xffff)+(pixels[dstIndex]&0xffff); + if (dst<0) dst = 0; + if (dst>65535) dst = 65535; + pixels[dstIndex++] = (short)dst; + } + break; + case AVERAGE: + for (int i=r1.width; --i>=0;) { + dst = ((srcPixels[srcIndex++]&0xffff)+(pixels[dstIndex]&0xffff))/2; + pixels[dstIndex++] = (short)dst; + } + break; + case DIFFERENCE: + for (int i=r1.width; --i>=0;) { + dst = (pixels[dstIndex]&0xffff)-(srcPixels[srcIndex++]&0xffff); + if (dst<0) dst = -dst; + if (dst>65535) dst = 65535; + pixels[dstIndex++] = (short)dst; + } + break; + case SUBTRACT: + for (int i=r1.width; --i>=0;) { + dst = (pixels[dstIndex]&0xffff)-(srcPixels[srcIndex++]&0xffff); + if (dst<0) dst = 0; + if (dst>65535) dst = 65535; + pixels[dstIndex++] = (short)dst; + } + break; + case MULTIPLY: + for (int i=r1.width; --i>=0;) { + dst = (srcPixels[srcIndex++]&0xffff)*(pixels[dstIndex]&0xffff); + if (dst<0) dst = 0; + if (dst>65535) dst = 65535; + pixels[dstIndex++] = (short)dst; + } + break; + case DIVIDE: + for (int i=r1.width; --i>=0;) { + src = srcPixels[srcIndex++]&0xffff; + if (src==0) + dst = 65535; + else + dst = (pixels[dstIndex]&0xffff)/src; + pixels[dstIndex++] = (short)dst; + } + break; + case AND: + for (int i=r1.width; --i>=0;) { + dst = srcPixels[srcIndex++]&pixels[dstIndex]&0xffff; + pixels[dstIndex++] = (short)dst; + } + break; + case OR: + for (int i=r1.width; --i>=0;) { + dst = srcPixels[srcIndex++]|pixels[dstIndex]; + pixels[dstIndex++] = (short)dst; + } + break; + case XOR: + for (int i=r1.width; --i>=0;) { + dst = srcPixels[srcIndex++]^pixels[dstIndex]; + pixels[dstIndex++] = (short)dst; + } + break; + case MIN: + for (int i=r1.width; --i>=0;) { + src = srcPixels[srcIndex++]&0xffff; + dst = pixels[dstIndex]&0xffff; + if (src=0;) { + src = srcPixels[srcIndex++]&0xffff; + dst = pixels[dstIndex]&0xffff; + if (src>dst) dst = src; + pixels[dstIndex++] = (short)dst; + } + break; + } + } + } +} diff --git a/src/ij/process/ShortProcessor.java b/src/ij/process/ShortProcessor.java new file mode 100644 index 0000000..449d2f8 --- /dev/null +++ b/src/ij/process/ShortProcessor.java @@ -0,0 +1,1270 @@ + package ij.process; + +import java.util.*; +import java.awt.*; +import java.awt.image.*; +import ij.gui.*; + +/** ShortProcessors contain a 16-bit unsigned image + and methods that operate on that image. */ +public class ShortProcessor extends ImageProcessor { + + private int min, max, snapshotMin, snapshotMax; + private short[] pixels; + private byte[] pixels8; + private short[] snapshotPixels; + private byte[] LUT; + private boolean fixedScale; + private int bgValue; + + + /** Creates a new ShortProcessor using the specified pixel array and ColorModel. + Set 'cm' to null to use the default grayscale LUT. */ + public ShortProcessor(int width, int height, short[] pixels, ColorModel cm) { + if (pixels!=null && width*height!=pixels.length) + throw new IllegalArgumentException(WRONG_LENGTH); + init(width, height, pixels, cm); + } + + /** Creates a blank ShortProcessor using the default grayscale LUT that + displays zero as black. Call invertLut() to display zero as white. */ + public ShortProcessor(int width, int height) { + this(width, height, new short[width*height], null); + } + + /** Creates a ShortProcessor from a TYPE_USHORT_GRAY BufferedImage. */ + public ShortProcessor(BufferedImage bi) { + if (bi.getType()!=BufferedImage.TYPE_USHORT_GRAY) + throw new IllegalArgumentException("Type!=TYPE_USHORT_GRAY"); + WritableRaster raster = bi.getRaster(); + DataBuffer buffer = raster.getDataBuffer(); + short[] data = ((DataBufferUShort) buffer).getData(); + //short[] data2 = new short[data.length]; + //System.arraycopy(data, 0, data2, 0, data.length); + init(raster.getWidth(), raster.getHeight(), data, null); + } + + void init(int width, int height, short[] pixels, ColorModel cm) { + this.width = width; + this.height = height; + this.pixels = pixels; + this.cm = cm; + resetRoi(); + } + + /** + * @deprecated + * 16 bit images are normally unsigned but signed images can be simulated by + * subtracting 32768 and using a calibration function to restore the original values. + */ + public ShortProcessor(int width, int height, short[] pixels, ColorModel cm, boolean unsigned) { + this(width, height, pixels, cm); + } + + /** Obsolete. 16 bit images are normally unsigned but signed images can be used by + subtracting 32768 and using a calibration function to restore the original values. */ + public ShortProcessor(int width, int height, boolean unsigned) { + this(width, height); + } + + public void findMinAndMax() { + if (fixedScale || pixels==null) + return; + int size = width*height; + int value; + int min = pixels[0]&0xffff; + int max = pixels[0]&0xffff; + for (int i=1; imax) + max = value; + } + this.min = min; + this.max = max; + minMaxSet = true; + } + + /** Create an 8-bit AWT image by scaling pixels in the range min-max to 0-255. */ + public Image createImage() { + if (!minMaxSet) + findMinAndMax(); + boolean firstTime = pixels8==null; + boolean thresholding = minThreshold!=NO_THRESHOLD && lutUpdateMode=t1 && value<=t2) + pixels8[i] = (byte)255; + else + pixels8[i] = (byte)0; + } + } else { // threshold red + for (int i=0; i=t1 && value<=t2) + pixels8[i] = (byte)255; + } + } + } + return createBufferedImage(); + } + + // create 8-bit image by linearly scaling from 16-bits to 8-bits + private byte[] create8BitImage(boolean thresholding) { + int size = width*height; + if (pixels8==null) + pixels8 = new byte[size]; + int value; + int min2=(int)getMin(), max2=(int)getMax(); + int maxValue = 255; + double scale = 256.0/(max2-min2+1); + if (thresholding) { + maxValue = 254; + scale = 255.0/(max2-min2+1); + } + for (int i=0; imaxValue) value = maxValue; + pixels8[i] = (byte)value; + } + return pixels8; + } + + @Override + byte[] create8BitImage() { + return create8BitImage(false); + } + + Image createBufferedImage() { + if (raster==null) { + SampleModel sm = getIndexSampleModel(); + DataBuffer db = new DataBufferByte(pixels8, width*height, 0); + raster = Raster.createWritableRaster(sm, db, null); + } + if (image==null || cm!=cm2) { + if (cm==null) cm = getDefaultColorModel(); + image = new BufferedImage(cm, raster, false, null); + cm2 = cm; + } + lutAnimation = false; + return image; + } + + /** Returns this image as an 8-bit BufferedImage . */ + public BufferedImage getBufferedImage() { + return convertToByte(true).getBufferedImage(); + } + + /** Returns a copy of this image as a TYPE_USHORT_GRAY BufferedImage. */ + public BufferedImage get16BitBufferedImage() { + BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_GRAY); + Raster raster = bi.getData(); + DataBufferUShort db = (DataBufferUShort)raster.getDataBuffer(); + System.arraycopy(getPixels(), 0, db.getData(), 0, db.getData().length); + bi.setData(raster); + return bi; + } + + /** Returns a new, blank ShortProcessor with the specified width and height. */ + public ImageProcessor createProcessor(int width, int height) { + ImageProcessor ip2 = new ShortProcessor(width, height, new short[width*height], getColorModel()); + ip2.setMinAndMax(getMin(), getMax()); + ip2.setInterpolationMethod(interpolationMethod); + return ip2; + } + + public void snapshot() { + snapshotWidth=width; + snapshotHeight=height; + snapshotMin=(int)getMin(); + snapshotMax=(int)getMax(); + if (snapshotPixels==null || (snapshotPixels!=null && snapshotPixels.length!=pixels.length)) + snapshotPixels = new short[width * height]; + System.arraycopy(pixels, 0, snapshotPixels, 0, width*height); + } + + public void reset() { + if (snapshotPixels==null) + return; + min=snapshotMin; + max=snapshotMax; + minMaxSet = true; + System.arraycopy(snapshotPixels, 0, pixels, 0, width*height); + } + + public void reset(ImageProcessor mask) { + if (mask==null || snapshotPixels==null) + return; + if (mask.getWidth()!=roiWidth||mask.getHeight()!=roiHeight) + throw new IllegalArgumentException(maskSizeError(mask)); + byte[] mpixels = (byte[])mask.getPixels(); + for (int y=roiY, my=0; y<(roiY+roiHeight); y++, my++) { + int i = y * width + roiX; + int mi = my * roiWidth; + for (int x=roiX; x<(roiX+roiWidth); x++) { + if (mpixels[mi++]==0) + pixels[i] = snapshotPixels[i]; + i++; + } + } + } + + /** Swaps the pixel and snapshot (undo) arrays. */ + public void swapPixelArrays() { + if (snapshotPixels==null) return; + short pixel; + for (int i=0; i65535.0) + maximum = 65535.0; + min = (int)minimum; + max = (int)maximum; + fixedScale = true; + minMaxSet = true; + resetThreshold(); + } + + /** Recalculates the min and max values used to scale pixel + values to 0-255 for display. This ensures that this + ShortProcessor is set up to correctly display the image. */ + public void resetMinAndMax() { + fixedScale = false; + findMinAndMax(); + resetThreshold(); + } + + public int getPixel(int x, int y) { + if (x>=0 && x=0 && y=width-1.0) x = width-1.001; + if (y<0.0) y = 0.0; + if (y>=height-1.0) y = height-1.001; + return getInterpolatedPixel(x, y, pixels); + } + } + + final public int getPixelInterpolated(double x, double y) { + if (interpolationMethod==BILINEAR) { + if (x<0.0 || y<0.0 || x>=width-1 || y>=height-1) + return 0; + else + return (int)Math.round(getInterpolatedPixel(x, y, pixels)); + } else if (interpolationMethod==BICUBIC) { + int value = (int)(getBicubicInterpolatedPixel(x, y, this)+0.5); + if (value<0) value = 0; + if (value>65535) value = 65535; + return value; + } else + return getPixel((int)(x+0.5), (int)(y+0.5)); + } + + /** Stores the specified value at (x,y). Does + nothing if (x,y) is outside the image boundary. + Values outside the range 0-65535 are clipped. + */ + public final void putPixel(int x, int y, int value) { + if (x>=0 && x=0 && y65535) value = 65535; + if (value<0) value = 0; + pixels[y*width + x] = (short)value; + } + } + + /** Stores the specified real value at (x,y). Does nothing + if (x,y) is outside the image boundary. Values outside + the range 0-65535 (-32768-32767 for signed images) + are clipped. Support for signed values requires a calibration + table, which is set up automatically with PlugInFilters. + */ + public void putPixelValue(int x, int y, double value) { + if (x>=0 && x=0 && y65535.0) + value = 65535.0; + else if (value<0.0) + value = 0.0; + pixels[y*width + x] = (short)(value+0.5); + } + } + + /** Draws a pixel in the current foreground color. */ + public void drawPixel(int x, int y) { + if (x>=clipXMin && x<=clipXMax && y>=clipYMin && y<=clipYMax) + putPixel(x, y, fgColor); + } + + /** Returns the value of the pixel at (x,y) as a float. For signed + images, returns a signed value if a calibration table has + been set using setCalibrationTable() (this is done automatically + in PlugInFilters). */ + public float getPixelValue(int x, int y) { + if (x>=0 && x=0 && y=lineStart;) { + v = lut[pixels[i]&0xffff]; + pixels[i] = (short)v; + } + } + findMinAndMax(); + } + + private void process(int op, double value) { + int v1, v2; + double range = getMax()-getMin(); + //boolean resetMinMax = roiWidth==width && roiHeight==height && !(op==FILL); + int offset = isSigned16Bit()?32768:0; + int min2 = (int)getMin() - offset; + int max2 = (int)getMax() - offset; + int fgColor2 = fgColor - offset; + int intValue = (int)value; + + for (int y=roiY; y<(roiY+roiHeight); y++) { + int i = y * width + roiX; + for (int x=roiX; x<(roiX+roiWidth); x++) { + v1 = (pixels[i]&0xffff) - offset; + switch(op) { + case INVERT: + v2 = max2 - (v1 - min2); + //v2 = 65535 - (v1+offset); + break; + case FILL: + v2 = fgColor2; + break; + case SET: + v2 = intValue; + break; + case ADD: + v2 = v1 + intValue; + break; + case MULT: + v2 = (int)Math.round(v1*value); + break; + case AND: + v2 = v1 & intValue; + break; + case OR: + v2 = v1 | intValue; + break; + case XOR: + v2 = v1 ^ intValue; + break; + case GAMMA: + if (range<=0.0 || v1==min2) + v2 = v1; + else + v2 = (int)(Math.exp(value*Math.log((v1-min2)/range))*range+min2); + break; + case LOG: + if (v1<=0) + v2 = 0; + else + v2 = (int)(Math.log(v1)*(max2/Math.log(max2))); + break; + case EXP: + v2 = (int)(Math.exp(v1*(Math.log(max2)/max2))); + break; + case SQR: + double d1 = v1; + v2 = (int)(d1*d1); + break; + case SQRT: + v2 = (int)Math.sqrt(v1); + break; + case ABS: + v2 = (int)Math.abs(v1); + break; + case MINIMUM: + if (v1value) + v2 = intValue; + else + v2 = v1; + break; + default: + v2 = v1; + } + v2 += offset; + if (v2 < 0) + v2 = 0; + if (v2 > 65535) + v2 = 65535; + pixels[i++] = (short)v2; + } + } + } + + public void invert() { + int range = 65536; + int defaultRange = ij.ImagePlus.getDefault16bitRange(); + if (defaultRange>0 && !isSigned16Bit()) + range = (int)Math.pow(2,defaultRange); + setMinAndMax(0, range-1); + process(INVERT, 0.0); + resetMinAndMax(); + } + + public void add(int value) {process(ADD, value);} + public void add(double value) {process(ADD, value);} + public void set(double value) {process(SET, value);} + public void multiply(double value) {process(MULT, value);} + public void and(int value) {process(AND, value);} + public void or(int value) {process(OR, value);} + public void xor(int value) {process(XOR, value);} + public void gamma(double value) {process(GAMMA, value);} + public void log() {process(LOG, 0.0);} + public void exp() {process(EXP, 0.0);} + public void sqr() {process(SQR, 0.0);} + public void sqrt() {process(SQRT, 0.0);} + public void abs() {process(ABS, 0.0);} + public void min(double value) {process(MINIMUM, value);} + public void max(double value) {process(MAXIMUM, value);} + + /** Fills the current rectangular ROI. */ + public void fill() { + process(FILL, 0.0); + } + + /** Fills pixels that are within roi and part of the mask. + Does nothing if the mask is not the same as the ROI. */ + public void fill(ImageProcessor mask) { + if (mask==null) + {fill(); return;} + int roiWidth=this.roiWidth, roiHeight=this.roiHeight; + int roiX=this.roiX, roiY=this.roiY; + if (mask.getWidth()!=roiWidth||mask.getHeight()!=roiHeight) + return; + byte[] mpixels = (byte[])mask.getPixels(); + for (int y=roiY, my=0; y<(roiY+roiHeight); y++, my++) { + int i = y * width + roiX; + int mi = my * roiWidth; + for (int x=roiX; x<(roiX+roiWidth); x++) { + if (mpixels[mi++]!=0) + pixels[i] = (short)fgColor; + i++; + } + } + } + + /** Does 3x3 convolution. */ + public void convolve3x3(int[] kernel) { + filter3x3(CONVOLVE, kernel); + } + + /** Filters using a 3x3 neighborhood. */ + public void filter(int type) { + filter3x3(type, null); + } + + /** 3x3 filter operations, code partly based on 3x3 convolution code + * contributed by Glynne Casteel. */ + void filter3x3(int type, int[] kernel) { + int v1, v2, v3; //input pixel values around the current pixel + int v4, v5, v6; + int v7, v8, v9; + int k1=0, k2=0, k3=0; //kernel values (used for CONVOLVE only) + int k4=0, k5=0, k6=0; + int k7=0, k8=0, k9=0; + int scale = 0; + if (type==CONVOLVE) { + k1=kernel[0]; k2=kernel[1]; k3=kernel[2]; + k4=kernel[3]; k5=kernel[4]; k6=kernel[5]; + k7=kernel[6]; k8=kernel[7]; k9=kernel[8]; + for (int i=0; i0 ? 1 : 0); //will point to v6, currently lower + int p3 = p6 - (y>0 ? width : 0); //will point to v3, currently lower + int p9 = p6 + (y0) { p3++; p6++; p9++; } + v3 = pixels2[p3]&0xffff; + v6 = pixels2[p6]&0xffff; + v9 = pixels2[p9]&0xffff; + + switch (type) { + case BLUR_MORE: + for (int x=roiX; x65535.0) result = 65535.0; + pixels[p] = (short)result; + } + break; + case CONVOLVE: + for (int x=roiX; x65535) sum = 65535; + if(sum<0) sum = 0; + pixels[p] = (short)sum; + } + break; + } + } + } + + /** Rotates the image or ROI 'angle' degrees clockwise. + @see ImageProcessor#setInterpolate + */ + public void rotate(double angle) { + short[] pixels2 = (short[])getPixelsCopy(); + ImageProcessor ip2 = null; + if (interpolationMethod==BICUBIC) + ip2 = new ShortProcessor(getWidth(), getHeight(), pixels2, null); + double centerX = roiX + (roiWidth-1)/2.0; + double centerY = roiY + (roiHeight-1)/2.0; + int xMax = roiX + this.roiWidth - 1; + + double angleRadians = -angle/(180.0/Math.PI); + double ca = Math.cos(angleRadians); + double sa = Math.sin(angleRadians); + double tmp1 = centerY*sa-centerX*ca; + double tmp2 = -centerX*sa-centerY*ca; + double tmp3, tmp4, xs, ys; + int index, ixs, iys; + double dwidth=width,dheight=height; + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + // zero is 32768 for signed images + int background = isSigned16Bit()?bgValue+32768:bgValue; + + if (interpolationMethod==BICUBIC) { + for (int y=roiY; y<(roiY + roiHeight); y++) { + index = y*width + roiX; + tmp3 = tmp1 - y*sa + centerX; + tmp4 = tmp2 + y*ca + centerY; + for (int x=roiX; x<=xMax; x++) { + xs = x*ca + tmp3; + ys = x*sa + tmp4; + int value = (int)(getBicubicInterpolatedPixel(xs, ys, ip2)+0.5); + if (value<0) value = 0; + if (value>65535) value = 65535; + pixels[index++] = (short)value; + } + } + } else { + for (int y=roiY; y<(roiY + roiHeight); y++) { + index = y*width + roiX; + tmp3 = tmp1 - y*sa + centerX; + tmp4 = tmp2 + y*ca + centerY; + for (int x=roiX; x<=xMax; x++) { + xs = x*ca + tmp3; + ys = x*sa + tmp4; + if ((xs>=-0.01) && (xs=-0.01) && (ys=xlimit) xs = xlimit2; + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + pixels[index++] = (short)(getInterpolatedPixel(xs, ys, pixels2)+0.5); + } else { + ixs = (int)(xs+0.5); + iys = (int)(ys+0.5); + if (ixs>=width) ixs = width - 1; + if (iys>=height) iys = height -1; + pixels[index++] = pixels2[width*iys+ixs]; + } + } else + pixels[index++] = (short)background; + } + } + } + } + + public void flipVertical() { + int index1,index2; + short tmp; + for (int y=0; y1.0) && (yScale>1.0)) { + //expand roi + xmin = (int)(xCenter-(xCenter-roiX)*xScale); + if (xmin<0) xmin = 0; + xmax = xmin + (int)(roiWidth*xScale) - 1; + if (xmax>=width) xmax = width - 1; + ymin = (int)(yCenter-(yCenter-roiY)*yScale); + if (ymin<0) ymin = 0; + ymax = ymin + (int)(roiHeight*yScale) - 1; + if (ymax>=height) ymax = height - 1; + } else { + xmin = roiX; + xmax = roiX + roiWidth - 1; + ymin = roiY; + ymax = roiY + roiHeight - 1; + } + short[] pixels2 = (short[])getPixelsCopy(); + ImageProcessor ip2 = null; + if (interpolationMethod==BICUBIC) + ip2 = new ShortProcessor(getWidth(), getHeight(), pixels2, null); + boolean checkCoordinates = (xScale < 1.0) || (yScale < 1.0); + short min2 = (short)getMin(); + int index1, index2, xsi, ysi; + double ys, xs; + if (interpolationMethod==BICUBIC) { + for (int y=ymin; y<=ymax; y++) { + ys = (y-yCenter)/yScale + yCenter; + int index = y*width + xmin; + for (int x=xmin; x<=xmax; x++) { + xs = (x-xCenter)/xScale + xCenter; + int value = (int)(getBicubicInterpolatedPixel(xs, ys, ip2)+0.5); + if (value<0) value=0; if (value>65535) value=65535; + pixels[index++] = (short)value; + } + } + } else { + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + for (int y=ymin; y<=ymax; y++) { + ys = (y-yCenter)/yScale + yCenter; + ysi = (int)ys; + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + index1 = y*width + xmin; + index2 = width*(int)ys; + for (int x=xmin; x<=xmax; x++) { + xs = (x-xCenter)/xScale + xCenter; + xsi = (int)xs; + if (checkCoordinates && ((xsixmax) || (ysiymax))) + pixels[index1++] = min2; + else { + if (interpolationMethod==BILINEAR) { + if (xs<0.0) xs = 0.0; + if (xs>=xlimit) xs = xlimit2; + pixels[index1++] = (short)(getInterpolatedPixel(xs, ys, pixels2)+0.5); + } else + pixels[index1++] = pixels2[index2+xsi]; + } + } + } + } + } + + /** Uses bilinear interpolation to find the pixel value at real coordinates (x,y). */ + private final double getInterpolatedPixel(double x, double y, short[] pixels) { + int xbase = (int)x; + int ybase = (int)y; + double xFraction = x - xbase; + double yFraction = y - ybase; + int offset = ybase * width + xbase; + int lowerLeft = pixels[offset]&0xffff; + int lowerRight = pixels[offset + 1]&0xffff; + int upperRight = pixels[offset + width + 1]&0xffff; + int upperLeft = pixels[offset + width]&0xffff; + double upperAverage = upperLeft + xFraction * (upperRight - upperLeft); + double lowerAverage = lowerLeft + xFraction * (lowerRight - lowerLeft); + return lowerAverage + yFraction * (upperAverage - lowerAverage); + } + + /** Creates a new ShortProcessor containing a scaled copy of this image or selection. */ + public ImageProcessor resize(int dstWidth, int dstHeight) { + if (roiWidth==dstWidth && roiHeight==dstHeight) + return crop(); + if ((width==1||height==1) && interpolationMethod!=NONE) + return resizeLinearly(dstWidth, dstHeight); + double srcCenterX = roiX + roiWidth/2.0; + double srcCenterY = roiY + roiHeight/2.0; + double dstCenterX = dstWidth/2.0; + double dstCenterY = dstHeight/2.0; + double xScale = (double)dstWidth/roiWidth; + double yScale = (double)dstHeight/roiHeight; + if (interpolationMethod!=NONE) { + if (dstWidth!=width) dstCenterX+=xScale/4.0; + if (dstHeight!=height) dstCenterY+=yScale/4.0; + } + int inc = getProgressIncrement(dstWidth,dstHeight); + ImageProcessor ip2 = createProcessor(dstWidth, dstHeight); + short[] pixels2 = (short[])ip2.getPixels(); + double xs, ys; + if (interpolationMethod==BICUBIC) { + for (int y=0; y<=dstHeight-1; y++) { + if (inc>0&&y%inc==0) showProgress((double)y/dstHeight); + ys = (y-dstCenterY)/yScale + srcCenterY; + int index2 = y*dstWidth; + for (int x=0; x<=dstWidth-1; x++) { + xs = (x-dstCenterX)/xScale + srcCenterX; + int value = (int)(getBicubicInterpolatedPixel(xs, ys, this)+0.5); + if (value<0) value=0; if (value>65535) value=65535; + pixels2[index2++] = (short)value; + } + } + } else { + double xlimit = width-1.0, xlimit2 = width-1.001; + double ylimit = height-1.0, ylimit2 = height-1.001; + int index1, index2; + for (int y=0; y<=dstHeight-1; y++) { + if (inc>0&&y%inc==0) showProgress((double)y/dstHeight); + ys = (y-dstCenterY)/yScale + srcCenterY; + if (interpolationMethod==BILINEAR) { + if (ys<0.0) ys = 0.0; + if (ys>=ylimit) ys = ylimit2; + } + index1 = width*(int)ys; + index2 = y*dstWidth; + for (int x=0; x<=dstWidth-1; x++) { + xs = (x-dstCenterX)/xScale + srcCenterX; + if (interpolationMethod==BILINEAR) { + if (xs<0.0) xs = 0.0; + if (xs>=xlimit) xs = xlimit2; + pixels2[index2++] = (short)(getInterpolatedPixel(xs, ys, pixels)+0.5); + } else + pixels2[index2++] = pixels[index1+(int)xs]; + } + } + } + if (inc>0) showProgress(1.0); + return ip2; + } + + public ImageProcessor crop() { + ImageProcessor ip2 = createProcessor(roiWidth, roiHeight); + short[] pixels2 = (short[])ip2.getPixels(); + for (int ys=roiY; ys0 && getMin()==0.0 && getMax()==0.0) { + setValue(bestIndex); + setMinAndMax(0.0,255.0); + } else if (bestIndex==0 && getMin()>0.0 && (color.getRGB()&0xffffff)==0) { + if (isSigned16Bit()) + setValue(32768); + else + setValue(0.0); + } else + fgColor = (int)(getMin() + (getMax()-getMin())*(bestIndex/255.0)); + fillValueSet = true; + } + + /** Sets the default fill/draw value, where 0<=value<=65535). */ + public void setValue(double value) { + fgColor = (int)value; + if (fgColor<0) fgColor = 0; + if (fgColor>65535) fgColor = 65535; + fillValueSet = true; + } + + /** Returns the foreground fill/draw value. */ + public double getForegroundValue() { + return fgColor; + } + + public void setBackgroundValue(double value) { + bgValue = (int)value; + if (bgValue<0) bgValue = 0; + if (bgValue>65535) bgValue = 65535; + } + + public double getBackgroundValue() { + return bgValue; + } + + /** Returns 65,536 bin histogram of the current ROI, which + can be non-rectangular. */ + public int[] getHistogram() { + if (mask!=null) + return getHistogram(mask); + int roiX=this.roiX, roiY=this.roiY; + int roiWidth=this.roiWidth, roiHeight=this.roiHeight; + int[] histogram = new int[65536]; + for (int y=roiY; y<(roiY+roiHeight); y++) { + int i = y*width + roiX; + for (int x=roiX; x<(roiX+roiWidth); x++) + histogram[pixels[i++]&0xffff]++; + } + return histogram; + } + + int[] getHistogram(ImageProcessor mask) { + if (mask.getWidth()!=roiWidth||mask.getHeight()!=roiHeight) + throw new IllegalArgumentException(maskSizeError(mask)); + int roiX=this.roiX, roiY=this.roiY; + int roiWidth=this.roiWidth, roiHeight=this.roiHeight; + byte[] mpixels = (byte[])mask.getPixels(); + int[] histogram = new int[65536]; + for (int y=roiY, my=0; y<(roiY+roiHeight); y++, my++) { + int i = y * width + roiX; + int mi = my * roiWidth; + for (int x=roiX; x<(roiX+roiWidth); x++) { + if (mpixels[mi++]!=0) + histogram[pixels[i]&0xffff]++; + i++; + } + } + return histogram; + } + + /** Creates a histogram of length maxof(max+1,256). For small + images or selections, computations using these histograms + are faster compared to 65536 element histograms. */ + int[] getHistogram2() { + if (mask!=null) + return getHistogram2(mask); + int roiX=this.roiX, roiY=this.roiY; + int roiWidth=this.roiWidth, roiHeight=this.roiHeight; + int max = 0; + int value; + for (int y=roiY; y<(roiY+roiHeight); y++) { + int index = y*width + roiX; + for (int i=0; imax) + max = value; + } + } + int size = max + 1; + if (size<256) size = 256; + int[] histogram = new int[size]; + for (int y=roiY; y<(roiY+roiHeight); y++) { + int index = y*width + roiX; + for (int i=0; imax) + max = value; + } + } + int size = max + 1; + if (size<256) size = 256; + int[] histogram = new int[size]; + for (int y=roiY, my=0; y<(roiY+roiHeight); y++, my++) { + int index = y * width + roiX; + int mi = my * roiWidth; + for (int i=0; i65535.0) maxThreshold = 65535.0; + int min2=(int)getMin(), max2=(int)getMax(); + if (max2>min2) { + if (lutUpdate==OVER_UNDER_LUT) { + double minT = ((minThreshold-getMin())/(getMax()-getMin())*255.0); + double maxT = ((maxThreshold-getMin())/(getMax()-getMin())*255.0); + super.setThreshold(minT, maxT, lutUpdate); // update LUT + } else { + lutUpdateMode = lutUpdate; + if (rLUT1==null) { + if (cm==null) + makeDefaultColorModel(); + baseCM = cm; + IndexColorModel m = (IndexColorModel)cm; + rLUT1 = new byte[256]; gLUT1 = new byte[256]; bLUT1 = new byte[256]; + m.getReds(rLUT1); m.getGreens(gLUT1); m.getBlues(bLUT1); + rLUT2 = new byte[256]; gLUT2 = new byte[256]; bLUT2 = new byte[256]; + } + if (lutUpdateMode==RED_LUT) + cm = getThresholdColorModel(); + else + cm = getDefaultColorModel(); + } + } else + super.resetThreshold(); + this.minThreshold = Math.round(minThreshold); + this.maxThreshold = Math.round(maxThreshold); + //ij.IJ.log("setThreshold: "+lutUpdateMode+" "+this.minThreshold+" "+this.maxThreshold); + } + + /** Performs a convolution operation using the specified kernel. */ + public void convolve(float[] kernel, int kernelWidth, int kernelHeight) { + ImageProcessor ip2 = convertToFloat(); + ip2.setRoi(getRoi()); + new ij.plugin.filter.Convolver().convolve(ip2, kernel, kernelWidth, kernelHeight); + ip2 = ip2.convertToShort(false); + short[] pixels2 = (short[])ip2.getPixels(); + System.arraycopy(pixels2, 0, pixels, 0, pixels.length); + } + + /** Adds pseudorandom, Gaussian ("normally") distributed values, with + mean 0.0 and the specified standard deviation, to this image or ROI. */ + public void noise(double standardDeviation) { + if (rnd==null) + rnd = new Random(); + if (!Double.isNaN(seed)) + rnd.setSeed((int) seed); + seed = Double.NaN; + int v, ran; + boolean inRange; + for (int y=roiY; y<(roiY+roiHeight); y++) { + int i = y * width + roiX; + for (int x=roiX; x<(roiX+roiWidth); x++) { + inRange = false; + do { + ran = (int)Math.round(rnd.nextGaussian()*standardDeviation); + v = (pixels[i] & 0xffff) + ran; + inRange = v>=0 && v<=65535; + if (inRange) pixels[i] = (short)v; + } while (!inRange); + i++; + } + } + resetMinAndMax(); + } + + public void threshold(int level) { + for (int i=0; i65535f) value = 65535f; + pixels[i] = (short)value; + } + setMinAndMax(fp.getMin(), fp.getMax()); + } + + /** Returns the maximum possible pixel value. */ + public double maxValue() { + return 65535.0; + } + + public int getBitDepth() { + return 16; + } + + /** Returns 'true' if this is a signed 16-bit image. */ + public boolean isSigned16Bit() { + return cTable!=null && cTable[0]==-32768f && cTable[1]==-32767f; + } + + /** Returns a binary mask, or null if a threshold is not set. */ + public ByteProcessor createMask() { + if (getMinThreshold()==NO_THRESHOLD) + return null; + int minThreshold = (int)getMinThreshold(); + int maxThreshold = (int)getMaxThreshold(); + ByteProcessor mask = new ByteProcessor(width, height); + byte[] mpixels = (byte[])mask.getPixels(); + for (int i=0; i=minThreshold && value<=maxThreshold) + mpixels[i] = (byte)255; + } + return mask; + } + + /** Not implemented. */ + public void medianFilter() {} + /** Not implemented. */ + public void erode() {} + /** Not implemented. */ + public void dilate() {} + +} + diff --git a/src/ij/process/ShortStatistics.java b/src/ij/process/ShortStatistics.java new file mode 100644 index 0000000..ab647a9 --- /dev/null +++ b/src/ij/process/ShortStatistics.java @@ -0,0 +1,224 @@ +package ij.process; +import ij.measure.Calibration; +import java.awt.Rectangle; + +/** 16-bit image statistics, including histogram. */ +public class ShortStatistics extends ImageStatistics { + + /** Construct an ImageStatistics object from a ShortProcessor + using the standard measurement options (area, mean, + mode, min and max). */ + public ShortStatistics(ImageProcessor ip) { + this(ip, AREA+MEAN+MODE+MIN_MAX, null); + } + + /** Constructs a ShortStatistics object from a ShortProcessor using + the specified measurement options. The 'cal' argument, which + can be null, is currently ignored. */ + public ShortStatistics(ImageProcessor ip, int mOptions, Calibration cal) { + this.width = ip.getWidth(); + this.height = ip.getHeight(); + setup(ip, cal); + nBins = 256; + double minT = ip.getMinThreshold(); + int minThreshold,maxThreshold; + boolean limitToThreshold = (mOptions&LIMIT)!=0; + if (!limitToThreshold || minT==ImageProcessor.NO_THRESHOLD) { + minThreshold=0; + maxThreshold=65535; + } else { + minThreshold=(int)minT; + maxThreshold=(int)ip.getMaxThreshold(); + } + if (limitToThreshold) + saveThreshold(minThreshold, maxThreshold, cal); + Rectangle r = ip.getRoi(); + boolean smallRoi = r.width*r.height<250000; + int[] hist = smallRoi&&(ip instanceof ShortProcessor)?((ShortProcessor)ip).getHistogram2():ip.getHistogram(); + if (maxThreshold>hist.length-1) + maxThreshold = hist.length-1; + histogram16 = hist; + float[] cTable = cal!=null?cal.getCTable():null; + getRawMinAndMax(hist, minThreshold, maxThreshold); + histMin = min; + histMax = max; + getStatistics(ip, hist, (int)min, (int)max, cTable); + if ((mOptions&MODE)!=0) + getMode(); + if ((mOptions&ELLIPSE)!=0 || (mOptions&SHAPE_DESCRIPTORS)!=0) + fitEllipse(ip, mOptions); + else if ((mOptions&CENTROID)!=0) + getCentroid(ip, minThreshold, maxThreshold); + if ((mOptions&(CENTER_OF_MASS|SKEWNESS|KURTOSIS))!=0) + calculateMoments(ip, minThreshold, maxThreshold, cTable); + if ((mOptions&MIN_MAX)!=0 && cTable!=null) + getCalibratedMinAndMax(hist, (int)min, (int)max, cTable); + if ((mOptions&MEDIAN)!=0) { + if (pixelCount>0) + calculateMedian(hist, minThreshold, maxThreshold, cal); + else + median = Double.NaN; + } + if ((mOptions&AREA_FRACTION)!=0) + calculateAreaFraction(ip, hist); + } + + void getRawMinAndMax(int[] hist, int minThreshold, int maxThreshold) { + int min = minThreshold; + if (min0)) + max--; + this.max = max; + } + + void getStatistics(ImageProcessor ip, int[] hist, int min, int max, float[] cTable) { + int count; + double value; + double sum = 0.0; + double sum2 = 0.0; + nBins = ip.getHistogramSize(); + histMin = ip.getHistogramMin(); + histMax = ip.getHistogramMax(); + if (histMin==0.0 && histMax==0.0) { + histMin = min; + histMax = max; + } else { + if (minhistMax) max = (int)histMax; + } + binSize = (histMax-histMin)/nBins; + double scale = 1.0/binSize; + int hMin = (int)histMin; + histogram = new int[nBins]; // 256 bin histogram + int index; + int maxCount = 0; + + for (int i=min; i<=max; i++) { + count = hist[i]; + if (count>maxCount) { + maxCount = count; + dmode = i; + } + pixelCount += count; + value = cTable==null?i:cTable[i]; + sum += value*count; + sum2 += (value*value)*count; + index = (int)(scale*(i-hMin)); + if (index>=nBins) + index = nBins-1; + histogram[index] += count; + } + area = pixelCount*pw*ph; + mean = sum/pixelCount; + umean = mean; + calculateStdDev(pixelCount, sum, sum2); + if (cTable!=null) + dmode = cTable[(int)dmode]; + } + + void getMode() { + int count; + maxCount = 0; + for (int i=0; i maxCount) { + maxCount = count; + mode = i; + } + } + } + + void getCentroid(ImageProcessor ip, int minThreshold, int maxThreshold) { + short[] pixels = (short[])ip.getPixels(); + byte[] mask = ip.getMaskArray(); + boolean limit = minThreshold>0 || maxThreshold<65535; + int count=0, i, mi, v; + double xsum=0.0, ysum=0.0; + for (int y=ry,my=0; y<(ry+rh); y++,my++) { + i = y*width + rx; + mi = my*rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null||mask[mi++]!=0) { + if (limit) { + v = pixels[i]&0xffff; + if (v>=minThreshold&&v<=maxThreshold) { + count++; + xsum+=x; + ysum+=y; + } + } else { + count++; + xsum+=x; + ysum+=y; + } + } + i++; + } + } + xCentroid = xsum/count+0.5; + yCentroid = ysum/count+0.5; + if (cal!=null) { + xCentroid = cal.getX(xCentroid); + yCentroid = cal.getY(yCentroid, height); + } + } + + void calculateMoments(ImageProcessor ip, int minThreshold, int maxThreshold, float[] cTable) { + short[] pixels = (short[])ip.getPixels(); + byte[] mask = ip.getMaskArray(); + int i, mi, iv; + double v, v2, sum1=0.0, sum2=0.0, sum3=0.0, sum4=0.0, xsum=0.0, ysum=0.0; + for (int y=ry,my=0; y<(ry+rh); y++,my++) { + i = y*width + rx; + mi = my*rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null || mask[mi++]!=0) { + iv = pixels[i]&0xffff; + if (iv>=minThreshold&&iv<=maxThreshold) { + v = cTable!=null?cTable[iv]:iv; + v2 = v*v; + sum1 += v; + sum2 += v2; + sum3 += v*v2; + sum4 += v2*v2; + xsum += x*v; + ysum += y*v; + } + } + i++; + } + } + double mean2 = mean*mean; + double variance = sum2/pixelCount - mean2; + double sDeviation = Math.sqrt(variance); + skewness = ((sum3 - 3.0*mean*sum2)/pixelCount + 2.0*mean*mean2)/(variance*sDeviation); + kurtosis = (((sum4 - 4.0*mean*sum3 + 6.0*mean2*sum2)/pixelCount - 3.0*mean2*mean2)/(variance*variance)-3.0); + xCenterOfMass = xsum/sum1+0.5; + yCenterOfMass = ysum/sum1+0.5; + if (cal!=null) { + xCenterOfMass = cal.getX(xCenterOfMass); + yCenterOfMass = cal.getY(yCenterOfMass, height); + } + } + + void getCalibratedMinAndMax(int[] hist, int minValue, int maxValue, float[] cTable) { + min = Double.MAX_VALUE; + max = -Double.MAX_VALUE; + double v = 0.0; + for (int i=minValue; i<=maxValue; i++) { + if (hist[i]>0) { + v = cTable[i]; + if (vmax) + max = v; + } + } + } + +} diff --git a/src/ij/process/StackConverter.java b/src/ij/process/StackConverter.java new file mode 100644 index 0000000..2ab0fb5 --- /dev/null +++ b/src/ij/process/StackConverter.java @@ -0,0 +1,313 @@ +package ij.process; + +import java.awt.*; +import java.awt.image.*; +import ij.*; +import ij.gui.*; +import ij.measure.*; +import ij.plugin.RGBStackConverter; + +/** This class does stack type conversions. */ +public class StackConverter { + ImagePlus imp; + int type, nSlices, width, height; + + public StackConverter(ImagePlus imp) { + this.imp = imp; + type = imp.getType(); + nSlices = imp.getStackSize(); + if (nSlices<2) + throw new IllegalArgumentException("Stack required"); + width = imp.getWidth(); + height = imp.getHeight(); + } + + /** Converts this Stack to 8-bit grayscale. */ + public void convertToGray8() { + ImageStack stack1 = imp.getStack(); + int currentSlice = imp.getCurrentSlice(); + ImageProcessor ip = imp.getProcessor(); + boolean colorLut = ip.isColorLut(); + boolean pseudoColorLut = colorLut && ip.isPseudoColorLut(); + boolean composite = imp.isComposite(); + if (type==ImagePlus.GRAY8 && pseudoColorLut && !composite) { + boolean invertedLut = ip.isInvertedLut(); + ip.setColorModel(LookUpTable.createGrayscaleColorModel(invertedLut)); + stack1.setColorModel(ip.getColorModel()); + imp.updateAndDraw(); + return; + } + if (!composite && (type==ImagePlus.COLOR_RGB||type==ImagePlus.COLOR_256||colorLut)) { + convertRGBToGray8(); + imp.setSlice(currentSlice); + return; + } + + ImageStack stack2 = new ImageStack(width, height); + Image img; + String label; + double min = ip.getMin(); + double max = ip.getMax(); + int inc = nSlices/20; + if (inc<1) inc = 1; + LUT[] luts = composite?((CompositeImage)imp).getLuts():null; + for(int i=1; i<=nSlices; i++) { + label = stack1.getSliceLabel(1); + ip = stack1.getProcessor(1); + stack1.deleteSlice(1); + if (luts!=null) { + int index = (i-1)%luts.length; + min = luts[index].min; + max = luts[index].max; + } + ip.setMinAndMax(min, max); + boolean scale = ImageConverter.getDoScaling(); + stack2.addSlice(label, ip.convertToByte(scale)); + if ((i%inc)==0) { + IJ.showProgress((double)i/nSlices); + IJ.showStatus("Converting to 8-bits: "+i+"/"+nSlices); + } + } + imp.setStack(null, stack2); + imp.setCalibration(imp.getCalibration()); //update calibration + if (imp.isComposite()) { + ((CompositeImage)imp).resetDisplayRanges(); + ((CompositeImage)imp).updateAllChannelsAndDraw(); + } + imp.setSlice(currentSlice); + IJ.showProgress(1.0); + } + + /** Converts an RGB or 8-bit color stack to 8-bit grayscale. */ + void convertRGBToGray8() { + ImageStack stack1 = imp.getStack(); + if (stack1 instanceof PlotVirtualStack) { + ((PlotVirtualStack)stack1).setBitDepth(8); + imp.setStack(stack1); + return; + } + ImageStack stack2 = new ImageStack(width, height); + ImageProcessor ip; + Image img; + String label; + int inc = nSlices/20; + if (inc<1) inc = 1; + for(int i=1; i<=nSlices; i++) { + label = stack1.getSliceLabel(1); + ip = stack1.getProcessor(1); + stack1.deleteSlice(1); + if (ip instanceof ByteProcessor) + ip = new ColorProcessor(ip.createImage()); + boolean scale = ImageConverter.getDoScaling(); + stack2.addSlice(label, ip.convertToByte(scale)); + if ((i%inc)==0) { + IJ.showProgress((double)i/nSlices); + IJ.showStatus("Converting to 8-bits: "+i+"/"+nSlices); + } + } + imp.setStack(null, stack2); + IJ.showProgress(1.0); + } + + /** Converts this Stack to 16-bit grayscale. */ + public void convertToGray16() { + if (type==ImagePlus.GRAY16) + return; + if (!(type==ImagePlus.GRAY8 || type==ImagePlus.GRAY32)) + throw new IllegalArgumentException("Unsupported conversion"); + ImageStack stack1 = imp.getStack(); + ImageStack stack2 = new ImageStack(width, height); + String label; + int inc = nSlices/20; + if (inc<1) inc = 1; + boolean scale = type==ImagePlus.GRAY32 && ImageConverter.getDoScaling(); + ImageProcessor ip1, ip2; + for(int i=1; i<=nSlices; i++) { + label = stack1.getSliceLabel(1); + ip1 = stack1.getProcessor(1); + ip2 = ip1.convertToShort(scale); + stack1.deleteSlice(1); + stack2.addSlice(label, ip2); + if ((i%inc)==0) { + IJ.showProgress((double)i/nSlices); + IJ.showStatus("Converting to 16-bits: "+i+"/"+nSlices); + } + } + IJ.showProgress(1.0); + imp.setStack(null, stack2); + } + + /** Converts this Stack to 32-bit (float) grayscale. */ + public void convertToGray32() { + if (type==ImagePlus.GRAY32) + return; + if (!(type==ImagePlus.GRAY8||type==ImagePlus.GRAY16||type==ImagePlus.COLOR_RGB)) + throw new IllegalArgumentException("Unsupported conversion"); + ImageStack stack1 = imp.getStack(); + ImageStack stack2 = new ImageStack(width, height); + String label; + int inc = nSlices/20; + if (inc<1) inc = 1; + ImageProcessor ip1, ip2; + Calibration cal = imp.getCalibration(); + for(int i=1; i<=nSlices; i++) { + label = stack1.getSliceLabel(1); + ip1 = stack1.getProcessor(1); + ip1.setCalibrationTable(cal.getCTable()); + ip2 = ip1.convertToFloat(); + stack1.deleteSlice(1); + stack2.addSlice(label, ip2); + if ((i%inc)==0) { + IJ.showProgress((double)i/nSlices); + IJ.showStatus("Converting to 32-bits: "+i+"/"+nSlices); + } + } + IJ.showProgress(1.0); + imp.setStack(null, stack2); + imp.setCalibration(imp.getCalibration()); //update calibration + if (type==ImagePlus.COLOR_RGB) { + imp.resetDisplayRange(); + imp.updateAndDraw(); + } + } + + /** Converts the Stack to RGB. */ + public void convertToRGB() { + int z = imp.getNSlices(); + int t = imp.getNFrames(); + if (imp.isComposite()) { + RGBStackConverter.convertToRGB(imp); + return; + } + ImageStack stack1 = imp.getStack(); + if (stack1 instanceof PlotVirtualStack) { + ((PlotVirtualStack)stack1).setBitDepth(24); + imp.setStack(stack1); + return; + } + ImageStack stack2 = new ImageStack(width, height); + String label; + int inc = nSlices/20; + if (inc<1) inc = 1; + ImageProcessor ip1, ip2; + Calibration cal = imp.getCalibration(); + for(int i=1; i<=nSlices; i++) { + label = stack1.getSliceLabel(i); + ip1 = stack1.getProcessor(i); + ip2 = ip1.convertToRGB(); + stack2.addSlice(label, ip2); + if ((i%inc)==0) { + IJ.showProgress((double)i/nSlices); + IJ.showStatus("Converting to RGB: "+i+"/"+nSlices); + } + } + IJ.showProgress(1.0); + imp.setStack(null, stack2); + imp.setDimensions(1, z, t); + imp.setCalibration(imp.getCalibration()); //update calibration + } + + /** Converts the stack (which must be RGB) to a + 3 channel (red, green and blue) hyperstack. */ + public void convertToRGBHyperstack() { + if (type!=ImagePlus.COLOR_RGB) + throw new IllegalArgumentException("RGB stack required"); + new ij.plugin.CompositeConverter().run("composite"); + } + + /** Converts the stack (which must be RGB) to a 3 channel + (hue, saturation and brightness) hyperstack. */ + public void convertToHSBHyperstack() { + if (type!=ImagePlus.COLOR_RGB) + throw new IllegalArgumentException("RGB stack required"); + ImageStack stack1 = imp.getStack(); + ImageStack stack2 = new ImageStack(width,height); + int nSlices = stack1.getSize(); + Calibration cal = imp.getCalibration(); + int inc = nSlices/20; + if (inc<1) inc = 1; + for(int i=1; i<=nSlices; i++) { + String label = stack1.getSliceLabel(i); + ColorProcessor cp = (ColorProcessor)stack1.getProcessor(i); + ImageStack stackHSB = cp.getHSBStack(); + stack2.addSlice(label,stackHSB.getProcessor(1)); + stack2.addSlice(label,stackHSB.getProcessor(2)); + stack2.addSlice(label,stackHSB.getProcessor(3)); + if ((i%inc)==0) { + IJ.showProgress((double)i/nSlices); + IJ.showStatus("Converting to HSB: "+i+"/"+nSlices); + } + } + IJ.showProgress(1.0); + imp.setStack(null,stack2); + imp.setProp("HSB_Stack", "true"); + imp.setCalibration(cal); + imp.setDimensions(3, nSlices, 1); + CompositeImage ci = new CompositeImage(imp, IJ.GRAYSCALE); + ci.show(); + imp.hide(); + } + + /** Converts the stack (which must be RGB) to a 3 channel + (hue, saturation and brightness) 32-bit hyperstack. */ + public void convertToHSB32Hyperstack() { + if (type!=ImagePlus.COLOR_RGB) + throw new IllegalArgumentException("RGB stack required"); + ImageStack stack1 = imp.getStack(); + ImageStack stack2 = new ImageStack(width,height); + int nSlices = stack1.getSize(); + Calibration cal = imp.getCalibration(); + int inc = nSlices/20; + if (inc<1) inc = 1; + for(int i=1; i<=nSlices; i++) { + String label = stack1.getSliceLabel(i); + ColorProcessor cp = (ColorProcessor)stack1.getProcessor(i); + ImageStack stackHSB = cp.getHSB32Stack(); + stack2.addSlice(label,stackHSB.getProcessor(1)); + stack2.addSlice(label,stackHSB.getProcessor(2)); + stack2.addSlice(label,stackHSB.getProcessor(3)); + if ((i%inc)==0) { + IJ.showProgress((double)i/nSlices); + IJ.showStatus("Converting to HSB: "+i+"/"+nSlices); + } + } + IJ.showProgress(1.0); + imp.setStack(null,stack2); + imp.setCalibration(cal); + imp.setDimensions(3, nSlices, 1); + CompositeImage ci = new CompositeImage(imp, IJ.GRAYSCALE); + ci.show(); + imp.hide(); + } + + /** Converts the stack (which must be RGB) to a 3 channel + Lab hyperstack. */ + public void convertToLabHyperstack() { + if (type!=ImagePlus.COLOR_RGB) + throw new IllegalArgumentException("RGB stack required"); + if (imp!=null) + throw new IllegalArgumentException("Stacks currently not supported"); + } + + /** Converts the stack to 8-bits indexed color. 'nColors' must + be greater than 1 and less than or equal to 256. */ + public void convertToIndexedColor(int nColors) { + if (type!=ImagePlus.COLOR_RGB) + throw new IllegalArgumentException("RGB stack required"); + ImageStack stack = imp.getStack(); + int size = stack.size(); + ImageProcessor montage = new ColorProcessor(width*size, height); + for (int i=0; i1 && ip!=null) + ip.setProgressBar(null); + } + + static final int FLIPH=0, FLIPV=1, SCALE=2, INVERT=3, APPLY_TABLE=4, SCALE_WITH_FILL=5; + + void process(int command) { + String s = ""; + ImageProcessor ip2 = stack.getProcessor(1); + switch (command) { + case FLIPH: case FLIPV: s="Flip: "; break; + case SCALE: s="Scale: "; break; + case SCALE_WITH_FILL: s="Scale: "; ip2.setBackgroundValue(fillValue); break; + case INVERT: s="Invert: "; break; + case APPLY_TABLE: s="Apply: "; break; + } + if (ip==null) + ip = ip2; + ip2.setRoi(this.ip.getRoi()); + ip2.setInterpolate(this.ip.getInterpolate()); + for (int i=1; i<=nSlices; i++) { + showStatus(s,i,nSlices); + ip2.setPixels(stack.getPixels(i)); + if (nSlices==1 && i==1 && command==SCALE) + ip2.snapshot(); + switch (command) { + case FLIPH: ip2.flipHorizontal(); break; + case FLIPV: ip2.flipVertical(); break; + case SCALE: case SCALE_WITH_FILL: ip2.scale(xScale, yScale); break; + case INVERT: ip2.invert(); break; + case APPLY_TABLE: ip2.applyTable(table); break; + } + IJ.showProgress((double)i/nSlices); + } + IJ.showProgress(1.0); + } + + public void invert() { + process(INVERT); + } + + public void flipHorizontal() { + process(FLIPH); + } + + public void flipVertical() { + process(FLIPV); + } + + public void applyTable(int[] table) { + this.table = table; + process(APPLY_TABLE); + } + + public void scale(double xScale, double yScale) { + this.xScale = xScale; + this.yScale = yScale; + process(SCALE); + } + + public void scale(double xScale, double yScale, double fillValue) { + this.xScale = xScale; + this.yScale = yScale; + this.fillValue = fillValue; + process(SCALE_WITH_FILL); + } + + /** Creates a new stack with dimensions 'newWidth' x 'newHeight'. + To reduce memory requirements, the orginal stack is deleted + as the new stack is created. */ + public ImageStack resize(int newWidth, int newHeight) { + return resize(newWidth, newHeight, false); + } + + public ImageStack resize(int newWidth, int newHeight, boolean averageWhenDownsizing) { + ImageStack stack2 = new ImageStack(newWidth, newHeight); + ImageProcessor ip2; + Rectangle roi = ip!=null?ip.getRoi():null; + if (ip==null) + ip = stack.getProcessor(1).duplicate(); + try { + for (int i=1; i<=nSlices; i++) { + showStatus("Resize: ",i,nSlices); + ip.setPixels(stack.getPixels(1)); + String label = stack.getSliceLabel(1); + stack.deleteSlice(1); + ip2 = ip.resize(newWidth, newHeight, averageWhenDownsizing); + if (ip2!=null) + stack2.addSlice(label, ip2); + IJ.showProgress((double)i/nSlices); + } + IJ.showProgress(1.0); + } catch(OutOfMemoryError o) { + while(stack.size()>1) + stack.deleteLastSlice(); + IJ.outOfMemory("StackProcessor.resize"); + IJ.showProgress(1.0); + } + return stack2; + } + + /** Crops the stack to the specified rectangle. */ + public ImageStack crop(int x, int y, int width, int height) { + ImageStack stack2 = new ImageStack(width, height); + ImageProcessor ip2; + for (int i=1; i<=nSlices; i++) { + ImageProcessor ip1 = stack.getProcessor(1); + ip1.setRoi(x, y, width, height); + String label = stack.getSliceLabel(1); + stack.deleteSlice(1); + ip2 = ip1.crop(); + stack2.addSlice(label, ip2); + IJ.showProgress((double)i/nSlices); + } + IJ.showProgress(1.0); + return stack2; + } + + ImageStack rotate90Degrees(boolean clockwise) { + ImageStack stack2 = new ImageStack(stack.getHeight(), stack.getWidth()); + ImageProcessor ip2; + if (ip==null) + ip = stack.getProcessor(1).duplicate(); + for (int i=1; i<=nSlices; i++) { + showStatus("Rotate: ",i,nSlices); + ip.setPixels(stack.getPixels(1)); + String label = stack.getSliceLabel(1); + stack.deleteSlice(1); + if (clockwise) + ip2 = ip.rotateRight(); + else + ip2 = ip.rotateLeft(); + if (ip2!=null) + stack2.addSlice(label, ip2); + if (!Interpreter.isBatchMode()) + IJ.showProgress((double)i/nSlices); + } + if (!Interpreter.isBatchMode()) + IJ.showProgress(1.0); + return stack2; + } + + public ImageStack rotateRight() { + return rotate90Degrees(true); + } + + public ImageStack rotateLeft() { + return rotate90Degrees(false); + } + + public void copyBits(ImageProcessor src, int xloc, int yloc, int mode) { + copyBits(src, null, xloc, yloc, mode); + } + + public void copyBits(ImageStack src, int xloc, int yloc, int mode) { + copyBits(null, src, xloc, yloc, mode); + } + + private void copyBits(ImageProcessor srcIp, ImageStack srcStack, int xloc, int yloc, int mode) { + int inc = nSlices/20; + if (inc<1) inc = 1; + boolean stackSource = srcIp==null; + for (int i=1; i<=nSlices; i++) { + if (stackSource) + srcIp = srcStack.getProcessor(i); + ImageProcessor dstIp = stack.getProcessor(i); + dstIp.copyBits(srcIp, xloc, yloc, mode); + if ((i%inc) == 0) IJ.showProgress((double)i/nSlices); + } + IJ.showProgress(1.0); + } + + void showStatus(String s, int n, int total) { + IJ.showStatus(s+n+"/"+total); + } + + /** + * Thomas Boudier Create a kernel neighorhood as an ellipsoid + * + * @param radx Radius x of the ellipsoid + * @param rady Radius x of the ellipsoid + * @param radz Radius x of the ellipsoid + * @return The kernel as an array + */ + private int[] createKernelEllipsoid(float radx, float rady, float radz) { + int vx = (int) Math.ceil(radx); + int vy = (int) Math.ceil(rady); + int vz = (int) Math.ceil(radz); + int[] ker = new int[(2 * vx + 1) * (2 * vy + 1) * (2 * vz + 1)]; + double dist; + + double rx2 = radx * radx; + double ry2 = rady * rady; + double rz2 = radz * radz; + + if (rx2 != 0) { + rx2 = 1.0 / rx2; + } else { + rx2 = 0; + } + if (ry2 != 0) { + ry2 = 1.0 / ry2; + } else { + ry2 = 0; + } + if (rz2 != 0) { + rz2 = 1.0 / rz2; + } else { + rz2 = 0; + } + + int idx = 0; + for (int k = -vz; k <= vz; k++) { + for (int j = -vy; j <= vy; j++) { + for (int i = -vx; i <= vx; i++) { + dist = ((double) (i * i)) * rx2 + ((double) (j * j)) * ry2 + ((double) (k * k)) * rz2; + if (dist <= 1.0) { + ker[idx] = 1; + } else { + ker[idx] = 0; + } + idx++; + } + } + } + + return ker; + } + + /** + * 3D filter using threads + * + * @param out + * @param radx Radius of mean filter in x + * @param rady Radius of mean filter in y + * @param radz Radius of mean filter in z + * @param zmin + * @param zmax + * @param filter + */ + public void filter3D(ImageStack out, float radx, float rady, float radz, int zmin, int zmax, int filter) { + int[] ker = this.createKernelEllipsoid(radx, rady, radz); + int nb = 0; + for (int i=0; istack.size()) + zmax = stack.size(); + int sizex = stack.getWidth(); + int sizey = stack.getHeight(); + double value; + for (int z=zmin; zstack.getWidth() || y0<0 || y0+h>stack.getHeight() || z0<0 || z0+d>stack.size()) + return getEdgeNeighborhood(ker, nbval, x, y, z, radx, rady, radz); + voxels = stack.getVoxels(x0, y0, z0, w, h, d, voxels); + return new ArrayUtil(voxels); + } + */ + + /** + * Gets the neighboring attribute of the Image3D with a kernel as a array + * + * @param ker The kernel array (>0 ok) + * @param nbval The number of non-zero values + * @param x Coordinate x of the pixel + * @param y Coordinate y of the pixel + * @param z Coordinate z of the pixel + * @param radx Radius x of the neighboring + * @param radz Radius y of the neighboring + * @param rady Radius z of the neighboring + * @return The values of the nieghbor pixels inside an array + */ + private ArrayUtil getNeighborhood(int[] ker, int nbval, int x, int y, int z, float radx, float rady, float radz) { + ArrayUtil pix = new ArrayUtil(nbval); + int vx = (int) Math.ceil(radx); + int vy = (int) Math.ceil(rady); + int vz = (int) Math.ceil(radz); + int index = 0; + int c = 0; + int sizex = stack.getWidth(); + int sizey = stack.getHeight(); + int sizez = stack.size(); + for (int k = z - vz; k <= z + vz; k++) { + for (int j = y - vy; j <= y + vy; j++) { + for (int i = x - vx; i <= x + vx; i++) { + if (ker[c]>0 && i>=0 && j>=0 && k>=0 && i=minThreshold && v<=maxThreshold) { + if (vroiMax) + roiMax = v; + } + } + i++; + } + } + } + min = roiMin; + max = roiMax; + if (fixedRange) { + if (minhistMax) max = histMax; + } else { + histMin = min; + histMax = max; + } + + // Generate histogram + double scale = nBins/( histMax-histMin); + pixelCount = 0; + int index; + boolean first = true; + for (int slice=1; slice<=size; slice++) { + IJ.showProgress(size/2+slice/2, size); + ip = stack.getProcessor(slice); + ip.setCalibrationTable(cTable); + for (int y=ry, my=0; y<(ry+rh); y++, my++) { + int i = y * width + rx; + int mi = my * rw; + for (int x=rx; x<(rx+rw); x++) { + if (mask==null || mask[mi++]!=0) { + v = ip.getPixelValue(x,y); + if (v>=minThreshold && v<=maxThreshold && v>=histMin && v<=histMax) { + longPixelCount++; + sum += v; + sum2 += v*v; + index = (int)(scale*(v-histMin)); + if (index>=nBins) + index = nBins-1; + longHistogram[index]++; + } + } + i++; + } + } + } + pixelCount = (int)longPixelCount; + area = longPixelCount*pw*ph; + mean = sum/longPixelCount; + calculateStdDev(longPixelCount, sum, sum2); + histMin = cal.getRawValue(histMin); + histMax = cal.getRawValue(histMax); + binSize = (histMax-histMin)/nBins; + int bits = imp.getBitDepth(); + if (histMin==0.0 && histMax==256.0 && (bits==8||bits==24)) + histMax = 255.0; + dmode = getMode(cal); + copyHistogram(nBins); + median = getMedian(longHistogram, (int)minThreshold, (int)maxThreshold, cal); + IJ.showStatus(""); + IJ.showProgress(1.0); + } + + void sum8BitHistograms(ImagePlus imp) { + Calibration cal = imp.getCalibration(); + boolean limitToThreshold = (Analyzer.getMeasurements()&LIMIT)!=0; + int minThreshold = 0; + int maxThreshold = 255; + ImageProcessor ip = imp.getProcessor(); + if (limitToThreshold && ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD) { + minThreshold = (int)ip.getMinThreshold(); + maxThreshold = (int)ip.getMaxThreshold(); + } + ImageStack stack = imp.getStack(); + Roi roi = imp.getRoi(); + longHistogram = new long[256]; + int n = stack.size(); + for (int slice=1; slice<=n; slice++) { + IJ.showProgress(slice, n); + ip = stack.getProcessor(slice); + if (roi!=null) ip.setRoi(roi); + int[] hist = ip.getHistogram(); + for (int i=0; i<256; i++) + longHistogram[i] += hist[i]; + } + pw=1.0; ph=1.0; + getRawStatistics(longHistogram, minThreshold, maxThreshold); + getRawMinAndMax(longHistogram, minThreshold, maxThreshold); + copyHistogram(256); + median = getMedian(longHistogram, minThreshold, maxThreshold, cal); + IJ.showStatus(""); + IJ.showProgress(1.0); + } + + private void copyHistogram(int nbins) { + histogram = new int[nbins]; + for (int i=0; ilongMaxCount) { + longMaxCount = count; + mode = i; + } + } + maxCount = (int)longMaxCount; + pixelCount = (int)longPixelCount; + area = longPixelCount*pw*ph; + mean = sum/longPixelCount; + umean = mean; + dmode = mode; + calculateStdDev(longPixelCount, sum, sum2); + histMin = 0.0; + histMax = 255.0; + } + + void getRawMinAndMax(long[] histogram, int minThreshold, int maxThreshold) { + int min = minThreshold; + while ((histogram[min]==0L) && (min<255)) + min++; + this.min = min; + int max = maxThreshold; + while ((histogram[max]==0L) && (max>0)) + max--; + this.max = max; + } + + void sum16BitHistograms(ImagePlus imp) { + Calibration cal = imp.getCalibration(); + boolean limitToThreshold = (Analyzer.getMeasurements()&LIMIT)!=0; + int minThreshold = 0; + int maxThreshold = 65535; + ImageProcessor ip = imp.getProcessor(); + if (limitToThreshold && ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD) { + minThreshold = (int)ip.getMinThreshold(); + maxThreshold = (int)ip.getMaxThreshold(); + } + ImageStack stack = imp.getStack(); + Roi roi = imp.getRoi(); + long[] hist16 = new long[65536]; + int n = stack.size(); + for (int slice=1; slice<=n; slice++) { + IJ.showProgress(slice, n); + IJ.showStatus(slice+"/"+n); + ip = stack.getProcessor(slice); + if (roi!=null) ip.setRoi(roi); + int[] hist = ip.getHistogram(); + for (int i=0; i<65536; i++) + hist16[i] += hist[i]; + } + pw=1.0; ph=1.0; + getRaw16BitMinAndMax(hist16, minThreshold, maxThreshold); + get16BitStatistics(hist16, (int)min, (int)max); + median = getMedian(hist16, minThreshold, maxThreshold, cal); + histogram16 = new int[65536]; + for (int i=0; i<65536; i++) { + long count = hist16[i]; + if (count<=Integer.MAX_VALUE) + histogram16[i] = (int)count; + else + histogram16[i] = Integer.MAX_VALUE; + } + IJ.showStatus(""); + IJ.showProgress(1.0); + } + + void getRaw16BitMinAndMax(long[] hist, int minThreshold, int maxThreshold) { + int min = minThreshold; + while ((hist[min]==0) && (min<65535)) + min++; + this.min = min; + int max = maxThreshold; + while ((hist[max]==0) && (max>0)) + max--; + this.max = max; + } + + void get16BitStatistics(long[] hist, int min, int max) { + long count; + double value; + double sum = 0.0; + double sum2 = 0.0; + nBins = 256; + histMin = min; + histMax = max; + binSize = (histMax-histMin)/nBins; + double scale = 1.0/binSize; + int hMin = (int)histMin; + longHistogram = new long[nBins]; // 256 bin histogram + int index; + maxCount = 0; + for (int i=min; i<=max; i++) { + count = hist[i]; + longPixelCount += count; + value = i; + sum += value*count; + sum2 += (value*value)*count; + index = (int)(scale*(i-hMin)); + if (index>=nBins) + index = nBins-1; + longHistogram[index] += count; + } + copyHistogram(nBins); + pixelCount = (int)longPixelCount; + area = longPixelCount*pw*ph; + mean = sum/longPixelCount; + umean = mean; + dmode = getMode(null); + calculateStdDev(longPixelCount, sum, sum2); + } + + double getMode(Calibration cal) { + long count; + long longMaxCount = 0L; + for (int i=0; ilongMaxCount) { + longMaxCount = count; + mode = i; + } + } + if (longMaxCount<=Integer.MAX_VALUE) + maxCount = (int)longMaxCount; + else + maxCount = Integer.MAX_VALUE; + double tmode = histMin+mode*binSize; + if (cal!=null) tmode = cal.getCValue(tmode); + return tmode; + } + + double getMedian(long[] hist, int first, int last, Calibration cal) { + //ij.IJ.log("getMedian: "+first+" "+last+" "+hist.length+" "+pixelCount); + if (pixelCount==0 || first<0 || last>hist.length) + return Double.NaN; + double sum = 0; + int i = first-1; + double halfCount = pixelCount/2.0; + do { + sum += hist[++i]; + } while (sum<=halfCount && i255) value = 255; + pixels8[i] = (byte)value; + } + return new ByteProcessor(width, height, pixels8, ip.getCurrentColorModel()); + } else { + int value; + for (int i=0; i255) value = 255; + pixels8[i] = (byte)value; + } + return new ByteProcessor(width, height, pixels8, ip.getColorModel()); + } + } + + /** Converts a FloatProcessor to a ByteProcessor. */ + ByteProcessor convertFloatToByte() { + if (doScaling) { + byte[] pixels8 = ip.create8BitImage(); + ByteProcessor bp = new ByteProcessor(ip.getWidth(), ip.getHeight(), pixels8); + bp.setColorModel(ip.getColorModel()); + return bp; + } else { + ByteProcessor bp = new ByteProcessor(width, height); + bp.setPixels(0, (FloatProcessor)ip); + bp.setColorModel(ip.getColorModel()); + bp.resetMinAndMax(); //don't take min&max from ip + return bp; + } + } + + /** Converts a ColorProcessor to a ByteProcessor. + The pixels are converted to grayscale using the formula + g=r/3+g/3+b/3. Call ColorProcessor.setRGBWeights() + to do weighted conversions. */ + ByteProcessor convertRGBToByte() { + if (ip.getNChannels()==1 && doScaling) { + byte[] pixels8 = ip.create8BitImage(); + ByteProcessor bp = new ByteProcessor(ip.getWidth(), ip.getHeight(), pixels8); + bp.setColorModel(ip.getColorModel()); + return bp; + } + int[] pixels32 = (int[])ip.getPixels(); + double[] w = ColorProcessor.getWeightingFactors(); + if (((ColorProcessor)ip).getRGBWeights()!=null) + w = ((ColorProcessor)ip).getRGBWeights(); + double rw=w[0], gw=w[1], bw=w[2]; + byte[] pixels8 = new byte[width*height]; + int c, r, g, b; + for (int i=0; i < width*height; i++) { + c = pixels32[i]; + r = (c&0xff0000)>>16; + g = (c&0xff00)>>8; + b = c&0xff; + pixels8[i] = (byte)(r*rw + g*gw + b*bw + 0.5); + } + return new ByteProcessor(width, height, pixels8, null); + } + + /** Converts a ColorProcessor to a FloatProcessor. + The pixels are converted to grayscale using the formula + g=r/3+g/3+b/3. Call ColorProcessor.setRGBWeights() + to do weighted conversions. */ + FloatProcessor convertRGBToFloat() { + int[] pixels = (int[])ip.getPixels(); + double[] w = ColorProcessor.getWeightingFactors(); + if (((ColorProcessor)ip).getRGBWeights()!=null) + w = ((ColorProcessor)ip).getRGBWeights(); + double rw=w[0], gw=w[1], bw=w[2]; + float[] pixels32 = new float[width*height]; + int c, r, g, b; + for (int i=0; i < width*height; i++) { + c = pixels[i]; + r = (c&0xff0000)>>16; + g = (c&0xff00)>>8; + b = c&0xff; + pixels32[i] = (float)(r*rw + g*gw + b*bw); + } + return new FloatProcessor(width, height, pixels32); + } + + /** Converts processor to a ShortProcessor. */ + public ImageProcessor convertToShort() { + switch (type) { + case BYTE: + return convertByteToShort(); + case SHORT: + return ip; + case FLOAT: + return convertFloatToShort(); + case RGB: + ip = convertRGBToByte(); + return convertByteToShort(); + default: + return null; + } + } + + /** Converts a ByteProcessor to a ShortProcessor. */ + ShortProcessor convertByteToShort() { + byte[] pixels8 = (byte[])ip.getPixels(); + short[] pixels16 = new short[width * height]; + for (int i=0,j=0; i65535.0) value = 65535.0; + pixels16[i] = (short)(value+0.5); + } + return new ShortProcessor(width, height, pixels16, ip.getColorModel()); + } + + /** Converts processor to a FloatProcessor. */ + public ImageProcessor convertToFloat(float[] ctable) { + switch (type) { + case BYTE: + return convertByteToFloat(ctable); + case SHORT: + return convertShortToFloat(ctable); + case FLOAT: + return ip; + case RGB: + return convertRGBToFloat(); + default: + return null; + } + } + + /** Converts a ByteProcessor to a FloatProcessor. Applies a + * calibration function if the 'cTable' is not null. + * @see ImageProcessor.setCalibrationTable + */ + FloatProcessor convertByteToFloat(float[] cTable) { + int n = width*height; + byte[] pixels8 = (byte[])ip.getPixels(); + float[] pixels32 = new float[n]; + int value; + if (cTable!=null && cTable.length==256) { + for (int i=0; i0 && tp.iColWidth[0]==0&&tp.iRowCount>0)) { + tp.iRowHeight=fMetrics.getHeight()+2; + for (int i=0; i=tp.iColWidth.length) break; + int w = tp.iColWidth[i]; + Color b=Color.white,t=Color.black; + if (j>=tp.selStart && j<=tp.selEnd) { + int w2 = w; + if (tp.iColCount==1) + w2 = iWidth; + b=Color.black; + t=Color.white; + gImage.setColor(b); + gImage.fillRect(x,y,w2-1,tp.iRowHeight); + } + gImage.setColor(t); + char[] chars = getChars(i,j); + if (chars!=null) + gImage.drawChars(chars,0,chars.length,x+2,y+tp.iRowHeight-5); + x+=w; + } + } + if (iImage!=null) + g.drawImage(iImage,0,0,null); + } + + void makeImage(int iWidth, int iHeight) { + iImage=createImage(iWidth, iHeight); + if (gImage!=null) + gImage.dispose(); + gImage=iImage.getGraphics(); + gImage.setFont(fFont); + Java2.setAntialiasedText(gImage, antialiased); + if(fMetrics==null) + fMetrics=gImage.getFontMetrics(); + } + + void drawColumnLabels(int iWidth) { + gImage.setColor(Color.darkGray); + gImage.drawLine(0,tp.iRowHeight,iWidth,tp.iRowHeight); + int x=-tp.iX; + for (int i=0; i0) { + gImage.setColor(Color.darkGray); + gImage.drawLine(x+w-1,0,x+w-1,tp.iRowHeight-1); + gImage.setColor(Color.white); + gImage.drawLine(x+w,0,x+w,tp.iRowHeight-1); + } + x+=w; + } + gImage.setColor(Color.lightGray); + gImage.fillRect(0,0,1,tp.iRowHeight); + gImage.fillRect(x+1,0,iWidth-x,tp.iRowHeight); + //gImage.drawLine(0,0,0,iRowHeight-1); + gImage.setColor(Color.darkGray); + gImage.drawLine(0,0,iWidth,0); + } + + synchronized char[] getChars(int column, int row) { + if (tp==null || tp.vData==null) + return null; + if (row>=tp.vData.size()) + return null; + char[] chars = rowtabs) { + if (chars[start]=='\t') + tabs++; + start++; + if (start>=length) + return null; + }; + if (start<0 || start>=chars.length) { + System.out.println("start="+start+", chars.length="+chars.length); + return null; + } + if (chars[start]=='\t') + return null; + + int end = start; + while (chars[end]!='\t' && end<(length-1)) + end++; + if (chars[end]=='\t') + end--; + + char[] chars2 = new char[end-start+1]; + for (int i=0,j=start; i=tp.iColWidth.length || gImage==null) + return; + if (fMetrics==null) + fMetrics=gImage.getFontMetrics(); + int w=15; + int maxRows = 20; + if (column==0 && tp.sColHead[0].equals(" ")) + w += 5; + else { + char[] chars = tp.sColHead[column].toCharArray(); + w = Math.max(w,fMetrics.charsWidth(chars,0,chars.length)); + } + int rowCount = Math.min(tp.iRowCount, maxRows); + for (int row=0; row0?getChars(column, tp.iRowCount-1):null; + if (chars!=null) + w = Math.max(w,fMetrics.charsWidth(chars,0,chars.length)); + if (column1 && iRowCount<=10 && !columnsManuallyAdjusted) + iColWidth[0] = 0; // forces column width calculation + tc.repaint(); + } + + String getCell(int column, int row) { + if (column<0||column>=iColCount||row<0||row>=iRowCount) + return null; + return new String(tc.getChars(column, row)); + } + + synchronized void adjustVScroll() { + if(iRowHeight==0) return; + Dimension d = tc.getSize(); + int value = iY/iRowHeight; + int visible = d.height/iRowHeight; + int maximum = iRowCount+1; + if (visible<0) visible=0; + if (visible>maximum) visible=maximum; + if (value>(maximum-visible)) value=maximum-visible; + sbVert.setValues(value,visible,0,maximum); + iY=iRowHeight*value; + } + + synchronized void adjustHScroll() { + if (iRowHeight==0) return; + Dimension d = tc.getSize(); + int w=0; + for (int i=0; i-1 && !s.endsWith(": ")) + s = s.substring(index+2); // remove sequence number added by ListFilesRecursively + if (s.indexOf(File.separator)!=-1 || s.indexOf(".")!=-1) { + filePath = s; + Thread thread = new Thread(this, "Open"); + thread.setPriority(thread.getPriority()-1); + thread.start(); + } + } + } + + private void handleDoubleClickInOverlayList(String s) {//Marcel Boeglin 2019.10.09 + ImagePlus imp = WindowManager.getCurrentImage(); + if (imp==null) + return; + Overlay overlay = imp.getOverlay(); + if (overlay==null) + return; + String[] columns = s.split("\t"); + int index = (int)Tools.parseDouble(columns[0]); + Roi roi = overlay.get(index); + if (imp.isHyperStack()) { + int c = roi.getCPosition(); + int z = roi.getZPosition(); + int t = roi.getTPosition(); + c = c==0?imp.getChannel():c; + z = z==0?imp.getSlice():z; + t = t==0?imp.getFrame():t; + imp.setPosition(c, z, t); + } else { + int p = roi.getPosition(); + if (p>=1 && p<=imp.getStackSize()) + imp.setPosition(p); + } + imp.setRoi(roi); + } + + /** For better performance, open double-clicked files on + separate thread instead of on event dispatch thread. */ + public void run() { + if (filePath!=null) IJ.open(filePath); + } + + public void mouseExited (MouseEvent e) { + if(bDrag) { + setCursor(defaultCursor); + bDrag=false; + } + } + + public void mouseMoved (MouseEvent e) { + int x=e.getX(), y=e.getY(); + if(y<=iRowHeight) { + int xb=x; + x=x+iX-iGridWidth; + int i=iColCount-1; + for(;i>=0;i--) { + if(x>-7 && x<7) break; + x+=iColWidth[i]; + } + if(i>=0) { + if(!bDrag) { + setCursor(resizeCursor); + bDrag=true; + iXDrag=xb-iColWidth[i]; + iColDrag=i; + } + return; + } + } + if(bDrag) { + setCursor(defaultCursor); + bDrag=false; + } + } + + public void mouseDragged (MouseEvent e) { + if (e.isPopupTrigger() || e.isMetaDown()) + return; + int x=e.getX(), y=e.getY(); + if(bDrag && x=0 && selEnd0 && tw.mb.getMenu(mbSize-1).getLabel().equals("Results")) + tw.mb.remove(mbSize-1); + title = title2; + rt2.show(title); + } + Menus.updateWindowMenuItem(title1, title2); + if (Recorder.record) { + if (Recorder.scriptMode()) + Recorder.recordString("IJ.renameResults(\""+title1+"\", \""+title2+"\");\n"); + else + Recorder.record("Table.rename", title1, title2); + } + } + + void duplicate() { + ResultsTable rt2 = getOrCreateResultsTable(); + if (rt2==null) + return; + rt2 = (ResultsTable)rt2.clone(); + String title2 = IJ.getString("Title:", getNewTitle(title)); + if (!title2.equals("")) { + if (title2.equals("Results")) title2 = "Results2"; + rt2.show(title2); + } + } + + private String getNewTitle(String oldTitle) { + if (oldTitle==null) + return "Table2"; + String title2 = oldTitle; + if (title2.endsWith("-1") || title2.endsWith("-2")) + title2 = title2.substring(0,title.length()-2); + String title3 = title2+"-1"; + if (title3.equals(oldTitle)) + title3 = title2+"-2"; + return title3; + } + + void select(int x,int y) { + Dimension d = tc.getSize(); + if(iRowHeight==0 || x>d.width || y>d.height) + return; + int r=(y/iRowHeight)-1+iFirstRow; + int lineWidth = iGridWidth; + if (iColCount==1 && tc.fMetrics!=null && r>=0 && r=0 && r=iRowCount) + selOrigin = iRowCount-1; + } + tc.repaint(); + selLine=r; + Interpreter interp = Interpreter.getInstance(); + if (interp!=null && title.equals("Debug")) + interp.showArrayInspector(r); + } + + void extendSelection(int x,int y) { + Dimension d = tc.getSize(); + if(iRowHeight==0 || x>d.width || y>d.height) + return; + int r=(y/iRowHeight)-1+iFirstRow; + if(r>=0 && r tc.getSize().height) + return -1; + else + return (y/iRowHeight)-1+iFirstRow; + } + + /** + Copies the current selection to the system clipboard. + Returns the number of characters copied. + */ + public int copySelection() { + if (Recorder.record && title.equals("Results")) + Recorder.record("String.copyResults"); + if (selStart==-1 || selEnd==-1) + return copyAll(); + StringBuffer sb = new StringBuffer(); + ResultsTable rt2 = getResultsTable(); + boolean hasRowNumers = rt2!=null && rt2.showRowNumbers(); + if (Prefs.copyColumnHeaders && labels!=null && !labels.equals("") && selStart==0 && selEnd==iRowCount-1) { + if (hasRowNumers && Prefs.noRowNumbers) { + String s = labels; + int index = s.indexOf("\t"); + if (index!=-1) + s = s.substring(index+1, s.length()); + sb.append(s); + } else + sb.append(labels); + sb.append('\n'); + } + for (int i=selStart; i<=selEnd; i++) { + char[] chars = (char[])(vData.elementAt(i)); + String s = new String(chars); + if (s.endsWith("\t")) + s = s.substring(0, s.length()-1); + if (hasRowNumers && Prefs.noRowNumbers && labels!=null && !labels.equals("")) { + int index = s.indexOf("\t"); + if (index!=-1) + s = s.substring(index+1, s.length()); + sb.append(s); + } else + sb.append(s); + if (iselStart) sb.append('\n'); + } + String s = new String(sb); + Clipboard clip = getToolkit().getSystemClipboard(); + if (clip==null) return 0; + StringSelection cont = new StringSelection(s); + clip.setContents(cont,this); + if (s.length()>0) { + IJ.showStatus((selEnd-selStart+1)+" lines copied to clipboard"); + if (this.getParent() instanceof ImageJ) + Analyzer.setUnsavedMeasurements(false); + } + return s.length(); + } + + int copyAll() { + selectAll(); + int count = selEnd - selStart + 1; + if (count>0) + copySelection(); + resetSelection(); + unsavedLines = false; + return count; + } + + void cutSelection() { + if (selStart==-1 || selEnd==-1) + selectAll(); + copySelection(); + clearSelection(); + } + + /** Implements the Clear command. */ + public void doClear() { + if (getLineCount()>0 && selStart!=-1 && selEnd!=-1) + clearSelection(); + else if ("Results".equals(title)) + IJ.doCommand("Clear Results"); + else { + selectAll(); + clearSelection(); + } + } + + /** Deletes the selected lines. */ + public void clearSelection() { + if (selStart==-1 || selEnd==-1) { + if (getLineCount()>0) + IJ.error("Text selection required"); + return; + } + if (Recorder.record) { + if (Recorder.scriptMode()) + Recorder.recordString("IJ.deleteRows("+selStart+", "+selEnd+");\n"); + else { + if ("Results".equals(title)) + Recorder.record("Table.deleteRows", selStart, selEnd); + else + Recorder.record("Table.deleteRows", selStart, selEnd, title); + } + } + int first=selStart, last=selEnd, rows=iRowCount; + if (selStart==0 && selEnd==(iRowCount-1)) { + vData.removeAllElements(); + iRowCount = 0; + if (rt!=null) { + if (IJ.isResultsWindow() && IJ.getTextPanel()==this) { + Analyzer.setUnsavedMeasurements(false); + Analyzer.resetCounter(); + } else + rt.reset(); + } + } else { + int rowCount = iRowCount; + boolean atEnd = rowCount-selEnd<8; + int count = selEnd-selStart+1; + for (int i=0; i0) + tc.repaint(); + } + + /** Creates a selection and insures that it is visible. */ + public void setSelection (int startLine, int endLine) { + if (startLine>endLine) endLine = startLine; + if (startLine<0) startLine = 0; + if (endLine<0) endLine = 0; + if (startLine>=iRowCount) startLine = iRowCount-1; + if (endLine>=iRowCount) endLine = iRowCount-1; + selOrigin = startLine; + selStart = startLine; + selEnd = endLine; + int vstart = sbVert.getValue(); + int visible = sbVert.getVisibleAmount()-1; + if (startLine=vstart+visible) { + vstart = endLine - visible + 1; + if (vstart<0) vstart = 0; + sbVert.setValue(vstart); + iY=iRowHeight*vstart; + } + tc.repaint(); + } + + + + /** Writes all the text in this TextPanel to a file. */ + public void save(PrintWriter pw) { + resetSelection(); + if (labels!=null && !labels.equals("")) { + String labels2 = labels; + if (saveAsCSV) + labels2 = labels2.replaceAll("\t",","); + pw.println(labels2); + } + for (int i=0; i=2?getLine(iRowCount-2):null; + summarized = lastLine!=null && lastLine.startsWith("Max"); + } + String fileName = null; + if (rt!=null && rt.size()>0 && !summarized) { + if (path==null || path.equals("")) { + IJ.wait(10); + String name = isResults?"Results":title; + SaveDialog sd = new SaveDialog("Save Table", name, Prefs.defaultResultsExtension()); + fileName = sd.getFileName(); + if (fileName==null) + return false; + path = sd.getDirectory() + fileName; + } + rt.saveAndRename(path); + TextWindow tw = getTextWindow(); + String title2 = rt.getTitle(); + if (tw!=null && !"Results".equals(title)) { + tw.setTitle(title2); + Menus.updateWindowMenuItem(title, title2); + title = title2; + } + } else { + if (path.equals("")) { + IJ.wait(10); + boolean hasHeadings = !getColumnHeadings().equals(""); + String ext = isResults||hasHeadings?Prefs.defaultResultsExtension():".txt"; + SaveDialog sd = new SaveDialog("Save as Text", title, ext); + String file = sd.getFileName(); + if (file==null) + return false; + path = sd.getDirectory() + file; + } + PrintWriter pw = null; + try { + FileOutputStream fos = new FileOutputStream(path); + BufferedOutputStream bos = new BufferedOutputStream(fos); + pw = new PrintWriter(bos); + } + catch (IOException e) { + IJ.error("Save As>Text", e.getMessage()); + return true; + } + saveAsCSV = path.endsWith(".csv"); + save(pw); + saveAsCSV = false; + pw.close(); + } + if (isResults) { + Analyzer.setUnsavedMeasurements(false); + if (Recorder.record && !IJ.isMacro()) + Recorder.record("saveAs", "Results", path); + } else if (rt!=null) { + if (Recorder.record && !IJ.isMacro()) + Recorder.record("saveAs", "Results", path); + } else { + if (Recorder.record && !IJ.isMacro()) + Recorder.record("saveAs", "Text", path); + } + IJ.showStatus(""); + return true; + } + + /** Returns all the text as a string. */ + public synchronized String getText() { + if (vData==null) + return ""; + StringBuffer sb = new StringBuffer(); + if (labels!=null && !labels.equals("")) { + sb.append(labels); + sb.append('\n'); + } + for (int i=0; i=iRowCount) + throw new IllegalArgumentException("index out of range: "+index); + return new String((char[])(vData.elementAt(index))); + } + + /** Replaces the contents of the specified line, where 'index' + must be greater than or equal to zero and less than + the value returned by getLineCount(). */ + public void setLine(int index, String s) { + if (index<0 || index>=iRowCount) + throw new IllegalArgumentException("index out of range: "+index); + if (vData!=null) { + vData.setElementAt(s.toCharArray(), index); + tc.repaint(); + } + } + + /** Returns the index of the first selected line, or -1 + if there is no slection. */ + public int getSelectionStart() { + return selStart; + } + + /** Returns the index of the last selected line, or -1 + if there is no slection. */ + public int getSelectionEnd() { + return selEnd; + } + + /** Sets the ResultsTable associated with this TextPanel. */ + public void setResultsTable(ResultsTable rt) { + if (IJ.debugMode) IJ.log("setResultsTable: "+rt); + this.rt = rt; + if (!menusExtended) + extendMenus(); + } + + /** Returns the ResultsTable associated with this TextPanel, or null. */ + public ResultsTable getResultsTable() { + if (IJ.debugMode) IJ.log("getResultsTable: "+rt); + return rt; + } + + /** Returns the ResultsTable associated with this TextPanel, or + attempts to create one and returns the created table. */ + public ResultsTable getOrCreateResultsTable() { + if ((rt==null||rt.size()==0) && iRowCount>0 && labels!=null && !labels.equals("")) { + String tmpDir = IJ.getDir("temp"); + if (tmpDir==null) { + if (IJ.debugMode) IJ.log("getOrCreateResultsTable: tmpDir null"); + return null; + } + String path = tmpDir+"temp-table.csv"; + saveAs(path); + try { + rt = ResultsTable.open(path); + new File(path).delete(); + } catch (Exception e) { + rt = null; + if (IJ.debugMode) IJ.log("getOrCreateResultsTable: "+e); + } + } + if (IJ.debugMode) IJ.log("getOrCreateResultsTable: "+rt); + return rt; + } + + private void extendMenus() { + pm.addSeparator(); + addPopupItem("Rename..."); + addPopupItem("Duplicate..."); + addPopupItem("Apply Macro..."); + addPopupItem("Sort..."); + addPopupItem("Plot..."); + if (fileMenu!=null) { + fileMenu.add("Rename..."); + fileMenu.add("Duplicate..."); + } + if (editMenu!=null) { + editMenu.addSeparator(); + editMenu.add("Apply Macro..."); + } + menusExtended = true; + } + + public void scrollToTop() { + sbVert.setValue(0); + iY = 0; + for (int i=0; i0 && h>0) { + setSize(w, h); + setLocation(loc); + } else { + setSize(width, height); + if (!IJ.debugMode) + GUI.centerOnImageJScreen(this); + } + show(); + WindowManager.setWindow(this); + } + + /** + * Opens a new text window containing the contents of a text file. + * @param path the path to the text file + * @param width the width of the window in pixels + * @param height the height of the window in pixels + */ + public TextWindow(String path, int width, int height) { + super(""); + enableEvents(AWTEvent.WINDOW_EVENT_MASK); + textPanel = new TextPanel(); + textPanel.addKeyListener(IJ.getInstance()); + add("Center", textPanel); + if (openFile(path)) { + WindowManager.addWindow(this); + setSize(width, height); + show(); + WindowManager.setWindow(this); + } else + dispose(); + } + + void addMenuBar() { + mb = new MenuBar(); + if (Menus.getFontSize()!=0) + mb.setFont(Menus.getFont()); + Menu m = new Menu("File"); + m.add(new MenuItem("Save As...", new MenuShortcut(KeyEvent.VK_S))); + if (getTitle().equals("Results")) { + m.add(new MenuItem("Rename...")); + m.add(new MenuItem("Duplicate...")); + } + m.addActionListener(this); + mb.add(m); + textPanel.fileMenu = m; + m = new Menu("Edit"); + m.add(new MenuItem("Cut", new MenuShortcut(KeyEvent.VK_X))); + m.add(new MenuItem("Copy", new MenuShortcut(KeyEvent.VK_C))); + m.add(new MenuItem("Clear")); + m.add(new MenuItem("Select All", new MenuShortcut(KeyEvent.VK_A))); + m.addSeparator(); + m.add(new MenuItem("Find...", new MenuShortcut(KeyEvent.VK_F))); + m.add(new MenuItem("Find Next", new MenuShortcut(KeyEvent.VK_G))); + m.addActionListener(this); + mb.add(m); + textPanel.editMenu = m; + m = new Menu("Font"); + m.add(new MenuItem("Make Text Smaller")); + m.add(new MenuItem("Make Text Larger")); + m.addSeparator(); + antialiased = new CheckboxMenuItem("Antialiased", Prefs.get(FONT_ANTI, IJ.isMacOSX()?true:false)); + antialiased.addItemListener(this); + m.add(antialiased); + m.add(new MenuItem("Save Settings")); + m.addActionListener(this); + mb.add(m); + if (getTitle().equals("Results")) { + m = new Menu("Results"); + m.add(new MenuItem("Clear Results")); + m.add(new MenuItem("Summarize")); + m.add(new MenuItem("Distribution...")); + m.add(new MenuItem("Set Measurements...")); + m.add(new MenuItem("Sort...")); + m.add(new MenuItem("Plot...")); + m.add(new MenuItem("Options...")); + m.addActionListener(this); + mb.add(m); + } + setMenuBar(mb); + } + + /** + Adds one or more lines of text to the window. + @param text The text to be appended. Multiple + lines should be separated by \n. + */ + public void append(String text) { + textPanel.append(text); + } + + void setFont() { + if (font!=null) + textPanel.setFont(font, antialiased.getState()); + else + textPanel.setFont(new Font("SanSerif", Font.PLAIN, sizes[fontSize]), antialiased.getState()); + } + + boolean openFile(String path) { + OpenDialog od = new OpenDialog("Open Text File...", path); + String directory = od.getDirectory(); + String name = od.getFileName(); + if (name==null) + return false; + path = directory + name; + + IJ.showStatus("Opening: " + path); + try { + BufferedReader r = new BufferedReader(new FileReader(directory + name)); + load(r); + r.close(); + } + catch (Exception e) { + IJ.error(e.getMessage()); + return true; + } + textPanel.setTitle(name); + setTitle(name); + IJ.showStatus(""); + return true; + } + + /** Returns a reference to this TextWindow's TextPanel. */ + public TextPanel getTextPanel() { + return textPanel; + } + + /** Returns the ResultsTable associated with this TextWindow, or null. */ + public ResultsTable getResultsTable() { + return textPanel!=null?textPanel.getResultsTable():null; + } + + + /** Appends the text in the specified file to the end of this TextWindow. */ + public void load(BufferedReader in) throws IOException { + int count=0; + while (true) { + String s=in.readLine(); + if (s==null) break; + textPanel.appendLine(s); + } + } + + public void actionPerformed(ActionEvent evt) { + String cmd = evt.getActionCommand(); + if (cmd.equals("Make Text Larger")) + changeFontSize(true); + else if (cmd.equals("Make Text Smaller")) + changeFontSize(false); + else if (cmd.equals("Save Settings")) + saveSettings(); + else + textPanel.doCommand(cmd); + } + + public void processWindowEvent(WindowEvent e) { + super.processWindowEvent(e); + int id = e.getID(); + if (id==WindowEvent.WINDOW_CLOSING) + close(); + else if (id==WindowEvent.WINDOW_ACTIVATED && !"Log".equals(getTitle())) + WindowManager.setWindow(this); + } + + public void itemStateChanged(ItemEvent e) { + setFont(); + } + + public void close() { + close(true); + } + + /** Closes this TextWindow. Display a "save changes" dialog + if this is the "Results" window and 'showDialog' is true. */ + public void close(boolean showDialog) { + if (getTitle().equals("Results")) { + if (showDialog && !Analyzer.resetCounter()) + return; + IJ.setTextPanel(null); + Prefs.saveLocation(LOC_KEY, getLocation()); + Dimension d = getSize(); + Prefs.set(WIDTH_KEY, d.width); + Prefs.set(HEIGHT_KEY, d.height); + } else if (getTitle().equals("Log")) { + Prefs.saveLocation(LOG_LOC_KEY, getLocation()); + Dimension d = getSize(); + Prefs.set(LOG_WIDTH_KEY, d.width); + Prefs.set(LOG_HEIGHT_KEY, d.height); + IJ.setDebugMode(false); + IJ.log("\\Closed"); + IJ.notifyEventListeners(IJEventListener.LOG_WINDOW_CLOSED); + } else if (getTitle().equals("Debug")) { + Prefs.saveLocation(DEBUG_LOC_KEY, getLocation()); + } else if (textPanel!=null && textPanel.rt!=null) { + if (!saveContents()) return; + } + //setVisible(false); + dispose(); + WindowManager.removeWindow(this); + textPanel.flush(); + } + + public void rename(String title) { + textPanel.rename(title); + } + + boolean saveContents() { + int lineCount = textPanel.getLineCount(); + if (!textPanel.unsavedLines) lineCount = 0; + ImageJ ij = IJ.getInstance(); + boolean macro = IJ.macroRunning() || Interpreter.isBatchMode(); + boolean isResults = getTitle().contains("Results"); + if (lineCount>0 && !macro && ij!=null && !ij.quitting() && isResults) { + YesNoCancelDialog d = new YesNoCancelDialog(this, getTitle(), "Save "+lineCount+" measurements?"); + if (d.cancelPressed()) + return false; + else if (d.yesPressed()) { + if (!textPanel.saveAs("")) + return false; + } + } + textPanel.rt.reset(); + return true; + } + + void changeFontSize(boolean larger) { + int in = fontSize; + if (larger) { + fontSize++; + if (fontSize==sizes.length) + fontSize = sizes.length-1; + } else { + fontSize--; + if (fontSize<0) + fontSize = 0; + } + IJ.showStatus(sizes[fontSize]+" point"); + font = null; + setFont(); + } + + public static void setFont(String name, int style, int size) { + font = new Font(name,style,size); + } + + void saveSettings() { + Prefs.set(FONT_SIZE, fontSize); + Prefs.set(FONT_ANTI, antialiased.getState()); + IJ.showStatus("Font settings saved (size="+sizes[fontSize]+", antialiased="+antialiased.getState()+")"); + } + + public void focusGained(FocusEvent e) { + } + + public void focusLost(FocusEvent e) {} + +} diff --git a/src/ij/util/ArrayUtil.java b/src/ij/util/ArrayUtil.java new file mode 100644 index 0000000..7b715e8 --- /dev/null +++ b/src/ij/util/ArrayUtil.java @@ -0,0 +1,164 @@ +package ij.util; +import java.util.Arrays; + +public class ArrayUtil { + private int size; + float[] values; + boolean sorted; + + public void setSize(int si) { + size = si; + } + + /** + * constructeur + * + * @param size number of elements + */ + public ArrayUtil(int size) { + this.size = size; + values = new float[size]; + sorted = false; + } + + /** + * constructeur + * + * @param data float array + */ + public ArrayUtil(float[] data) { + this.size = data.length; + sorted = false; + values = data; + } + + /** + * put a value to a index + * + * @param pos position in the array + * @param value value to put + * @return false if position does not exist + */ + public boolean putValue(int pos, float value) { + if (pos max) { + max = values[i]; + } + } + return max; + } + + /** + * Variance value + * + * @return variance + */ + public double getVariance() { + if (size == 1) { + return 0; + } + + double total = 0; + double total2 = 0; + + for (int i = 0; i < size; i++) { + total += values[i]; + total2 += values[i] * values[i]; + } + + double var = (double) ((total2 - (total * total / size)) / (size - 1)); + return var; + } + + + /** + * information to be displayed + * + * @return text + */ + public String toString() { + String str = "{" + values[0]; + for (int i = 1; i < size; i++) { + str = str + ", " + values[i]; + } + return str + "}"; + } +} diff --git a/src/ij/util/DicomTools.java b/src/ij/util/DicomTools.java new file mode 100644 index 0000000..d48082e --- /dev/null +++ b/src/ij/util/DicomTools.java @@ -0,0 +1,169 @@ +package ij.util; +import ij.*; +import ij.process.*; +import ij.plugin.DICOM; + +/** DICOM utilities */ +public class DicomTools { + private static final int MAX_DIGITS = 5; + private static String[] sliceLabels; + + /** Sorts a DICOM stack by image number. */ + public static ImageStack sort(ImageStack stack) { + if (IJ.debugMode) IJ.log("Sorting by DICOM image number"); + if (stack.size()==1) return stack; + String[] strings = getSortStrings(stack, "0020,0013"); + if (strings==null) return stack; + StringSorter.sort(strings); + ImageStack stack2 = null; + if (stack.isVirtual()) + stack2 = ((VirtualStack)stack).sortDicom(strings, sliceLabels, MAX_DIGITS); + else + stack2 = sortStack(stack, strings); + return stack2!=null?stack2:stack; + } + + private static ImageStack sortStack(ImageStack stack, String[] strings) { + ImageProcessor ip = stack.getProcessor(1); + ImageStack stack2 = new ImageStack(ip.getWidth(), ip.getHeight(), ip.getColorModel()); + for (int i=0; i1) { + ImageStack stack = imp.getStack(); + String label = stack.getSliceLabel(imp.getCurrentSlice()); + if (label!=null && label.indexOf('\n')>0) metadata = label; + } + if (metadata==null) + metadata = (String)imp.getProperty("Info"); + return getTag(metadata, id); + } + + /** Returns the name of the specified DICOM tag id. */ + public static String getTagName(String id) { + return DICOM.getTagName(id); + } + + private static double getSeriesNumber(String tags) { + double series = getNumericTag(tags, "0020,0011"); + if (Double.isNaN(series)) series = 0; + return series; + } + + private static double getNumericTag(String hdr, String tag) { + String value = getTag(hdr, tag); + if (value==null) return Double.NaN; + int index3 = value.indexOf("\\"); + if (index3>0) + value = value.substring(0, index3); + return Tools.parseDouble(value); + } + + private static String getTag(String hdr, String tag) { + if (hdr==null) return null; + int index1 = hdr.indexOf(tag); + if (index1==-1) return null; + //IJ.log(hdr.charAt(index1+11)+" "+hdr.substring(index1,index1+20)); + if (hdr.charAt(index1+11)=='>') { + // ignore tags in sequences + index1 = hdr.indexOf(tag, index1+10); + if (index1==-1) return null; + } + index1 = hdr.indexOf(":", index1); + if (index1==-1) return null; + int index2 = hdr.indexOf("\n", index1); + String value = hdr.substring(index1+1, index2); + return value; + } + +} diff --git a/src/ij/util/FloatArray.java b/src/ij/util/FloatArray.java new file mode 100644 index 0000000..6d144dd --- /dev/null +++ b/src/ij/util/FloatArray.java @@ -0,0 +1,99 @@ +package ij.util; + +/** This class implements an expandable float array similar + * to an ArrayList or Vector but more efficient. + * Calls to this class are not synchronized. + * @author Michael Schmid +*/ + public class FloatArray { + private int size; + private float[] data; + + /** Creates a new expandable array with an initial capacity of 100. */ + public FloatArray() { + this(100); + } + + /** Creates a new expandable array with specified initial capacity. + * @throws IllegalArgumentException if the specified initial capacity is less than zero */ + public FloatArray(int initialCapacity) { + if (initialCapacity < 0) throw new IllegalArgumentException("Illegal FloatArray Capacity: "+initialCapacity); + data = new float[initialCapacity]; + } + + /** Returns the number of elements in the array. */ + public int size() { + return size; + } + + /** Removes all elements form this FloatArray. */ + public void clear() { + size = 0; + } + + /** Returns a float array containing all elements of this FloatArray. */ + public float[] toArray() { + float[] out = new float[size]; + System.arraycopy(data, 0, out, 0, size); + return out; + } + + /** Returns the element at the specified position in this FloatArray. + * @throws IndexOutOfBoundsException - if index is out of range (index < 0 || index >= size()). */ + public float get(int index) { + if (index<0 || index>= size) throw new IndexOutOfBoundsException("FloatArray Index out of Bounds: "+index); + return data[index]; + } + + /** Returns the last element of this FloatArray. + * @throws IndexOutOfBoundsException - if this FloatArray is empty */ + public float getLast() { + return get(size-1); + } + + /** Replaces the element at the specified position with the value given. + * @return the value previously at the specified position. + * @throws IndexOutOfBoundsException - if index is out of range (index < 0 || index >= size()). */ + public float set(int index, float value) { + if (index<0 || index>= size) throw new IndexOutOfBoundsException("FloatArray Index out of Bounds: "+index); + float previousValue = data[index]; + data[index] = value; + return previousValue; + } + + /** Appends the specified value to the end of this FloatArray. Returns the number of elements after adding. */ + public int add(float value) { + if (size >= data.length) { + float[] newData = new float[size*2 + 50]; + System.arraycopy(data, 0, newData, 0, size); + data = newData; + } + data[size++] = value; + return size; + } + + /** Appends the first n values from the specified array to this FloatArray. Returns the number of elements after adding. */ + public int add(float[] a, int n) { + if (size + n > data.length) { + float[] newData = new float[size*2 + n + 50]; + System.arraycopy(data, 0, newData, 0, size); + data = newData; + } + System.arraycopy(a, 0, data, size, n); + size += n; + return size; + } + + /** Deletes the last n element from this FloatArray. n may be larger than the number of elements; in that + * case, all elements are removed. */ + public void removeLast(int n) { + size -= Math.min(n, size); + } + + /** Trims the capacity of this FloatArray instance to be its current size, + * minimizing the storage of the FloatArray instance. */ + public void trimToSize() { + data = toArray(); + } + +} diff --git a/src/ij/util/FontUtil.java b/src/ij/util/FontUtil.java new file mode 100644 index 0000000..9b91158 --- /dev/null +++ b/src/ij/util/FontUtil.java @@ -0,0 +1,56 @@ +package ij.util; +import java.text.*; +import java.awt.*; + +/** This class contains static utility methods for replacing fonts that are not available on the + * current system. + */ + +public class FontUtil { + + /** Returns a font with the given family name or, if not available, a similar font, e.g. Helvetica replaced by Arial */ + public static Font getFont(String fontFamilyName, int style, float size) { + Font font = new Font(fontFamilyName, style, (int)size); + if (!font.getFamily().startsWith(fontFamilyName)) { + String[] similarFonts = getSimilarFontsList(fontFamilyName); + font = getFont(similarFonts, style, (int)size); + } + if (size != (int)size) + font = font.deriveFont(size); + return font; + } + + /** Returns the font for first element of the 'fontNames' array, where a Font Family Name starts with this name. + * E.g. if fontNames = {"Times New Roman", "Serif" and the system has no "Times New Roman", but finds "Serif", + * it would return a "Serif" font with suitable style and size */ + private static Font getFont(String[] fontNames, int style, int size) { + int iSize = (int)size; + Font font = null; + for (String fontName : fontNames) { + font = new Font(fontName, style, iSize); + if (font.getFamily().startsWith(fontName)) + break; + } + return font; + } + + /** For a few basic font types, gets a list of replacement font families + * Note that java's 'SansSerif' has wider characters (significantly different metrics) than the other + * sans-serif fonts in the list; thus it should be considered a fallback option only. + * Also note that some fonts (Times, Helvetica, Courier, Monospace, Serif) tend to truncate some + * diacritical marks when using FontMetrics.getHeight (as ImageJ does), e.g. the ring of the + * Angstrom symbol Å may be clipped (at least on Java 1.6/Mac) + */ + public static String[] getSimilarFontsList(String fontFamily) { + if (fontFamily.indexOf("Times")>=0 || fontFamily.indexOf("Serif")>=0) + return new String[]{"Times New Roman", "Times", "Liberation Serif", "Serif"}; + else if (fontFamily.indexOf("Arial")>=0 || fontFamily.indexOf("Helvetica")>=0 || fontFamily.indexOf("Sans")>=0) + return new String[]{"Arial", "Helvetica", "Helvetica Neue", "Liberation Sans", "SansSerif"}; + else if (fontFamily.indexOf("Courier")>=0 || fontFamily.indexOf("Mono")>=0) + return new String[]{"Courier New", "Courier", "Liberation Mono", "Monospaced"}; + else + return new String[]{fontFamily}; + } + + +} diff --git a/src/ij/util/IJMath.java b/src/ij/util/IJMath.java new file mode 100644 index 0000000..66d5707 --- /dev/null +++ b/src/ij/util/IJMath.java @@ -0,0 +1,34 @@ +package ij.util; + +public class IJMath { + + static double A = 0.05857895078654250866288; + static double B = -0.00626245895772819579; + static double C = -0.00299946450696036814; + static double D = 0.289389696496082416; + static double E = 0.0539962589851632982; + static double F = 0.00508516909930653109; + static double G = 0.000215969713046142876; + static double H = -0.000225663858340491571; + static double I = -3.06833213472529049e-7; + + /* This approximation of the error function erf has maximum absolute and relative errors below 1e-12 + * Except for low and high x, it is based on the type erf(x) = sgn(x) * sqrt(1-exp(-x^2*f(x)), + * with the function f(x) having a smooth transition from 4/pi = 1.273... at x=0 to 1 at high x */ + public static double erf(double x) { + double x2 = sqr(x); + if (x2 < 1e-8) + return (2/Math.sqrt(Math.PI))*x*(1+x2*(-1./3.+x2*(1./10.))); // Taylor series for low x + double erf = x2 > 36 ? 1 : //the polynomials go crazy for some large x2; erf(6) is 1 - 2e-17, less than ulp from 1 + Math.sqrt(1 - Math.exp(-sqr(x*((2/Math.sqrt(Math.PI) - A) + A * + (1 + x2*x2*(B + x2*(C + x2*(H + x2*I)))) / + (1 + x2*(D + x2*(E + x2*(F + x2*G)))) + )))); + return x>0 ? erf : -erf; //could be Math.copySign in Java 1.6 & up + } + + static double sqr(double x) { + return x*x; + } + +} diff --git a/src/ij/util/Java2.java b/src/ij/util/Java2.java new file mode 100644 index 0000000..59c2e5b --- /dev/null +++ b/src/ij/util/Java2.java @@ -0,0 +1,55 @@ +package ij.util; +import ij.*; +import ij.Prefs; +import java.awt.*; +import javax.swing.*; + +/** +This class contains static methods that use the Java 2 API. They are isolated +here to prevent errors when ImageJ is running on Java 1.1 JVMs. +*/ +public class Java2 { + + private static boolean lookAndFeelSet; + + public static void setAntialiased(Graphics g, boolean antialiased) { + Graphics2D g2d = (Graphics2D)g; + if (antialiased) + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + else + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + } + + public static void setAntialiasedText(Graphics g, boolean antialiasedText) { + Graphics2D g2d = (Graphics2D)g; + if (antialiasedText) + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + else + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); + } + + public static int getStringWidth(String s, FontMetrics fontMetrics, Graphics g) { + java.awt.geom.Rectangle2D r = fontMetrics.getStringBounds(s, g); + return (int)r.getWidth(); + } + + public static void setBilinearInterpolation(Graphics g, boolean bilinearInterpolation) { + Graphics2D g2d = (Graphics2D)g; + if (bilinearInterpolation) + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + else + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + } + + /** Sets the Swing look and feel to the system look and feel (Windows only). */ + public static void setSystemLookAndFeel() { + if (lookAndFeelSet || !IJ.isWindows()) return; + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch(Throwable t) {} + lookAndFeelSet = true; + IJ.register(Java2.class); + } + +} + diff --git a/src/ij/util/StringSorter.java b/src/ij/util/StringSorter.java new file mode 100644 index 0000000..9b4a626 --- /dev/null +++ b/src/ij/util/StringSorter.java @@ -0,0 +1,110 @@ +package ij.util; + +/** A simple QuickSort for String arrays. */ +public class StringSorter { + + /** Sorts the array. */ + public static void sort(String[] a) { + if (!alreadySorted(a)) + sort(a, 0, a.length - 1); + } + + static void sort(String[] a, int from, int to) { + int i = from, j = to; + String center = a[ (from + to) / 2 ]; + do { + while ( i < to && center.compareTo(a[i]) > 0 ) i++; + while ( j > from && center.compareTo(a[j]) < 0 ) j--; + if (i < j) {String temp = a[i]; a[i] = a[j]; a[j] = temp; } + if (i <= j) { i++; j--; } + } while(i <= j); + if (from < j) sort(a, from, j); + if (i < to) sort(a, i, to); + } + + static boolean alreadySorted(String[] a) { + for ( int i=1; i maxLen) { + maxLen = names[jj].length(); + } + } + int maxNums = maxLen / 2 + 1;//calc array sizes + int[][] numberStarts = new int[names.length][maxNums]; + int[][] numberLengths = new int[names.length][maxNums]; + int[] maxDigits = new int[maxNums]; + + //a) record position and digit count of 1st, 2nd, .. n-th number in string + for (int jj = 0; jj < names.length; jj++) { + String name = names[jj]; + boolean inNumber = false; + int nNumbers = 0; + int nDigits = 0; + for (int pos = 0; pos < name.length(); pos++) { + boolean isDigit = name.charAt(pos) >= '0' && name.charAt(pos) <= '9'; + if (isDigit) { + nDigits++; + if (!inNumber) { + numberStarts[jj][nNumbers] = pos; + inNumber = true; + } + } + if (inNumber && (!isDigit || (pos == name.length() - 1))) { + inNumber = false; + if (maxDigits[nNumbers] < nDigits) { + maxDigits[nNumbers] = nDigits; + } + numberLengths[jj][nNumbers] = nDigits; + nNumbers++; + nDigits = 0; + } + } + } + + //b) perform padding + for (int jj = 0; jj < names.length; jj++) { + String name = names[jj]; + int numIndex = 0; + StringBuilder destName = new StringBuilder(); + for (int srcPtr = 0; srcPtr < name.length(); srcPtr++) { + if (srcPtr == numberStarts[jj][numIndex]) { + int numLen = numberLengths[jj][numIndex]; + if (numLen > 0) { + for (int pad = 0; pad < (maxDigits[numIndex] - numLen); pad++) { + destName.append('0'); + } + } + numIndex++; + } + destName.append(name.charAt(srcPtr)); + } + paddedNames[jj] = destName.toString(); + } + return paddedNames; + } + +} diff --git a/src/ij/util/ThreadUtil.java b/src/ij/util/ThreadUtil.java new file mode 100644 index 0000000..022c35f --- /dev/null +++ b/src/ij/util/ThreadUtil.java @@ -0,0 +1,149 @@ +package ij.util; +import java.util.concurrent.*; + +public class ThreadUtil { + + /** Start all given threads and wait on each of them until all are done. + * From Stephan Preibisch's Multithreading.java class. See: + * http://repo.or.cz/w/trakem2.git?a=blob;f=mpi/fruitfly/general/MultiThreading.java;hb=HEAD + * @param threads + */ + public static void startAndJoin(Thread[] threads) { + for (int ithread = 0; ithread < threads.length; ++ithread) { + threads[ithread].setPriority(Thread.NORM_PRIORITY); + threads[ithread].start(); + } + + try { + for (int ithread = 0; ithread < threads.length; ++ithread) { + threads[ithread].join(); + } + } catch (InterruptedException ie) { + throw new RuntimeException(ie); + } + } + + public static Thread[] createThreadArray(int nb) { + if (nb == 0) { + nb = getNbCpus(); + } + Thread[] threads = new Thread[nb]; + + return threads; + } + + public static Thread[] createThreadArray() { + return createThreadArray(0); + } + + public static int getNbCpus() { + return Runtime.getRuntime().availableProcessors(); + } + + /*--------------------------------------------------------------------------*/ + /* The following is for parallelization using a ThreadPool, which avoids the + * overhead of creating threads, and is therefore faster if each thread has + * only a short task to perform */ + + /** The threadPoolExecutor holds at least as many threads for parallel execution as the number of + * processors; additional threads are added as required. These additional threads will be + * terminated if idle for 120 seconds. */ + public static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( + Runtime.getRuntime().availableProcessors(), //minimum number of threads + Integer.MAX_VALUE, //maximum number of threads + 120, //unused threads are terminated after this time + TimeUnit.SECONDS, + new SynchronousQueue() //requests will be processed immediately (not a real queue) + ); + + /** Starts all callables for parallel execution (using a ThreadPoolExecutor) + * and waits until each of them has finished. + * If the current thread is interrupted, each of the callables gets + * cancelled and interrupted. Also in that case, waits until all callables have + * finished. The 'interrupted' status of the current thread is + * preserved, as required for preview in an ImageJ ExtendedPlugInFilter. + * Note that ImageJ requires that all callables can run concurrently, + * and none of them must stay in the queue while others run. + * (This is required by the RankFilters, where the threads are not independent) + * @param callables Array of tasks. If no return value is needed, + * best use Callable (then the Void call() method + * should return null). If the array size is 1, the call() method + * is executed in the current thread. + * @return Array of the java.util.concurrent.Futures, + * corresponding to the callables. If the call methods of the callables + * return results, the get() methods of these Futures may be used to get the results. + */ + public static Future[] startAndJoin(Callable[] callables) { + if (callables.length == 1) { //special case: call in current thread and create a Future + Object callResult = null; + try { + callResult = callables[0].call(); + } catch (Exception e) { + ij.IJ.handleException(e); + } + final Object result = callResult; + Future[] futures = new Future[] { + new Future() { + public boolean cancel(boolean mayInterruptIfRunning) {return false;} + public Object get() {return result;} + public Object get(long timeout, TimeUnit unit) {return result;} + public boolean isCancelled() {return false;} + public boolean isDone() {return true;} + } + }; + return futures; + } else { + Future[] futures = start(callables); + joinAll(futures); + return futures; + } + } + + /** Starts all callables for parallel execution (using a ThreadPoolExecutor) + * without waiting for the results. + * @param callables Array of tasks; these might be Callable + * if no return value is needed (then the call methods should + * return null). + * @return Array of the java.util.concurrent.Futures, + * corresponding to the callables. The futures may be used to wait for + * completion of the callables or cancel them. + * If the call methods of the callables return results, these Futures + * may be used to get the results. + */ + public static Future[] start(Callable[] callables) { + Future[] futures = new Future[callables.length]; + for (int i=0; iCallables corresponding to the + * Futures given. + * If the current thread is interrupted, each of the Callables + * gets cancelled and interrupted. Also in that case, this method waits + * until all callables have finished. + * The 'interrupted' status of the current thread is preserved, + * as required for preview in an ImageJ ExtendedPlugInFilter. + */ + public static void joinAll(Future[] futures) { + boolean interrupted = false; + for (int i=0; i=1; pos--) { + buf7[pos] = hexDigits[i&0xf]; + i >>>= 4; + } + return new String(buf7); + } + + /** Converts a float to an 9 byte hex string starting with '#'. */ + public static String f2hex(float f) { + int i = Float.floatToIntBits(f); + char[] buf9 = new char[9]; + buf9[0] = '#'; + for (int pos=8; pos>=1; pos--) { + buf9[pos] = hexDigits[i&0xf]; + i >>>= 4; + } + return new String(buf9); + } + + /** Converts an int to a zero-padded hex string of fixed length 'digits'. + * If the number is too high, it gets truncated, keeping only the lowest 'digits' characters. */ + public static String int2hex(int i, int digits) { + char[] buf = new char[digits]; + for (int pos=buf.length-1; pos>=0; pos--) { + buf[pos] = hexDigits[i&0xf]; + i >>>= 4; + } + return new String(buf); + } + + public static ImageStatistics getStatistics(double[] a) { + ImageProcessor ip = new FloatProcessor(a.length, 1, a); + return ip.getStats(); + } + + public static double[] getMinMax(double[] a) { + double min = a[0]; + double max = a[0]; + double value; + for (int i=1; imax) + max = value; + } + double[] minAndMax = new double[2]; + minAndMax[0] = min; + minAndMax[1] = max; + return minAndMax; + } + + public static double[] getMinMax(float[] a) { + double min = a[0]; + double max = a[0]; + double value; + for (int i=1; imax) + max = value; + } + double[] minAndMax = new double[2]; + minAndMax[0] = min; + minAndMax[1] = max; + return minAndMax; + } + + /** Converts the float array 'a' to a double array. */ + public static double[] toDouble(float[] a) { + int len = a.length; + double[] d = new double[len]; + for (int i=0; iString. + * + * @param s the string to be parsed. + * @param defaultValue the value returned if s + * does not contain a parsable double + * @return The double value represented by the string argument or + * defaultValue if the string does not contain a parsable double + */ + public static double parseDouble(String s, double defaultValue) { + if (s==null) + return defaultValue; + try { + defaultValue = Double.parseDouble(s); + } catch (NumberFormatException e) {} + return defaultValue; + } + + /** + * Returns a double containg the value represented by the + * specified String. + * + * @param s the string to be parsed. + * @return The double value represented by the string argument or + * Double.NaN if the string does not contain a parsable double + */ + public static double parseDouble(String s) { + return parseDouble(s, Double.NaN); + } + + /** Returns the number of decimal places needed to display a + number, or -2 if exponential notation should be used. */ + public static int getDecimalPlaces(double n) { + if ((int)n==n || Double.isNaN(n)) + return 0; + String s = ""+n; + if (s.contains("E")) + return -2; + while (s.endsWith("0")) + s = s.substring(0,s.length()-1); + int index = s.indexOf("."); + if (index==-1) return 0; + int digits = s.length() - index - 1; + if (digits>4) digits=4; + return digits; + } + + /** Returns the number of decimal places needed to display two numbers, + or -2 if exponential notation should be used. */ + public static int getDecimalPlaces(double n1, double n2) { + if ((int)n1==n1 && (int)n2==n2) + return 0; + int digits = getDecimalPlaces(n1); + int digits2 = getDecimalPlaces(n2); + if (digits==0) + return digits2; + if (digits2==0) + return digits; + if (digits<0 || digits2<0) + return digits; + if (digits2>digits) + digits = digits2; + return digits; + } + + /** Splits a string into substrings using the default delimiter set, + which is " \t\n\r" (space, tab, newline and carriage-return). */ + public static String[] split(String str) { + return split(str, " \t\n\r"); + } + + /** Splits a string into substring using the characters + contained in the second argument as the delimiter set. */ + public static String[] split(String str, String delim) { + if (delim.equals("\n")) + return splitLines(str); + StringTokenizer t = new StringTokenizer(str, delim); + int tokens = t.countTokens(); + String[] strings; + if (tokens>0) { + strings = new String[tokens]; + for(int i=0; i() { + public int compare(final Integer o1, final Integer o2) { + return data[o1].compareTo(data[o2]); + } + }); + int[] indexes2 = new int[n]; + for (int i=0; i() { + public int compare(final Integer o1, final Integer o2) { + return data[o1].compareToIgnoreCase(data[o2]); + } + }); + int[] indexes2 = new int[n]; + for (int i=0; i + * Wildcard characters can be escaped (default: by an '\').

+ * This class always matches for the whole word.

+ * Examples: + *

+ * WildcardMatch wm = new WildcardMatch();
+ * System.out.println(wm.match("CfgOptions.class", "C*.class"));	  // true
+ * System.out.println(wm.match("CfgOptions.class", "?gOpti*c?as?"));  // false
+ * System.out.println(wm.match("CfgOptions.class", "??gOpti*c?ass")); // true
+ * System.out.println(wm.match("What's this?",	   "What*\\?"));	  // true
+ * System.out.println(wm.match("What's this?",	   "What*?"));		  // true
+ * System.out.println(wm.match("A \\ backslash", "*\\\\?back*"));	  // true
+ * 
+ */ +public class WildcardMatch { + + public WildcardMatch() { + } + + public WildcardMatch(char singleChar, char multipleChars) { + this.sc = singleChar; + this.mc = multipleChars; + } + + /** + * Sets new characters to be used as wildcard characters, overriding the + * the default of '?' for any single character match and '*' for any + * amount of characters, including 0 characters. + * @param singleChar The char used to match exactly ONE character. + * @param multipleChars The char used to match any amount of characters + * including o characters. + */ + public void setWildcardChars(char singleChar, char multipleChars) { + this.sc = singleChar; + this.mc = multipleChars; + } + + /** + * Sets the new character to be used as an escape character, overriding the + * the default of '\'. + * @param escapeChar The char used to match escape wildcard characters. + */ + public void setEscapeChar(char escapeChar) { + this.ec = escapeChar; + } + + /** + * Returns the character used to specify exactly one character. + * @return Wildcard character matching any single character. + */ + public char getSingleWildcardChar() { + return sc; + } + + /** + * Returns the character used to specify any amount of characters. + * @return Wildcard character matching any count of characters. + */ + public char getMultipleWildcardChar() { + return mc; + } + + /** + * Returns the character used to escape the wildcard functionality of a + * wildcard character. If two escape characters are used in sequence, they + * mean the escape character itself. It defaults to '\'. + * @return Escape character. + */ + public char getEscapeChar() { + return ec; + } + + /** + * Makes pattern matching case insensitive. + * @param caseSensitive false for case insensitivity. Default is case + * sensitive match. + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + /** + * Returns the current state of case sensitivity. + * @return true for case sensitive pattern matching, false otherwise. + */ + public boolean getCaseSensitive() { + return caseSensitive; + } + boolean preceededByMultipleChar = false; + + /** + * Matches a string against a pattern with wildcards. Two wildcard types + * are supported: single character match (defaults to '?') and ANY + * character match ('*'), matching any count of characters including 0. + * Wildcard characters may be escaped by an escape character, which + * defaults to '\'. + * @param s The string, in which the search should be performed. + * @param pattern The search pattern string including wildcards. + * @return true, if string 's' matches 'pattern'. + */ + public boolean match(String s, String pattern) { + preceededByMultipleChar = false; + isEscaped = false; + if (!caseSensitive) { + pattern = pattern.toLowerCase(); + s = s.toLowerCase(); + } + int offset = 0; + + while (true) { + String ps = getNextSubString(pattern); + int len = ps.length(); + pattern = pattern.substring(len + escCnt); + + if (len > 0 && isWildcard(ps.charAt(0)) && escCnt == 0) { + offset = getWildcardOffset(ps.charAt(0)); + if (isSingleWildcardChar(ps.charAt(0))) { + s = s.substring(1); +// This is not yet enough: If a '*' precedes '?', 's' might be SHORTER +// than seen here, for this we need preceededByMultipleChar variable... + } + if (pattern.length() == 0) { + return s.length() <= offset || preceededByMultipleChar; + } + } else { + int idx = s.indexOf(ps); + if (idx < 0 || (idx > offset && !preceededByMultipleChar)) { + return false; + } + s = s.substring(idx + len); + preceededByMultipleChar = false; + } + if (pattern.length() == 0) { + return s.length() == 0; + } + } + } + private char sc = '?'; + private char mc = '*'; + private char ec = '\\'; // Escape character + private boolean caseSensitive = true; + private boolean isEscaped = false; + private int escCnt = 0; + + private String getNextSubString(String pat) { + escCnt = 0; + if ("".equals(pat)) { + return ""; + } + if (isWildcard(pat.charAt(0))) { + // if '?' is preceeded by '*', we need special considerations: + if (pat.length() > 1 && !isSingleWildcardChar(pat.charAt(0)) && isSingleWildcardChar(pat.charAt(1))) { + preceededByMultipleChar = true; + } + return pat.substring(0, 1); + } else { + String s = ""; + int i = 0; + while (i < pat.length() && !isWildcard(pat.charAt(i), isEscaped)) { + if (pat.charAt(i) == ec) { + isEscaped = !isEscaped; + if (!isEscaped) { + s += pat.charAt(i); + } + escCnt++; + } else if (isWildcard(pat.charAt(i))) { + isEscaped = false; + s += pat.charAt(i); + } else { + s += pat.charAt(i); + } + i++; + } + return s; + } + } + + private boolean isWildcard(char c, boolean isEsc) { + return !isEsc && isWildcard(c); + } + + private boolean isSingleWildcardChar(char c) { + return c == sc; + } + + private boolean isWildcard(char c) { + return c == mc || c == sc; + } + + private int getWildcardOffset(char c) { + if (c == mc) { + return Integer.MAX_VALUE; + } + return 0; + } +} \ No newline at end of file