// 섬 데이터와 외곽선 데이터를 이용해 색칠을 자동으로 해 본다. // 색칠 후 이미지에 문제가 없는지 확인하기 위한 테스트 과정이다. static void ExecuteDetermineIslandTest(string sourceFileName, string bytesFileName) { Console.Out.WriteLine($"Running {nameof(ExecuteDetermineIslandTest)}"); var targetFileName = AppendToFileName(sourceFileName, "-DIT"); StageData stageData; using (var bytesFileStream = new FileStream(bytesFileName, FileMode.Open)) { var formatter = new BinaryFormatter(); try { // Deserialize the hashtable from the file and // assign the reference to the local variable. stageData = (StageData)formatter.Deserialize(bytesFileStream); } catch (SerializationException e) { Console.WriteLine("Failed to deserialize. Reason: " + e.Message); throw; } } using (var image = Image.Load <Rgba32>(sourceFileName)) { foreach (var island in stageData.islandDataByMinPoint) { var minPoint = UInt32ToVector2Int(island.Key); var targetColor = UInt32ToRgba32(island.Value.rgba); var fillMinPoint = FloodFill.ExecuteFillIf(image, minPoint, Rgba32.White, targetColor, out var pixelArea, out _, out _); if (fillMinPoint != new Vector2Int(image.Width, image.Height) && pixelArea == island.Value.pixelArea) { } else { Console.WriteLine("Logic error in ExecuteDetermineIslandTest()!"); } } using (var stream = new FileStream(targetFileName, FileMode.Create)) { image.SaveAsPng(stream); stream.Close(); } } }
// 아주 작은 비검은색을 검은색으로 메운다. static string ExecuteFillSmallNotBlack(string sourceFileName, int threshold = 4 * 4 * 4) { Logger.WriteLine($"Running {nameof(ExecuteFillSmallNotBlack)}"); var targetFileName = AppendToFileName(sourceFileName, "-FSNB"); // Min Point 별 (섬 별) 섬 픽셀 수(면적) var islandPixelAreaByMinPoint = new Dictionary <Vector2Int, int>(); using (var image = Image.Load <Rgba32>(sourceFileName)) { // 각 픽셀에 대해서 반복한다. for (var h = 0; h < image.Height; h++) { for (var w = 0; w < image.Width; w++) { var pixelColor = image[w, h]; if (pixelColor == Black) { // 경계선 색상(검은색)이면 할 일이 없다. } else { // (w, h) 좌표부터 검은색이 아닌 색을 검은색으로 채우면서 픽셀 수집한다. // 수집한 모든 픽셀은 points에, points의 min point는 반환값으로 받는다. var coord = new Vector2Int(w, h); var fillMinPoint = FloodFill.ExecuteFillIfNotBlack(image, coord, Black, out var pixelArea, out _, out _); if (fillMinPoint != new Vector2Int(image.Width, image.Height)) { islandPixelAreaByMinPoint[fillMinPoint] = pixelArea; } else { throw new Exception("Invalid fill min point!"); } } } } } // 메모리상 image 변수는 직전 과정에서 변경되었으므로, 다시 읽어들이자. // 여기서부터 본격적으로 작은 비검정색칸을 검정색칸으로 채운다. using (var image = Image.Load <Rgba32>(sourceFileName)) { foreach (var island in islandPixelAreaByMinPoint) { if (island.Value < threshold) { var fillMinPoint = FloodFill.ExecuteFillIfNotBlack(image, island.Key, Black, out var pixelArea, out _, out _); if (fillMinPoint != new Vector2Int(image.Width, image.Height) && pixelArea == island.Value) { } else { Logger.WriteErrorLine("Logic error in ExecuteFillSmallNotBlack()!"); } } } // 그리고 저장! var targetDirName = Path.GetDirectoryName(targetFileName); if (string.IsNullOrEmpty(targetDirName)) { return(string.Empty); } Directory.CreateDirectory(targetDirName); using (var stream = new FileStream(targetFileName, FileMode.Create)) { image.SaveAsPng(stream); stream.Close(); } } return(targetFileName); }
// 입력 이미지로 섬 데이터를 만든다. // 섬 데이터는 유니티에서 사용하게 된다. static string ExecuteDetermineIsland(string sourceFileName, string startFileName) { Logger.WriteLine($"Running {nameof(ExecuteDetermineIsland)}"); // 이미지 파일을 열어봅시다~ using (var image = Image.Load <Rgba32>(sourceFileName)) { // 색상 별 픽셀 수 var pixelCountByColor = new Dictionary <Rgba32, int>(); // Min Point 별 (섬 별) 섬 색상 var islandColorByMinPoint = new Dictionary <Vector2Int, Rgba32>(); // Min Point 별 (섬 별) 섬 픽셀 수(면적) var islandPixelAreaByMinPoint = new Dictionary <Vector2Int, int>(); // 색상 별 섬 수 var islandCountByColor = new Dictionary <Rgba32, int>(); // 픽셀 수(면적) 별 섬 수 var islandCountByPixelArea = new Dictionary <int, int>(); // Min Point 별 (섬 별) Max Rect var maxRectByMinPoint = new Dictionary <uint, ulong>(); // 각 픽셀에 대해서 반복한다. for (var h = 0; h < image.Height; h++) { for (var w = 0; w < image.Width; w++) { var pixelColor = image[w, h]; if (pixelColor == Black) { // 경계선 색상(검은색)이면 할 일이 없다. } else { // (w, h) 좌표부터 검은색이 아닌 색을 검은색으로 채우면서 픽셀 수집한다. // 수집한 모든 픽셀은 points에, points의 min point는 반환값으로 받는다. var coord = new Vector2Int(w, h); var fillMinPoint = FloodFill.ExecuteFillIfNotBlack(image, coord, Black, out var pixelArea, out var points, out var originalColors); if (fillMinPoint != new Vector2Int(image.Width, image.Height)) { if (originalColors.Count > 1) { // 한 섬에 색상이 여러 가지라면 가장 많은 색상이 최종 색깔이 되도록 하자. // 주로 경계선 주변에서 경계선과 섬 색깔이 블렌딩되면서 다른 색깔이 되는 패턴이다. //var prominentColor = originalColors.Aggregate((l, r) => l.Value > r.Value ? l : r).Key; var prominentColor = originalColors.OrderByDescending(e => e.Value) .First(e => e.Key != White).Key; pixelColor = prominentColor; // foreach (var originalColor in originalColors) { // Logger.WriteLine($"{originalColor.Key} = {originalColor.Value}"); // } // throw new Exception($"Island color is not uniform! It has {originalColors.Count} colors in it! coord={coord}"); } if (originalColors.Count == 0) { throw new Exception("Island color is empty. Is this possible?"); } if (pixelColor == White) { throw new Exception("Island color is WHITE?! Fix it!"); } IncreaseCountOfDictionaryValue(pixelCountByColor, pixelColor); islandColorByMinPoint[fillMinPoint] = pixelColor; islandPixelAreaByMinPoint[fillMinPoint] = pixelArea; IncreaseCountOfDictionaryValue(islandCountByPixelArea, pixelArea); IncreaseCountOfDictionaryValue(islandCountByColor, pixelColor); var xMax = points.Max(e => e.x); var xMin = points.Min(e => e.x); var yMax = points.Max(e => e.y); var yMin = points.Min(e => e.y); var subRectW = xMax - xMin + 1; var subRectH = yMax - yMin + 1; var A = Enumerable.Range(0, subRectH).Select(e => new int[subRectW]).ToArray(); foreach (var point in points) { A[point.y - yMin][point.x - xMin] = 1; } var area = MaxSubRect.MaxRectangle(subRectH, subRectW, A, out var beginIndexR, out var endIndexR, out var beginIndexC, out var endIndexC); Logger.WriteLine( $"Sub Rect: area:{area} [({yMin + beginIndexR},{xMin + beginIndexC})-({yMin + endIndexR},{xMin + endIndexC})]"); maxRectByMinPoint[Vector2IntToUInt32(fillMinPoint)] = GetRectRange( xMin + beginIndexC, yMin + beginIndexR, xMin + endIndexC, yMin + endIndexR); } else { throw new Exception("Invalid fill min point!"); } } } } Logger.WriteLine($"Total Pixel Count: {image.Width * image.Height}"); pixelCountByColor.TryGetValue(White, out var whiteCount); Logger.WriteLine($"White Count: {whiteCount}"); if (islandColorByMinPoint.Count < 1) { throw new IslandCountException(); } var islandIndex = 1; // 0번째 island는 외곽선을 위해 예비한 값이다. foreach (var kv in islandColorByMinPoint.OrderBy(kv => Vector2IntToUInt32(kv.Key))) { Logger.WriteLine( $"Island #{islandIndex} fillMinPoint={kv.Key}, color={kv.Value}, area={islandPixelAreaByMinPoint[kv.Key]}"); islandIndex++; } var colorCountIndex = 1; // 0 번째 컬러는 외곽선을 위해 예비한 블랙 값이다. foreach (var kv in pixelCountByColor) { islandCountByColor.TryGetValue(kv.Key, out var islandCount); Logger.WriteLine( $"Color #{colorCountIndex} {kv.Key}: pixelCount={kv.Value}, islandCount={islandCount}"); colorCountIndex++; if (kv.Key == White) { throw new Exception("Palette color should not be white!"); } } var pixelAreaCountIndex = 0; foreach (var kv in islandCountByPixelArea.OrderByDescending(kv => kv.Key)) { Logger.WriteLine($"Pixel Area #{pixelAreaCountIndex} {kv.Key}: islandCount={kv.Value}"); pixelAreaCountIndex++; } var stageData = new StageData(); var islandIndex2 = 1; foreach (var kv in islandPixelAreaByMinPoint.OrderBy(kv => Vector2IntToUInt32(kv.Key))) { var p = Vector2IntToUInt32(kv.Key); stageData.islandDataByMinPoint[p] = new IslandData { index = islandIndex2, pixelArea = islandPixelAreaByMinPoint[kv.Key], rgba = Rgba32ToUInt32(islandColorByMinPoint[kv.Key]), maxRect = maxRectByMinPoint[p] }; islandIndex2++; } var outputPath = Path.ChangeExtension(startFileName, "bytes"); if (string.IsNullOrEmpty(outputPathReplaceFrom) == false && string.IsNullOrEmpty(outputPathReplaceTo) == false) { outputPath = outputPath.Replace(outputPathReplaceFrom, outputPathReplaceTo); } using (var stream = File.Create(outputPath)) { var formatter = new BinaryFormatter(); formatter.Serialize(stream, stageData); stream.Close(); } Logger.WriteLine($"{stageData.islandDataByMinPoint.Count} islands loaded."); Logger.WriteLine($"Written to {outputPath}"); return(outputPath); } }
// 섬 데이터와 외곽선 데이터를 이용해 색칠을 자동으로 해 본다. // 색칠 후 이미지에 문제가 없는지 확인하기 위한 테스트 과정이다. static string ExecuteDetermineIslandTest(string sourceFileName, string bytesFileName, bool errorAsWarning, bool writeA1A2Tex) { Logger.WriteLine($"Running {nameof(ExecuteDetermineIslandTest)}"); var targetFileName = AppendToFileName(sourceFileName, "-DIT"); var a1TexFileName = AppendToFileName(targetFileName, "-A1"); var a2TexFileName = AppendToFileName(targetFileName, "-A2"); StageData stageData; using (var bytesFileStream = new FileStream(bytesFileName, FileMode.Open)) { var formatter = new BinaryFormatter(); try { // Deserialize the hashtable from the file and // assign the reference to the local variable. stageData = (StageData)formatter.Deserialize(bytesFileStream); } catch (SerializationException e) { Logger.WriteErrorLine("Failed to deserialize. Reason: " + e.Message); throw; } } var colorUintArray = stageData.CreateColorUintArray(); var colorUintDict = new Dictionary <uint, int>(); for (var i = 0; i < colorUintArray.Length; i++) { colorUintDict[colorUintArray[i]] = i + 1; // Palette Index 0은 외곽선 용으로 예비한다. } using (var image = Image.Load <Rgba32>(sourceFileName)) { var a1Tex = new Image <Rgba32>(image.Width, image.Height, AllZeros); var a2Tex = new Image <Rgba32>(image.Width, image.Height, AllZeros); var islandIndex = 1; // Island Index 0은 외곽선 용으로 예비한다. // dictionary의 이터레이션 순서에 의존하면 안되고, island data에서 지정한 index 순서로 하자. foreach (var island in stageData.islandDataByMinPoint.OrderBy(e => e.Value.index)) { var minPoint = UInt32ToVector2Int(island.Key); var targetColor = UInt32ToRgba32(island.Value.rgba); var paletteIndex = colorUintDict[island.Value.rgba]; var fillMinPoint = FloodFill.ExecuteFillIf(image, minPoint, White, targetColor, out var pixelArea, out _, out _, islandIndex, (islandIndexCallback, fx, fy) => { GetAlpha8Pair(islandIndexCallback, paletteIndex, out var a1, out var a2); a1Tex[fx, fy] = new Rgba32 { A = a1 }; a2Tex[fx, fy] = new Rgba32 { A = a2 }; }); if (fillMinPoint == new Vector2Int(image.Width, image.Height)) { if (errorAsWarning) { // 이번엔 오류로 안친다. Logger.WriteLine("Logic error in ExecuteDetermineIslandTest()! Invalid fillMinPoint"); } else { Logger.WriteErrorLine("Logic error in ExecuteDetermineIslandTest()! Invalid fillMinPoint"); } } if (pixelArea != island.Value.pixelArea) { if (errorAsWarning) { // 이번엔 오류로 안친다. Logger.WriteLine( $"Logic error in ExecuteDetermineIslandTest()! Pixel area {pixelArea} expected to be {island.Value.pixelArea}"); } else { Logger.WriteErrorLine( $"Logic error in ExecuteDetermineIslandTest()! Pixel area {pixelArea} expected to be {island.Value.pixelArea}"); } } islandIndex++; } using (var stream = new FileStream(targetFileName, FileMode.Create)) { image.SaveAsPng(stream); stream.Close(); } if (writeA1A2Tex) { using (var stream = new FileStream(a1TexFileName, FileMode.Create)) { a1Tex.SaveAsPng(stream); stream.Close(); } using (var stream = new FileStream(a2TexFileName, FileMode.Create)) { a2Tex.SaveAsPng(stream); stream.Close(); } } } return(targetFileName); }