feat: 机关门检测

This commit is contained in:
unanmed 2026-07-15 17:01:54 +08:00
parent 32227d2c44
commit d406b97c90
11 changed files with 165 additions and 33 deletions

View File

@ -243,7 +243,7 @@ const labelConfig: IAutoLabelConfig = {
wall: 1, wall: 1,
decoration: 16, decoration: 16,
commonDoors: [2], commonDoors: [2],
specialDoors: [2, 2], specialDoors: [6, 6],
keys: [3], keys: [3],
redGems: [3], redGems: [3],
blueGems: [3], blueGems: [3],

View File

@ -13,6 +13,7 @@ export class MapTileConverter implements IMapTileConverter {
private readonly emptyTiles = new Set<number>([0]); private readonly emptyTiles = new Set<number>([0]);
private readonly doorTiles = new Set<number>(); private readonly doorTiles = new Set<number>();
private readonly specialDoorTiles = new Set<number>();
private readonly enemyTiles = new Set<number>(); private readonly enemyTiles = new Set<number>();
private readonly resourceTiles = new Set<number>(); private readonly resourceTiles = new Set<number>();
private readonly keyTiles = new Set<number>(); private readonly keyTiles = new Set<number>();
@ -224,8 +225,13 @@ export class MapTileConverter implements IMapTileConverter {
if (isDoor) { if (isDoor) {
this.doorTiles.add(tile); this.doorTiles.add(tile);
this.noPassMap.set(tile, false); this.noPassMap.set(tile, false);
const label = labels.commonDoors[0]; if (blockId === 'specialDoor') {
this.labelMap.set(tile, label); this.specialDoorTiles.add(tile);
this.labelMap.set(tile, labels.specialDoors[0]);
} else {
const label = labels.commonDoors[0];
this.labelMap.set(tile, label);
}
} else if (isEnemy) { } else if (isEnemy) {
this.enemyTiles.add(tile); this.enemyTiles.add(tile);
this.noPassMap.set(tile, false); this.noPassMap.set(tile, false);

View File

@ -22,7 +22,7 @@ class MockTileConverter implements IMapTileConverter {
} }
isDoor(tile: number): boolean { isDoor(tile: number): boolean {
return tile === 2; return tile === 2 || tile === 6;
} }
isEnemy(tile: number): boolean { isEnemy(tile: number): boolean {
@ -76,7 +76,7 @@ const config: IAutoLabelConfig = {
wall: 1, wall: 1,
decoration: 16, decoration: 16,
commonDoors: [2], commonDoors: [2],
specialDoors: [6, 7], specialDoors: [6, 6],
keys: [3], keys: [3],
redGems: [3], redGems: [3],
blueGems: [3], blueGems: [3],

View File

@ -1,6 +1,7 @@
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import { import {
BranchType, BranchType,
DoorKind,
GraphNodeType, GraphNodeType,
IAutoLabelConfig, IAutoLabelConfig,
IFloorInfo, IFloorInfo,
@ -466,12 +467,21 @@ function collectReachableNodes(
*/ */
function isUselessBranchNode( function isUselessBranchNode(
topo: MapTopology, topo: MapTopology,
branchNode: MapGraphNode branchNode: MapGraphNode,
specialDoorLinkedEnemies?: Set<MapGraphNode>
): boolean { ): boolean {
if (branchNode.type !== GraphNodeType.Branch) { if (branchNode.type !== GraphNodeType.Branch) {
return false; return false;
} }
if (
branchNode.branch === BranchType.Enemy &&
specialDoorLinkedEnemies &&
specialDoorLinkedEnemies.has(branchNode)
) {
return false;
}
const branchTile = getNodeTile(branchNode); const branchTile = getNodeTile(branchNode);
if (countGridPassableDirections(topo, branchTile) <= 1) { if (countGridPassableDirections(topo, branchTile) <= 1) {
// 格子层只有一个可通行方向时,直接按死胡同分支处理。 // 格子层只有一个可通行方向时,直接按死胡同分支处理。
@ -598,13 +608,18 @@ function computeBranchClusterStats(branchNodes: Iterable<MapGraphNode>): {
* @param branchNodes * @param branchNodes
* @returns / * @returns /
*/ */
function computeIdleBranchStats(branchNodes: Iterable<MapGraphNode>): { function computeIdleBranchStats(
branchNodes: Iterable<MapGraphNode>,
specialDoorLinkedEnemies?: Set<MapGraphNode>
): {
idleDoorBranchCount: number; idleDoorBranchCount: number;
idleEnemyBranchCount: number; idleEnemyBranchCount: number;
ignoredIdleEnemyBySpecialDoorCount: number;
hasIdleBranch: boolean; hasIdleBranch: boolean;
} { } {
let idleDoorBranchCount = 0; let idleDoorBranchCount = 0;
let idleEnemyBranchCount = 0; let idleEnemyBranchCount = 0;
let ignoredIdleEnemyBySpecialDoorCount = 0;
for (const node of branchNodes) { for (const node of branchNodes) {
if (node.type !== GraphNodeType.Branch || node.neighbors.size !== 1) { if (node.type !== GraphNodeType.Branch || node.neighbors.size !== 1) {
@ -613,6 +628,11 @@ function computeIdleBranchStats(branchNodes: Iterable<MapGraphNode>): {
if (node.branch === BranchType.Door) { if (node.branch === BranchType.Door) {
idleDoorBranchCount++; idleDoorBranchCount++;
} else if (
specialDoorLinkedEnemies &&
specialDoorLinkedEnemies.has(node)
) {
ignoredIdleEnemyBySpecialDoorCount++;
} else { } else {
idleEnemyBranchCount++; idleEnemyBranchCount++;
} }
@ -621,6 +641,7 @@ function computeIdleBranchStats(branchNodes: Iterable<MapGraphNode>): {
return { return {
idleDoorBranchCount, idleDoorBranchCount,
idleEnemyBranchCount, idleEnemyBranchCount,
ignoredIdleEnemyBySpecialDoorCount,
hasIdleBranch: idleDoorBranchCount + idleEnemyBranchCount > 0 hasIdleBranch: idleDoorBranchCount + idleEnemyBranchCount > 0
}; };
} }
@ -704,6 +725,59 @@ function buildMergedNonBranchAreas(graph: IMapGraph): {
return { areas, areaMap }; return { areas, areaMap };
} }
function findSpecialDoorLinkedEnemyNodes(
branchNodes: Iterable<MapGraphNode>,
areaMap: Map<MapGraphNode, IMergedNonBranchArea>
): Set<MapGraphNode> {
const specialDoorNodes = new Set<MapGraphNode>();
for (const node of branchNodes) {
if (
node.type === GraphNodeType.Branch &&
node.branch === BranchType.Door &&
node.doorKind === DoorKind.Special
) {
specialDoorNodes.add(node);
}
}
if (specialDoorNodes.size === 0) {
return new Set();
}
const specialDoorLinkedAreas = new Set<IMergedNonBranchArea>();
for (const specialDoor of specialDoorNodes) {
for (const neighbor of specialDoor.neighbors) {
const area = areaMap.get(neighbor);
if (area) {
specialDoorLinkedAreas.add(area);
}
}
}
if (specialDoorLinkedAreas.size === 0) {
return new Set();
}
const linkedEnemyNodes = new Set<MapGraphNode>();
for (const node of branchNodes) {
if (
node.type !== GraphNodeType.Branch ||
node.branch !== BranchType.Enemy
) {
continue;
}
for (const neighbor of node.neighbors) {
const area = areaMap.get(neighbor);
if (area && specialDoorLinkedAreas.has(area)) {
linkedEnemyNodes.add(node);
break;
}
}
}
return linkedEnemyNodes;
}
function buildRepeatedGuardCandidates( function buildRepeatedGuardCandidates(
branchNodes: Iterable<MapGraphNode>, branchNodes: Iterable<MapGraphNode>,
areaMap: Map<MapGraphNode, IMergedNonBranchArea> areaMap: Map<MapGraphNode, IMergedNonBranchArea>
@ -936,18 +1010,44 @@ export function parseFloorInfo(
hasLargeEnemyCluster hasLargeEnemyCluster
} = computeBranchClusterStats(branchNodes); } = computeBranchClusterStats(branchNodes);
const { idleDoorBranchCount, idleEnemyBranchCount, hasIdleBranch } = const mergedAreas = buildMergedNonBranchAreas(topo.graph);
computeIdleBranchStats(branchNodes); const specialDoorLinkedEnemies = findSpecialDoorLinkedEnemyNodes(
branchNodes,
mergedAreas.areaMap
);
const specialDoorLinkedEnemyCount = specialDoorLinkedEnemies.size;
const {
idleDoorBranchCount,
idleEnemyBranchCount,
ignoredIdleEnemyBySpecialDoorCount,
hasIdleBranch
} = computeIdleBranchStats(branchNodes, specialDoorLinkedEnemies);
const { const {
repeatedGuardDoorBranchCount, repeatedGuardDoorBranchCount,
repeatedGuardEnemyBranchCount, repeatedGuardEnemyBranchCount,
hasRepeatedGuardIdleBranch hasRepeatedGuardIdleBranch
} = computeRepeatedGuardIdleStats(topo.graph, branchNodes, width); } = computeRepeatedGuardIdleStats(topo.graph, branchNodes, width);
// 无用分支逐点判定:只要任意一个分支命中,整层就带有无用分支标签。 let hasUselessBranch = false;
const hasUselessBranch = [...branchNodes].some(node => let ignoredUselessBranchBySpecialDoorCount = 0;
isUselessBranchNode(topo, node) for (const node of branchNodes) {
); const useless = isUselessBranchNode(
topo,
node,
specialDoorLinkedEnemies
);
if (useless) {
if (
node.branch === BranchType.Enemy &&
specialDoorLinkedEnemies.has(node)
) {
ignoredUselessBranchBySpecialDoorCount++;
} else {
hasUselessBranch = true;
}
}
}
// 统计拓扑图信息 // 统计拓扑图信息
let maxEmptyArea = 0; let maxEmptyArea = 0;
@ -984,6 +1084,9 @@ export function parseFloorInfo(
itemDensity: count(flattened, itemTiles) / area, itemDensity: count(flattened, itemTiles) / area,
entryCount: count(flattened, entryTiles), entryCount: count(flattened, entryTiles),
specialDoorCount: count(flattened, specialDoorTiles), specialDoorCount: count(flattened, specialDoorTiles),
specialDoorLinkedEnemyCount,
ignoredIdleEnemyBySpecialDoorCount,
ignoredUselessBranchBySpecialDoorCount,
maxDoorClusterSize, maxDoorClusterSize,
maxEnemyClusterSize, maxEnemyClusterSize,
hasLargeDoorCluster, hasLargeDoorCluster,

View File

@ -2,6 +2,7 @@ import {
CannotInOut, CannotInOut,
GraphNodeType, GraphNodeType,
BranchType, BranchType,
DoorKind,
ResourceType, ResourceType,
type IMapTopology, type IMapTopology,
type IMapGraph, type IMapGraph,
@ -160,14 +161,19 @@ export class MapTopology implements IMapTopology {
if (type === GraphNodeType.Branch) { if (type === GraphNodeType.Branch) {
const tile = this.originMap[y][x]; const tile = this.originMap[y][x];
const isDoor = converter.isDoor(tile);
const convertedTile = this.convertedMap[y][x];
nodeMap.set(idx, { nodeMap.set(idx, {
type: GraphNodeType.Branch, type: GraphNodeType.Branch,
index: nodeIndex++, index: nodeIndex++,
tiles, tiles,
neighbors, neighbors,
branch: converter.isDoor(tile) branch: isDoor ? BranchType.Door : BranchType.Enemy,
? BranchType.Door doorKind: isDoor
: BranchType.Enemy ? this.config.specialDoors.includes(convertedTile)
? DoorKind.Special
: DoorKind.Common
: DoorKind.Common
}); });
continue; continue;
} }

View File

@ -89,6 +89,12 @@ export interface IFloorInfo {
readonly entryCount: number; readonly entryCount: number;
/** 机关门数量 */ /** 机关门数量 */
readonly specialDoorCount: number; readonly specialDoorCount: number;
/** 机关门关联怪数量 */
readonly specialDoorLinkedEnemyCount: number;
/** 因机关门关联而被豁免的闲置怪数量 */
readonly ignoredIdleEnemyBySpecialDoorCount: number;
/** 因机关门关联而被豁免的无用分支数量 */
readonly ignoredUselessBranchBySpecialDoorCount: number;
/** 同类门分支连通块的最大大小 */ /** 同类门分支连通块的最大大小 */
readonly maxDoorClusterSize: number; readonly maxDoorClusterSize: number;
/** 同类怪分支连通块的最大大小 */ /** 同类怪分支连通块的最大大小 */
@ -360,6 +366,11 @@ export const enum BranchType {
Enemy Enemy
} }
export const enum DoorKind {
Common,
Special
}
export interface IMapGraphNodeBase { export interface IMapGraphNodeBase {
/** 节点类型 */ /** 节点类型 */
readonly type: GraphNodeType; readonly type: GraphNodeType;
@ -385,6 +396,8 @@ export interface IBranchMapGraphNode extends IMapGraphNodeBase {
readonly type: GraphNodeType.Branch; readonly type: GraphNodeType.Branch;
/** 分支节点类型 */ /** 分支节点类型 */
readonly branch: BranchType; readonly branch: BranchType;
/** 门子类型,仅 Door 分支有效 */
readonly doorKind: DoorKind;
} }
export interface IEntryMapGraphNode extends IMapGraphNodeBase { export interface IEntryMapGraphNode extends IMapGraphNodeBase {

View File

@ -1,10 +1,10 @@
// 基本图块定义 // 基本图块定义
// 新方案 ID0=空地 1=墙壁 2=门 3=资源(all) 4=怪物 5=入口 6=掩码 // 新方案 ID0=空地 1=墙壁 2=普通门 3=资源(all) 4=怪物 5=入口 6=机关门 7=掩码
export const emptyTiles = new Set([0]); export const emptyTiles = new Set([0]);
export const wallTiles = new Set([1]); export const wallTiles = new Set([1]);
export const decorationTiles = new Set([16]); export const decorationTiles = new Set([16]);
export const commonDoorTiles = new Set([2]); export const commonDoorTiles = new Set([2]);
export const specialDoorTiles = new Set([2]); export const specialDoorTiles = new Set([6]);
export const keyTiles = new Set([3]); export const keyTiles = new Set([3]);
export const redGemTiles = new Set([3]); export const redGemTiles = new Set([3]);
export const blueGemTiles = new Set([3]); export const blueGemTiles = new Set([3]);

View File

@ -28,7 +28,8 @@ class GinkaSeperatedDataset(Dataset):
RESOURCE = 3 RESOURCE = 3
MONSTER = 4 MONSTER = 4
ENTRANCE = 5 ENTRANCE = 5
MASK_ID = 6 SPECIAL_DOOR = 6
MASK_ID = 7
MAP_SIZE = 13 * 13 MAP_SIZE = 13 * 13
def __init__( def __init__(
@ -51,7 +52,7 @@ class GinkaSeperatedDataset(Dataset):
def compute_density_stats(self) -> dict: def compute_density_stats(self) -> dict:
wall_densities = [self.count_tile(item['map'], self.WALL) / self.MAP_SIZE for item in self.data] wall_densities = [self.count_tile(item['map'], self.WALL) / self.MAP_SIZE for item in self.data]
door_densities = [self.count_tile(item['map'], self.DOOR) / self.MAP_SIZE for item in self.data] door_densities = [self.count_tile(item['map'], self.DOOR) / self.MAP_SIZE + self.count_tile(item['map'], self.SPECIAL_DOOR) / self.MAP_SIZE for item in self.data]
monster_densities = [self.count_tile(item['map'], self.MONSTER) / self.MAP_SIZE for item in self.data] monster_densities = [self.count_tile(item['map'], self.MONSTER) / self.MAP_SIZE for item in self.data]
entrance_densities = [self.count_tile(item['map'], self.ENTRANCE) / self.MAP_SIZE for item in self.data] entrance_densities = [self.count_tile(item['map'], self.ENTRANCE) / self.MAP_SIZE for item in self.data]
resource_densities = [self.count_tile(item['map'], self.RESOURCE) / self.MAP_SIZE for item in self.data] resource_densities = [self.count_tile(item['map'], self.RESOURCE) / self.MAP_SIZE for item in self.data]
@ -79,7 +80,7 @@ class GinkaSeperatedDataset(Dataset):
def build_target_density(self, map_data: list) -> torch.Tensor: def build_target_density(self, map_data: list) -> torch.Tensor:
return torch.FloatTensor([ return torch.FloatTensor([
self.count_tile(map_data, self.WALL) / self.MAP_SIZE, self.count_tile(map_data, self.WALL) / self.MAP_SIZE,
self.count_tile(map_data, self.DOOR) / self.MAP_SIZE, (self.count_tile(map_data, self.DOOR) + self.count_tile(map_data, self.SPECIAL_DOOR)) / self.MAP_SIZE,
self.count_tile(map_data, self.MONSTER) / self.MAP_SIZE, self.count_tile(map_data, self.MONSTER) / self.MAP_SIZE,
self.count_tile(map_data, self.ENTRANCE) / self.MAP_SIZE, self.count_tile(map_data, self.ENTRANCE) / self.MAP_SIZE,
self.count_tile(map_data, self.RESOURCE) / self.MAP_SIZE self.count_tile(map_data, self.RESOURCE) / self.MAP_SIZE
@ -153,7 +154,7 @@ class GinkaSeperatedDataset(Dataset):
def create_degreaded(self, raw: np.ndarray): def create_degreaded(self, raw: np.ndarray):
# 阶段一:仅生成墙壁骨架 # 阶段一:仅生成墙壁骨架
target1 = raw.copy() target1 = raw.copy()
self.degrade_tile(target1, [self.DOOR, self.RESOURCE, self.MONSTER, self.ENTRANCE]) self.degrade_tile(target1, [self.DOOR, self.SPECIAL_DOOR, self.RESOURCE, self.MONSTER, self.ENTRANCE])
inp1 = target1.copy() inp1 = target1.copy()
# 阶段二:生成怪物、门,同时也允许生成入口以适配结构 # 阶段二:生成怪物、门,同时也允许生成入口以适配结构
@ -164,7 +165,7 @@ class GinkaSeperatedDataset(Dataset):
# 阶段三:生成资源 # 阶段三:生成资源
target3 = raw.copy() target3 = raw.copy()
self.degrade_tile(target3, [self.WALL, self.DOOR, self.MONSTER, self.ENTRANCE]) self.degrade_tile(target3, [self.WALL, self.DOOR, self.SPECIAL_DOOR, self.MONSTER, self.ENTRANCE])
inp3 = raw.copy() inp3 = raw.copy()
return target1, inp1, target2, inp2, target3, inp3 return target1, inp1, target2, inp2, target3, inp3
@ -182,7 +183,7 @@ class GinkaSeperatedDataset(Dataset):
inp1[self.std_mask()] = self.MASK_ID inp1[self.std_mask()] = self.MASK_ID
# stage2对 floor+功能元素区域 std_mask # stage2对 floor+功能元素区域 std_mask
need_mask = np.isin(inp2, [self.FLOOR, self.DOOR, self.MONSTER, self.ENTRANCE]) need_mask = np.isin(inp2, [self.FLOOR, self.DOOR, self.SPECIAL_DOOR, self.MONSTER, self.ENTRANCE])
inp2[need_mask & self.std_mask()] = self.MASK_ID inp2[need_mask & self.std_mask()] = self.MASK_ID
# stage3对 floor+resource 区域 std_mask # stage3对 floor+resource 区域 std_mask
@ -201,7 +202,7 @@ class GinkaSeperatedDataset(Dataset):
need_mask = np.isin(inp2, [self.FLOOR, self.WALL]) need_mask = np.isin(inp2, [self.FLOOR, self.WALL])
inp1[need_mask & self.std_mask()] = self.MASK_ID inp1[need_mask & self.std_mask()] = self.MASK_ID
need_mask = np.isin(inp2, [self.FLOOR, self.DOOR, self.MONSTER, self.ENTRANCE]) need_mask = np.isin(inp2, [self.FLOOR, self.DOOR, self.SPECIAL_DOOR, self.MONSTER, self.ENTRANCE])
inp2[need_mask] = self.MASK_ID inp2[need_mask] = self.MASK_ID
need_mask = np.isin(inp3, [self.FLOOR, self.RESOURCE]) need_mask = np.isin(inp3, [self.FLOOR, self.RESOURCE])
inp3[need_mask] = self.MASK_ID inp3[need_mask] = self.MASK_ID

View File

@ -33,7 +33,7 @@ from shared.image import matrix_to_image_cv
# stage3 → resource资源点 # stage3 → resource资源点
# 图块 ID 定义: # 图块 ID 定义:
# 0. 空地 1. 墙壁 2. 门 3. 资源 4. 怪物 5. 入口 6. 掩码MASK_TOKEN # 0. 空地 1. 墙壁 2. 普通门 3. 资源 4. 怪物 5. 入口 6. 机关门 7. 掩码MASK_TOKEN
# 共用 VQ-VAE 超参 # 共用 VQ-VAE 超参
# 三组编码器vq1/vq2/vq3共享相同超参分别对三阶段地图上下文独立编码 # 三组编码器vq1/vq2/vq3共享相同超参分别对三阶段地图上下文独立编码
@ -76,8 +76,8 @@ STAGE2_VQ_WEIGHT = 0.5
STAGE3_VQ_WEIGHT = 0.5 STAGE3_VQ_WEIGHT = 0.5
# 全局参数 # 全局参数
NUM_CLASSES = 7 # 图块类型数 NUM_CLASSES = 8 # 图块类型数
MASK_TOKEN = 6 # 掩码图块 MASK_TOKEN = 7 # 掩码图块
MAP_W = 13 # 地图宽度 MAP_W = 13 # 地图宽度
MAP_H = 13 # 地图高度 MAP_H = 13 # 地图高度
MAP_SIZE = MAP_W * MAP_H # 地图大小 MAP_SIZE = MAP_W * MAP_H # 地图大小
@ -276,7 +276,7 @@ def compute_remaining(
remain = torch.zeros(current.size(0), DENSITY_DIM, device=current.device) remain = torch.zeros(current.size(0), DENSITY_DIM, device=current.device)
visible_wall = (current == 1).sum(dim=1).float() / MAP_SIZE visible_wall = (current == 1).sum(dim=1).float() / MAP_SIZE
visible_door = (current == 2).sum(dim=1).float() / MAP_SIZE visible_door = ((current == 2) | (current == 6)).sum(dim=1).float() / MAP_SIZE
visible_monster = (current == 4).sum(dim=1).float() / MAP_SIZE visible_monster = (current == 4).sum(dim=1).float() / MAP_SIZE
visible_entrance = (current == 5).sum(dim=1).float() / MAP_SIZE visible_entrance = (current == 5).sum(dim=1).float() / MAP_SIZE
visible_resource = (current == 3).sum(dim=1).float() / MAP_SIZE visible_resource = (current == 3).sum(dim=1).float() / MAP_SIZE
@ -419,7 +419,7 @@ def full_generate_specific_z(
pred2_np = maskgit_sample( pred2_np = maskgit_sample(
mg2, inp2, z2, struct, target_density, 2, mg2, inp2, z2, struct, target_density, 2,
GENERATE_STEP, target_tiles=[2, 4, 5], keep_fixed=keep_fixed[1] GENERATE_STEP, target_tiles=[2, 6, 4, 5], keep_fixed=keep_fixed[1]
) )
merged12 = pred1_np.copy() merged12 = pred1_np.copy()
merged12[pred2_np != 0] = pred2_np[pred2_np != 0] merged12[pred2_np != 0] = pred2_np[pred2_np != 0]
@ -742,6 +742,9 @@ def validate(
true_map = true_map_batch[batch_idx] true_map = true_map_batch[batch_idx]
pred_count = float((pred_map == tile_id).sum().item()) pred_count = float((pred_map == tile_id).sum().item())
true_count = float((true_map == tile_id).sum().item()) true_count = float((true_map == tile_id).sum().item())
if tile_id == 2:
pred_count += float((pred_map == 6).sum().item())
true_count += float((true_map == 6).sum().item())
density_metrics[tile_id]["mae"] += abs(pred_count - true_count) / MAP_SIZE density_metrics[tile_id]["mae"] += abs(pred_count - true_count) / MAP_SIZE
density_metrics[tile_id]["over"] += max(pred_count - true_count, 0.0) / MAP_SIZE density_metrics[tile_id]["over"] += max(pred_count - true_count, 0.0) / MAP_SIZE
density_metrics[tile_id]["count"] += 1 density_metrics[tile_id]["count"] += 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 B

After

Width:  |  Height:  |  Size: 995 B

BIN
tiles/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B