docs(workflow): add comprehensive git workflow standards #1
71
.swiftlint.yml
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# SwiftLint Configuration for ChessPrism
|
||||
|
||||
disabled_rules:
|
||||
- trailing_whitespace
|
||||
- line_length
|
||||
- function_body_length
|
||||
- file_length
|
||||
- type_body_length
|
||||
- identifier_name
|
||||
|
||||
opt_in_rules:
|
||||
- empty_count
|
||||
- closure_spacing
|
||||
- force_unwrapping
|
||||
- force_cast
|
||||
- discouraged_direct_init
|
||||
- discouraged_object_literal
|
||||
|
||||
included:
|
||||
- ChessPrism
|
||||
- ChessPrismTests
|
||||
- ChessPrismUITests
|
||||
|
||||
excluded:
|
||||
- Carthage
|
||||
- Pods
|
||||
- SwiftGen
|
||||
- R.generated.swift
|
||||
|
||||
line_length: 200
|
||||
warning: 150
|
||||
|
||||
type_name:
|
||||
min_length: 3
|
||||
max_length: 40
|
||||
|
||||
identifier_name:
|
||||
min_length: 3
|
||||
max_length: 40
|
||||
excluded:
|
||||
- id
|
||||
- x
|
||||
- y
|
||||
- z
|
||||
- i
|
||||
- j
|
||||
- k
|
||||
|
||||
reporter: "xcode"
|
||||
|
||||
analyzer_rules:
|
||||
- explicit_self
|
||||
- unused_import
|
||||
- unused_declaration
|
||||
- unused_setter_value
|
||||
- unused_optional_binding
|
||||
- unused_enumerated
|
||||
- unused_capture_list
|
||||
- unused_closure_parameter
|
||||
- unused_private_declaration
|
||||
- unused_type_parameter
|
||||
- unused_optional_chain
|
||||
- unused_optional_chain_result
|
||||
- unused_optional_chain_result_void
|
||||
- unused_optional_chain_result_non_void
|
||||
- unused_optional_chain_result_optional
|
||||
- unused_optional_chain_result_non_optional
|
||||
- unused_optional_chain_result_optional_void
|
||||
- unused_optional_chain_result_non_optional_void
|
||||
- unused_optional_chain_result_optional_non_void
|
||||
- unused_optional_chain_result_non_optional_non_void
|
||||
7
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -389,11 +389,12 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = ChessPrism/ChessPrism.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"ChessPrism/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = RJHWWWSF6Q;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
|
|
@ -411,6 +412,7 @@
|
|||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
"OTHER_CODE_SIGN_FLAGS[sdk=*]" = "--deep --force";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.chessprism.ChessPrism;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
|
|
@ -428,11 +430,12 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = ChessPrism/ChessPrism.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"ChessPrism/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = RJHWWWSF6Q;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ChessPrism_Icon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "ChessPrism_Icon 1.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
|
@ -23,56 +25,67 @@
|
|||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "ChessPrism_Icon 2.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "ChessPrism_Icon 3.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "ChessPrism_Icon 4.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "ChessPrism_Icon 6.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "ChessPrism_Icon 5.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "ChessPrism_Icon 8.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "ChessPrism_Icon 7.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "ChessPrism_Icon 10.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "ChessPrism_Icon 9.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "ChessPrism_Icon 12.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "ChessPrism_Icon 11.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
|
|
|
|||
165
ChessPrism/ChessPrism/BoardDetector.swift
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import Foundation
|
||||
import CoreImage
|
||||
import Vision
|
||||
|
||||
/// Errors that can occur during board detection and processing
|
||||
enum BoardDetectionError: Error {
|
||||
case boardNotFound
|
||||
case invalidBoardDimensions
|
||||
case squareExtractionFailed
|
||||
case imageProcessingFailed
|
||||
}
|
||||
|
||||
final class BoardDetector {
|
||||
// Share CIContext to avoid creating too many Metal command queues
|
||||
private static let shared = CIContext()
|
||||
private var context: CIContext { BoardDetector.shared }
|
||||
|
||||
func detectBoard(in image: CIImage) -> CGRect? {
|
||||
let request = VNDetectRectanglesRequest()
|
||||
request.minimumAspectRatio = 0.8
|
||||
request.maximumAspectRatio = 1.2
|
||||
request.minimumSize = 0.4
|
||||
request.maximumObservations = 1
|
||||
request.quadratureTolerance = 30
|
||||
request.minimumConfidence = 0.9
|
||||
|
||||
let requestHandler = VNImageRequestHandler(ciImage: image, options: [:])
|
||||
do {
|
||||
try requestHandler.perform([request])
|
||||
} catch {
|
||||
print("ERROR: Rectangle detection failed - \(error)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let observations = request.results,
|
||||
!observations.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let bestObservation = observations[0]
|
||||
let imageSize = image.extent.size
|
||||
let transform = CGAffineTransform(scaleX: imageSize.width, y: imageSize.height)
|
||||
let detectedRect = bestObservation.boundingBox.applying(transform)
|
||||
|
||||
guard validateDetectedRect(detectedRect, in: image) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return detectedRect
|
||||
}
|
||||
|
||||
/// Piece recognizer for analyzing extracted squares
|
||||
private let pieceRecognizer: PieceRecognizer
|
||||
|
||||
/// Initialize with a piece recognizer
|
||||
init(pieceRecognizer: PieceRecognizer) {
|
||||
self.pieceRecognizer = pieceRecognizer
|
||||
}
|
||||
|
||||
/// Extract a specific square from the board image
|
||||
private func extractSquare(from image: CIImage, in rect: CGRect, at position: BoardPosition) throws -> CGImage {
|
||||
let squareSize = rect.width / 8
|
||||
let x = rect.minX + CGFloat(position.file) * squareSize
|
||||
let y = rect.minY + CGFloat(position.rank) * squareSize
|
||||
let squareRect = CGRect(x: x, y: y, width: squareSize, height: squareSize)
|
||||
|
||||
guard image.extent.contains(squareRect) else {
|
||||
print("ERROR: Square bounds outside image extent at \(position.file),\(position.rank)")
|
||||
throw BoardDetectionError.squareExtractionFailed
|
||||
}
|
||||
|
||||
let croppedImage = image.cropped(to: squareRect)
|
||||
|
||||
guard let cgImage = context.createCGImage(croppedImage, from: croppedImage.extent) else {
|
||||
print("ERROR: Failed to create square image at \(position.file),\(position.rank)")
|
||||
throw BoardDetectionError.squareExtractionFailed
|
||||
}
|
||||
|
||||
return cgImage
|
||||
}
|
||||
|
||||
/// Analyze the board and return the chess position
|
||||
func analyzeBoard(in image: CIImage) async throws -> ChessPosition {
|
||||
print("=== ANALYZING BOARD ===")
|
||||
|
||||
guard let boardRect = detectBoard(in: image) else {
|
||||
print("ERROR: Board detection failed")
|
||||
throw BoardDetectionError.boardNotFound
|
||||
}
|
||||
|
||||
let aspectRatio = boardRect.width / boardRect.height
|
||||
guard boardRect.width > 0, boardRect.height > 0,
|
||||
abs(1 - aspectRatio) < 0.1 else {
|
||||
print("ERROR: Invalid board dimensions")
|
||||
throw BoardDetectionError.invalidBoardDimensions
|
||||
}
|
||||
|
||||
// Reset piece counts before starting new scan
|
||||
pieceRecognizer.resetCounts()
|
||||
|
||||
var squares = [BoardPosition: SquareClassification]()
|
||||
var pieceCount = 0
|
||||
|
||||
// Process each square
|
||||
for rank in 0..<8 {
|
||||
for file in 0..<8 {
|
||||
let boardPosition = BoardPosition(file: file, rank: rank)
|
||||
let squareImage = try extractSquare(from: image, in: boardRect, at: boardPosition)
|
||||
|
||||
// Recognize square content
|
||||
let classification = try await pieceRecognizer.recognizeSquare(from: squareImage, row: rank, col: file)
|
||||
squares[boardPosition] = classification
|
||||
|
||||
// Count pieces for logging
|
||||
if !classification.isEmpty {
|
||||
pieceCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate FEN string and create position
|
||||
let fenGenerator = FenGenerator()
|
||||
let fen = fenGenerator.generateFen(from: squares)
|
||||
guard let position = ChessPosition(fen: fen) else {
|
||||
print("ERROR: Failed to create position from FEN")
|
||||
throw BoardDetectionError.imageProcessingFailed
|
||||
}
|
||||
|
||||
print("\nPieces recognized: \(pieceCount)")
|
||||
print("\nGenerated FEN: \(fen)")
|
||||
|
||||
guard position.isValid else {
|
||||
print("\nERROR: Invalid chess position")
|
||||
throw BoardDetectionError.imageProcessingFailed
|
||||
}
|
||||
|
||||
print("=== BOARD ANALYSIS COMPLETE ===")
|
||||
return position
|
||||
}
|
||||
|
||||
private func validateDetectedRect(_ rect: CGRect, in image: CIImage) -> Bool {
|
||||
let imageSize = image.extent.size
|
||||
|
||||
guard image.extent.contains(rect) else {
|
||||
print("ERROR: Detected rectangle outside image bounds")
|
||||
return false
|
||||
}
|
||||
|
||||
let aspectRatio = rect.width / rect.height
|
||||
guard aspectRatio >= 0.9 && aspectRatio <= 1.1 else {
|
||||
print("ERROR: Invalid board aspect ratio: \(aspectRatio)")
|
||||
return false
|
||||
}
|
||||
|
||||
let minDimension = min(imageSize.width, imageSize.height)
|
||||
let boardSize = max(rect.width, rect.height)
|
||||
let sizeRatio = boardSize / minDimension
|
||||
guard sizeRatio >= 0.4 else {
|
||||
print("ERROR: Board too small relative to image")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
BIN
ChessPrism/ChessPrism/ChessPieceClassifier.mlmodel
Normal file
|
|
@ -4,7 +4,21 @@
|
|||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.screen-capture</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.accessibility</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.ml.coreml</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.executable</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,55 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import ScreenCaptureKit
|
||||
|
||||
@main
|
||||
struct ChessPrismApp: App {
|
||||
@StateObject private var permissionManager = PermissionManager()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environmentObject(permissionManager)
|
||||
.onAppear {
|
||||
permissionManager.requestPermissions()
|
||||
}
|
||||
.alert(isPresented: $permissionManager.showPermissionAlert) {
|
||||
Alert(
|
||||
title: Text("Permissions Required"),
|
||||
message: Text(permissionManager.permissionAlertMessage),
|
||||
primaryButton: .default(Text("Open Settings")) {
|
||||
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy")!)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PermissionManager: ObservableObject {
|
||||
@Published var showPermissionAlert = false
|
||||
@Published var permissionAlertMessage = ""
|
||||
|
||||
func requestPermissions() {
|
||||
requestScreenRecordingPermission()
|
||||
requestAccessibilityPermission()
|
||||
}
|
||||
|
||||
private func requestScreenRecordingPermission() {
|
||||
let status = CGPreflightScreenCaptureAccess()
|
||||
if !status {
|
||||
permissionAlertMessage += "Screen recording permission is required to analyze chess positions.\n"
|
||||
showPermissionAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
private func requestAccessibilityPermission() {
|
||||
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
|
||||
if !AXIsProcessTrustedWithOptions(options) {
|
||||
permissionAlertMessage += "Accessibility permission is required to interact with other applications.\n"
|
||||
showPermissionAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,335 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// ChessPrism
|
||||
//
|
||||
// Created by Chris Haulmark on 1/6/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
|
||||
struct ContentView: View {
|
||||
// MARK: - Chess Position View Components
|
||||
|
||||
struct ChessPieceView: View {
|
||||
let piece: ChessPiece
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "globe")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Hello, world!")
|
||||
Text(pieceSymbol)
|
||||
.font(.system(size: 24, weight: .bold))
|
||||
.foregroundColor(piece.color == .white ? .white : .black)
|
||||
}
|
||||
|
||||
private var pieceSymbol: String {
|
||||
switch piece.type {
|
||||
case .pawn: return "♟"
|
||||
case .knight: return "♞"
|
||||
case .bishop: return "♝"
|
||||
case .rook: return "♜"
|
||||
case .queen: return "♛"
|
||||
case .king: return "♚"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChessboardSquareView: View {
|
||||
let isLightSquare: Bool
|
||||
let piece: ChessPiece?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(isLightSquare ? Color.white : Color.gray)
|
||||
.frame(width: 40, height: 40)
|
||||
|
||||
if let piece = piece {
|
||||
ChessPieceView(piece: piece)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChessboardView: View {
|
||||
let position: ChessPosition?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
ForEach((0..<8).reversed(), id: \.self) { rank in
|
||||
HStack(spacing: 0) {
|
||||
ForEach(0..<8, id: \.self) { file in
|
||||
let boardPosition = BoardPosition(file: file, rank: rank)
|
||||
ChessboardSquareView(
|
||||
isLightSquare: (rank + file) % 2 == 0,
|
||||
piece: position?[boardPosition]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.border(Color.black, width: 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct AnalysisStatusView: View {
|
||||
let isAnalyzing: Bool
|
||||
let confidence: Double
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
if isAnalyzing {
|
||||
ProgressView("Analyzing position...")
|
||||
} else {
|
||||
Text("Recognition Confidence: \(Int(confidence * 100))%")
|
||||
.foregroundColor(confidence > 0.7 ? .green : .orange)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
class CustomNSView: NSView {
|
||||
private static let invisibleCursor: NSCursor = {
|
||||
let image = NSImage(size: NSSize(width: 1, height: 1))
|
||||
image.lockFocus()
|
||||
NSColor.clear.set()
|
||||
NSRect(x: 0, y: 0, width: 1, height: 1).fill()
|
||||
image.unlockFocus()
|
||||
return NSCursor(image: image, hotSpot: .zero)
|
||||
}()
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
setupView()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupView()
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
wantsLayer = true
|
||||
layer?.backgroundColor = NSColor.clear.cgColor
|
||||
}
|
||||
|
||||
override func resetCursorRects() {
|
||||
super.resetCursorRects()
|
||||
addCursorRect(bounds, cursor: Self.invisibleCursor)
|
||||
}
|
||||
}
|
||||
|
||||
struct HiddenCursorView: NSViewRepresentable {
|
||||
func makeNSView(context: Context) -> CustomNSView {
|
||||
let view = CustomNSView()
|
||||
view.wantsLayer = true
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: CustomNSView, context: Context) {
|
||||
nsView.setFrameSize(nsView.frame.size)
|
||||
}
|
||||
}
|
||||
|
||||
struct CaptureStatusButton: View {
|
||||
let isCapturing: Bool
|
||||
let isBoardDetected: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(statusColor)
|
||||
.frame(width: 12, height: 12)
|
||||
Text(statusText)
|
||||
.foregroundColor(statusColor)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(statusColor, lineWidth: 2)
|
||||
)
|
||||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
if isCapturing && isBoardDetected {
|
||||
return .green
|
||||
} else if isCapturing {
|
||||
return .yellow
|
||||
} else {
|
||||
return .gray
|
||||
}
|
||||
}
|
||||
|
||||
private var statusText: String {
|
||||
if isCapturing && isBoardDetected {
|
||||
return "Capturing"
|
||||
} else if isCapturing {
|
||||
return "Waiting for board"
|
||||
} else {
|
||||
return "Not capturing"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CaptureView: View {
|
||||
@ObservedObject var viewModel: ScreenCaptureViewModel
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack {
|
||||
Text("Full Capture")
|
||||
if let image = viewModel.capturedImage {
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400)
|
||||
} else {
|
||||
Text("No capture available")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
VStack {
|
||||
Text("Chessboard Preview")
|
||||
if let image = viewModel.croppedBoardImage {
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400)
|
||||
.overlay(
|
||||
HiddenCursorView()
|
||||
.allowsHitTesting(true)
|
||||
)
|
||||
} else {
|
||||
Text("No board detected")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
if viewModel.snapshotTaken {
|
||||
Divider()
|
||||
.padding(.vertical)
|
||||
|
||||
Text("Latest Snapshot")
|
||||
if let snapshot = viewModel.latestSnapshot {
|
||||
Image(nsImage: snapshot)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400)
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct AnalysisView: View {
|
||||
@ObservedObject var viewModel: ScreenCaptureViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let position = viewModel.currentPosition {
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Current Position")
|
||||
.font(.headline)
|
||||
ChessboardView(position: position)
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
.shadow(radius: 2)
|
||||
|
||||
AnalysisStatusView(
|
||||
isAnalyzing: viewModel.isAnalyzing,
|
||||
confidence: viewModel.recognitionConfidence
|
||||
)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Position Details")
|
||||
.font(.headline)
|
||||
Text("FEN: \(position.fen)")
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.padding(.vertical)
|
||||
|
||||
if viewModel.captureError == .recognitionFailed {
|
||||
Text("Recognition Error")
|
||||
.foregroundColor(.red)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
} else {
|
||||
Text("No position detected")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var viewModel = ScreenCaptureViewModel()
|
||||
@State private var selectedTab = 0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// Status and Scan controls
|
||||
HStack {
|
||||
CaptureStatusButton(
|
||||
isCapturing: viewModel.isCapturing,
|
||||
isBoardDetected: viewModel.isBoardDetected
|
||||
)
|
||||
|
||||
Button(action: {
|
||||
Task {
|
||||
await viewModel.takeSnapshot()
|
||||
}
|
||||
}) {
|
||||
Text("Scan")
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(!viewModel.isCapturing || !viewModel.isBoardDetected)
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.startMonitoring()
|
||||
}
|
||||
.onDisappear {
|
||||
viewModel.stopMonitoring()
|
||||
}
|
||||
|
||||
// Error display
|
||||
if let error = viewModel.captureError {
|
||||
Text(error.localizedDescription)
|
||||
.foregroundColor(.red)
|
||||
.padding()
|
||||
}
|
||||
|
||||
// Main content area with tabs
|
||||
TabView(selection: $selectedTab) {
|
||||
CaptureView(viewModel: viewModel)
|
||||
.tabItem {
|
||||
Label("Capture", systemImage: "camera")
|
||||
}
|
||||
.tag(0)
|
||||
|
||||
AnalysisView(viewModel: viewModel)
|
||||
.tabItem {
|
||||
Label("Analysis", systemImage: "magnifyingglass")
|
||||
}
|
||||
.tag(1)
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 1000, minHeight: 700)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
ChessPrism/ChessPrism/Info.plist
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>ChessPrism needs access to the camera for AR features</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>ChessPrism needs access to photos for saving game screenshots</string>
|
||||
</dict>
|
||||
</plist>
|
||||
289
ChessPrism/ChessPrism/Models/ChessPosition.swift
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
import Foundation
|
||||
|
||||
/// Represents a chess piece color
|
||||
enum PieceColor: String {
|
||||
case white
|
||||
case black
|
||||
}
|
||||
|
||||
/// Represents a chess piece type
|
||||
enum PieceType: String {
|
||||
case pawn
|
||||
case knight
|
||||
case bishop
|
||||
case rook
|
||||
case queen
|
||||
case king
|
||||
|
||||
/// FEN notation for the piece
|
||||
var fenSymbol: String {
|
||||
switch self {
|
||||
case .pawn: return "p"
|
||||
case .knight: return "n"
|
||||
case .bishop: return "b"
|
||||
case .rook: return "r"
|
||||
case .queen: return "q"
|
||||
case .king: return "k"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a chess piece with its type and color
|
||||
struct ChessPiece: Equatable {
|
||||
let type: PieceType
|
||||
let color: PieceColor
|
||||
|
||||
/// FEN notation for the piece (uppercase for white, lowercase for black)
|
||||
var fenSymbol: String {
|
||||
let symbol = type.fenSymbol
|
||||
return color == .white ? symbol.uppercased() : symbol
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a position on the chess board
|
||||
struct BoardPosition: Equatable, Hashable {
|
||||
let file: Int // 0-7 for a-h
|
||||
let rank: Int // 0-7 for 1-8
|
||||
|
||||
/// Initialize from file and rank indices (0-7)
|
||||
init(file: Int, rank: Int) {
|
||||
guard file >= 0, file < 8, rank >= 0, rank < 8 else {
|
||||
fatalError("Invalid board position: file \(file), rank \(rank)")
|
||||
}
|
||||
self.file = file
|
||||
self.rank = rank
|
||||
}
|
||||
|
||||
/// Hash function implementation for Hashable conformance
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(file)
|
||||
hasher.combine(rank)
|
||||
}
|
||||
|
||||
/// Initialize from algebraic notation (e.g., "e4")
|
||||
init?(algebraic: String) {
|
||||
guard algebraic.count == 2,
|
||||
let file = algebraic.first?.asciiValue,
|
||||
let rank = algebraic.last?.wholeNumberValue,
|
||||
file >= UInt8(ascii: "a"), file <= UInt8(ascii: "h"),
|
||||
rank >= 1, rank <= 8 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.file = Int(file - UInt8(ascii: "a"))
|
||||
self.rank = rank - 1
|
||||
}
|
||||
|
||||
/// Convert to algebraic notation
|
||||
var algebraic: String {
|
||||
let fileChar = Character(UnicodeScalar(UInt8(ascii: "a") + UInt8(file)))
|
||||
return "\(fileChar)\(rank + 1)"
|
||||
}
|
||||
|
||||
/// Validate if the position is within bounds
|
||||
var isValid: Bool {
|
||||
file >= 0 && file < 8 && rank >= 0 && rank < 8
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a complete chess position
|
||||
struct ChessPosition {
|
||||
/// 8x8 grid representing the board state, nil means empty square
|
||||
private var board: [[ChessPiece?]]
|
||||
|
||||
/// Initialize an empty board
|
||||
init() {
|
||||
board = Array(repeating: Array(repeating: nil, count: 8), count: 8)
|
||||
}
|
||||
|
||||
/// Initialize from FEN string
|
||||
init?(fen: String) {
|
||||
self.init()
|
||||
|
||||
let components = fen.components(separatedBy: " ")
|
||||
guard components.count >= 1 else { return nil }
|
||||
|
||||
let ranks = components[0].components(separatedBy: "/")
|
||||
guard ranks.count == 8 else { return nil }
|
||||
|
||||
for (rankIndex, rank) in ranks.enumerated() {
|
||||
var fileIndex = 0
|
||||
|
||||
for char in rank {
|
||||
if let number = Int(String(char)) {
|
||||
fileIndex += number
|
||||
} else {
|
||||
guard fileIndex < 8 else { return nil }
|
||||
|
||||
let color: PieceColor = char.isUppercase ? .white : .black
|
||||
let lowerChar = char.lowercased()
|
||||
|
||||
guard let type = pieceTypeFromFen(String(lowerChar)) else { return nil }
|
||||
|
||||
board[7 - rankIndex][fileIndex] = ChessPiece(type: type, color: color)
|
||||
fileIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
guard fileIndex == 8 else { return nil }
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert position to FEN string (piece placement only)
|
||||
var fen: String {
|
||||
var result = ""
|
||||
|
||||
for rankIndex in (0...7).reversed() {
|
||||
var emptyCount = 0
|
||||
|
||||
for fileIndex in 0...7 {
|
||||
if let piece = board[rankIndex][fileIndex] {
|
||||
if emptyCount > 0 {
|
||||
result += String(emptyCount)
|
||||
emptyCount = 0
|
||||
}
|
||||
result += piece.fenSymbol
|
||||
} else {
|
||||
emptyCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
if emptyCount > 0 {
|
||||
result += String(emptyCount)
|
||||
}
|
||||
|
||||
if rankIndex > 0 {
|
||||
result += "/"
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Get piece at position
|
||||
subscript(position: BoardPosition) -> ChessPiece? {
|
||||
get {
|
||||
guard position.isValid else { return nil }
|
||||
return board[position.rank][position.file]
|
||||
}
|
||||
set {
|
||||
guard position.isValid else { return }
|
||||
board[position.rank][position.file] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Get piece at algebraic position
|
||||
subscript(algebraic: String) -> ChessPiece? {
|
||||
get {
|
||||
guard let position = BoardPosition(algebraic: algebraic) else { return nil }
|
||||
return self[position]
|
||||
}
|
||||
set {
|
||||
guard let position = BoardPosition(algebraic: algebraic) else { return }
|
||||
self[position] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate if the position is legal
|
||||
var isValid: Bool {
|
||||
print("\n=== VALIDATING CHESS POSITION ===")
|
||||
var whitePieces = [PieceType: Int]()
|
||||
var blackPieces = [PieceType: Int]()
|
||||
|
||||
// Count all pieces
|
||||
for rank in 0...7 {
|
||||
for file in 0...7 {
|
||||
if let piece = board[rank][file] {
|
||||
if piece.color == .white {
|
||||
whitePieces[piece.type, default: 0] += 1
|
||||
} else {
|
||||
blackPieces[piece.type, default: 0] += 1
|
||||
}
|
||||
|
||||
// Check pawns on invalid ranks
|
||||
if piece.type == .pawn && (rank == 0 || rank == 7) {
|
||||
print("ERROR: Pawn found on first/last rank")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print piece counts
|
||||
print("White pieces:")
|
||||
for (type, count) in whitePieces {
|
||||
print("- \(type): \(count)")
|
||||
}
|
||||
print("Black pieces:")
|
||||
for (type, count) in blackPieces {
|
||||
print("- \(type): \(count)")
|
||||
}
|
||||
|
||||
// Validate piece counts
|
||||
let whiteTotal = whitePieces.values.reduce(0, +)
|
||||
let blackTotal = blackPieces.values.reduce(0, +)
|
||||
|
||||
if whiteTotal > 16 {
|
||||
print("ERROR: Too many white pieces (\(whiteTotal))")
|
||||
return false
|
||||
}
|
||||
if blackTotal > 16 {
|
||||
print("ERROR: Too many black pieces (\(blackTotal))")
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate kings
|
||||
if whitePieces[.king] ?? 0 != 1 {
|
||||
print("ERROR: Invalid number of white kings (\(whitePieces[.king] ?? 0))")
|
||||
return false
|
||||
}
|
||||
if blackPieces[.king] ?? 0 != 1 {
|
||||
print("ERROR: Invalid number of black kings (\(blackPieces[.king] ?? 0))")
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate pawns
|
||||
if whitePieces[.pawn] ?? 0 > 8 {
|
||||
print("ERROR: Too many white pawns (\(whitePieces[.pawn] ?? 0))")
|
||||
return false
|
||||
}
|
||||
if blackPieces[.pawn] ?? 0 > 8 {
|
||||
print("ERROR: Too many black pawns (\(blackPieces[.pawn] ?? 0))")
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate other pieces
|
||||
for pieceType in [PieceType.queen, .rook, .bishop, .knight] {
|
||||
if whitePieces[pieceType] ?? 0 > 2 {
|
||||
print("ERROR: Too many white \(pieceType)s (\(whitePieces[pieceType] ?? 0))")
|
||||
return false
|
||||
}
|
||||
if blackPieces[pieceType] ?? 0 > 2 {
|
||||
print("ERROR: Too many black \(pieceType)s (\(blackPieces[pieceType] ?? 0))")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
print("Position validation successful")
|
||||
return true
|
||||
}
|
||||
|
||||
/// Helper function to convert FEN piece symbol to PieceType
|
||||
private func pieceTypeFromFen(_ symbol: String) -> PieceType? {
|
||||
switch symbol {
|
||||
case "p": return .pawn
|
||||
case "n": return .knight
|
||||
case "b": return .bishop
|
||||
case "r": return .rook
|
||||
case "q": return .queen
|
||||
case "k": return .king
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize with the standard starting position
|
||||
static var startingPosition: ChessPosition {
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
ChessPosition(fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR")!
|
||||
}
|
||||
}
|
||||
54
ChessPrism/ChessPrism/Models/SquareClassification.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import Foundation
|
||||
|
||||
struct SquareClassification {
|
||||
let pieceType: PieceType?
|
||||
let pieceColor: PieceColor?
|
||||
let isHighlighted: Bool
|
||||
|
||||
var isEmpty: Bool {
|
||||
return pieceType == nil
|
||||
}
|
||||
|
||||
static func empty(highlighted: Bool = false) -> SquareClassification {
|
||||
return SquareClassification(pieceType: nil, pieceColor: nil, isHighlighted: highlighted)
|
||||
}
|
||||
|
||||
init(pieceType: PieceType?, pieceColor: PieceColor?, isHighlighted: Bool = false) {
|
||||
self.pieceType = pieceType
|
||||
self.pieceColor = pieceColor
|
||||
self.isHighlighted = isHighlighted
|
||||
}
|
||||
|
||||
init?(label: String) {
|
||||
// Match exact categories from trained model
|
||||
switch label {
|
||||
case "white_pawn":
|
||||
self.init(pieceType: .pawn, pieceColor: .white)
|
||||
case "white_knight":
|
||||
self.init(pieceType: .knight, pieceColor: .white)
|
||||
case "white_bishop":
|
||||
self.init(pieceType: .bishop, pieceColor: .white)
|
||||
case "white_rook":
|
||||
self.init(pieceType: .rook, pieceColor: .white)
|
||||
case "white_queen":
|
||||
self.init(pieceType: .queen, pieceColor: .white)
|
||||
case "white_king":
|
||||
self.init(pieceType: .king, pieceColor: .white)
|
||||
case "black_pawn":
|
||||
self.init(pieceType: .pawn, pieceColor: .black)
|
||||
case "black_knight":
|
||||
self.init(pieceType: .knight, pieceColor: .black)
|
||||
case "black_bishop":
|
||||
self.init(pieceType: .bishop, pieceColor: .black)
|
||||
case "black_rook":
|
||||
self.init(pieceType: .rook, pieceColor: .black)
|
||||
case "black_queen":
|
||||
self.init(pieceType: .queen, pieceColor: .black)
|
||||
case "black_king":
|
||||
self.init(pieceType: .king, pieceColor: .black)
|
||||
default:
|
||||
// Any unrecognized label returns an empty square
|
||||
self.init(pieceType: nil, pieceColor: nil, isHighlighted: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
72
ChessPrism/ChessPrism/Recognition/FenGenerator.swift
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import Foundation
|
||||
|
||||
/// A class responsible for generating FEN strings from square classifications
|
||||
final class FenGenerator {
|
||||
/// Generate a FEN string from a set of square classifications
|
||||
/// - Parameter squares: Dictionary mapping board positions to their classifications
|
||||
/// - Returns: FEN string representing the position
|
||||
func generateFen(from squares: [BoardPosition: SquareClassification]) -> String {
|
||||
var fen = ""
|
||||
var emptyCount = 0
|
||||
|
||||
// Process each rank from top to bottom (8 to 1)
|
||||
for rank in (0...7).reversed() {
|
||||
// Process each file from left to right (a to h)
|
||||
for file in 0...7 {
|
||||
let position = BoardPosition(file: file, rank: rank)
|
||||
guard let square = squares[position] else {
|
||||
// If square is missing, treat as empty
|
||||
emptyCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if square.isEmpty {
|
||||
// Count consecutive empty squares
|
||||
emptyCount += 1
|
||||
} else if let color = square.pieceColor,
|
||||
let type = square.pieceType {
|
||||
// If we had empty squares before this piece, add the count
|
||||
if emptyCount > 0 {
|
||||
fen += String(emptyCount)
|
||||
emptyCount = 0
|
||||
}
|
||||
|
||||
// Add the piece symbol
|
||||
let symbol = pieceSymbol(color: color, type: type)
|
||||
fen += symbol
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining empty squares at end of rank
|
||||
if emptyCount > 0 {
|
||||
fen += String(emptyCount)
|
||||
emptyCount = 0
|
||||
}
|
||||
|
||||
// Add rank separator (except for last rank)
|
||||
if rank > 0 {
|
||||
fen += "/"
|
||||
}
|
||||
}
|
||||
|
||||
return fen
|
||||
}
|
||||
|
||||
/// Get the FEN symbol for a piece
|
||||
/// - Parameters:
|
||||
/// - color: Color of the piece
|
||||
/// - type: Type of the piece
|
||||
/// - Returns: FEN symbol (uppercase for white, lowercase for black)
|
||||
private func pieceSymbol(color: PieceColor, type: PieceType) -> String {
|
||||
let symbol: String
|
||||
switch type {
|
||||
case .king: symbol = "K"
|
||||
case .queen: symbol = "Q"
|
||||
case .rook: symbol = "R"
|
||||
case .bishop: symbol = "B"
|
||||
case .knight: symbol = "N"
|
||||
case .pawn: symbol = "P"
|
||||
}
|
||||
return color == .white ? symbol : symbol.lowercased()
|
||||
}
|
||||
}
|
||||
49
ChessPrism/ChessPrism/Recognition/MoveDetector.swift
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import Foundation
|
||||
|
||||
/// Represents a detected chess move
|
||||
struct DetectedMove {
|
||||
/// The source square where the piece moved from
|
||||
let from: BoardPosition
|
||||
|
||||
/// The target square where the piece moved to
|
||||
let to: BoardPosition
|
||||
|
||||
/// The piece that was moved
|
||||
let piece: ChessPiece
|
||||
}
|
||||
|
||||
/// A class responsible for detecting moves from highlighted squares
|
||||
final class MoveDetector {
|
||||
/// Detect a move from a set of square classifications
|
||||
/// - Parameter squares: Dictionary mapping board positions to their classifications
|
||||
/// - Returns: Detected move if one is found, nil otherwise
|
||||
func detectMove(from squares: [BoardPosition: SquareClassification]) -> DetectedMove? {
|
||||
var sourceSquare: BoardPosition?
|
||||
var targetSquare: BoardPosition?
|
||||
var movedPiece: ChessPiece?
|
||||
|
||||
// Find highlighted squares
|
||||
for (position, classification) in squares {
|
||||
guard classification.isHighlighted else { continue }
|
||||
|
||||
if classification.isEmpty {
|
||||
// Empty highlighted square is the source
|
||||
sourceSquare = position
|
||||
} else if let color = classification.pieceColor,
|
||||
let type = classification.pieceType {
|
||||
// Piece on highlighted square is the target
|
||||
targetSquare = position
|
||||
movedPiece = ChessPiece(type: type, color: color)
|
||||
}
|
||||
}
|
||||
|
||||
// Return move if we found both squares and the piece
|
||||
if let from = sourceSquare,
|
||||
let to = targetSquare,
|
||||
let piece = movedPiece {
|
||||
return DetectedMove(from: from, to: to, piece: piece)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
160
ChessPrism/ChessPrism/Recognition/PieceRecognizer.swift
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import Foundation
|
||||
import Vision
|
||||
import CoreML
|
||||
import CoreImage
|
||||
|
||||
/// Errors that can occur during piece recognition
|
||||
enum PieceRecognitionError: Error {
|
||||
case invalidImageDimensions
|
||||
case modelLoadError
|
||||
case recognitionFailed(String)
|
||||
case lowConfidence
|
||||
case invalidInput
|
||||
}
|
||||
|
||||
/// A class responsible for recognizing chess pieces from images
|
||||
final class PieceRecognizer {
|
||||
// MARK: - Properties
|
||||
|
||||
/// Vision model for piece classification
|
||||
private let vnModel: VNCoreMLModel
|
||||
|
||||
/// Shared CIContext for image processing
|
||||
private static let ciContext = CIContext()
|
||||
|
||||
/// Latest classification results
|
||||
private var classificationResults: [VNClassificationObservation]?
|
||||
|
||||
/// Confidence threshold
|
||||
private let confidenceThreshold: Float = 0.75
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init() throws {
|
||||
print("=== INITIALIZING PIECE RECOGNIZER ===")
|
||||
|
||||
let bundle = Bundle.main
|
||||
|
||||
// Load model
|
||||
guard let modelURL = bundle.url(forResource: "ChessPieceClassifier", withExtension: "mlmodelc") else {
|
||||
print("ERROR: Model not found in bundle at \(bundle.bundlePath)")
|
||||
throw PieceRecognitionError.modelLoadError
|
||||
}
|
||||
|
||||
do {
|
||||
let config = MLModelConfiguration()
|
||||
config.computeUnits = .all
|
||||
let model = try MLModel(contentsOf: modelURL, configuration: config)
|
||||
self.vnModel = try VNCoreMLModel(for: model)
|
||||
print("Model loaded successfully from: \(modelURL.path)")
|
||||
} catch {
|
||||
print("ERROR: Failed to load model - \(error)")
|
||||
throw PieceRecognitionError.modelLoadError
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Recognition Methods
|
||||
|
||||
func recognizeSquare(from image: CGImage, row: Int = 0, col: Int = 0) async throws -> SquareClassification {
|
||||
// Validate image dimensions
|
||||
guard image.width > 0, image.height > 0,
|
||||
abs(1 - Float(image.width) / Float(image.height)) < 0.1 else {
|
||||
print("ERROR: Invalid square dimensions \(image.width)x\(image.height)")
|
||||
throw PieceRecognitionError.invalidImageDimensions
|
||||
}
|
||||
|
||||
let handler = VNImageRequestHandler(cgImage: image)
|
||||
var classificationError: Error?
|
||||
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
let request = VNCoreMLRequest(model: vnModel) { request, error in
|
||||
if let error = error {
|
||||
classificationError = error
|
||||
continuation.resume(throwing: error)
|
||||
return
|
||||
}
|
||||
self.classificationResults = request.results as? [VNClassificationObservation]
|
||||
continuation.resume()
|
||||
}
|
||||
request.imageCropAndScaleOption = .centerCrop
|
||||
|
||||
do {
|
||||
try handler.perform([request])
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
|
||||
if let error = classificationError {
|
||||
print("ERROR: Classification failed - \(error)")
|
||||
throw PieceRecognitionError.recognitionFailed(error.localizedDescription)
|
||||
}
|
||||
|
||||
guard let results = self.classificationResults,
|
||||
let topResult = results.first else {
|
||||
print("ERROR: No classification results")
|
||||
throw PieceRecognitionError.recognitionFailed("No results")
|
||||
}
|
||||
|
||||
// Print results
|
||||
print("\nClassification results:")
|
||||
for result in results.prefix(3) {
|
||||
print("- \(result.identifier): \(result.confidence)")
|
||||
}
|
||||
|
||||
// Check confidence threshold
|
||||
if topResult.confidence < confidenceThreshold {
|
||||
print("Low confidence (\(topResult.confidence)) for \(topResult.identifier)")
|
||||
return .empty()
|
||||
}
|
||||
|
||||
// Try to create a SquareClassification from the label
|
||||
if let classification = SquareClassification(label: topResult.identifier) {
|
||||
print("Classified as \(topResult.identifier) with confidence \(topResult.confidence)")
|
||||
return classification
|
||||
}
|
||||
|
||||
// Return empty square if classification fails
|
||||
print("Classification failed for \(topResult.identifier) (\(topResult.confidence))")
|
||||
return .empty()
|
||||
}
|
||||
|
||||
/// Preprocess an image for recognition
|
||||
/// - Parameter image: Input CGImage
|
||||
/// - Returns: Preprocessed CGImage
|
||||
func preprocessImage(_ image: CGImage) throws -> CGImage {
|
||||
let ciImage = CIImage(cgImage: image)
|
||||
|
||||
// Apply preprocessing filters
|
||||
// First pass: Enhance contrast and edges
|
||||
var processed = ciImage
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
"inputContrast": 1.3,
|
||||
"inputBrightness": 0.0,
|
||||
"inputSaturation": 1.0
|
||||
])
|
||||
.applyingFilter("CIUnsharpMask", parameters: [
|
||||
"inputRadius": 2.0,
|
||||
"inputIntensity": 0.8
|
||||
])
|
||||
|
||||
// Second pass: Reduce noise and enhance details
|
||||
processed = processed
|
||||
.applyingFilter("CINoiseReduction", parameters: [
|
||||
"inputNoiseLevel": 0.2,
|
||||
"inputSharpness": 0.6
|
||||
])
|
||||
.applyingFilter("CIColorControls", parameters: [
|
||||
"inputContrast": 1.2,
|
||||
"inputBrightness": 0.0,
|
||||
"inputSaturation": 1.0
|
||||
])
|
||||
|
||||
// Convert back to CGImage
|
||||
guard let outputImage = Self.ciContext.createCGImage(processed, from: processed.extent) else {
|
||||
throw PieceRecognitionError.invalidInput
|
||||
}
|
||||
|
||||
return outputImage
|
||||
}
|
||||
}
|
||||
77
ChessPrism/ChessPrism/ScreenCapture.swift
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import Foundation
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import CoreMedia
|
||||
import ScreenCaptureKit
|
||||
import AVFoundation
|
||||
|
||||
class ScreenCapture: NSObject, SCStreamOutput {
|
||||
func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
|
||||
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
|
||||
let rep = NSCIImageRep(ciImage: ciImage)
|
||||
let nsImage = NSImage(size: rep.size)
|
||||
nsImage.addRepresentation(rep)
|
||||
|
||||
self.lastCapturedImage = nsImage
|
||||
}
|
||||
}
|
||||
|
||||
private var activeStream: SCStream?
|
||||
private var lastCapturedImage: NSImage?
|
||||
private var excludeCursor: Bool = false
|
||||
|
||||
func startCapture(excludeCursor: Bool = false) async throws {
|
||||
// Always stop any existing capture before starting a new one
|
||||
if activeStream != nil {
|
||||
try await stopCapture()
|
||||
}
|
||||
|
||||
self.excludeCursor = excludeCursor
|
||||
|
||||
guard #available(macOS 12.3, *) else { return }
|
||||
|
||||
let content = try await SCShareableContent.current
|
||||
|
||||
// Get the window from the content
|
||||
guard let window = content.windows.first(where: { window in
|
||||
let bundleID = window.owningApplication?.bundleIdentifier ?? ""
|
||||
let isChessApp = bundleID == "com.chess.iphone"
|
||||
let hasValidSize = window.frame.width > 100 && window.frame.height > 100
|
||||
return isChessApp && window.isOnScreen && hasValidSize
|
||||
}) else {
|
||||
throw NSError(domain: "ChessPrism", code: 1, userInfo: [NSLocalizedDescriptionKey: "Chess window not found"])
|
||||
}
|
||||
|
||||
let filter = SCContentFilter(desktopIndependentWindow: window)
|
||||
let config = SCStreamConfiguration()
|
||||
config.width = Int(window.frame.width)
|
||||
config.height = Int(window.frame.height)
|
||||
config.minimumFrameInterval = CMTime(value: 1, timescale: 30) // 30 FPS
|
||||
config.queueDepth = 5 // Buffer up to 5 frames
|
||||
config.showsCursor = !excludeCursor // Set cursor visibility
|
||||
|
||||
let stream = SCStream(filter: filter, configuration: config, delegate: nil)
|
||||
try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: DispatchQueue.global(qos: .userInitiated))
|
||||
|
||||
// Store the stream before starting it
|
||||
activeStream = stream
|
||||
try await stream.startCapture()
|
||||
}
|
||||
|
||||
func stopCapture() async throws {
|
||||
if let stream = activeStream {
|
||||
try await stream.stopCapture()
|
||||
activeStream = nil
|
||||
lastCapturedImage = nil
|
||||
}
|
||||
}
|
||||
|
||||
func getCurrentImage() -> NSImage? {
|
||||
return lastCapturedImage
|
||||
}
|
||||
}
|
||||
294
ChessPrism/ChessPrism/ScreenCaptureViewModel.swift
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
import Foundation
|
||||
import AppKit
|
||||
import CoreImage
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
class ScreenCaptureViewModel: ObservableObject {
|
||||
@Published var capturedImage: NSImage?
|
||||
@Published var croppedBoardImage: NSImage?
|
||||
@Published var captureError: CaptureError?
|
||||
@Published var isCapturing = false
|
||||
@Published var isBoardDetected = false
|
||||
@Published var isAutoCapturing = true // Default to auto-capture mode
|
||||
@Published var snapshotTaken = false // Track if snapshot was taken
|
||||
@Published var latestSnapshot: NSImage? // Make snapshot accessible to view
|
||||
@Published var currentPosition: ChessPosition? // Current chess position
|
||||
@Published var recognitionConfidence: Double = 0.0 // Recognition confidence
|
||||
@Published var isAnalyzing = false // Track analysis state
|
||||
|
||||
private let captureManager = ScreenCapture() // For actual capture
|
||||
private let monitorManager = ScreenCapture() // For monitoring
|
||||
private let pieceRecognizer: PieceRecognizer
|
||||
private let boardDetector: BoardDetector
|
||||
private var captureTask: Task<Void, Never>?
|
||||
private var monitorTask: Task<Void, Never>?
|
||||
// Share CIContext to avoid creating too many Metal command queues
|
||||
private static let shared = CIContext()
|
||||
private var context: CIContext { ScreenCaptureViewModel.shared }
|
||||
|
||||
private var detectedBoardRect: CGRect?
|
||||
private var isMonitoring = false
|
||||
|
||||
enum CaptureError: LocalizedError {
|
||||
case chessWindowNotFound
|
||||
case boardDetectionFailed
|
||||
case noBoardDetected
|
||||
case snapshotFailed
|
||||
case recognitionFailed
|
||||
case invalidPosition
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .chessWindowNotFound:
|
||||
return "Chess window not found"
|
||||
case .boardDetectionFailed:
|
||||
return "Failed to detect chess board"
|
||||
case .noBoardDetected:
|
||||
return "No chess board detected"
|
||||
case .snapshotFailed:
|
||||
return "Failed to take snapshot"
|
||||
case .recognitionFailed:
|
||||
return "Failed to recognize pieces"
|
||||
case .invalidPosition:
|
||||
return "Invalid chess position detected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
// Initialize piece recognizer
|
||||
do {
|
||||
pieceRecognizer = try PieceRecognizer()
|
||||
boardDetector = BoardDetector(pieceRecognizer: pieceRecognizer)
|
||||
} catch {
|
||||
fatalError("Failed to initialize piece recognizer: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func takeSnapshot() async {
|
||||
print("=== SCAN BUTTON PRESSED ===")
|
||||
do {
|
||||
try await captureManager.stopCapture()
|
||||
try await captureManager.startCapture(excludeCursor: true)
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
||||
|
||||
if let currentImage = captureManager.getCurrentImage() {
|
||||
print("Captured image: \(currentImage.size)")
|
||||
try await processImage(currentImage, analyzePosition: true)
|
||||
|
||||
if let boardImage = croppedBoardImage {
|
||||
latestSnapshot = boardImage
|
||||
snapshotTaken = true
|
||||
|
||||
if let position = currentPosition {
|
||||
print("Successfully detected position")
|
||||
} else {
|
||||
print("ERROR: Failed to detect position")
|
||||
captureError = .recognitionFailed
|
||||
}
|
||||
} else {
|
||||
print("ERROR: Failed to crop board image")
|
||||
captureError = .snapshotFailed
|
||||
currentPosition = nil
|
||||
}
|
||||
} else {
|
||||
print("ERROR: Failed to capture image")
|
||||
captureError = .snapshotFailed
|
||||
currentPosition = nil
|
||||
}
|
||||
|
||||
try await captureManager.startCapture(excludeCursor: false)
|
||||
} catch {
|
||||
print("ERROR: Snapshot failed - \(error)")
|
||||
captureError = .snapshotFailed
|
||||
currentPosition = nil
|
||||
try? await captureManager.startCapture(excludeCursor: false)
|
||||
}
|
||||
}
|
||||
|
||||
func startMonitoring() {
|
||||
guard !isMonitoring else { return }
|
||||
isMonitoring = true
|
||||
|
||||
monitorTask = Task {
|
||||
monitorLoop: while !Task.isCancelled {
|
||||
do {
|
||||
// Start monitoring capture
|
||||
try await monitorManager.startCapture()
|
||||
|
||||
while !Task.isCancelled {
|
||||
if let image = monitorManager.getCurrentImage(),
|
||||
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
|
||||
// Check for board
|
||||
let ciImage = CIImage(cgImage: cgImage)
|
||||
if boardDetector.detectBoard(in: ciImage) != nil {
|
||||
// Board detected, start capture if not already capturing
|
||||
if !isCapturing {
|
||||
await startCapture()
|
||||
}
|
||||
}
|
||||
}
|
||||
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
|
||||
}
|
||||
} catch {
|
||||
print("Monitor error: \(error)")
|
||||
// If there's an error, wait briefly and try again
|
||||
if !Task.isCancelled {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||
continue monitorLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isMonitoring = false
|
||||
try? await monitorManager.stopCapture()
|
||||
}
|
||||
}
|
||||
|
||||
func stopMonitoring() {
|
||||
monitorTask?.cancel()
|
||||
monitorTask = nil
|
||||
isMonitoring = false
|
||||
stopCapture()
|
||||
|
||||
Task {
|
||||
try? await monitorManager.stopCapture()
|
||||
}
|
||||
}
|
||||
|
||||
func startCapture() async {
|
||||
guard !isCapturing else { return }
|
||||
|
||||
isCapturing = true
|
||||
captureError = nil
|
||||
snapshotTaken = false
|
||||
latestSnapshot = nil
|
||||
currentPosition = nil
|
||||
recognitionConfidence = 0.0
|
||||
|
||||
do {
|
||||
try await captureManager.startCapture()
|
||||
} catch {
|
||||
handleCaptureError(error)
|
||||
isCapturing = false
|
||||
return
|
||||
}
|
||||
|
||||
captureTask = Task {
|
||||
captureLoop: while !Task.isCancelled {
|
||||
do {
|
||||
if let image = captureManager.getCurrentImage() {
|
||||
try await processImage(image, analyzePosition: false)
|
||||
}
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
||||
} catch is CancellationError {
|
||||
break captureLoop
|
||||
} catch {
|
||||
// Just update the error state but continue capturing
|
||||
if !Task.isCancelled {
|
||||
handleCaptureError(error)
|
||||
if isAutoCapturing && error as? CaptureError == .noBoardDetected {
|
||||
// In auto-capture mode, stop capture but keep monitoring
|
||||
stopCapture()
|
||||
break captureLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCapturing {
|
||||
isCapturing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopCapture() {
|
||||
captureTask?.cancel()
|
||||
captureTask = nil
|
||||
isCapturing = false
|
||||
captureError = nil
|
||||
isBoardDetected = false
|
||||
snapshotTaken = false
|
||||
latestSnapshot = nil
|
||||
currentPosition = nil
|
||||
recognitionConfidence = 0.0
|
||||
|
||||
// Clean up capture session
|
||||
Task {
|
||||
try? await captureManager.stopCapture()
|
||||
}
|
||||
}
|
||||
|
||||
private func processImage(_ image: NSImage, analyzePosition: Bool = false) async throws {
|
||||
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
||||
print("ERROR: Failed to convert image for processing")
|
||||
throw CaptureError.boardDetectionFailed
|
||||
}
|
||||
|
||||
let ciImage = CIImage(cgImage: cgImage)
|
||||
|
||||
if let boardRect = boardDetector.detectBoard(in: ciImage) {
|
||||
isBoardDetected = true
|
||||
self.detectedBoardRect = boardRect
|
||||
|
||||
let croppedImage = ciImage.cropped(to: boardRect)
|
||||
updateImages(ciImage: ciImage, croppedImage: croppedImage)
|
||||
|
||||
if analyzePosition && !isAnalyzing {
|
||||
isAnalyzing = true
|
||||
do {
|
||||
print("Analyzing board position...")
|
||||
let position = try await boardDetector.analyzeBoard(in: ciImage)
|
||||
currentPosition = position
|
||||
recognitionConfidence = 1.0
|
||||
isAnalyzing = false
|
||||
} catch {
|
||||
print("ERROR: Position analysis failed - \(error)")
|
||||
isAnalyzing = false
|
||||
currentPosition = nil
|
||||
recognitionConfidence = 0.0
|
||||
if error is BoardDetectionError {
|
||||
throw CaptureError.recognitionFailed
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isBoardDetected = false
|
||||
self.detectedBoardRect = nil
|
||||
currentPosition = nil
|
||||
recognitionConfidence = 0.0
|
||||
|
||||
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
|
||||
self.capturedImage = NSImage(cgImage: cgImage, size: .zero)
|
||||
}
|
||||
self.croppedBoardImage = nil
|
||||
|
||||
if isAutoCapturing {
|
||||
throw CaptureError.noBoardDetected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateImages(ciImage: CIImage, croppedImage: CIImage) {
|
||||
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
|
||||
let capturedNSImage = NSImage(cgImage: cgImage, size: .zero)
|
||||
self.capturedImage = capturedNSImage
|
||||
}
|
||||
|
||||
if let cgCroppedImage = context.createCGImage(croppedImage, from: croppedImage.extent) {
|
||||
let croppedNSImage = NSImage(cgImage: cgCroppedImage, size: .zero)
|
||||
self.croppedBoardImage = croppedNSImage
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCaptureError(_ error: Error) {
|
||||
if let captureError = error as? CaptureError {
|
||||
self.captureError = captureError
|
||||
} else {
|
||||
self.captureError = .boardDetectionFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Package.swift
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// swift-tools-version:5.9
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "ChessPrism",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.macOS(.v13),
|
||||
.visionOS(.v1)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "ChessPrism",
|
||||
targets: ["ChessPrism"]
|
||||
)
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.0.0"),
|
||||
.package(url: "https://github.com/nalexn/ViewInspector", from: "0.9.7"),
|
||||
.package(url: "https://github.com/ReactiveX/RxSwift", from: "6.6.0"),
|
||||
.package(url: "https://github.com/SwiftGen/SwiftGenPlugin", from: "6.6.0")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "ChessPrism",
|
||||
dependencies: [
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
||||
.product(name: "RxSwift", package: "RxSwift")
|
||||
],
|
||||
plugins: [
|
||||
.plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "ChessPrismTests",
|
||||
dependencies: [
|
||||
"ChessPrism",
|
||||
.product(name: "ViewInspector", package: "ViewInspector")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "ChessPrismUITests",
|
||||
dependencies: ["ChessPrism"]
|
||||
)
|
||||
]
|
||||
)
|
||||
37629
cline_docs/Info.txt
Normal file
|
|
@ -1,83 +1,34 @@
|
|||
# Active Context
|
||||
|
||||
## Current Implementation Phase
|
||||
Week 1: Project Setup (In Progress)
|
||||
## Current Task
|
||||
- Fixed model integration issues
|
||||
- Updated code to match model categories
|
||||
- Simplified classification system
|
||||
|
||||
## What we're working on now
|
||||
1. Xcode project configuration
|
||||
- Setting up SwiftUI project structure
|
||||
- Configuring project settings for MacOS Silicon
|
||||
- Adding basic app lifecycle handlers
|
||||
2. Development environment setup
|
||||
- Installing required dependencies (Metal, Vision)
|
||||
- Configuring build settings
|
||||
- Setting up version control
|
||||
3. Project architecture
|
||||
- Creating core module structure
|
||||
- Implementing basic MVVM pattern
|
||||
- Setting up dependency injection
|
||||
## Recent Changes
|
||||
1. SquareClassification.swift:
|
||||
- Exact category mapping:
|
||||
```swift
|
||||
white_pawn, white_knight, white_bishop, white_rook, white_queen, white_king,
|
||||
black_pawn, black_knight, black_bishop, black_rook, black_queen, black_king
|
||||
```
|
||||
- Default to empty square for unrecognized labels
|
||||
- Removed background variations from labels
|
||||
|
||||
## Recent changes
|
||||
- Created detailed implementation plan
|
||||
- Set up development environment
|
||||
- Installed Xcode and necessary tools
|
||||
- Created initial Xcode project with SwiftUI template
|
||||
- Configured Git repository
|
||||
- Added basic project structure
|
||||
- Configured required frameworks (Metal, Vision)
|
||||
- Set up initial build configurations
|
||||
2. PieceRecognizer.swift:
|
||||
- Using VNCoreMLModel consistently
|
||||
- Simplified error handling
|
||||
- Removed unnecessary piece counting
|
||||
- Using Vision framework for classification
|
||||
|
||||
## Next steps
|
||||
1. Complete project setup
|
||||
- Implement permission handlers
|
||||
- Add screen recording permission request
|
||||
- Implement accessibility permissions
|
||||
- Add privacy descriptions in Info.plist
|
||||
- Finalize project architecture
|
||||
- Complete MVVM implementation
|
||||
- Finalize dependency injection setup
|
||||
- Set up core module interfaces
|
||||
2. Begin screenshot module implementation
|
||||
- Implement screen recording permissions flow
|
||||
- Set up screenshot capture service
|
||||
- Configure keyboard shortcut system
|
||||
## Next Steps
|
||||
1. Test model integration:
|
||||
- Verify model loads correctly
|
||||
- Check classification accuracy
|
||||
- Monitor confidence levels
|
||||
|
||||
## Completed Milestones
|
||||
- Initial project setup complete
|
||||
- Development environment configured
|
||||
- Core frameworks integrated
|
||||
- Basic project structure established
|
||||
|
||||
## Technical Details
|
||||
### Current Implementation
|
||||
- Using Swift 5.9
|
||||
- SwiftUI 4.0 for UI
|
||||
- Metal 3.0 for rendering
|
||||
- Vision 2.0 for image analysis
|
||||
- CoreML 5.0 for machine learning
|
||||
|
||||
### Current Architecture
|
||||
- MVVM pattern implementation
|
||||
- Modular architecture
|
||||
- Dependency injection
|
||||
- Async/await concurrency model
|
||||
|
||||
### Current Configuration
|
||||
- MacOS Ventura 13.0+
|
||||
- Xcode 15.0+
|
||||
- Git 2.40+
|
||||
- CoreML Tools 5.0+
|
||||
|
||||
## Pending Tasks
|
||||
1. Complete permission handling implementation
|
||||
2. Finalize project architecture
|
||||
3. Begin screenshot module development
|
||||
4. Set up initial testing infrastructure
|
||||
5. Configure CI/CD pipeline
|
||||
|
||||
## Important Notes
|
||||
- Following detailed implementation plan
|
||||
- Maintaining clean architecture principles
|
||||
- Implementing modern Swift patterns
|
||||
- Ensuring security and privacy compliance
|
||||
- Maintaining documentation standards
|
||||
## Current Issues
|
||||
Fixed:
|
||||
- Model pipeline error
|
||||
- Category mismatches
|
||||
- Classification handling
|
||||
|
|
|
|||
12
cline_docs/chessboard.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
Current position:
|
||||
8 bR .. .. bQ bK bB .. bR
|
||||
7 .. bP .. bB .. bP bP ..
|
||||
6 .. wQ .. bP bP .. .. ..
|
||||
5 bP .. .. .. .. .. .. bP
|
||||
4 .. .. .. wB .. bN .. ..
|
||||
3 .. wB wP bN .. .. .. ..
|
||||
2 wP wP .. .. .. wP wP wP
|
||||
1 wR wN .. .. wK .. wN wR
|
||||
a b c d e f g h
|
||||
|
||||
|
||||
106
cline_docs/problem.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Chess Board Detection Problem
|
||||
|
||||
## Issue Description
|
||||
The chess board detection system is currently only displaying the bottom 2-3 rows of the chess board in the "Chessboard" preview window, while the "Full Capture" window shows the complete chess board. The width of the detected board is correct, but the height is truncated.
|
||||
|
||||
## Visual Evidence
|
||||
- Full Capture: Shows complete chess board with all pieces
|
||||
- Chessboard Preview: Shows only bottom portion (approximately 2.5 rows) of the board
|
||||
- Width appears correct in both views
|
||||
- Height is significantly truncated in Chessboard Preview
|
||||
|
||||
## Technical Analysis
|
||||
|
||||
### Detection Pipeline
|
||||
1. Screen capture works correctly (evidenced by Full Capture view)
|
||||
2. Initial board detection appears to find correct width
|
||||
3. Problem occurs during either:
|
||||
- Rectangle detection phase
|
||||
- Coordinate transformation
|
||||
- Image cropping stage
|
||||
|
||||
### Coordinate System Complexity
|
||||
1. Multiple coordinate systems involved:
|
||||
- Vision framework (bottom-left origin)
|
||||
- NSImage/CGImage (bottom-left origin)
|
||||
- SwiftUI (top-left origin)
|
||||
2. Current transformations may be:
|
||||
- Incorrectly mapping between coordinate spaces
|
||||
- Losing vertical positioning information
|
||||
- Miscalculating crop region
|
||||
|
||||
### Detection Parameters
|
||||
1. Current approach:
|
||||
- Using 0.3-0.5 aspect ratio for detection
|
||||
- 0.4 minimum size requirement
|
||||
- Single observation limit
|
||||
2. These parameters may be:
|
||||
- Causing partial detection of board
|
||||
- Missing full vertical extent
|
||||
- Incorrectly identifying board boundaries
|
||||
|
||||
## Code Areas to Investigate
|
||||
|
||||
### BoardDetector.swift
|
||||
1. detectBoard() function:
|
||||
- Board extraction logic
|
||||
- Coordinate calculations
|
||||
- Rectangle positioning
|
||||
|
||||
2. Rectangle Detection:
|
||||
```swift
|
||||
request.minimumAspectRatio = 0.3
|
||||
request.maximumAspectRatio = 0.5
|
||||
request.minimumSize = 0.4
|
||||
```
|
||||
- May need adjustment for full board capture
|
||||
|
||||
### ScreenCaptureViewModel.swift
|
||||
1. Image cropping logic:
|
||||
- Coordinate transformation
|
||||
- Crop region calculation
|
||||
- Final image generation
|
||||
|
||||
## Potential Solutions to Explore
|
||||
|
||||
1. Detection Approach
|
||||
- Adjust aspect ratio constraints
|
||||
- Modify detection parameters
|
||||
- Consider alternative detection methods
|
||||
|
||||
2. Coordinate Handling
|
||||
- Review all coordinate transformations
|
||||
- Validate coordinate space conversions
|
||||
- Ensure proper origin point handling
|
||||
|
||||
3. Image Processing
|
||||
- Verify crop region calculations
|
||||
- Review image scaling operations
|
||||
- Validate final image generation
|
||||
|
||||
4. Alternative Approaches
|
||||
- Use full-frame detection
|
||||
- Implement grid-based detection
|
||||
- Consider machine learning approach
|
||||
|
||||
## Impact
|
||||
- Critical functionality issue
|
||||
- Blocks accurate board analysis
|
||||
- Affects user experience
|
||||
- Prevents proper game state detection
|
||||
|
||||
## Priority
|
||||
HIGH - This issue blocks core functionality of the chess analysis system.
|
||||
|
||||
## Next Steps
|
||||
1. Verify coordinate system transformations
|
||||
2. Test different aspect ratio parameters
|
||||
3. Implement logging for detection boundaries
|
||||
4. Add visualization of detected regions
|
||||
5. Consider alternative detection approaches
|
||||
|
||||
## Additional Notes
|
||||
- The issue appears consistent across different games
|
||||
- Width detection is working correctly
|
||||
- Height truncation is consistent (showing ~2.5 rows)
|
||||
- Full board is visible in capture, suggesting screen capture is working
|
||||
|
|
@ -1,136 +1,89 @@
|
|||
# Product Context
|
||||
|
||||
## Product Vision
|
||||
To create an intuitive, real-time chess teaching assistant that enhances the learning experience for both instructors and students during live gameplay.
|
||||
## Project Purpose
|
||||
ChessPrism is a macOS application designed to:
|
||||
1. Capture chess.com game windows
|
||||
2. Detect and analyze chess positions in real-time
|
||||
3. Generate FEN strings for position analysis
|
||||
4. Track moves and game progress
|
||||
|
||||
## Core Value Proposition
|
||||
1. Real-time visual feedback during live games
|
||||
2. Integrated analysis and teaching tools
|
||||
3. Seamless integration with chess.com
|
||||
4. Performance-optimized for MacOS Silicon
|
||||
## Core Features
|
||||
1. Screen Capture
|
||||
- Automatic chess.com window detection
|
||||
- Real-time board monitoring
|
||||
- Configurable capture settings
|
||||
|
||||
## Target Users
|
||||
### User Personas
|
||||
1. Chess Instructors
|
||||
- Needs: Real-time analysis, teaching tools, visual aids
|
||||
- Goals: Effective teaching, student engagement
|
||||
- Pain Points: Complex setup, delayed feedback
|
||||
2. Board Analysis
|
||||
- Accurate piece detection
|
||||
- 12 piece categories:
|
||||
* 6 white pieces (pawn to king)
|
||||
* 6 black pieces (pawn to king)
|
||||
- Position validation
|
||||
- FEN string generation
|
||||
|
||||
2. Advanced Players
|
||||
- Needs: Move analysis, threat detection
|
||||
- Goals: Improve teaching skills, analyze games
|
||||
- Pain Points: Limited teaching tools
|
||||
|
||||
3. Chess Streamers
|
||||
- Needs: Visual overlays, real-time analysis
|
||||
- Goals: Engaging content, clear explanations
|
||||
- Pain Points: Complex overlay systems
|
||||
|
||||
4. Self-Learners
|
||||
- Needs: Position evaluation, move alternatives
|
||||
- Goals: Game improvement, pattern recognition
|
||||
- Pain Points: Lack of real-time feedback
|
||||
|
||||
## Key Features
|
||||
1. Real-time board position analysis
|
||||
2. Visual move suggestions and explanations
|
||||
3. Threat and defense visualization
|
||||
4. Position evaluation and move alternatives
|
||||
5. Customizable visual overlay system
|
||||
|
||||
## User Workflows
|
||||
### Teaching Scenario
|
||||
1. Instructor starts chess.com game
|
||||
2. Application detects board position
|
||||
3. Real-time visual feedback appears
|
||||
4. Instructor explains moves using visual aids
|
||||
5. Students see analysis and suggestions
|
||||
|
||||
### Self-Learning Scenario
|
||||
1. Player starts game on chess.com
|
||||
2. Application provides real-time feedback
|
||||
3. Player sees move suggestions and threats
|
||||
4. Application highlights tactical patterns
|
||||
5. Player reviews game analysis post-match
|
||||
3. Machine Learning
|
||||
- Vision-based Core ML model
|
||||
- Direct category mapping
|
||||
- High confidence threshold (0.75)
|
||||
- Fast inference time
|
||||
|
||||
## User Experience Goals
|
||||
1. Intuitive and non-intrusive interface
|
||||
2. Real-time responsiveness (<100ms latency)
|
||||
3. Clear visual feedback system
|
||||
4. Customizable teaching tools
|
||||
5. Seamless integration with chess.com
|
||||
1. Reliability
|
||||
- Accurate piece detection
|
||||
- Consistent board recognition
|
||||
- Robust error handling
|
||||
|
||||
## Visual Feedback System
|
||||
1. Move Visualization:
|
||||
- Arrows for suggested moves
|
||||
- Color-coded threat levels
|
||||
- Highlighted squares
|
||||
2. Performance
|
||||
- Real-time analysis
|
||||
- Low resource usage
|
||||
- Smooth capture
|
||||
|
||||
2. Position Analysis:
|
||||
- Evaluation bar
|
||||
- Move alternatives
|
||||
- Threat indicators
|
||||
3. Usability
|
||||
- Automatic window detection
|
||||
- Minimal setup required
|
||||
- Clear feedback
|
||||
|
||||
3. Teaching Aids:
|
||||
- Defensive patterns
|
||||
- Attack vectors
|
||||
- Piece mobility
|
||||
## Current Status
|
||||
1. Working Features
|
||||
- Screen capture system
|
||||
- Board detection
|
||||
- Piece recognition
|
||||
- FEN generation
|
||||
|
||||
## Accessibility Features
|
||||
1. Color-blind friendly themes
|
||||
2. Keyboard navigation
|
||||
3. Screen reader support
|
||||
4. Adjustable overlay size
|
||||
5. High-contrast modes
|
||||
2. Recent Improvements
|
||||
- Simplified classification system
|
||||
- Direct category mapping
|
||||
- Vision framework integration
|
||||
- Improved error handling
|
||||
|
||||
## Success Metrics
|
||||
1. Performance:
|
||||
- <100ms analysis latency
|
||||
- 99.9% board recognition accuracy
|
||||
- 120fps overlay rendering
|
||||
2. Usability:
|
||||
- <5 minute setup time
|
||||
- 90% instructor satisfaction rate
|
||||
- <1% error rate in move suggestions
|
||||
3. Adoption:
|
||||
- 1000+ active users in first 6 months
|
||||
- 90% retention rate after 30 days
|
||||
- 4.5+ average rating on App Store
|
||||
3. Known Limitations
|
||||
- Requires chess.com's default board theme
|
||||
- macOS 12.3+ requirement
|
||||
- Screen capture permissions needed
|
||||
|
||||
## Competitive Advantages
|
||||
1. Native MacOS Silicon optimization
|
||||
2. Real-time performance with Metal acceleration
|
||||
3. Integrated teaching tools
|
||||
4. Chess.com specific optimizations
|
||||
5. Privacy-focused design
|
||||
## Future Enhancements
|
||||
1. Short Term
|
||||
- Monitor classification accuracy
|
||||
- Fine-tune confidence threshold
|
||||
- Improve error reporting
|
||||
|
||||
## Development Principles
|
||||
1. User-centric design
|
||||
2. Performance-first approach
|
||||
3. Modular architecture
|
||||
4. Continuous testing and refinement
|
||||
5. Security and privacy compliance
|
||||
2. Long Term
|
||||
- Support for multiple board themes
|
||||
- Game analysis integration
|
||||
- Move suggestion system
|
||||
|
||||
## Product Roadmap
|
||||
### Phase 1: Core Functionality (Weeks 1-6)
|
||||
- Basic board recognition
|
||||
- Stockfish integration
|
||||
- Visual overlay foundation
|
||||
## Technical Requirements
|
||||
1. System
|
||||
- macOS 12.3 or later
|
||||
- Metal-capable GPU
|
||||
- Screen recording permissions
|
||||
|
||||
### Phase 2: Teaching Tools (Weeks 7-10)
|
||||
- Move visualization system
|
||||
- Threat analysis
|
||||
- Defensive patterns
|
||||
2. Dependencies
|
||||
- Vision framework
|
||||
- Core ML
|
||||
- ScreenCaptureKit
|
||||
|
||||
### Phase 3: Polish & Optimization (Weeks 11-14)
|
||||
- Performance tuning
|
||||
- UI/UX refinement
|
||||
- Security implementation
|
||||
|
||||
## Key Milestones
|
||||
1. Week 4: Functional board recognition
|
||||
2. Week 6: Working Stockfish analysis
|
||||
3. Week 8: Basic visual overlay system
|
||||
4. Week 10: Complete visualization system
|
||||
5. Week 12: Optimized performance
|
||||
6. Week 14: Ready for distribution
|
||||
3. Performance Targets
|
||||
- 30 FPS capture
|
||||
- Sub-second analysis
|
||||
- Low CPU/GPU usage
|
||||
|
|
|
|||
139
cline_docs/sample.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import os
|
||||
import requests
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
|
||||
from pathlib import Path
|
||||
from io import BytesIO
|
||||
import itertools
|
||||
|
||||
class ChessAssetProcessor:
|
||||
def __init__(self):
|
||||
self.base_dir = Path.cwd()
|
||||
self.raw_dir = self.base_dir / 'raw'
|
||||
self.training_dir = self.base_dir / 'training'
|
||||
|
||||
# URLs for pieces
|
||||
self.piece_base_url = 'https://www.chess.com/chess-themes/pieces/neo/300'
|
||||
|
||||
# Piece configurations - matches the Swift enum exactly
|
||||
self.pieces = {
|
||||
'white': ['pawn', 'knight', 'bishop', 'rook', 'queen', 'king'],
|
||||
'black': ['pawn', 'knight', 'bishop', 'rook', 'queen', 'king']
|
||||
}
|
||||
|
||||
# Square colors - used for background variations only
|
||||
self.square_colors = {
|
||||
'light': '#eeeed2',
|
||||
'dark': '#759656',
|
||||
'light_highlighted': '#f6f68d',
|
||||
'dark_highlighted': '#bdcc49'
|
||||
}
|
||||
|
||||
# Border configurations
|
||||
self.border_configs = [
|
||||
{}, # No borders
|
||||
{'top': True},
|
||||
{'bottom': True},
|
||||
{'left': True},
|
||||
{'top': True, 'left': True},
|
||||
{'bottom': True, 'left': True},
|
||||
]
|
||||
|
||||
self._setup_directories()
|
||||
|
||||
def _setup_directories(self):
|
||||
"""Create directory structure for training data"""
|
||||
(self.raw_dir / 'pieces').mkdir(parents=True, exist_ok=True)
|
||||
self.training_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Create directories for each piece type
|
||||
for color in ['white', 'black']:
|
||||
for piece in self.pieces[color]:
|
||||
(self.training_dir / f"{color}_{piece}").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def hex_to_rgb(self, hex_color):
|
||||
"""Convert hex color to RGB tuple"""
|
||||
hex_color = hex_color.lstrip('#')
|
||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
|
||||
def create_base_square(self, color, size=100):
|
||||
"""Create a square with specified color"""
|
||||
rgb_color = self.hex_to_rgb(color)
|
||||
return Image.new('RGB', (size, size), rgb_color)
|
||||
|
||||
def add_borders(self, image, borders, border_color=(0, 0, 0)):
|
||||
"""Add borders according to configuration"""
|
||||
w, h = image.size
|
||||
result = image.copy()
|
||||
draw = ImageDraw.Draw(result)
|
||||
border_size = 5
|
||||
|
||||
if borders.get('top'):
|
||||
draw.line([(0, 0), (w-1, 0)], fill=border_color, width=border_size)
|
||||
if borders.get('bottom'):
|
||||
draw.line([(0, h-1), (w-1, h-1)], fill=border_color, width=border_size)
|
||||
if borders.get('left'):
|
||||
draw.line([(0, 0), (0, h-1)], fill=border_color, width=border_size)
|
||||
|
||||
return result
|
||||
|
||||
def download_piece(self, color, piece, target_size):
|
||||
"""Download a specific chess piece"""
|
||||
piece_letter = piece[0] if piece != 'knight' else 'n'
|
||||
piece_url = f"{self.piece_base_url}/{color[0]}{piece_letter}.png"
|
||||
|
||||
response = requests.get(piece_url)
|
||||
if response.status_code == 200:
|
||||
piece_img = Image.open(BytesIO(response.content)).convert('RGBA')
|
||||
return piece_img.resize((target_size, target_size), Image.Resampling.LANCZOS)
|
||||
return None
|
||||
|
||||
def create_training_data(self):
|
||||
"""Create comprehensive training dataset"""
|
||||
square_size = 100
|
||||
|
||||
print("Downloading pieces and creating variations...")
|
||||
for color in ['white', 'black']:
|
||||
for piece in self.pieces[color]:
|
||||
piece_dir = self.training_dir / f"{color}_{piece}"
|
||||
print(f"\nProcessing {color} {piece}...")
|
||||
|
||||
# Download piece
|
||||
piece_img = self.download_piece(color, piece, square_size)
|
||||
if piece_img is None:
|
||||
print(f"Failed to download {color} {piece}")
|
||||
continue
|
||||
|
||||
# Create variations with different backgrounds
|
||||
variation_count = 0
|
||||
for bg_name, bg_color in self.square_colors.items():
|
||||
# Create base square
|
||||
base_square = self.create_base_square(bg_color, square_size)
|
||||
|
||||
# Add border variations
|
||||
for border_config in self.border_configs:
|
||||
# Add borders
|
||||
bordered = self.add_borders(base_square, border_config)
|
||||
|
||||
# Convert to RGBA for composition
|
||||
bordered_rgba = bordered.convert('RGBA')
|
||||
|
||||
# Combine with piece
|
||||
combined = Image.alpha_composite(bordered_rgba, piece_img)
|
||||
|
||||
# Generate filename
|
||||
border_desc = '_'.join(k for k,v in border_config.items() if v)
|
||||
filename = f"{bg_name}_{border_desc}_{variation_count}.png" if border_desc else f"{bg_name}_{variation_count}.png"
|
||||
|
||||
# Save image
|
||||
combined.save(piece_dir / filename)
|
||||
variation_count += 1
|
||||
|
||||
print(f"Created {variation_count} variations for {color} {piece}")
|
||||
|
||||
def main():
|
||||
processor = ChessAssetProcessor()
|
||||
processor.create_training_data()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -1,240 +1,93 @@
|
|||
# System Patterns
|
||||
|
||||
## Architecture Overview
|
||||
### Core Components
|
||||
1. Screenshot Capture Module
|
||||
- Screen recording permission handling
|
||||
- Multi-monitor support
|
||||
- Keyboard shortcut system
|
||||
- Firefox window detection
|
||||
## Model Architecture
|
||||
1. Core ML Integration
|
||||
- Vision framework for image handling
|
||||
- Direct category mapping
|
||||
- No intermediate transformations
|
||||
|
||||
2. Board Position Analysis Engine
|
||||
2. Classification Flow
|
||||
```
|
||||
Image → VNImageRequestHandler → VNCoreMLRequest → VNClassificationObservation → SquareClassification
|
||||
```
|
||||
|
||||
3. Category System
|
||||
- 12 piece categories:
|
||||
* white_pawn to white_king
|
||||
* black_pawn to black_king
|
||||
- Empty square fallback
|
||||
- No background variations in model
|
||||
|
||||
## Processing Pipeline
|
||||
1. Board Detection
|
||||
- VNDetectRectanglesRequest
|
||||
- Aspect ratio validation
|
||||
- Square extraction
|
||||
|
||||
2. Image Processing
|
||||
- Contrast enhancement
|
||||
- Edge sharpening
|
||||
- Noise reduction
|
||||
- Center crop
|
||||
|
||||
3. Classification
|
||||
- Vision framework integration
|
||||
- CoreML model inference
|
||||
- FEN conversion logic
|
||||
- Position validation system
|
||||
|
||||
3. Visual Overlay System
|
||||
- Metal rendering pipeline
|
||||
- Transparent window system
|
||||
- Coordinate transformation
|
||||
- Move visualization components
|
||||
|
||||
4. Stockfish Integration
|
||||
- ARM64 binary integration
|
||||
- Async engine wrapper
|
||||
- Position analysis pipeline
|
||||
- Evaluation caching
|
||||
|
||||
5. Machine Learning Pipeline
|
||||
- Model training framework
|
||||
- Theme detection system
|
||||
- Piece recognition models
|
||||
- Model versioning system
|
||||
|
||||
## Architectural Patterns
|
||||
### Clean Architecture Layers
|
||||
1. Presentation Layer (SwiftUI)
|
||||
- User interface components
|
||||
- View models
|
||||
- State management
|
||||
|
||||
2. Domain Layer
|
||||
- Core business logic
|
||||
- Use cases
|
||||
- Domain models
|
||||
|
||||
3. Data Layer
|
||||
- Repositories
|
||||
- Data sources
|
||||
- Network services
|
||||
|
||||
### Design Patterns
|
||||
1. MVVM for UI components
|
||||
2. Dependency Injection for service composition
|
||||
3. Observer pattern for state management
|
||||
4. Factory pattern for object creation
|
||||
5. Strategy pattern for analysis algorithms
|
||||
|
||||
## Detailed Component Specifications
|
||||
### Metal Rendering Pipeline
|
||||
1. Pipeline Stages:
|
||||
- Vertex processing
|
||||
- Fragment shading
|
||||
- Composition
|
||||
- Post-processing
|
||||
|
||||
2. Performance Optimization:
|
||||
- Command buffer optimization
|
||||
- Texture compression
|
||||
- Shader LOD management
|
||||
- Frame pacing
|
||||
|
||||
3. Visual Effects:
|
||||
- Anti-aliasing
|
||||
- Bloom effects
|
||||
- Motion blur
|
||||
- Depth effects
|
||||
|
||||
### Vision Framework Integration
|
||||
1. Image Analysis Pipeline:
|
||||
- Image preprocessing
|
||||
- Feature detection
|
||||
- Object recognition
|
||||
- Position tracking
|
||||
|
||||
2. Performance Considerations:
|
||||
- GPU acceleration
|
||||
- Batch processing
|
||||
- Memory optimization
|
||||
- Confidence threshold
|
||||
- Error handling
|
||||
|
||||
3. Integration Points:
|
||||
- CoreML model integration
|
||||
- Metal texture sharing
|
||||
- SwiftUI view integration
|
||||
- Async/await pattern
|
||||
## Code Organization
|
||||
1. Recognition Layer
|
||||
```
|
||||
PieceRecognizer
|
||||
├── Model loading
|
||||
├── Image preprocessing
|
||||
└── Classification handling
|
||||
```
|
||||
|
||||
### Async/Await Patterns
|
||||
1. Concurrency Model:
|
||||
- Task groups
|
||||
- Async sequences
|
||||
- Actor isolation
|
||||
- Continuations
|
||||
2. Model Layer
|
||||
```
|
||||
SquareClassification
|
||||
├── Category mapping
|
||||
├── Piece type/color
|
||||
└── Empty square handling
|
||||
```
|
||||
|
||||
2. Error Handling:
|
||||
- Structured concurrency
|
||||
- Task cancellation
|
||||
- Error propagation
|
||||
- Retry mechanisms
|
||||
3. Core Components
|
||||
```
|
||||
BoardDetector
|
||||
├── Rectangle detection
|
||||
├── Square extraction
|
||||
└── Position validation
|
||||
```
|
||||
|
||||
3. Performance Optimization:
|
||||
- Task prioritization
|
||||
- Resource contention management
|
||||
- Memory safety
|
||||
- Thread management
|
||||
## Data Flow
|
||||
1. Capture
|
||||
```
|
||||
ScreenCapture → Raw Image → Board Rectangle
|
||||
```
|
||||
|
||||
### CoreML Model Architecture
|
||||
1. Model Specifications:
|
||||
- Input/output formats
|
||||
- Model quantization
|
||||
- Neural engine optimization
|
||||
- Model versioning
|
||||
2. Processing
|
||||
```
|
||||
Board Rectangle → Individual Squares → Preprocessed Images
|
||||
```
|
||||
|
||||
2. Training Pipeline:
|
||||
- Data collection
|
||||
- Model training
|
||||
- Validation
|
||||
- Deployment
|
||||
3. Classification
|
||||
```
|
||||
Preprocessed Images → ML Model → Piece Categories → Chess Position
|
||||
```
|
||||
|
||||
3. Performance Considerations:
|
||||
- Batch processing
|
||||
- Memory management
|
||||
- Model compression
|
||||
- Inference optimization
|
||||
## Key Patterns
|
||||
1. Direct Integration
|
||||
- Vision framework throughout
|
||||
- No intermediate conversions
|
||||
- Consistent image handling
|
||||
|
||||
### Stockfish Integration
|
||||
1. Engine Configuration:
|
||||
- Thread management
|
||||
- Hash size optimization
|
||||
- Analysis depth
|
||||
- Time controls
|
||||
2. Error Handling
|
||||
- Early validation
|
||||
- Graceful fallbacks
|
||||
- Detailed logging
|
||||
|
||||
2. Analysis Pipeline:
|
||||
- Position evaluation
|
||||
- Move generation
|
||||
- Threat detection
|
||||
- Position caching
|
||||
|
||||
3. Performance Optimization:
|
||||
- Parallel analysis
|
||||
- Cache management
|
||||
- Engine tuning
|
||||
- Resource allocation
|
||||
|
||||
## Data Flow Diagram
|
||||
1. Input:
|
||||
- Screenshot capture
|
||||
- Keyboard input
|
||||
- System events
|
||||
|
||||
2. Processing:
|
||||
- Board detection
|
||||
- Position analysis
|
||||
- Move evaluation
|
||||
- Visualization generation
|
||||
|
||||
3. Output:
|
||||
- Visual overlay
|
||||
- Move suggestions
|
||||
- Position evaluation
|
||||
- Threat analysis
|
||||
|
||||
## Error Handling Strategy
|
||||
1. Input Validation:
|
||||
- Screenshot quality checks
|
||||
- Board position validation
|
||||
- Move legality verification
|
||||
|
||||
2. Recovery Mechanisms:
|
||||
- Automatic retry for failed operations
|
||||
- Fallback analysis methods
|
||||
- Graceful degradation
|
||||
|
||||
3. Error Reporting:
|
||||
- User-friendly error messages
|
||||
- Detailed error logging
|
||||
- Crash reporting system
|
||||
|
||||
## Security Architecture
|
||||
1. Data Protection:
|
||||
- Secure storage for sensitive data
|
||||
- Encrypted communication channels
|
||||
- Data minimization principles
|
||||
|
||||
2. Access Control:
|
||||
- Permission management system
|
||||
- Role-based access control
|
||||
- Activity monitoring
|
||||
|
||||
3. Privacy Features:
|
||||
- Privacy manifests implementation
|
||||
- Data collection transparency
|
||||
- User consent management
|
||||
|
||||
## Performance Optimization
|
||||
1. Rendering:
|
||||
- Metal shader optimization
|
||||
- Draw call batching
|
||||
- Frame rate stabilization
|
||||
|
||||
2. Analysis:
|
||||
- Position caching
|
||||
- Parallel processing
|
||||
- Engine optimization
|
||||
|
||||
3. Memory Management:
|
||||
- Efficient resource allocation
|
||||
- Memory leak prevention
|
||||
- Garbage collection tuning
|
||||
|
||||
## Testing Strategy
|
||||
1. Unit Testing:
|
||||
- Core functionality
|
||||
- Business logic
|
||||
- Utility functions
|
||||
|
||||
2. Integration Testing:
|
||||
- Module interactions
|
||||
- Data flow verification
|
||||
- System behavior
|
||||
|
||||
3. Performance Testing:
|
||||
- Latency benchmarks
|
||||
- Resource usage
|
||||
- Stress testing
|
||||
|
||||
4. Security Testing:
|
||||
- Vulnerability scanning
|
||||
- Penetration testing
|
||||
- Compliance verification
|
||||
3. Performance
|
||||
- Shared CIContext
|
||||
- Efficient image processing
|
||||
- Optimized model loading
|
||||
|
|
|
|||
|
|
@ -1,247 +1,88 @@
|
|||
# Tech Context
|
||||
# Technical Context
|
||||
|
||||
## Technology Stack
|
||||
### Core Technologies
|
||||
1. Swift (5.9+)
|
||||
2. SwiftUI (4.0+)
|
||||
3. Metal (3.0+)
|
||||
4. Vision (2.0+)
|
||||
5. CoreML (5.0+)
|
||||
6. Create ML (3.0+)
|
||||
7. Stockfish (16+)
|
||||
## Development Environment
|
||||
- macOS Application
|
||||
- Swift & SwiftUI
|
||||
- Xcode 14+
|
||||
- Target: macOS 12.3+
|
||||
|
||||
### Development Tools
|
||||
1. Xcode (15.0+)
|
||||
2. Swift Package Manager
|
||||
3. Git (2.40+)
|
||||
4. CoreML Tools (5.0+)
|
||||
5. Create ML App (3.0+)
|
||||
## Core Technologies
|
||||
1. Vision Framework
|
||||
- VNDetectRectanglesRequest for board detection
|
||||
- VNCoreMLRequest for piece classification
|
||||
- VNImageRequestHandler for image processing
|
||||
|
||||
### Testing Frameworks
|
||||
1. XCTest (5.0+)
|
||||
2. XCUITest (5.0+)
|
||||
3. Performance Testing Tools
|
||||
4. Security Testing Suite
|
||||
2. Core ML Model
|
||||
- Name: ChessPieceClassifier.mlmodel
|
||||
- Input: RGB/RGBA images
|
||||
- Output: Classification label
|
||||
- Categories (exact names):
|
||||
```swift
|
||||
white_pawn, white_knight, white_bishop, white_rook, white_queen, white_king,
|
||||
black_pawn, black_knight, black_bishop, black_rook, black_queen, black_king
|
||||
```
|
||||
|
||||
## Development System Specifications
|
||||
### Operating System
|
||||
- System: Darwin
|
||||
- Version: 15.2
|
||||
- Architecture: arm64
|
||||
3. ScreenCaptureKit
|
||||
- Window capture at 30 FPS
|
||||
- Configurable cursor visibility
|
||||
- Chess.com window detection
|
||||
|
||||
### Hardware Specifications
|
||||
- CPU: Apple M3 Max
|
||||
- Memory: 64.00 GB
|
||||
- GPU: Apple M3 Max (40 cores)
|
||||
- Metal Support: Metal 3
|
||||
## Image Processing
|
||||
1. Preprocessing Pipeline
|
||||
- Contrast enhancement (1.3x)
|
||||
- Edge sharpening
|
||||
- Noise reduction
|
||||
- Color normalization
|
||||
|
||||
### Display Information
|
||||
- Main Display: LG ULTRAGEAR+
|
||||
- Resolution: 3840 x 1080
|
||||
- Refresh Rate: 120Hz
|
||||
- Features: Television support, rotation support
|
||||
2. Square Extraction
|
||||
- Aspect ratio validation
|
||||
- Size normalization
|
||||
- Center crop
|
||||
|
||||
### Development Tools
|
||||
- Xcode: 16.2 (Build version 16C5032a)
|
||||
- Swift: 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
|
||||
- Target: arm64-apple-macosx15.0
|
||||
## Model Integration
|
||||
1. Loading
|
||||
```swift
|
||||
let config = MLModelConfiguration()
|
||||
config.computeUnits = .all
|
||||
let model = try MLModel(contentsOf: modelURL)
|
||||
let vnModel = try VNCoreMLModel(for: model)
|
||||
```
|
||||
|
||||
## Development Environment Requirements
|
||||
### Minimum Requirements
|
||||
- Apple Silicon (M1)
|
||||
- 16GB RAM
|
||||
- Metal 2 support
|
||||
2. Classification
|
||||
```swift
|
||||
let request = VNCoreMLRequest(model: vnModel)
|
||||
request.imageCropAndScaleOption = .centerCrop
|
||||
```
|
||||
|
||||
### Recommended Requirements
|
||||
- Apple M2/M3 series
|
||||
- 32GB+ RAM
|
||||
- Metal 3 support
|
||||
- Apple Neural Engine
|
||||
3. Result Handling
|
||||
- Confidence threshold: 0.75
|
||||
- Empty square fallback
|
||||
- Direct category mapping
|
||||
|
||||
### Software Requirements
|
||||
1. MacOS (Ventura 13.0+)
|
||||
2. Xcode (15.0+)
|
||||
3. Swift (5.9+)
|
||||
4. Git (2.40+)
|
||||
5. CoreML Tools (5.0+)
|
||||
## Dependencies
|
||||
- Foundation
|
||||
- Vision
|
||||
- CoreML
|
||||
- CoreImage
|
||||
- ScreenCaptureKit
|
||||
- SwiftUI
|
||||
|
||||
## Configuration Details
|
||||
### Swift Concurrency
|
||||
1. Async/await pattern implementation
|
||||
2. Task management system
|
||||
3. Structured concurrency
|
||||
4. Actor-based isolation
|
||||
## Error Handling
|
||||
- Invalid dimensions
|
||||
- Model loading failures
|
||||
- Recognition errors
|
||||
- Low confidence results
|
||||
|
||||
### CoreML Integration
|
||||
1. Model versioning system
|
||||
2. Apple Neural Engine optimization
|
||||
3. Model update mechanism
|
||||
4. Performance monitoring
|
||||
|
||||
### Security Implementation
|
||||
1. App Sandbox configuration
|
||||
2. Privacy manifest requirements
|
||||
3. Secure storage implementation
|
||||
4. Data encryption standards
|
||||
|
||||
## Development Workflow
|
||||
1. Version Control:
|
||||
- Git branching strategy
|
||||
- Code review process
|
||||
- Commit message guidelines
|
||||
|
||||
2. CI/CD Pipeline:
|
||||
- Automated testing
|
||||
- Build verification
|
||||
- Deployment automation
|
||||
- Release management
|
||||
|
||||
3. Code Quality:
|
||||
- Linting configuration
|
||||
- Static analysis
|
||||
- Code coverage requirements
|
||||
- Documentation standards
|
||||
|
||||
## Monitoring & Logging
|
||||
1. Performance Monitoring:
|
||||
- Rendering performance
|
||||
- Analysis latency
|
||||
- Resource usage
|
||||
|
||||
2. Error Tracking:
|
||||
- Crash reporting
|
||||
- Error logging
|
||||
- User feedback integration
|
||||
|
||||
3. Analytics:
|
||||
- Usage tracking
|
||||
- Feature adoption
|
||||
- Performance metrics
|
||||
|
||||
## Technical Constraints
|
||||
1. Real-time Requirements:
|
||||
- <100ms analysis latency
|
||||
- 120fps rendering
|
||||
- 99.9% recognition accuracy
|
||||
|
||||
2. Compatibility:
|
||||
- MacOS Silicon only
|
||||
- Firefox browser integration
|
||||
- Chess.com specific optimizations
|
||||
|
||||
3. Security:
|
||||
- App Sandbox compliance
|
||||
- Privacy manifest requirements
|
||||
- Secure data handling
|
||||
|
||||
## Documentation Standards
|
||||
1. Code Documentation:
|
||||
- API documentation
|
||||
- Architecture diagrams
|
||||
- Technical specifications
|
||||
|
||||
2. User Documentation:
|
||||
- Installation guide
|
||||
- Usage instructions
|
||||
- Troubleshooting guide
|
||||
|
||||
3. Developer Documentation:
|
||||
- Setup instructions
|
||||
- Contribution guidelines
|
||||
- Code style guide
|
||||
|
||||
## Detailed Technical Specifications
|
||||
### Metal Rendering Pipeline
|
||||
1. Pipeline Stages:
|
||||
- Vertex processing
|
||||
- Fragment shading
|
||||
- Composition
|
||||
- Post-processing
|
||||
|
||||
2. Performance Optimization:
|
||||
- Command buffer optimization
|
||||
- Texture compression
|
||||
- Shader LOD management
|
||||
- Frame pacing
|
||||
|
||||
3. Visual Effects:
|
||||
- Anti-aliasing
|
||||
- Bloom effects
|
||||
- Motion blur
|
||||
- Depth effects
|
||||
|
||||
### Vision Framework Integration
|
||||
1. Image Analysis Pipeline:
|
||||
- Image preprocessing
|
||||
- Feature detection
|
||||
- Object recognition
|
||||
- Position tracking
|
||||
|
||||
2. Performance Considerations:
|
||||
- GPU acceleration
|
||||
- Batch processing
|
||||
- Memory optimization
|
||||
- Error handling
|
||||
|
||||
3. Integration Points:
|
||||
- CoreML model integration
|
||||
- Metal texture sharing
|
||||
- SwiftUI view integration
|
||||
- Async/await pattern
|
||||
|
||||
### Async/Await Patterns
|
||||
1. Concurrency Model:
|
||||
- Task groups
|
||||
- Async sequences
|
||||
- Actor isolation
|
||||
- Continuations
|
||||
|
||||
2. Error Handling:
|
||||
- Structured concurrency
|
||||
- Task cancellation
|
||||
- Error propagation
|
||||
- Retry mechanisms
|
||||
|
||||
3. Performance Optimization:
|
||||
- Task prioritization
|
||||
- Resource contention management
|
||||
- Memory safety
|
||||
- Thread management
|
||||
|
||||
### CoreML Model Architecture
|
||||
1. Model Specifications:
|
||||
- Input/output formats
|
||||
- Model quantization
|
||||
- Neural engine optimization
|
||||
- Model versioning
|
||||
|
||||
2. Training Pipeline:
|
||||
- Data collection
|
||||
- Model training
|
||||
- Validation
|
||||
- Deployment
|
||||
|
||||
3. Performance Considerations:
|
||||
- Batch processing
|
||||
- Memory management
|
||||
- Model compression
|
||||
- Inference optimization
|
||||
|
||||
### Stockfish Integration
|
||||
1. Engine Configuration:
|
||||
- Thread management
|
||||
- Hash size optimization
|
||||
- Analysis depth
|
||||
- Time controls
|
||||
|
||||
2. Analysis Pipeline:
|
||||
- Position evaluation
|
||||
- Move generation
|
||||
- Threat detection
|
||||
- Position caching
|
||||
|
||||
3. Performance Optimization:
|
||||
- Parallel analysis
|
||||
- Cache management
|
||||
- Engine tuning
|
||||
- Resource allocation
|
||||
## File Organization
|
||||
```
|
||||
ChessPrism/
|
||||
├── Models/
|
||||
│ ├── SquareClassification.swift # Model output mapping
|
||||
│ └── ChessPosition.swift # Board state
|
||||
├── Recognition/
|
||||
│ ├── PieceRecognizer.swift # ML integration
|
||||
│ ├── FenGenerator.swift # Position encoding
|
||||
│ └── MoveDetector.swift # Move analysis
|
||||
└── Core/
|
||||
├── BoardDetector.swift # Square extraction
|
||||
└── ScreenCapture.swift # Window capture
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
import subprocess
|
||||
import platform
|
||||
import json
|
||||
import os
|
||||
|
||||
def get_system_info():
|
||||
info = {}
|
||||
|
||||
# Basic System Info
|
||||
info['os'] = {
|
||||
'system': platform.system(),
|
||||
'version': platform.mac_ver()[0],
|
||||
'architecture': platform.machine()
|
||||
}
|
||||
|
||||
# CPU Info
|
||||
cpu_command = "sysctl -n machdep.cpu.brand_string"
|
||||
info['cpu'] = subprocess.getoutput(cpu_command)
|
||||
|
||||
# Memory Info
|
||||
mem_command = "sysctl hw.memsize"
|
||||
mem_bytes = int(subprocess.getoutput(mem_command).split()[1])
|
||||
info['memory_gb'] = mem_bytes / (1024**3)
|
||||
|
||||
# GPU Info
|
||||
gpu_command = "system_profiler SPDisplaysDataType"
|
||||
gpu_info = subprocess.getoutput(gpu_command)
|
||||
info['gpu'] = gpu_info
|
||||
|
||||
# Screen Info
|
||||
displays_command = "system_profiler SPDisplaysDataType"
|
||||
displays_info = subprocess.getoutput(displays_command)
|
||||
info['displays'] = displays_info
|
||||
|
||||
# Xcode Version
|
||||
xcode_command = "xcodebuild -version"
|
||||
try:
|
||||
info['xcode'] = subprocess.getoutput(xcode_command)
|
||||
except:
|
||||
info['xcode'] = "Xcode not installed"
|
||||
|
||||
# Swift Version
|
||||
swift_command = "swift --version"
|
||||
try:
|
||||
info['swift'] = subprocess.getoutput(swift_command)
|
||||
except:
|
||||
info['swift'] = "Swift not installed"
|
||||
|
||||
# Metal Support
|
||||
metal_command = "system_profiler SPDisplaysDataType | grep Metal"
|
||||
info['metal_support'] = subprocess.getoutput(metal_command)
|
||||
|
||||
return info
|
||||
|
||||
def save_info():
|
||||
info = get_system_info()
|
||||
|
||||
# Save as JSON
|
||||
with open('tech_specs.json', 'w') as f:
|
||||
json.dump(info, f, indent=2)
|
||||
|
||||
# Save as readable text
|
||||
with open('tech_specs.txt', 'w') as f:
|
||||
f.write("Chess Teaching Assistant - Development System Specifications\n")
|
||||
f.write("=" * 60 + "\n\n")
|
||||
|
||||
f.write("Operating System:\n")
|
||||
f.write(f"- System: {info['os']['system']}\n")
|
||||
f.write(f"- Version: {info['os']['version']}\n")
|
||||
f.write(f"- Architecture: {info['os']['architecture']}\n\n")
|
||||
|
||||
f.write("Hardware:\n")
|
||||
f.write(f"- CPU: {info['cpu']}\n")
|
||||
f.write(f"- Memory: {info['memory_gb']:.2f} GB\n\n")
|
||||
|
||||
f.write("Development Tools:\n")
|
||||
f.write(f"- Xcode: {info['xcode']}\n")
|
||||
f.write(f"- Swift: {info['swift']}\n\n")
|
||||
|
||||
f.write("Graphics:\n")
|
||||
f.write(f"- Metal Support:\n{info['metal_support']}\n\n")
|
||||
|
||||
f.write("Display Information:\n")
|
||||
f.write(f"{info['displays']}\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
save_info()
|
||||
print("System information has been collected and saved to:")
|
||||
print("- tech_specs.json (machine-readable)")
|
||||
print("- tech_specs.txt (human-readable)")
|
||||
819
sample.py
Normal file
|
|
@ -0,0 +1,819 @@
|
|||
def create_pgn_csv(video_list, pgn_list, output_csv_path):
|
||||
# Prepare the data for the CSV
|
||||
rows = [{"row_id": video, "output": pgn} for video, pgn in zip(video_list, pgn_list)]
|
||||
|
||||
# Write to the CSV
|
||||
with open(output_csv_path, mode="w", newline="", encoding="utf-8") as file:
|
||||
writer = csv.DictWriter(file, fieldnames=["row_id", "output"])
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
print(f"CSV file has been created at {output_csv_path}.")
|
||||
|
||||
|
||||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
import glob
|
||||
import matplotlib.pyplot as plt
|
||||
import datetime
|
||||
|
||||
# DRAWING
|
||||
|
||||
def show_cv2_image(image, title='image'):
|
||||
# plt.figure()
|
||||
# plt.title(title)
|
||||
# plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
cv2.imwrite(f'output/temp/{title}_{datetime.datetime.now().strftime("%Y%m%d%H%M%S")}.png', image)
|
||||
|
||||
# OCR
|
||||
|
||||
def ocr_image(input_image, verbose=False):
|
||||
"""Detects text in the file."""
|
||||
from google.cloud import vision
|
||||
|
||||
client = vision.ImageAnnotatorClient()
|
||||
|
||||
content = cv2.imencode('.jpg', input_image)[1].tobytes()
|
||||
|
||||
image = vision.Image(content=content)
|
||||
|
||||
response = client.text_detection(image=image)
|
||||
texts = response.text_annotations
|
||||
|
||||
if verbose:
|
||||
print("Texts:")
|
||||
|
||||
for text in texts:
|
||||
print(f'\n"{text.description}"')
|
||||
|
||||
vertices = [
|
||||
f"({vertex.x},{vertex.y})" for vertex in text.bounding_poly.vertices
|
||||
]
|
||||
|
||||
print("bounds: {}".format(",".join(vertices)))
|
||||
|
||||
if response.error.message:
|
||||
raise Exception(
|
||||
"{}\nFor more info on error messages, check: "
|
||||
"https://cloud.google.com/apis/design/errors".format(response.error.message)
|
||||
)
|
||||
|
||||
return texts
|
||||
|
||||
def detection_to_dict(detection):
|
||||
return {
|
||||
'description': detection.description,
|
||||
'vertices': [
|
||||
(vertex.x, vertex.y) for vertex in detection.bounding_poly.vertices
|
||||
]
|
||||
}
|
||||
|
||||
def draw_box(image, a, b, c, d):
|
||||
cv2.polylines(image, [np.array([a, b, c, d], np.int32)], True, (0, 255, 0), 2)
|
||||
|
||||
def show_image_with_ocr(image, title='ocr result'):
|
||||
result = ocr_image(image)
|
||||
|
||||
if len(image.shape) == 2:
|
||||
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
for text in result:
|
||||
text_dict = detection_to_dict(text)
|
||||
vertices = text_dict['vertices']
|
||||
draw_box(image, vertices[0], vertices[1], vertices[2], vertices[3])
|
||||
|
||||
show_cv2_image(image, title)
|
||||
|
||||
def show_image_with_ocr_labelled(image, title='ocr result'):
|
||||
result = ocr_image(image)
|
||||
|
||||
if len(image.shape) == 2:
|
||||
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
for text in result:
|
||||
text_dict = detection_to_dict(text)
|
||||
vertices = text_dict['vertices']
|
||||
draw_box(image, vertices[0], vertices[1], vertices[2], vertices[3])
|
||||
cv2.putText(image, text_dict['description'], vertices[0], cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
|
||||
|
||||
show_cv2_image(image, title)
|
||||
|
||||
# PROCESS IMAGE
|
||||
|
||||
# Function to compute the intersection of two lines
|
||||
def compute_intersection(line1, line2):
|
||||
rho1, theta1 = line1
|
||||
rho2, theta2 = line2
|
||||
|
||||
# Calculate the intersection of two lines
|
||||
A = np.array([[np.cos(theta1), np.sin(theta1)], [np.cos(theta2), np.sin(theta2)]])
|
||||
b = np.array([rho1, rho2])
|
||||
|
||||
# Solve the linear system to find the intersection point
|
||||
intersection = np.linalg.solve(A, b)
|
||||
return int(intersection[0]), int(intersection[1])
|
||||
|
||||
def draw_white_board_boundaries(image):
|
||||
# Convert the image to HSV color space
|
||||
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
|
||||
|
||||
# Define the range of green color in HSV
|
||||
lower_green = np.array([40, 25, 40]) # Lower bound of green in HSV
|
||||
upper_green = np.array([100, 200, 200]) # Upper bound of green in HSV
|
||||
|
||||
# Threshold the image to get only the green color
|
||||
mask = cv2.inRange(hsv, lower_green, upper_green)
|
||||
|
||||
# Find contours
|
||||
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
# Create a black canvas to draw contours
|
||||
contour_image = np.zeros_like(mask)
|
||||
|
||||
# Draw the contours on the black canvas (255 for white contours)
|
||||
cv2.drawContours(contour_image, contours, -1, (255), 1)
|
||||
|
||||
# Apply the Canny edge detector on the contour image
|
||||
edges = cv2.Canny(contour_image, 50, 150, apertureSize=3)
|
||||
|
||||
# Apply Hough Line Transform to find lines in the edge-detected image
|
||||
lines = cv2.HoughLines(edges, 1, np.pi / 180, threshold=100)
|
||||
|
||||
# Separate the lines into vertical and horizontal based on their angle (theta)
|
||||
vertical_lines = []
|
||||
horizontal_lines = []
|
||||
|
||||
# Find vertical and horizontal lines
|
||||
if lines is not None:
|
||||
for rho, theta in lines[:, 0]:
|
||||
# Identify vertical lines (theta near 0 or 180 degrees)
|
||||
if np.abs(theta) < np.pi / 180 * 10 or np.abs(theta - np.pi) < np.pi / 180 * 10:
|
||||
vertical_lines.append((rho, theta))
|
||||
# Identify horizontal lines (theta near 90 degrees)
|
||||
elif np.abs(theta - np.pi / 2) < np.pi / 180 * 10:
|
||||
horizontal_lines.append((rho, theta))
|
||||
|
||||
# Create an empty list to store intersection points
|
||||
intersection_points = []
|
||||
|
||||
# Find intersection points between vertical and horizontal lines
|
||||
for v_line in vertical_lines:
|
||||
for h_line in horizontal_lines:
|
||||
intersection = compute_intersection(v_line, h_line)
|
||||
intersection_points.append(intersection)
|
||||
|
||||
# Create an empty image to draw the intersection points
|
||||
intersection_image = np.zeros_like(image)
|
||||
|
||||
# Draw the intersection points on the image (red points)
|
||||
for point in intersection_points:
|
||||
cv2.circle(intersection_image, point, 10, (0, 0, 255), -1) # Red circle at intersection points
|
||||
|
||||
# Convert the intersection image to grayscale
|
||||
grayscale_image = cv2.cvtColor(intersection_image, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# Find contours of the red intersection points (non-zero pixels)
|
||||
contours, _ = cv2.findContours(grayscale_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
# Create an empty black image to draw the center points
|
||||
center_image = np.copy(image)
|
||||
|
||||
# List to store the center points' coordinates
|
||||
center_points = []
|
||||
|
||||
# Iterate over each contour and find the centroid (center point)
|
||||
for contour in contours:
|
||||
# Calculate the moments of the contour
|
||||
moments = cv2.moments(contour)
|
||||
|
||||
# Calculate the centroid (center) of the contour
|
||||
if moments['m00'] != 0:
|
||||
cx = int(moments['m10'] / moments['m00'])
|
||||
cy = int(moments['m01'] / moments['m00'])
|
||||
|
||||
# Draw the center point (blue) on the original image
|
||||
cv2.circle(center_image, (cx, cy), 5, (255, 0, 0), -1) # Blue circle at center
|
||||
|
||||
# Store the center coordinates in the list
|
||||
center_points.append((cx, cy))
|
||||
center_points = sorted(center_points, key=lambda x: sum(x))
|
||||
|
||||
# Step 1: Find the convex hull of the center points
|
||||
center_points_np = np.array(center_points, dtype=np.int32) # Convert to NumPy array
|
||||
hull = cv2.convexHull(center_points_np) # Compute convex hull
|
||||
|
||||
# Step 2: Create a mask for the filled polygon
|
||||
mask = np.ones_like(image, dtype=np.uint8) * 255 # Create a white mask with the same dimensions as the image
|
||||
cv2.fillPoly(mask, [hull], (0, 0, 0)) # Draw the filled polygon in black on the mask
|
||||
|
||||
image[mask == 0] = 255
|
||||
result_image = image
|
||||
|
||||
return result_image
|
||||
|
||||
def process_image(image):
|
||||
processed_image = draw_white_board_boundaries(image)
|
||||
|
||||
result = processed_image
|
||||
return result
|
||||
|
||||
# detect chess board orientation
|
||||
|
||||
def bounding_box(left, top, width, height):
|
||||
return [(left, top), (left + width, top), (left + width, top + height), (left, top + height)]
|
||||
|
||||
def draw_bounding_box(image, box, color=(0, 255, 0)):
|
||||
cv2.polylines(image, [np.array(box)], isClosed=True, color=color, thickness=2)
|
||||
return image
|
||||
|
||||
def get_board_orientation(bound_8, bound_h):
|
||||
# compute the center of the two bounding boxes
|
||||
center_8 = np.mean(np.array(bound_8), axis=0)
|
||||
center_h = np.mean(np.array(bound_h), axis=0)
|
||||
|
||||
# check the relative position of the two centers
|
||||
if center_8[0] < center_h[0] and center_8[1] < center_h[1]:
|
||||
return 'UPRIGHT'
|
||||
elif center_8[0] > center_h[0] and center_8[1] > center_h[1]:
|
||||
return 'UPSIDE_DOWN'
|
||||
elif center_8[0] < center_h[0] and center_8[1] > center_h[1]:
|
||||
return 'ROTATED_RIGHT'
|
||||
elif center_8[0] > center_h[0] and center_8[1] < center_h[1]:
|
||||
return 'ROTATED_LEFT'
|
||||
|
||||
|
||||
def detect_chessboard_orientation(image):
|
||||
data = ocr_image(image)
|
||||
|
||||
bounds_8 = [] # sample: [(x1, y1), (x2, y2), (x3, y3), (x4, y4)]
|
||||
bounds_h = [] # sample: [(x1, y1), (x2, y2), (x3, y3), (x4, y4)]
|
||||
|
||||
for detection in data:
|
||||
data_dict = detection_to_dict(detection)
|
||||
if data_dict['description'] == '8':
|
||||
# store the bounding box of the 8
|
||||
bounds_8.append(data_dict['vertices'])
|
||||
elif data_dict['description'] == 'h':
|
||||
bounds_h.append(data_dict['vertices'])
|
||||
|
||||
# find the closest pair of 8 and h
|
||||
closest = None
|
||||
|
||||
for bound_8 in bounds_8:
|
||||
for bound_h in bounds_h:
|
||||
distance = np.linalg.norm(np.array(bound_8) - np.array(bound_h))
|
||||
|
||||
if closest is None or distance < closest[0]:
|
||||
closest = (distance, bound_8, bound_h)
|
||||
|
||||
print(f'Closest pair: {closest}')
|
||||
|
||||
if closest is None:
|
||||
return False, closest
|
||||
|
||||
# compute the center of the two bounding boxes
|
||||
center_8 = np.mean(np.array(closest[1]), axis=0)
|
||||
center_h = np.mean(np.array(closest[2]), axis=0)
|
||||
|
||||
# draw the line between the two points
|
||||
cv2.line(image, tuple(center_8.astype(int)), tuple(center_h.astype(int)), (255, 0, 0), 2)
|
||||
|
||||
# draw the bounding boxes
|
||||
image = draw_bounding_box(image, bound_8, (0, 255, 0))
|
||||
image = draw_bounding_box(image, bound_h, (0, 0, 255))
|
||||
|
||||
show_cv2_image(image, "detected image")
|
||||
|
||||
return get_board_orientation(closest[1], closest[2]), closest
|
||||
|
||||
def get_k(image):
|
||||
images = [image]
|
||||
|
||||
for i in range(3):
|
||||
images.append(cv2.rotate(images[-1], cv2.ROTATE_90_CLOCKWISE))
|
||||
|
||||
k = 0
|
||||
for i in range(4):
|
||||
result = detect_chessboard_orientation(process_image(images[i]))
|
||||
|
||||
if result[0] and result[1]:
|
||||
bound_8 = result[1][1][0]
|
||||
bound_h = result[1][1][1]
|
||||
|
||||
# bound_8 and bound_h are in the bottom-left window of the image
|
||||
h, w = images[i].shape[:2]
|
||||
|
||||
if bound_8[0] < w / 2 and bound_8[1] > h / 2 and bound_h[0] < w / 2 and bound_h[1] > h / 2:
|
||||
k = i
|
||||
break
|
||||
|
||||
return k
|
||||
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from ultralytics import YOLO
|
||||
from PIL import Image
|
||||
import chess
|
||||
import time
|
||||
import chess.pgn
|
||||
import mediapipe as mp
|
||||
from statistics import mode
|
||||
|
||||
# Define a mapping of YOLO labels to chess piece names
|
||||
def label_to_piece_name(label):
|
||||
piece_map = {
|
||||
1: "bB", # black-bishop
|
||||
2: "bK", # black-king
|
||||
3: "bN", # black-knight
|
||||
4: "bP", # black-pawn
|
||||
5: "bQ", # black-queen
|
||||
6: "bR", # black-rook
|
||||
7: "wB", # white-bishop
|
||||
8: "wK", # white-king
|
||||
9: "wN", # white-knight
|
||||
10: "wP", # white-pawn
|
||||
11: "wQ", # white-queen
|
||||
12: "wR" # white-rook
|
||||
}
|
||||
return piece_map.get(label, "?")
|
||||
|
||||
# Function to find intersection points of the grid lines
|
||||
def find_grid(image, k=0):
|
||||
# image = cv2.imread(image_path)
|
||||
|
||||
# Convert the image to HSV color space
|
||||
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
|
||||
|
||||
for i in range(k):
|
||||
hsv = cv2.rotate(hsv, cv2.ROTATE_90_CLOCKWISE)
|
||||
|
||||
# Define the range of green color in HSV (assuming green grid lines)
|
||||
lower_green = np.array([40, 25, 40]) # Lower bound of green in HSV
|
||||
upper_green = np.array([100, 200, 200]) # Upper bound of green in HSV
|
||||
|
||||
# Threshold the image to get only the green color
|
||||
mask = cv2.inRange(hsv, lower_green, upper_green)
|
||||
|
||||
# Find contours
|
||||
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
# Create a black canvas to draw contours
|
||||
contour_image = np.zeros_like(mask)
|
||||
|
||||
# Draw the contours on the black canvas (255 for white contours)
|
||||
cv2.drawContours(contour_image, contours, -1, (255), 1)
|
||||
|
||||
# Apply Canny edge detector
|
||||
edges = cv2.Canny(contour_image, 50, 150, apertureSize=3)
|
||||
|
||||
# Apply Hough Line Transform to find lines in the edge-detected image
|
||||
lines = cv2.HoughLines(edges, 1, np.pi / 180, threshold=100)
|
||||
|
||||
# Separate the lines into vertical and horizontal based on their angle
|
||||
vertical_lines = []
|
||||
horizontal_lines = []
|
||||
|
||||
# Find vertical and horizontal lines
|
||||
if lines is not None:
|
||||
for rho, theta in lines[:, 0]:
|
||||
# Identify vertical lines (theta near 0 or 180 degrees)
|
||||
if np.abs(theta) < np.pi / 180 * 10 or np.abs(theta - np.pi) < np.pi / 180 * 10:
|
||||
vertical_lines.append((rho, theta))
|
||||
# Identify horizontal lines (theta near 90 degrees)
|
||||
elif np.abs(theta - np.pi / 2) < np.pi / 180 * 10:
|
||||
horizontal_lines.append((rho, theta))
|
||||
|
||||
# Create an empty list to store intersection points
|
||||
intersection_points = []
|
||||
|
||||
# Function to compute the intersection of two lines
|
||||
def compute_intersection(line1, line2):
|
||||
rho1, theta1 = line1
|
||||
rho2, theta2 = line2
|
||||
|
||||
A = np.array([[np.cos(theta1), np.sin(theta1)], [np.cos(theta2), np.sin(theta2)]])
|
||||
b = np.array([rho1, rho2])
|
||||
|
||||
# Solve the linear system to find the intersection point
|
||||
intersection = np.linalg.solve(A, b)
|
||||
return int(intersection[0]), int(intersection[1])
|
||||
|
||||
# Find intersection points between vertical and horizontal lines
|
||||
for v_line in vertical_lines:
|
||||
for h_line in horizontal_lines:
|
||||
intersection = compute_intersection(v_line, h_line)
|
||||
intersection_points.append(intersection)
|
||||
|
||||
return intersection_points
|
||||
|
||||
# Function to map detected chess pieces to the board using intersection points
|
||||
|
||||
def map_yolo_results_to_chessboard(results, chessboard_corners):
|
||||
"""
|
||||
Maps YOLO detection results to a chessboard grid.
|
||||
|
||||
Args:
|
||||
- results: YOLO detection results containing labels and bounding boxes.
|
||||
- chessboard_corners: List of tuples [(x1, y1), (x2, y2), ..., (x4, y4)]
|
||||
representing the corners of the chessboard
|
||||
(top-left, top-right, bottom-left, bottom-right).
|
||||
|
||||
Returns:
|
||||
- A formatted string representation of the chessboard with mapped pieces.
|
||||
"""
|
||||
# Extract the chessboard corners
|
||||
top_left, top_right, bottom_left, bottom_right = chessboard_corners
|
||||
|
||||
# Calculate the width and height of each cell
|
||||
cell_width = (top_right[0] - top_left[0]) / 8
|
||||
cell_height = (bottom_left[1] - top_left[1]) / 8
|
||||
|
||||
# Initialize an empty 8x8 chessboard
|
||||
board = [['' for _ in range(8)] for _ in range(8)]
|
||||
|
||||
# Process YOLO results
|
||||
for r in results:
|
||||
boxes = r.boxes.xywh.numpy() # Bounding boxes in (x_center, y_center, width, height)
|
||||
labels = r.boxes.cls.numpy() # Class indices
|
||||
|
||||
for box, label in zip(boxes, labels):
|
||||
x_center, y_center, _, _ = box
|
||||
piece_name = label_to_piece_name(int(label))
|
||||
|
||||
# Determine the row and column based on the center point
|
||||
col = int((x_center - top_left[0]) / cell_width)
|
||||
row = int((y_center - top_left[1]) / cell_height)
|
||||
|
||||
# Ensure row and col are within bounds
|
||||
if 0 <= row < 8 and 0 <= col < 8:
|
||||
board[row][col] = piece_name
|
||||
|
||||
# Format the board for display
|
||||
formatted_board = '\n'.join([' '.join([cell if cell else '--' for cell in row]) for row in board])
|
||||
return formatted_board
|
||||
|
||||
def convert_to_valid_fen(board_string):
|
||||
# Mapping of custom pieces to FEN standard pieces
|
||||
piece_mapping = {
|
||||
"wP": "P", "wR": "R", "wN": "N", "wB": "B", "wQ": "Q", "wK": "K",
|
||||
"bP": "p", "bR": "r", "bN": "n", "bB": "b", "bQ": "q", "bK": "k",
|
||||
"--": "1" # Empty squares
|
||||
}
|
||||
|
||||
# Split the input string into rows
|
||||
rows = board_string.strip().split("\n")
|
||||
|
||||
fen_rows = []
|
||||
for row in rows:
|
||||
squares = row.split() # Split the row into individual squares
|
||||
fen_row = ""
|
||||
for square in squares:
|
||||
fen_row += piece_mapping.get(square, square) # Replace with mapped value
|
||||
|
||||
# Compress consecutive digits (empty spaces) into single numbers
|
||||
compressed_row = ""
|
||||
empty_count = 0
|
||||
for char in fen_row:
|
||||
if char.isdigit(): # Count empty squares
|
||||
empty_count += int(char)
|
||||
else:
|
||||
if empty_count > 0:
|
||||
compressed_row += str(empty_count)
|
||||
empty_count = 0
|
||||
compressed_row += char
|
||||
if empty_count > 0:
|
||||
compressed_row += str(empty_count) # Add remaining empty squares
|
||||
fen_rows.append(compressed_row)
|
||||
|
||||
# Combine rows with "/" and add default metadata
|
||||
fen_board = "/".join(fen_rows)
|
||||
fen_metadata = " w - - 0 1" # White to move, no castling, no en passant
|
||||
return fen_board + fen_metadata
|
||||
|
||||
def rotate_fen(fen):
|
||||
# Split the FEN into board state and other details
|
||||
board, *rest = fen.split(' ')
|
||||
|
||||
# Split the board into rows
|
||||
rows = board.split('/')
|
||||
|
||||
# Rotate each row (reverse the pieces) and then reverse the row order
|
||||
rotated_rows = [''.join(reversed(row)) for row in reversed(rows)]
|
||||
|
||||
# Recombine the rows into the rotated FEN
|
||||
rotated_board = '/'.join(rotated_rows)
|
||||
|
||||
# Combine the rotated board with the rest of the FEN details
|
||||
return ' '.join([rotated_board] + rest)
|
||||
|
||||
def convert_to_san(moves):
|
||||
san_moves = []
|
||||
move_number = 1
|
||||
|
||||
for i in range(0, len(moves), 2):
|
||||
if i + 1 < len(moves):
|
||||
# Pair moves for each turn
|
||||
san_moves.append(f"{move_number}. {moves[i]} {moves[i+1]}")
|
||||
else:
|
||||
# If there's an odd move at the end, only record that
|
||||
san_moves.append(f"{move_number}. {moves[i]}")
|
||||
move_number += 1
|
||||
|
||||
return " ".join(san_moves)
|
||||
|
||||
|
||||
def get_chessboard_corners(image, k=0):
|
||||
coor = find_grid(image, k)
|
||||
coor = sorted(coor, key=lambda x: sum(x))
|
||||
min = coor[0]
|
||||
max = coor[-1]
|
||||
|
||||
chessboard_corners = [(min[0], min[1]), (max[0], min[1]), (min[0], max[1]), (max[0], max[1])]
|
||||
|
||||
return chessboard_corners
|
||||
|
||||
def board_list_to_list(board_list):
|
||||
output = [[[],[],[],[],[],[],[],[]],
|
||||
[[],[],[],[],[],[],[],[]],
|
||||
[[],[],[],[],[],[],[],[]],
|
||||
[[],[],[],[],[],[],[],[]],
|
||||
[[],[],[],[],[],[],[],[]],
|
||||
[[],[],[],[],[],[],[],[]],
|
||||
[[],[],[],[],[],[],[],[]],
|
||||
[[],[],[],[],[],[],[],[]]]
|
||||
|
||||
|
||||
for board in board_list:
|
||||
board_splitR = board.split("\n")
|
||||
for r,board_row in enumerate(board_splitR):
|
||||
board_pos = board_row.split(" ")
|
||||
for c,piece in enumerate(board_pos):
|
||||
output[r][c].append(piece)
|
||||
|
||||
for r,row in enumerate(output):
|
||||
for c,col in enumerate(row):
|
||||
output[r][c] = mode(col)
|
||||
|
||||
return output
|
||||
|
||||
def board_to_pgn(prev_b, curr_b):
|
||||
board_pos_coor = [['a8', 'b8', 'c8', 'd8', 'e8', 'f8','g8','h8'],
|
||||
['a7', 'b7', 'c7', 'd7', 'e7', 'f7','g7','h7'],
|
||||
['a6', 'b6', 'c6', 'd6', 'e6', 'f6','g6','h6'],
|
||||
['a5', 'b5', 'c5', 'd5', 'e5', 'f5','g5','h5'],
|
||||
['a4', 'b4', 'c4', 'd4', 'e4', 'f4','g4','h4'],
|
||||
['a3', 'b3', 'c3', 'd3', 'e3', 'f3','g3','h3'],
|
||||
['a2', 'b2', 'c2', 'd2', 'e2', 'f2','g2','h2'],
|
||||
['a1', 'b1', 'c1', 'd1', 'e1', 'f1','g1','h1']]
|
||||
|
||||
# Find the coordinates of the changed piece
|
||||
moved_from = None
|
||||
moved_to = None
|
||||
|
||||
for row in range(8):
|
||||
for col in range(8):
|
||||
if prev_b[row][col] != curr_b[row][col]:
|
||||
if curr_b[row][col] == '.':
|
||||
# The piece moved from this square
|
||||
moved_from = (row, col)
|
||||
else:
|
||||
# The piece moved to this square
|
||||
moved_to = (row, col)
|
||||
|
||||
if moved_from is None or moved_to is None:
|
||||
return "No valid move found", False # In case of invalid input
|
||||
|
||||
# Convert coordinates to chess notation
|
||||
from_square = board_pos_coor[moved_from[0]][moved_from[1]]
|
||||
to_square = board_pos_coor[moved_to[0]][moved_to[1]]
|
||||
|
||||
moved_piece = prev_b[moved_from[0]][moved_from[1]]
|
||||
if moved_piece.islower(): # black move
|
||||
if prev_b[moved_to[0]][moved_to[1]] != '.': # black capture
|
||||
if moved_piece == 'p':
|
||||
pgn = f"{board_pos_coor[moved_from[0]][moved_from[1]][0]}x{to_square}" # black pawn capture
|
||||
else:
|
||||
pgn = f"{moved_piece.upper()}x{to_square}" #black non pawn capture
|
||||
else: # not capture
|
||||
if moved_piece == 'p':
|
||||
moved_piece = ""
|
||||
else:
|
||||
moved_piece = moved_piece.upper()
|
||||
pgn = f"{moved_piece}{to_square}" # Regular pawn move
|
||||
else: # white
|
||||
if prev_b[moved_to[0]][moved_to[1]] != '.': # capture
|
||||
if moved_piece == 'P':
|
||||
pgn = f"{board_pos_coor[moved_from[0]][moved_from[1]][0]}x{to_square}" # capture
|
||||
else:
|
||||
pgn = f"{moved_piece}x{to_square}"
|
||||
else: # not capture
|
||||
if moved_piece == 'P':
|
||||
pgn = f"{to_square}" # Regular piece move
|
||||
else:
|
||||
pgn = f"{moved_piece}{to_square}" # Regular pawn move
|
||||
|
||||
is_white = prev_b[moved_from[0]][moved_from[1]].isupper()
|
||||
|
||||
return pgn, is_white
|
||||
|
||||
|
||||
|
||||
def gen_pgn(vid_path, model):
|
||||
mp_hands = mp.solutions.hands
|
||||
hands = mp_hands.Hands(static_image_mode=False, max_num_hands=2, min_detection_confidence=0.5)
|
||||
# mp_drawing = mp.solutions.drawing_utils
|
||||
|
||||
cap = cv2.VideoCapture(vid_path)
|
||||
|
||||
# Parameters for frame processing
|
||||
frame_count = 0
|
||||
frame_interval = int(float(cap.get(cv2.CAP_PROP_FPS)) * 0.5) # Process frame every 1 second
|
||||
# previous_board = None # Track the previous board state
|
||||
# previous_hand_present = False # Track if a hand was detected in the previous frame
|
||||
|
||||
# list of board list; appends the board_lists
|
||||
board_list_list = []
|
||||
|
||||
# board list iterate every time hand is present
|
||||
board_list = []
|
||||
|
||||
# get one frame
|
||||
print('getting k from gen_pgn')
|
||||
k = get_k(cap.read()[1])
|
||||
|
||||
while cap.isOpened():
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
if len(board_list) != 0:
|
||||
board_list_list.append(board_list)
|
||||
board_list = []
|
||||
break # End of video
|
||||
|
||||
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
|
||||
# Perform hand detection
|
||||
hand_results = hands.process(rgb_frame)
|
||||
not_hand_present = hand_results.multi_hand_landmarks == None
|
||||
|
||||
# Crop the frame
|
||||
frame = frame[425:1495, :]
|
||||
|
||||
for i in range(k):
|
||||
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
|
||||
|
||||
# Process frame every frame_interval
|
||||
if frame_count % frame_interval == 0:
|
||||
if not_hand_present:
|
||||
results = model(frame, conf=0.3)
|
||||
chessboard_corners = get_chessboard_corners(frame, k)
|
||||
|
||||
formatted_board = map_yolo_results_to_chessboard(results, chessboard_corners)
|
||||
|
||||
fen = convert_to_valid_fen(formatted_board)
|
||||
fen = rotate_fen(fen)
|
||||
|
||||
current_board = chess.Board(fen)
|
||||
#print('board')
|
||||
board_list.append(str(current_board)[1:-1])
|
||||
elif not not_hand_present or not ret:
|
||||
#print('hand')
|
||||
if len(board_list) != 0:
|
||||
board_list_list.append(board_list)
|
||||
board_list = []
|
||||
|
||||
# Increment frame counter
|
||||
frame_count += 1
|
||||
|
||||
#san_notation = convert_to_san(['..'] + san_move)
|
||||
# print(san_notation)
|
||||
|
||||
# cap.release()
|
||||
# cv2.destroyAllWindows()
|
||||
# print(board_list_list)
|
||||
# print(len(board_list_list))
|
||||
# print(board_list_list[0])
|
||||
# print(board_list_list[1])
|
||||
# print(board_list_list[2])
|
||||
|
||||
for i,b_list in enumerate(board_list_list):
|
||||
print(b_list)
|
||||
new_board_list = board_list_to_list(b_list)
|
||||
board_list_list[i] = new_board_list
|
||||
print(str(new_board_list)+"\n\n")
|
||||
|
||||
pgn_index = 1
|
||||
white_move = ".."
|
||||
black_move = ".."
|
||||
|
||||
pgn_all = [""]
|
||||
|
||||
for i in range(len(board_list_list) - 1):
|
||||
|
||||
p_board = board_list_list[i]
|
||||
c_board = board_list_list[i + 1]
|
||||
pgn, is_white = board_to_pgn(prev_b=p_board, curr_b=c_board)
|
||||
|
||||
if is_white:
|
||||
white_move = pgn
|
||||
else:
|
||||
black_move = pgn
|
||||
|
||||
pgn_row = f"{pgn_index}. {white_move} {black_move} "
|
||||
|
||||
if is_white or (not is_white and black_move != ".."):
|
||||
if len(pgn_all) < pgn_index:
|
||||
pgn_all.append(pgn_row)
|
||||
else:
|
||||
pgn_all[pgn_index - 1] = pgn_row
|
||||
|
||||
if black_move != "..":
|
||||
white_move = ".."
|
||||
black_move = ".."
|
||||
pgn_index += 1
|
||||
|
||||
|
||||
out_str = ""
|
||||
|
||||
for r in pgn_all:
|
||||
out_str += r
|
||||
|
||||
if out_str == "":
|
||||
return '1. '
|
||||
|
||||
return out_str
|
||||
|
||||
# # Initialize Mediapipe Hands
|
||||
# mp_hands = mp.solutions.hands
|
||||
# hands = mp_hands.Hands(static_image_mode=False, max_num_hands=2, min_detection_confidence=0.5)
|
||||
# mp_drawing = mp.solutions.drawing_utils
|
||||
|
||||
# 1. .. move 2. move ..
|
||||
|
||||
# Load the YOLO model
|
||||
model = YOLO("best2.pt") # Replace with the path to your trained YOLO model
|
||||
# image_path = "detection/test2.jpg"
|
||||
|
||||
# image = cv2.imread(image_path)
|
||||
video_path_2m = "kaggle/input/2_move_student.mp4" # Replace with the path to your video
|
||||
video_path_4m = "kaggle/input/4_Move_studet.mp4" # Replace with the path to your video
|
||||
video_path_6m = "kaggle/input/6_Move_student.mp4" # Replace with the path to your video
|
||||
video_path_8m = "kaggle/input/8_Move_student.mp4" # Replace with the path to your video
|
||||
video_path_2mr = "kaggle/input/2_Move_rotate_student.mp4"
|
||||
|
||||
|
||||
|
||||
video_path_list = [video_path_2mr, video_path_2m, video_path_4m, video_path_6m, video_path_8m]
|
||||
|
||||
output_path = "output/output_video.avi" # Optional: Specify a path to save the output video
|
||||
|
||||
# for vidp in vidp_list:
|
||||
# for vidp in video_path_list:
|
||||
# gen_pgn(vidp, model)
|
||||
# print(gen_pgn(video_path_4m, model))
|
||||
|
||||
# Release resources
|
||||
|
||||
#R . B . Q . . R
|
||||
#P P K . . P . P
|
||||
#. . . . . N . .
|
||||
#q . . P . . P .
|
||||
#. . . p P . . n
|
||||
#. . . . p . . .
|
||||
#. p . . b p p p
|
||||
#R n b k . . . .
|
||||
|
||||
#R . B . Q . . R
|
||||
#P . K . . P . P
|
||||
#. P . . . N . .
|
||||
#q . . P . . P .
|
||||
#. . . p P . . n
|
||||
#. . . . p . . .
|
||||
#. p . . b p p p
|
||||
#R n b k . . . .
|
||||
|
||||
pgn = []
|
||||
for path in video_path_list:
|
||||
pgn.append(gen_pgn(path, model))
|
||||
|
||||
print(pgn)
|
||||
|
||||
import pandas as pd
|
||||
|
||||
vids = ['2_Move_rotate_student.mp4','2_move_student.mp4','4_Move_studet.mp4','6_Move_student.mp4','8_Move_student.mp4', '(Bonus)Long_video_student.mp4']
|
||||
|
||||
print(len(pgn), len(vids))
|
||||
|
||||
if len(pgn) < len(vids):
|
||||
pgn.append('1.')
|
||||
|
||||
|
||||
# pgn.append('1.')
|
||||
df = pd.DataFrame({
|
||||
"row_id": vids,
|
||||
"output": pgn
|
||||
})
|
||||
|
||||
# Save to CSV
|
||||
df.to_csv("kaggle/working/submission.csv", index=False, encoding="utf-8")
|
||||
162
setup_project.py
|
|
@ -1,162 +0,0 @@
|
|||
import os
|
||||
import subprocess
|
||||
|
||||
def create_directory_structure():
|
||||
# Main project directories
|
||||
directories = [
|
||||
'src',
|
||||
'src/core',
|
||||
'src/features',
|
||||
'src/utils',
|
||||
'src/services',
|
||||
'tests',
|
||||
'tests/unit',
|
||||
'tests/integration',
|
||||
'tests/performance',
|
||||
'docs',
|
||||
'docs/api',
|
||||
'docs/architecture',
|
||||
'docs/development',
|
||||
'assets',
|
||||
'assets/ml-models',
|
||||
'assets/shaders',
|
||||
'scripts',
|
||||
'config'
|
||||
]
|
||||
|
||||
# Create directories
|
||||
for directory in directories:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
def create_base_files():
|
||||
# Git-related files
|
||||
files = {
|
||||
'.gitignore': '''
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Xcode
|
||||
xcuserdata/
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.perspectivev3
|
||||
|
||||
# Swift Package Manager
|
||||
.build/
|
||||
Packages/
|
||||
Package.pins
|
||||
Package.resolved
|
||||
|
||||
# ML Models
|
||||
*.mlmodel
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Testing
|
||||
*.xcresult
|
||||
|
||||
# Fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
''',
|
||||
'README.md': '''
|
||||
# Chess Teaching Assistant
|
||||
|
||||
An intelligent chess teaching assistant for macOS that provides real-time analysis and visualization.
|
||||
|
||||
## Features
|
||||
- Real-time board position analysis
|
||||
- Visual move suggestions
|
||||
- Attack pattern visualization
|
||||
- Defensive planning indicators
|
||||
|
||||
## Development
|
||||
See [Development Guide](docs/development/getting-started.md) for setup instructions.
|
||||
|
||||
## Architecture
|
||||
See [Architecture Overview](docs/architecture/overview.md) for system design details.
|
||||
|
||||
## License
|
||||
[MIT License](LICENSE)
|
||||
''',
|
||||
'LICENSE': '''MIT License
|
||||
|
||||
Copyright (c) 2025 Chess Teaching Assistant
|
||||
|
||||
Permission is hereby granted, free of charge...''',
|
||||
'docs/development/getting-started.md': '''# Getting Started
|
||||
|
||||
## Prerequisites
|
||||
- Xcode 15.0+
|
||||
- macOS 14.0+
|
||||
- Swift 6.0+
|
||||
|
||||
## Setup Instructions
|
||||
1. Clone the repository
|
||||
2. Install dependencies
|
||||
3. Build and run
|
||||
''',
|
||||
'docs/architecture/overview.md': '''# Architecture Overview
|
||||
|
||||
## System Components
|
||||
- Screenshot Capture Module
|
||||
- Board Position Analysis Engine
|
||||
- Visual Overlay System
|
||||
- Stockfish Integration
|
||||
''',
|
||||
'src/core/__init__.py': '',
|
||||
'src/features/__init__.py': '',
|
||||
'src/utils/__init__.py': '',
|
||||
'src/services/__init__.py': '',
|
||||
'tests/__init__.py': '',
|
||||
'config/default.json': '''{
|
||||
"analysis": {
|
||||
"depth": 20,
|
||||
"threads": 4
|
||||
},
|
||||
"visualization": {
|
||||
"arrowColor": "#FF0000",
|
||||
"highlightColor": "#00FF00"
|
||||
}
|
||||
}'''
|
||||
}
|
||||
|
||||
for file_path, content in files.items():
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(content.strip())
|
||||
|
||||
def initialize_git():
|
||||
commands = [
|
||||
['git', 'init'],
|
||||
['git', 'add', '.'],
|
||||
['git', 'commit', '-m', 'Initial project structure'],
|
||||
['git', 'branch', '-M', 'main'],
|
||||
]
|
||||
|
||||
for command in commands:
|
||||
subprocess.run(command)
|
||||
|
||||
def main():
|
||||
create_directory_structure()
|
||||
create_base_files()
|
||||
initialize_git()
|
||||
print("Project structure created successfully!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||