Click here to show toolbars of the Web Online Help System: show toolbars |
//******************************************************************************* // // File name: CanInspection.cpp // Location: See Matrox Example Launcher in the MIL Control Center // // // Synopsis: Demonstrates continuous inspection of cans using 3d data. // // Copyright (C) Matrox Electronic Systems Ltd., 1992-2016. // All Rights Reserved #include "CanInspection.h" //**************************************************************************** // Example description. //**************************************************************************** void PrintHeader() { MosPrintf(MIL_TEXT("[EXAMPLE NAME]\n")); MosPrintf(MIL_TEXT("CanInspection\n\n")); MosPrintf(MIL_TEXT("[SYNOPSIS]\n")); MosPrintf(MIL_TEXT("This example demonstrates the continuous inspection of cans using") MIL_TEXT("\n3d sheet-of-light profiling. The system consists of two cameras") MIL_TEXT("\nand one laser. Note that during the setup of the grab, the ") MIL_TEXT("cameras\nwere synchronized so the same laser scan was provided to ") MIL_TEXT("all\ncameras at the same time.\n")); MosPrintf(MIL_TEXT("\n\n")); MosPrintf(MIL_TEXT("[MODULES USED]\n")); MosPrintf(MIL_TEXT("Modules used: Application, system, display, buffer, ") MIL_TEXT("graphic, \nimage processing, calibration, ")); MosPrintf(MIL_TEXT("3d reconstruction, model finder. \n")); } // Constants. static const MIL_INT MAP_SIZE_X = 400; static const MIL_INT MAP_SIZE_Y = 1020; //***************************************************************************** // Main. //***************************************************************************** int MosMain(void) { PrintHeader(); // Allocate the MIL application. MIL_ID MilApplication = MappAlloc(M_NULL, M_DEFAULT, M_NULL); // Initialization. CExampleManagerFor3D* pExampleMngrFor3D = MakeExampleManager(); if (!pExampleMngrFor3D) { MappFree(MilApplication); return -1; } MosPrintf(MIL_TEXT("Press <Enter> to start.\n\n")); MosGetch(); //....................................................................... // 1. To calibrate the setup, the first step is to calibrate the cameras. // Camera calibration specifications. const MIL_DOUBLE COL_SPACING [NUM_CAMERAS] = { 5.0, 5.0 }; const MIL_DOUBLE ROW_SPACING [NUM_CAMERAS] = { 5.0, 5.0 }; const MIL_INT NB_ROWS [NUM_CAMERAS] = { 16, 16 }; const MIL_INT NB_COLS [NUM_CAMERAS] = { 15, 15 }; const MIL_DOUBLE CORNER_HINT_X [NUM_CAMERAS] = { 1200, 750 }; const MIL_DOUBLE CORNER_HINT_Y [NUM_CAMERAS] = { 300, 200 }; const MIL_DOUBLE OFFSET_Z [NUM_CAMERAS] = { 0.0, 0.0 }; const MIL_INT64 CALIBRATION_TYPE [NUM_CAMERAS] = { M_CHESSBOARD_GRID, M_CHESSBOARD_GRID}; const MIL_CONST_TEXT_PTR GRID_IMG_FILENAME [NUM_CAMERAS] = { EX_PATH("Cam1_grid.mim"), EX_PATH("Cam2_grid.mim") }; // Initialize data. SCameraCalibrationInfo CAMERA_CALIBRATION_INFO[NUM_CAMERAS]; for (MIL_INT c = 0; c < NUM_CAMERAS; c++) { SCameraCalibrationInfo& CCI = CAMERA_CALIBRATION_INFO[c]; CCI.CornerHintX = CORNER_HINT_X[c]; CCI.CornerHintY = CORNER_HINT_Y[c]; CCI.OffsetZ = OFFSET_Z[c]; CCI.NbRows = NB_ROWS[c]; CCI.NbCols = NB_COLS[c]; CCI.RowSpacing = ROW_SPACING[c]; CCI.ColSpacing = COL_SPACING[c]; CCI.CalibrationType = CALIBRATION_TYPE[c]; CCI.GridImageFilename = GRID_IMG_FILENAME[c]; CCI.Relocate = NO_RELOCATE; CCI.RelocatedGridImageFilename = NULL; } //................................. // 1.1 Execute cameras calibration. MIL_ID CameraCalibrations[NUM_CAMERAS]; bool CameraCalibrationOk = pExampleMngrFor3D->CalibrateCameras(CAMERA_CALIBRATION_INFO, NUM_CAMERAS, &CameraCalibrations[0]); // 2. Then continue to calibrate the laser planes (sheets-of-light). if(CameraCalibrationOk) { MosPrintf(MIL_TEXT("Press <Enter> to calibrate laser planes.\n\n")); MosGetch(); // Sheet-of-Light (laser plane) calibration const MIL_INT NUM_REF_PLANES = 5; const MIL_DOUBLE CAL_MIN_CONTRAST [NUM_CAMERAS] = { 100, 100 }; const MIL_INT CAL_NB_REF_PLANES [NUM_CAMERAS] = { NUM_REF_PLANES, NUM_REF_PLANES }; const MIL_INT CAL_SCAN_ORIENTATION[NUM_CAMERAS] = { M_HORIZONTAL, M_HORIZONTAL }; const MIL_INT CAL_PEAK_WIDTH [NUM_CAMERAS] = { 5, 5 }; const MIL_INT CAL_PEAK_WIDTH_DELTA[NUM_CAMERAS] = { 3, 3 }; const MIL_INT LASER_LABELS [NUM_CAMERAS] = { 1, 1 }; const MIL_INT CAMERA_LABELS [NUM_CAMERAS] = { 1, 2 }; const MIL_DOUBLE PLANE_Z[NUM_CAMERAS][MAX_NB_REF_PLANES] = { { 11.72, 5.86, 0.00, -5.86, -11.72 }, { 11.72, 5.86, 0.00, -5.86, -11.72 } }; const SRefPlaneInfo LASER_CALIBRATION_PLANES[NUM_CAMERAS][MAX_NB_REF_PLANES] = { { // first camera // RefImageName Zs { EX_PATH("Cam1RefPlanes/Cam1_laser_h-2.mim"), PLANE_Z[0][0] }, { EX_PATH("Cam1RefPlanes/Cam1_laser_h-1.mim"), PLANE_Z[0][1] }, { EX_PATH("Cam1RefPlanes/Cam1_laser_h0.mim") , PLANE_Z[0][2] }, { EX_PATH("Cam1RefPlanes/Cam1_laser_h1.mim") , PLANE_Z[0][3] }, { EX_PATH("Cam1RefPlanes/Cam1_laser_h2.mim") , PLANE_Z[0][4] } }, { // second camera // RefImageName Zs { EX_PATH("Cam2RefPlanes/Cam2_laser_h-2.mim"), PLANE_Z[0][0] }, { EX_PATH("Cam2RefPlanes/Cam2_laser_h-1.mim"), PLANE_Z[0][1] }, { EX_PATH("Cam2RefPlanes/Cam2_laser_h0.mim") , PLANE_Z[0][2] }, { EX_PATH("Cam2RefPlanes/Cam2_laser_h1.mim") , PLANE_Z[0][3] }, { EX_PATH("Cam2RefPlanes/Cam2_laser_h2.mim") , PLANE_Z[0][4] } } }; const MIL_INT NUM_LASERS_PER_IMAGE = 1; SCameraLaserInfo LASER_CALIBRATION_INFO[NUM_CAMERAS * NUM_LASERS_PER_IMAGE]; for(MIL_INT c = 0; c < NUM_CAMERAS; c++) { SCameraLaserInfo& LCI = LASER_CALIBRATION_INFO[c]; LCI.NumLasersPerImage = NUM_LASERS_PER_IMAGE; LCI.NumRefPlanes = NUM_REF_PLANES; LCI.CalMinContrast = CAL_MIN_CONTRAST[c]; LCI.CalNbRefPlanes = CAL_NB_REF_PLANES[c]; LCI.CalScanOrientation = CAL_SCAN_ORIENTATION[c]; LCI.CalPeakWidthNominal= CAL_PEAK_WIDTH[c]; LCI.CalPeakWidthDelta = CAL_PEAK_WIDTH_DELTA[c]; for(MIL_INT l = 0; l < LCI.CalNbRefPlanes; l++) { LCI.LaserCalibrationPlanes[l] = LASER_CALIBRATION_PLANES[c][l]; } LCI.LaserLabel = LASER_LABELS[c]; LCI.CameraLabel = CAMERA_LABELS[c]; LCI.LineExtractionInROI = eLineNoROI; } //............................................................ // 2.1 Execute the calibration of the laser planes. // Generates the needed calibrated camera-laser pair contexts. MIL_ID CameraLaserCtxts[NUM_CAMERAS * NUM_LASERS_PER_IMAGE]; bool SheetOfLightOk = pExampleMngrFor3D->CalibrateSheetOfLight(&LASER_CALIBRATION_INFO[0], &CameraCalibrations[0], &CameraLaserCtxts[0]); if (SheetOfLightOk) { // Map generation specifications. const MIL_DOUBLE D3D_DISPLAY_REFRESH_PER_SEC = 0.9; // 3d Display FPS const MIL_DOUBLE D3D_DISPLAY_LOOK_AT_X = 21.26; const MIL_DOUBLE D3D_DISPLAY_LOOK_AT_Y = 123.16; const MIL_DOUBLE D3D_DISPLAY_LOOK_AT_Z = 28.41; const MIL_DOUBLE D3D_DISPLAY_EYE_DIST = 517.23; const MIL_DOUBLE D3D_DISPLAY_EYE_THETA = 43.55; const MIL_DOUBLE D3D_DISPLAY_EYE_PHI = 56.72; const MIL_INT CAMERA_MAP_MIN_CONTRAST[] = { 100, 100}; const MIL_INT CAMERA_MAP_PEAK_WIDTH[] = { 4 , 4 }; const MIL_INT CAMERA_MAP_PEAK_DELTA[] = { 16 , 16 }; const MIL_DOUBLE CAMERA_MAP_SCAN_SPEED[] = { 0.2927, 0.2927 }; const MIL_DOUBLE CAMERA_MAX_FRAMES = 2670; const MIL_DOUBLE CAMERA_DISPLACEMENT_MODE = M_CURRENT; // Visualization volume information. SMapGeneration MapData; MapData.BoxCornerX = - 26.90; MapData.BoxCornerY = 5.47; MapData.BoxCornerZ = 1.00; MapData.BoxSizeX = 118.00; MapData.BoxSizeY = 300.00; MapData.BoxSizeZ = - 13.00; MapData.MapSizeX = MAP_SIZE_X; MapData.MapSizeY = MAP_SIZE_Y; MapData.PixelSizeX = MapData.BoxSizeX / (MAP_SIZE_X - 1.0); MapData.PixelSizeY = MapData.BoxSizeY / (MAP_SIZE_Y - 1.0); MapData.GrayScaleZ = MapData.BoxSizeZ / 65534.0; MapData.IntensityMapType = 8 + M_UNSIGNED; MapData.SetExtractOverlap= true; MapData.ExtractOverlap = M_MIN; MapData.FillXThreshold = 1.0; MapData.FillYThreshold = 1.0; // Scan and analyze information. SPointCloudAcquisitionInfo SCAN_INFO = { // SD3DSysInfo { D3D_DISPLAY_REFRESH_PER_SEC, SHOW_COLOR, D3D_DISPLAY_LOOK_AT_X, D3D_DISPLAY_LOOK_AT_Y, D3D_DISPLAY_LOOK_AT_Z, D3D_DISPLAY_EYE_DIST, D3D_DISPLAY_EYE_THETA, D3D_DISPLAY_EYE_PHI }, { CAMERA_MAP_MIN_CONTRAST[0] , CAMERA_MAP_MIN_CONTRAST[1] }, { CAMERA_MAP_PEAK_WIDTH[0] , CAMERA_MAP_PEAK_WIDTH[1] }, { CAMERA_MAP_PEAK_DELTA[0] , CAMERA_MAP_PEAK_DELTA[1] }, { CAMERA_MAP_SCAN_SPEED[0] , CAMERA_MAP_SCAN_SPEED[1] }, CAMERA_MAX_FRAMES, CAMERA_DISPLACEMENT_MODE, eLineNoROI, // SLineExtractionInROI { 0, 0, 0, 0 }, MapData, { // DigInfo // DigFormat SX SY SB Type Nb Frames { EX_PATH("Cam1_cans.avi"), 0, 0, 0, 0, 0 }, { EX_PATH("Cam2_cans.avi"), 0, 0, 0, 0, 0 } }, MIL_TEXT("All scan results are continuously accumulated into a single result\n") MIL_TEXT("object. Each can is inspected when most of it has been scanned.\n") MIL_TEXT("Color legend:\n \tGray\t \t= missing data\n \tDark blue \t= ") MIL_TEXT("minimum height\n \tGreen, Yellow \t= middle height\n \tDark ") MIL_TEXT("red \t= maximum height\n\n") }; // Update some information from the sequences on disk. for(MIL_INT d = 0; d < NUM_CAMERAS; d++) { SCAN_INFO.DigInfo[d].UpdateInfoFromDisk(); } //.................................................... // 3. Acquire a 3d point cloud by scanning the object. // The point cloud container will hold one point cloud per camera-laser pair. // Perform the analysis during the acquisition continuously. CContinuousCanInspection ProcObj(MapData); const MIL_INT NB_FRAME_FOR_ANALYSIS = 20; MIL_ID PointCloudContainer = M_NULL; pExampleMngrFor3D->AcquirePointCloud(eScanWithContinuousAnalysis, &SCAN_INFO, CameraLaserCtxts, &PointCloudContainer, &ProcObj, NB_FRAME_FOR_ANALYSIS); // Free camera-laser contexts. for (MIL_INT c = 0; c < NUM_CAMERAS; c++) { for (MIL_INT l = 0; l < NUM_LASERS_PER_IMAGE; l++) { MIL_ID& CameraLaserCtx = CameraLaserCtxts[(c*NUM_LASERS_PER_IMAGE) + l]; if (CameraLaserCtx != M_NULL) { M3dmapFree(CameraLaserCtx); CameraLaserCtx = M_NULL; } } } M3dmapFree(PointCloudContainer); } } else { // A problem occurred calibrating the cameras. MosPrintf(MIL_TEXT("Press <Enter> to end.\n\n")); MosGetch(); } // Free camera calibrations. for (MIL_INT c = 0; c < NUM_CAMERAS; c++) { if(CameraCalibrations[c] != M_NULL) { McalFree(CameraCalibrations[c]); CameraCalibrations[c] = M_NULL; } } delete pExampleMngrFor3D; pExampleMngrFor3D = NULL; // Free the MIL application. MappFree(MilApplication); return 0; } //******************************************************************************* // Function that analyzes the scanned object. //******************************************************************************* void CContinuousCanInspection::Analyze(SCommonAnalysisObjects& CommonAnalysisObjects) { // Processing constants. const MIL_INT NUM_SMOOTH = 99; const MIL_DOUBLE BINARIZE_THRESHOLD = 62; const MIL_INT CAN_DELTA_X = 60; const MIL_INT CAN_DELTA_Y = 60; const MIL_INT MAX_CAN_MISSING_DATA = 1000; const MIL_DOUBLE MIN_TAB_HEIGHT = 2.5; const MIL_DOUBLE MAX_TAB_HEIGHT = 5.5; const MIL_INT YOffset = 30; // Color specifications. const MIL_DOUBLE PROC_TEXT_COLOR = M_COLOR_GREEN; const MIL_DOUBLE PROC_PASS_COLOR = M_COLOR_GREEN; const MIL_DOUBLE PROC_FAIL_COLOR = M_COLOR_RED; const MIL_INT PROC_TEXT_OFFSET_X = 320; MIL_ID MilSystem = CommonAnalysisObjects.MilSystem; MIL_ID MilGraphics = CommonAnalysisObjects.MilGraphics; MIL_ID MilGraphicList = CommonAnalysisObjects.MilGraphicList; MIL_ID MilDepthMap = CommonAnalysisObjects.MilDepthMap; CMILDisplayManager* MilResultsDisplay = CommonAnalysisObjects.MilResultsDisplay; // Disable update to display. MilResultsDisplay->Control(M_UPDATE, M_DISABLE); // Disable graphics list update. MdispControl(MilResultsDisplay->GetDisplayID(), M_UPDATE_GRAPHIC_LIST, M_DISABLE); // Setup the display. MgraClear(M_DEFAULT, MilGraphicList); if(!m_CanFoundSoFar) { MgraText(MilGraphics, MilGraphicList, TEXT_OFFSET_X, TEXT_OFFSET_Y, MIL_TEXT("Scanning...")); } MbufClear(m_Remapped8BitImage, 0); // Remap 16-bit depth map to 8 bit. MimShift(MilDepthMap, m_Remapped8BitImage, -8); // Set the invalid data to 0. MbufClearCond(m_Remapped8BitImage, 0, 0, 0, m_Remapped8BitImage, M_EQUAL, 255.0); // Disassociate the calibration from the binarized image because we will not use it. McalAssociate(M_NULL, m_Remapped8BitImage, M_DEFAULT); // Find the cans. MmodFind(m_CanModel, m_Remapped8BitImage, m_CanModelResult); MIL_INT NumOfOccurences = 0; MIL_INT *PositionX, *PositionY; // Get information on the find. MmodGetResult(m_CanModelResult, M_DEFAULT, M_NUMBER + M_TYPE_MIL_INT, &NumOfOccurences); PositionX = new MIL_INT [NumOfOccurences]; PositionY = new MIL_INT [NumOfOccurences]; MmodControl(m_CanModelResult, M_DEFAULT, M_RESULT_OUTPUT_UNITS, M_PIXEL); MmodGetResult(m_CanModelResult, M_DEFAULT, M_POSITION_X + M_TYPE_MIL_INT, PositionX); MmodGetResult(m_CanModelResult, M_DEFAULT, M_POSITION_Y + M_TYPE_MIL_INT, PositionY); MIL_TEXT_CHAR CanString[MAX_STRING_LEN] = MIL_TEXT(""); // Find the tab for each can. for (MIL_INT i = 0; i < NumOfOccurences; i++) { m_CanFoundSoFar = true; bool CanOpen = false; MgraColor(MilGraphics, PROC_PASS_COLOR); MIL_INT PosX, PosY; PosX = Max(PositionX[i] - CAN_DELTA_X, MIL_INT(0)); PosY = Max(PositionY[i] - CAN_DELTA_Y, MIL_INT(0)); MIL_INT DeltaX = PositionX[i] - PosX; MIL_INT DeltaY = PositionY[i] - PosY; MIL_ID CanChild; MIL_DOUBLE MissingData = 0.0; MbufChild2d(MilDepthMap, PosX, PosY, Min(DeltaX * 2, MAP_SIZE_X), Min(DeltaY * 2, MAP_SIZE_Y), &CanChild); // Check if the can is open by looking for missing data. M3dmapStat(CanChild, M_NULL, M_NULL, M_NULL, M_NUMBER_OF_PIXELS_MISSING_DATA, M_DEFAULT, M_DEFAULT, &MissingData); if (MissingData > MAX_CAN_MISSING_DATA) { CanOpen = true; } MbufFree(CanChild); MmodDraw(MilGraphics, m_CanModelResult, MilGraphicList, M_DRAW_POSITION + M_DRAW_EDGES, i, M_DEFAULT); // Set the search area of the tab centered in the found can. MmodControl(m_TabModel, M_DEFAULT, M_POSITION_X, PositionX[i]); MmodControl(m_TabModel, M_DEFAULT, M_POSITION_Y, PositionY[i]); MmodFind(m_TabModel, m_Remapped8BitImage, m_TabModelResult); MIL_INT TabOccurences = 0; MmodGetResult(m_TabModelResult, M_DEFAULT, M_NUMBER + M_TYPE_MIL_INT, &TabOccurences); bool TabElevated = false; bool TabBroken = false; bool TabFound = (TabOccurences == 1); if(TabFound) { // Check if the tab is elevated. MmodGetResult(m_TabModelResult, M_DEFAULT, M_POSITION_X + M_TYPE_MIL_INT, PositionX); MmodGetResult(m_TabModelResult, M_DEFAULT, M_POSITION_Y + M_TYPE_MIL_INT, PositionY); MIL_ID TabChild; MIL_DOUBLE DeviationMean = -DBL_MAX; const MIL_INT STAT_SIZE = 4; const MIL_DOUBLE DEVIATION_OUTLIER_DISTANCE = 12.0; PosX = Max(PositionX[0] - STAT_SIZE, MIL_INT(0)); PosY = Max(PositionY[0] - STAT_SIZE, MIL_INT(0)); MbufChild2d(MilDepthMap, PosX, PosY, STAT_SIZE, STAT_SIZE, &TabChild); // Compute deviation from the plane Z = 0, if the negative direction M3dmapStat(TabChild, M_NULL, M_NULL, M_NULL, M_DEVIATION_MEAN + M_STAT_NEGATIVE, DEVIATION_OUTLIER_DISTANCE, M_DEFAULT, &DeviationMean); MbufFree(TabChild); // Heights are negative TabBroken = (DeviationMean < MIN_TAB_HEIGHT); TabElevated = (DeviationMean > MAX_TAB_HEIGHT); } // Draw surface status MgraColor(MilGraphics, CanOpen ? PROC_FAIL_COLOR : PROC_PASS_COLOR); MosSprintf(CanString, MAX_STRING_LEN, MIL_TEXT("Can surface: %s"), CanOpen ? MIL_TEXT("open") : MIL_TEXT("pass")); MgraText(MilGraphics, MilGraphicList, PROC_TEXT_OFFSET_X, PositionY[i]-CAN_DELTA_Y, CanString); // Draw tab status MIL_CONST_TEXT_PTR pTabStr = M_NULL; if(TabFound) { if(TabElevated) { MgraColor(MilGraphics, PROC_FAIL_COLOR); pTabStr = MIL_TEXT("elevated"); } else if(TabBroken) { MgraColor(MilGraphics, PROC_FAIL_COLOR); pTabStr = MIL_TEXT("broken"); } else { MgraColor(MilGraphics, PROC_PASS_COLOR); pTabStr = MIL_TEXT("pass"); } } else { MgraColor(MilGraphics, PROC_FAIL_COLOR); pTabStr = MIL_TEXT("missing"); } MosSprintf(CanString, MAX_STRING_LEN, MIL_TEXT("Tab : %s"), pTabStr); MgraText(MilGraphics, MilGraphicList, PROC_TEXT_OFFSET_X, PositionY[i]-CAN_DELTA_Y+YOffset, CanString); // Draw edges with the same tab status color. MmodDraw(MilGraphics, m_TabModelResult, MilGraphicList, M_DRAW_EDGES, M_ALL, M_DEFAULT); } // Enable graphics list update. MdispControl(MilResultsDisplay->GetDisplayID(), M_UPDATE_GRAPHIC_LIST, M_ENABLE); // Update the display. MilResultsDisplay->Control(M_TITLE, (MIL_DOUBLE)(M_PTR_TO_DOUBLE(MIL_TEXT("Inspection results")))); MilResultsDisplay->Show(m_Remapped8BitImage); MilResultsDisplay->Control(M_UPDATE, M_ENABLE); delete [] PositionX; delete [] PositionY; } //******************************************************************************* // Function that allocates processing objects. //******************************************************************************* void CContinuousCanInspection::AllocProcessingObjects(MIL_ID MilSystem) { const MIL_TEXT_CHAR* CAN_MODEL = EX_PATH("CanModel.mmf"); const MIL_TEXT_CHAR* TAB_MODEL = EX_PATH("TabModel.mmf"); // Scans per can. const MIL_INT CAN_SIZE_Y = 240; const MIL_INT RESULTS_SIZE_X = 630; const MIL_INT SizeY = CAN_SIZE_Y * 2; // Allocate the necessary buffers for processing. MbufAlloc2d(MilSystem, RESULTS_SIZE_X, SizeY, 8, M_IMAGE + M_PROC + M_DISP, &m_Remapped8BitImage); // Restore and setup the models. // Can model. MmodAllocResult(MilSystem, M_DEFAULT, &m_CanModelResult); MmodRestore(CAN_MODEL, MilSystem, M_DEFAULT, &m_CanModel); // Preprocess the model. MmodPreprocess(m_CanModel, M_DEFAULT); // Tab model. MmodAllocResult(MilSystem, M_DEFAULT, &m_TabModelResult); MmodRestore(TAB_MODEL, MilSystem, M_DEFAULT, &m_TabModel); // Preprocess the model. MmodPreprocess(m_TabModel, M_DEFAULT); } //******************************************************************************* // Function that frees processing objects. //******************************************************************************* void CContinuousCanInspection::FreeProcessingObjects() { MbufFree(m_Remapped8BitImage); MmodFree(m_CanModel); MmodFree(m_CanModelResult); MmodFree(m_TabModel); MmodFree(m_TabModelResult); m_CanFoundSoFar = false; }