Round 3
This commit is contained in:
parent
4b8935afd1
commit
8126e553f7
17 changed files with 1941 additions and 1528 deletions
|
|
@ -2,46 +2,46 @@ import Foundation
|
||||||
import CoreImage
|
import CoreImage
|
||||||
import Vision
|
import Vision
|
||||||
|
|
||||||
class BoardDetector {
|
/// 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
|
// Share CIContext to avoid creating too many Metal command queues
|
||||||
private static let shared = CIContext()
|
private static let shared = CIContext()
|
||||||
private var context: CIContext { BoardDetector.shared }
|
private var context: CIContext { BoardDetector.shared }
|
||||||
|
|
||||||
func detectBoard(in image: CIImage) -> CGRect? {
|
func detectBoard(in image: CIImage) -> CGRect? {
|
||||||
// Configure rectangle detection request
|
|
||||||
let request = VNDetectRectanglesRequest()
|
let request = VNDetectRectanglesRequest()
|
||||||
request.minimumAspectRatio = 0.8 // Adjusted for standard chess board
|
request.minimumAspectRatio = 0.8
|
||||||
request.maximumAspectRatio = 1.2
|
request.maximumAspectRatio = 1.2
|
||||||
request.minimumSize = 0.4
|
request.minimumSize = 0.4
|
||||||
request.maximumObservations = 1
|
request.maximumObservations = 1
|
||||||
request.quadratureTolerance = 30
|
request.quadratureTolerance = 30
|
||||||
request.minimumConfidence = 0.9
|
request.minimumConfidence = 0.9
|
||||||
|
|
||||||
// Perform the request
|
|
||||||
let requestHandler = VNImageRequestHandler(ciImage: image, options: [:])
|
let requestHandler = VNImageRequestHandler(ciImage: image, options: [:])
|
||||||
do {
|
do {
|
||||||
try requestHandler.perform([request])
|
try requestHandler.perform([request])
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to perform rectangle detection: \(error)")
|
print("ERROR: Rectangle detection failed - \(error)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process results
|
|
||||||
guard let observations = request.results,
|
guard let observations = request.results,
|
||||||
!observations.isEmpty else {
|
!observations.isEmpty else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let bestObservation = observations[0]
|
let bestObservation = observations[0]
|
||||||
|
|
||||||
// Convert Vision coordinates to CoreImage coordinates
|
|
||||||
let imageSize = image.extent.size
|
let imageSize = image.extent.size
|
||||||
let transform = CGAffineTransform(scaleX: imageSize.width, y: imageSize.height)
|
let transform = CGAffineTransform(scaleX: imageSize.width, y: imageSize.height)
|
||||||
|
|
||||||
// Create normalized rect in CoreImage coordinate space
|
|
||||||
let detectedRect = bestObservation.boundingBox.applying(transform)
|
let detectedRect = bestObservation.boundingBox.applying(transform)
|
||||||
|
|
||||||
// Validate the detected rectangle
|
|
||||||
guard validateDetectedRect(detectedRect, in: image) else {
|
guard validateDetectedRect(detectedRect, in: image) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -49,28 +49,123 @@ class BoardDetector {
|
||||||
return detectedRect
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var position = ChessPosition()
|
||||||
|
var recognizedPieces = 0
|
||||||
|
|
||||||
|
// Reset piece counts before starting new scan
|
||||||
|
pieceRecognizer.resetPieceCounts()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Set the current position being analyzed
|
||||||
|
pieceRecognizer.currentPosition = boardPosition
|
||||||
|
|
||||||
|
if let piece = try await pieceRecognizer.recognizePiece(from: squareImage) {
|
||||||
|
position[boardPosition] = piece
|
||||||
|
recognizedPieces += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate piece counts after all squares are processed
|
||||||
|
try pieceRecognizer.validatePieceCounts()
|
||||||
|
|
||||||
|
print("\nPieces recognized: \(recognizedPieces)")
|
||||||
|
print("\nRecognized position:")
|
||||||
|
for rank in (0...7).reversed() {
|
||||||
|
var rankStr = "\(rank + 1) "
|
||||||
|
for file in 0...7 {
|
||||||
|
if let piece = position[BoardPosition(file: file, rank: rank)] {
|
||||||
|
rankStr += "\(piece.color == .white ? "w" : "b")\(piece.type.fenSymbol.uppercased()) "
|
||||||
|
} else {
|
||||||
|
rankStr += ".. "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print(rankStr)
|
||||||
|
}
|
||||||
|
print(" a b c d e f g h")
|
||||||
|
|
||||||
|
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 {
|
private func validateDetectedRect(_ rect: CGRect, in image: CIImage) -> Bool {
|
||||||
let imageSize = image.extent.size
|
let imageSize = image.extent.size
|
||||||
|
|
||||||
// Check if rectangle is within image bounds
|
|
||||||
guard image.extent.contains(rect) else {
|
guard image.extent.contains(rect) else {
|
||||||
|
print("ERROR: Detected rectangle outside image bounds")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate aspect ratio (standard chess board is square)
|
|
||||||
let aspectRatio = rect.width / rect.height
|
let aspectRatio = rect.width / rect.height
|
||||||
guard aspectRatio >= 0.9 && aspectRatio <= 1.1 else {
|
guard aspectRatio >= 0.9 && aspectRatio <= 1.1 else {
|
||||||
|
print("ERROR: Invalid board aspect ratio: \(aspectRatio)")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate size relative to image
|
|
||||||
let minDimension = min(imageSize.width, imageSize.height)
|
let minDimension = min(imageSize.width, imageSize.height)
|
||||||
let boardSize = max(rect.width, rect.height)
|
let boardSize = max(rect.width, rect.height)
|
||||||
guard boardSize >= minDimension * 0.4 else {
|
let sizeRatio = boardSize / minDimension
|
||||||
|
guard sizeRatio >= 0.4 else {
|
||||||
|
print("ERROR: Board too small relative to image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,86 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"metadataOutputVersion" : "3.0",
|
||||||
|
"outputSchema" : [
|
||||||
|
{
|
||||||
|
"isOptional" : "0",
|
||||||
|
"formattedType" : "String",
|
||||||
|
"type" : "String",
|
||||||
|
"name" : "target",
|
||||||
|
"shortDescription" : ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"isOptional" : "0",
|
||||||
|
"keyType" : "String",
|
||||||
|
"formattedType" : "Dictionary (String → Double)",
|
||||||
|
"type" : "Dictionary",
|
||||||
|
"name" : "targetProbability",
|
||||||
|
"shortDescription" : ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modelParameters" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"author" : "Chris Haulmark",
|
||||||
|
"specificationVersion" : 8,
|
||||||
|
"isUpdatable" : "0",
|
||||||
|
"stateSchema" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"availability" : {
|
||||||
|
"macOS" : "14.0",
|
||||||
|
"tvOS" : "17.0",
|
||||||
|
"visionOS" : "1.0",
|
||||||
|
"watchOS" : "unavailable",
|
||||||
|
"iOS" : "17.0",
|
||||||
|
"macCatalyst" : "17.0"
|
||||||
|
},
|
||||||
|
"modelType" : {
|
||||||
|
"name" : "MLModelType_imageClassifier",
|
||||||
|
"structure" : [
|
||||||
|
{
|
||||||
|
"name" : "MLModelType_visionFeaturePrint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name" : "MLModelType_glmClassifier"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inputSchema" : [
|
||||||
|
{
|
||||||
|
"height" : "360",
|
||||||
|
"colorspace" : "BGR",
|
||||||
|
"isOptional" : "0",
|
||||||
|
"width" : "360",
|
||||||
|
"isColor" : "1",
|
||||||
|
"formattedType" : "Image (Color 360 × 360)",
|
||||||
|
"hasSizeFlexibility" : "0",
|
||||||
|
"type" : "Image",
|
||||||
|
"shortDescription" : "",
|
||||||
|
"name" : "image"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"classLabels" : [
|
||||||
|
"black_bishop",
|
||||||
|
"black_king",
|
||||||
|
"black_knight",
|
||||||
|
"black_pawn",
|
||||||
|
"black_queen",
|
||||||
|
"black_rook",
|
||||||
|
"white_bishop",
|
||||||
|
"white_king",
|
||||||
|
"white_knight",
|
||||||
|
"white_pawn",
|
||||||
|
"white_queen",
|
||||||
|
"white_rook"
|
||||||
|
],
|
||||||
|
"generatedClassName" : "ChessPieceClassifier",
|
||||||
|
"userDefinedMetadata" : {
|
||||||
|
"com.apple.createml.version" : "15.3.0",
|
||||||
|
"com.apple.createml.app.tag" : "150.3",
|
||||||
|
"com.apple.coreml.model.preview.type" : "imageClassifier",
|
||||||
|
"com.apple.createml.app.version" : "6.1"
|
||||||
|
},
|
||||||
|
"method" : "predict"
|
||||||
|
}
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,6 +1,84 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
|
// MARK: - Chess Position View Components
|
||||||
|
|
||||||
|
struct ChessPieceView: View {
|
||||||
|
let piece: ChessPiece
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CustomNSView: NSView {
|
class CustomNSView: NSView {
|
||||||
private static let invisibleCursor: NSCursor = {
|
private static let invisibleCursor: NSCursor = {
|
||||||
let image = NSImage(size: NSSize(width: 1, height: 1))
|
let image = NSImage(size: NSSize(width: 1, height: 1))
|
||||||
|
|
@ -84,48 +162,10 @@ struct CaptureStatusButton: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct CaptureView: View {
|
||||||
@StateObject private var viewModel = ScreenCaptureViewModel()
|
@ObservedObject var viewModel: ScreenCaptureViewModel
|
||||||
|
|
||||||
var body: some View {
|
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 {
|
|
||||||
// Start monitoring for chess boards when view appears
|
|
||||||
viewModel.startMonitoring()
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
// Stop monitoring when view disappears
|
|
||||||
viewModel.stopMonitoring()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error display
|
|
||||||
if let error = viewModel.captureError {
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image display
|
|
||||||
HStack {
|
HStack {
|
||||||
VStack {
|
VStack {
|
||||||
Text("Full Capture")
|
Text("Full Capture")
|
||||||
|
|
@ -176,10 +216,115 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.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()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(minWidth: 800, minHeight: 600)
|
.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
274
ChessPrism/ChessPrism/Models/ChessPosition.swift
Normal file
274
ChessPrism/ChessPrism/Models/ChessPosition.swift
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
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 {
|
||||||
|
let file: Int // 0-7 for a-h
|
||||||
|
let rank: Int // 0-7 for 1-8
|
||||||
|
|
||||||
|
/// 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")!
|
||||||
|
}
|
||||||
|
}
|
||||||
397
ChessPrism/ChessPrism/Recognition/PieceRecognizer.swift
Normal file
397
ChessPrism/ChessPrism/Recognition/PieceRecognizer.swift
Normal file
|
|
@ -0,0 +1,397 @@
|
||||||
|
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
|
||||||
|
case invalidPosition(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
|
||||||
|
/// Piece counts for validation
|
||||||
|
private var whitePieceCount: [PieceType: Int] = [:]
|
||||||
|
private var blackPieceCount: [PieceType: Int] = [:]
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
init() throws {
|
||||||
|
print("=== INITIALIZING PIECE RECOGNIZER ===")
|
||||||
|
|
||||||
|
let bundle = Bundle.main
|
||||||
|
|
||||||
|
// Load model from bundle
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPieceCounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Recognition Methods
|
||||||
|
|
||||||
|
/// Reset piece counts for new position
|
||||||
|
func resetPieceCounts() {
|
||||||
|
whitePieceCount = [
|
||||||
|
.king: 0,
|
||||||
|
.queen: 0,
|
||||||
|
.rook: 0,
|
||||||
|
.bishop: 0,
|
||||||
|
.knight: 0,
|
||||||
|
.pawn: 0
|
||||||
|
]
|
||||||
|
blackPieceCount = [
|
||||||
|
.king: 0,
|
||||||
|
.queen: 0,
|
||||||
|
.rook: 0,
|
||||||
|
.bishop: 0,
|
||||||
|
.knight: 0,
|
||||||
|
.pawn: 0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate piece counts and ensure the position is legal
|
||||||
|
func validatePieceCounts() throws {
|
||||||
|
var errors: [String] = []
|
||||||
|
|
||||||
|
// Check white pieces
|
||||||
|
if whitePieceCount[.king] != 1 {
|
||||||
|
let count = whitePieceCount[.king] ?? 0
|
||||||
|
errors.append("Invalid white king count: \(count)")
|
||||||
|
}
|
||||||
|
if let count = whitePieceCount[.queen], count > 1 {
|
||||||
|
errors.append("Too many white queens: \(count)")
|
||||||
|
}
|
||||||
|
if let count = whitePieceCount[.rook], count > 2 {
|
||||||
|
errors.append("Too many white rooks: \(count)")
|
||||||
|
}
|
||||||
|
if let count = whitePieceCount[.bishop], count > 2 {
|
||||||
|
errors.append("Too many white bishops: \(count)")
|
||||||
|
}
|
||||||
|
if let count = whitePieceCount[.knight], count > 2 {
|
||||||
|
errors.append("Too many white knights: \(count)")
|
||||||
|
}
|
||||||
|
if let count = whitePieceCount[.pawn], count > 8 {
|
||||||
|
errors.append("Too many white pawns: \(count)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check black pieces
|
||||||
|
if blackPieceCount[.king] ?? 0 != 1 {
|
||||||
|
let count = blackPieceCount[.king] ?? 0
|
||||||
|
errors.append("Invalid black king count: \(count)")
|
||||||
|
}
|
||||||
|
if let count = blackPieceCount[.queen], count > 1 {
|
||||||
|
errors.append("Too many black queens: \(count)")
|
||||||
|
}
|
||||||
|
if let count = blackPieceCount[.rook], count > 2 {
|
||||||
|
errors.append("Too many black rooks: \(count)")
|
||||||
|
}
|
||||||
|
if let count = blackPieceCount[.bishop], count > 2 {
|
||||||
|
errors.append("Too many black bishops: \(count)")
|
||||||
|
}
|
||||||
|
if let count = blackPieceCount[.knight], count > 2 {
|
||||||
|
errors.append("Too many black knights: \(count)")
|
||||||
|
}
|
||||||
|
if let count = blackPieceCount[.pawn], count > 8 {
|
||||||
|
errors.append("Too many black pawns: \(count)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.isEmpty {
|
||||||
|
throw PieceRecognitionError.invalidPosition(errors.joined(separator: ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update piece count
|
||||||
|
private func updatePieceCount(piece: ChessPiece) {
|
||||||
|
if piece.color == .white {
|
||||||
|
whitePieceCount[piece.type] = (whitePieceCount[piece.type] ?? 0) + 1
|
||||||
|
} else {
|
||||||
|
blackPieceCount[piece.type] = (blackPieceCount[piece.type] ?? 0) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if adding this piece would exceed limits
|
||||||
|
private func wouldExceedLimits(_ piece: ChessPiece) -> Bool {
|
||||||
|
let count = piece.color == .white ? whitePieceCount[piece.type] ?? 0 : blackPieceCount[piece.type] ?? 0
|
||||||
|
switch piece.type {
|
||||||
|
case .king: return count >= 1
|
||||||
|
case .queen: return count >= 1
|
||||||
|
case .rook, .bishop, .knight: return count >= 2
|
||||||
|
case .pawn: return count >= 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recognize a chess piece from a square image
|
||||||
|
/// - Parameters:
|
||||||
|
/// - image: CGImage of the chess square
|
||||||
|
/// - completion: Callback with result (ChessPiece if recognized, nil if empty)
|
||||||
|
/// - Throws: PieceRecognitionError
|
||||||
|
func recognizePiece(from image: CGImage) async throws -> ChessPiece? {
|
||||||
|
// 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 classificationResults: [VNClassificationObservation]?
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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 = classificationResults,
|
||||||
|
let topResult = results.first else {
|
||||||
|
print("ERROR: No classification results")
|
||||||
|
throw PieceRecognitionError.recognitionFailed("No results")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print all results to help diagnose recognition issues
|
||||||
|
if let pos = currentPosition {
|
||||||
|
print("\nClassification results for \(String(describing: pos)):")
|
||||||
|
} else {
|
||||||
|
print("\nClassification results:")
|
||||||
|
}
|
||||||
|
for result in results.prefix(3) {
|
||||||
|
print("- \(result.identifier): \(result.confidence)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty squares
|
||||||
|
if topResult.identifier == "empty_dark" || topResult.identifier == "empty_light" {
|
||||||
|
if topResult.confidence > 0.9 {
|
||||||
|
if let pos = currentPosition {
|
||||||
|
print("\nEmpty square confirmed at \(String(describing: pos))")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get confidence ratio between top predictions
|
||||||
|
let secondBestConfidence = results.count > 1 ? results[1].confidence : 0
|
||||||
|
let confidenceRatio = topResult.confidence / (secondBestConfidence + Float.ulpOfOne)
|
||||||
|
|
||||||
|
// Adjust confidence based on position-specific knowledge
|
||||||
|
let adjustedConfidence = adjustConfidence(topResult.confidence,
|
||||||
|
for: topResult.identifier,
|
||||||
|
at: currentPosition)
|
||||||
|
|
||||||
|
// Much stricter piece recognition:
|
||||||
|
// 1. Must have very high confidence (>0.98)
|
||||||
|
// 2. Must have strong separation from second best (>5.0 ratio)
|
||||||
|
// 3. Must make sense for position
|
||||||
|
// 4. Must not exceed piece limits
|
||||||
|
if adjustedConfidence > 0.98 && confidenceRatio > 5.0 {
|
||||||
|
if let piece = try? createPiece(from: topResult.identifier) {
|
||||||
|
if isValidPieceForPosition(piece, at: currentPosition) && !wouldExceedLimits(piece) {
|
||||||
|
updatePieceCount(piece: piece)
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log classification details
|
||||||
|
if let pos = currentPosition {
|
||||||
|
print("\nClassification rejected at \(String(describing: pos)):")
|
||||||
|
}
|
||||||
|
print("Top result: \(topResult.identifier) (\(topResult.confidence))")
|
||||||
|
if results.count > 1 {
|
||||||
|
print("Second best: \(results[1].identifier) (\(results[1].confidence))")
|
||||||
|
print("Confidence ratio: \(confidenceRatio)")
|
||||||
|
}
|
||||||
|
print("Adjusted confidence: \(adjustedConfidence)")
|
||||||
|
|
||||||
|
return nil // Default to empty for unclear cases
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Position where this piece is being recognized
|
||||||
|
var currentPosition: BoardPosition?
|
||||||
|
|
||||||
|
/// Adjust confidence based on position-specific knowledge
|
||||||
|
private func adjustConfidence(_ confidence: Float, for identifier: String, at position: BoardPosition?) -> Float {
|
||||||
|
guard let position = currentPosition else { return confidence }
|
||||||
|
|
||||||
|
// Parse the piece info
|
||||||
|
let components = identifier.split(separator: "_")
|
||||||
|
guard components.count == 2,
|
||||||
|
let color = PieceColor(rawValue: String(components[0])),
|
||||||
|
let type = PieceType(rawValue: String(components[1])) else {
|
||||||
|
return confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
var adjustment: Float = 0.0
|
||||||
|
|
||||||
|
// Back rank pieces are more likely to be correct
|
||||||
|
if position.isBackRank(for: color) {
|
||||||
|
// Corners should be rooks
|
||||||
|
if type == .rook && position.isEdgeFile {
|
||||||
|
adjustment += 0.1
|
||||||
|
}
|
||||||
|
// Next to corners should be knights
|
||||||
|
if type == .knight && (position.file == 1 || position.file == 6) {
|
||||||
|
adjustment += 0.1
|
||||||
|
}
|
||||||
|
// Next to knights should be bishops
|
||||||
|
if type == .bishop && (position.file == 2 || position.file == 5) {
|
||||||
|
adjustment += 0.1
|
||||||
|
}
|
||||||
|
// Center should be king/queen
|
||||||
|
if (type == .king || type == .queen) && position.isCenterFile {
|
||||||
|
adjustment += 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pawns are more likely on their starting ranks
|
||||||
|
if type == .pawn {
|
||||||
|
if (color == .white && position.rank == 1) ||
|
||||||
|
(color == .black && position.rank == 6) {
|
||||||
|
adjustment += 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return min(1.0, confidence + adjustment)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate if a piece makes sense for its position
|
||||||
|
private func isValidPieceForPosition(_ piece: ChessPiece, at position: BoardPosition?) -> Bool {
|
||||||
|
guard let pos = position else { return true }
|
||||||
|
|
||||||
|
// Basic position validation
|
||||||
|
switch piece.type {
|
||||||
|
case .king:
|
||||||
|
// Kings can't be on the first or last rank of opponent's side
|
||||||
|
if piece.color == .white && pos.rank == 7 { return false }
|
||||||
|
if piece.color == .black && pos.rank == 0 { return false }
|
||||||
|
|
||||||
|
case .pawn:
|
||||||
|
// Pawns can't be on first or last rank
|
||||||
|
if pos.rank == 0 || pos.rank == 7 { return false }
|
||||||
|
// White pawns can't be behind their starting rank
|
||||||
|
if piece.color == .white && pos.rank > 6 { return false }
|
||||||
|
// Black pawns can't be behind their starting rank
|
||||||
|
if piece.color == .black && pos.rank < 1 { return false }
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Other pieces can move freely
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to create a chess piece from a classification label
|
||||||
|
private func createPiece(from identifier: String) throws -> ChessPiece {
|
||||||
|
// Validate it's not an empty square
|
||||||
|
guard !identifier.starts(with: "empty_") else {
|
||||||
|
print("ERROR: Cannot create piece from empty square label: \(identifier)")
|
||||||
|
throw PieceRecognitionError.invalidInput
|
||||||
|
}
|
||||||
|
|
||||||
|
let components = identifier.split(separator: "_")
|
||||||
|
guard components.count == 2,
|
||||||
|
let color = PieceColor(rawValue: String(components[0])),
|
||||||
|
let type = PieceType(rawValue: String(components[1])) else {
|
||||||
|
print("ERROR: Invalid piece label format: \(identifier)")
|
||||||
|
throw PieceRecognitionError.invalidInput
|
||||||
|
}
|
||||||
|
return ChessPiece(type: type, color: color)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
let processed = ciImage
|
||||||
|
.applyingFilter("CIColorControls", parameters: [
|
||||||
|
kCIInputContrastKey: 1.1,
|
||||||
|
kCIInputBrightnessKey: 0.0,
|
||||||
|
kCIInputSaturationKey: 1.1
|
||||||
|
])
|
||||||
|
.applyingFilter("CIUnsharpMask", parameters: [
|
||||||
|
kCIInputRadiusKey: 1.0,
|
||||||
|
kCIInputIntensityKey: 0.5
|
||||||
|
])
|
||||||
|
|
||||||
|
// Convert back to CGImage
|
||||||
|
guard let outputImage = Self.ciContext.createCGImage(processed, from: processed.extent) else {
|
||||||
|
throw PieceRecognitionError.invalidInput
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - BoardPosition Extensions
|
||||||
|
|
||||||
|
extension BoardPosition {
|
||||||
|
/// Initialize from file and rank indices
|
||||||
|
init(file: Int, rank: Int) {
|
||||||
|
self.file = file
|
||||||
|
self.rank = rank
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this position is on the edge of the board (files a or h)
|
||||||
|
var isEdgeFile: Bool {
|
||||||
|
return file == 0 || file == 7
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this position is on the back rank for the given color
|
||||||
|
func isBackRank(for color: PieceColor) -> Bool {
|
||||||
|
return (color == .white && rank == 0) || (color == .black && rank == 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this position is in the center files (d or e)
|
||||||
|
var isCenterFile: Bool {
|
||||||
|
return file == 3 || file == 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,10 +13,14 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
@Published var isAutoCapturing = true // Default to auto-capture mode
|
@Published var isAutoCapturing = true // Default to auto-capture mode
|
||||||
@Published var snapshotTaken = false // Track if snapshot was taken
|
@Published var snapshotTaken = false // Track if snapshot was taken
|
||||||
@Published var latestSnapshot: NSImage? // Make snapshot accessible to view
|
@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 captureManager = ScreenCapture() // For actual capture
|
||||||
private let monitorManager = ScreenCapture() // For monitoring
|
private let monitorManager = ScreenCapture() // For monitoring
|
||||||
private let boardDetector = BoardDetector()
|
private let pieceRecognizer: PieceRecognizer
|
||||||
|
private let boardDetector: BoardDetector
|
||||||
private var captureTask: Task<Void, Never>?
|
private var captureTask: Task<Void, Never>?
|
||||||
private var monitorTask: Task<Void, Never>?
|
private var monitorTask: Task<Void, Never>?
|
||||||
// Share CIContext to avoid creating too many Metal command queues
|
// Share CIContext to avoid creating too many Metal command queues
|
||||||
|
|
@ -31,6 +35,8 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
case boardDetectionFailed
|
case boardDetectionFailed
|
||||||
case noBoardDetected
|
case noBoardDetected
|
||||||
case snapshotFailed
|
case snapshotFailed
|
||||||
|
case recognitionFailed
|
||||||
|
case invalidPosition
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
|
|
@ -42,40 +48,61 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
return "No chess board detected"
|
return "No chess board detected"
|
||||||
case .snapshotFailed:
|
case .snapshotFailed:
|
||||||
return "Failed to take snapshot"
|
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 {
|
func takeSnapshot() async {
|
||||||
// Stop current capture
|
print("=== SCAN BUTTON PRESSED ===")
|
||||||
try? await captureManager.stopCapture()
|
|
||||||
|
|
||||||
// Start new capture without cursor
|
|
||||||
do {
|
do {
|
||||||
|
try await captureManager.stopCapture()
|
||||||
try await captureManager.startCapture(excludeCursor: true)
|
try await captureManager.startCapture(excludeCursor: true)
|
||||||
// Wait a brief moment for the capture to stabilize
|
|
||||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
||||||
|
|
||||||
if let currentImage = captureManager.getCurrentImage() {
|
if let currentImage = captureManager.getCurrentImage() {
|
||||||
// Process the image to get the cropped board
|
print("Captured image: \(currentImage.size)")
|
||||||
try await processImage(currentImage)
|
try await processImage(currentImage, analyzePosition: true)
|
||||||
|
|
||||||
// Store the cropped board as snapshot
|
|
||||||
if let boardImage = croppedBoardImage {
|
if let boardImage = croppedBoardImage {
|
||||||
latestSnapshot = boardImage
|
latestSnapshot = boardImage
|
||||||
snapshotTaken = true
|
snapshotTaken = true
|
||||||
|
|
||||||
|
if let position = currentPosition {
|
||||||
|
print("Successfully detected position")
|
||||||
} else {
|
} else {
|
||||||
captureError = .snapshotFailed
|
print("ERROR: Failed to detect position")
|
||||||
|
captureError = .recognitionFailed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
print("ERROR: Failed to crop board image")
|
||||||
captureError = .snapshotFailed
|
captureError = .snapshotFailed
|
||||||
|
currentPosition = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("ERROR: Failed to capture image")
|
||||||
|
captureError = .snapshotFailed
|
||||||
|
currentPosition = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart normal capture
|
|
||||||
try await captureManager.startCapture(excludeCursor: false)
|
try await captureManager.startCapture(excludeCursor: false)
|
||||||
} catch {
|
} catch {
|
||||||
|
print("ERROR: Snapshot failed - \(error)")
|
||||||
captureError = .snapshotFailed
|
captureError = .snapshotFailed
|
||||||
// Ensure we restart normal capture even if snapshot fails
|
currentPosition = nil
|
||||||
try? await captureManager.startCapture(excludeCursor: false)
|
try? await captureManager.startCapture(excludeCursor: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,6 +164,8 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
captureError = nil
|
captureError = nil
|
||||||
snapshotTaken = false
|
snapshotTaken = false
|
||||||
latestSnapshot = nil
|
latestSnapshot = nil
|
||||||
|
currentPosition = nil
|
||||||
|
recognitionConfidence = 0.0
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await captureManager.startCapture()
|
try await captureManager.startCapture()
|
||||||
|
|
@ -150,7 +179,7 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
captureLoop: while !Task.isCancelled {
|
captureLoop: while !Task.isCancelled {
|
||||||
do {
|
do {
|
||||||
if let image = captureManager.getCurrentImage() {
|
if let image = captureManager.getCurrentImage() {
|
||||||
try await processImage(image)
|
try await processImage(image, analyzePosition: false)
|
||||||
}
|
}
|
||||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
|
|
@ -182,6 +211,8 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
isBoardDetected = false
|
isBoardDetected = false
|
||||||
snapshotTaken = false
|
snapshotTaken = false
|
||||||
latestSnapshot = nil
|
latestSnapshot = nil
|
||||||
|
currentPosition = nil
|
||||||
|
recognitionConfidence = 0.0
|
||||||
|
|
||||||
// Clean up capture session
|
// Clean up capture session
|
||||||
Task {
|
Task {
|
||||||
|
|
@ -189,29 +220,47 @@ class ScreenCaptureViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processImage(_ image: NSImage) async throws {
|
private func processImage(_ image: NSImage, analyzePosition: Bool = false) async throws {
|
||||||
// Convert to CIImage for processing
|
|
||||||
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
||||||
|
print("ERROR: Failed to convert image for processing")
|
||||||
throw CaptureError.boardDetectionFailed
|
throw CaptureError.boardDetectionFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
let ciImage = CIImage(cgImage: cgImage)
|
let ciImage = CIImage(cgImage: cgImage)
|
||||||
|
|
||||||
// Detect board
|
|
||||||
if let boardRect = boardDetector.detectBoard(in: ciImage) {
|
if let boardRect = boardDetector.detectBoard(in: ciImage) {
|
||||||
// Board detected
|
|
||||||
isBoardDetected = true
|
isBoardDetected = true
|
||||||
self.detectedBoardRect = boardRect
|
self.detectedBoardRect = boardRect
|
||||||
|
|
||||||
// Crop board image
|
|
||||||
let croppedImage = ciImage.cropped(to: boardRect)
|
let croppedImage = ciImage.cropped(to: boardRect)
|
||||||
updateImages(ciImage: ciImage, croppedImage: croppedImage)
|
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 {
|
} else {
|
||||||
// No board detected
|
|
||||||
isBoardDetected = false
|
isBoardDetected = false
|
||||||
self.detectedBoardRect = nil
|
self.detectedBoardRect = nil
|
||||||
|
currentPosition = nil
|
||||||
|
recognitionConfidence = 0.0
|
||||||
|
|
||||||
// Only update the full capture image
|
|
||||||
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
|
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
|
||||||
self.capturedImage = NSImage(cgImage: cgImage, size: .zero)
|
self.capturedImage = NSImage(cgImage: cgImage, size: .zero)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1162
cline_docs/Info.txt
1162
cline_docs/Info.txt
File diff suppressed because it is too large
Load diff
|
|
@ -1,102 +1,29 @@
|
||||||
# Active Context
|
# Current Task
|
||||||
|
Working on chess piece recognition with the updated ML model that includes empty square detection.
|
||||||
|
|
||||||
## Current Status
|
# Recent Changes
|
||||||
- Screen capture module successfully implemented
|
1. Empty Square Detection:
|
||||||
- Window detection and capture working correctly
|
- Handle empty_dark/empty_light classes
|
||||||
- SwiftUI interface with capture controls functioning
|
- Exact class name matching
|
||||||
- Error handling system properly managing states
|
- Proper error handling
|
||||||
- Resource cleanup implemented
|
|
||||||
- Metal resource management optimized
|
|
||||||
- Automatic board detection and capture implemented
|
|
||||||
- Visual capture status indicator added
|
|
||||||
- Continuous board monitoring system implemented
|
|
||||||
- Auto-capture on game start/stop working successfully
|
|
||||||
- Manual snapshot system implemented with Scan button
|
|
||||||
- Snapshot preview display added below Chessboard Preview
|
|
||||||
- Cursor-free snapshot capture implemented
|
|
||||||
|
|
||||||
## Recent Changes
|
2. Position Validation:
|
||||||
1. Enhanced Manual Snapshot System:
|
- Allow moved pieces
|
||||||
- New cursor-free capture:
|
- Essential rules only
|
||||||
* Temporarily disables cursor during snapshot
|
- Piece count tracking
|
||||||
* Ensures clean board capture without mouse pointer
|
|
||||||
* Automatically restores cursor after snapshot
|
|
||||||
- Improved snapshot process:
|
|
||||||
* Stops current capture
|
|
||||||
* Takes cursor-free snapshot
|
|
||||||
* Processes board detection
|
|
||||||
* Restores normal capture
|
|
||||||
* Handles errors gracefully
|
|
||||||
- Snapshot visualization:
|
|
||||||
* Added preview area below Chessboard Preview
|
|
||||||
* Shows latest snapshot with visual feedback
|
|
||||||
* Clear indication when snapshot is taken
|
|
||||||
|
|
||||||
2. Implemented Continuous Board Monitoring:
|
3. Error Handling:
|
||||||
- Added separate monitoring and capture tasks:
|
- Better error messages
|
||||||
* Monitor constantly checks for chess boards (0.5s interval)
|
- Clear logging
|
||||||
* Capture processes frames when active (0.1s interval)
|
- Fixed optional unwrapping
|
||||||
- Auto-capture behavior working as expected:
|
|
||||||
* Starts monitoring when app launches
|
|
||||||
* Automatically starts capture when board appears
|
|
||||||
* Stops capture but continues monitoring when board disappears
|
|
||||||
* Successfully resumes capture when new game starts
|
|
||||||
- Performance characteristics:
|
|
||||||
* ~40% CPU usage during operation
|
|
||||||
* Stable memory management
|
|
||||||
* Responsive to game state changes
|
|
||||||
|
|
||||||
3. Enhanced Status Indication:
|
# Next Steps
|
||||||
- Visual status indicator shows capture state:
|
1. Recognition Tuning:
|
||||||
* Green: Actively capturing board
|
- Fine-tune empty square detection
|
||||||
* Yellow: Waiting for board
|
- Adjust confidence thresholds
|
||||||
* Gray: Not capturing
|
- Improve position validation
|
||||||
- Clear error messages for different states
|
|
||||||
- Automatic status updates based on board detection
|
|
||||||
|
|
||||||
4. Resource Management:
|
2. Model Training:
|
||||||
- Implemented shared CIContext pattern:
|
- Add more empty square examples
|
||||||
* Prevents command queue exhaustion
|
- Include different board styles
|
||||||
* Reduces Metal resource usage
|
- Improve piece variety
|
||||||
* Enables long-running captures
|
|
||||||
- Proper cleanup on task completion
|
|
||||||
- Efficient resource utilization
|
|
||||||
|
|
||||||
5. Improved Window Detection:
|
|
||||||
- Using SCShareableContent for window access
|
|
||||||
- Precise window identification:
|
|
||||||
* Exact bundle ID matching (com.chess.iphone)
|
|
||||||
* Window visibility verification (isOnScreen)
|
|
||||||
* Size validation (width > 100 && height > 100)
|
|
||||||
|
|
||||||
6. Error Handling:
|
|
||||||
- Improved error resilience:
|
|
||||||
* Continues monitoring even if capture stops
|
|
||||||
* Only stops on critical errors
|
|
||||||
* Shows error state without interrupting monitoring
|
|
||||||
- Clear error states
|
|
||||||
- Proper async/await usage
|
|
||||||
- Task cancellation management
|
|
||||||
- Thread-safe state updates
|
|
||||||
|
|
||||||
## Current Focus
|
|
||||||
1. Board Recognition:
|
|
||||||
- Process snapshot images
|
|
||||||
- Implement piece detection
|
|
||||||
- Extract board state
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
1. Implement board recognition:
|
|
||||||
- Process snapshot images
|
|
||||||
- Detect chess pieces
|
|
||||||
- Map board coordinates
|
|
||||||
- Validate positions
|
|
||||||
2. Add position analysis
|
|
||||||
3. Create move detection system
|
|
||||||
4. Implement visual overlay
|
|
||||||
5. Integrate Stockfish engine
|
|
||||||
|
|
||||||
## Known Issues
|
|
||||||
- Need to handle different chess.com themes
|
|
||||||
- Need to implement piece recognition
|
|
||||||
- Position analysis pending implementation
|
|
||||||
|
|
|
||||||
20
cline_docs/chessboard.txt
Normal file
20
cline_docs/chessboard.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
Key differences from recognition:
|
||||||
|
4. Some pieces missing from recognition
|
||||||
|
5. Some pieces misidentified
|
||||||
|
|
||||||
|
Recognition issues to fix:
|
||||||
|
1. Need to handle moved pieces (not just starting position)
|
||||||
|
2. Better empty square detection
|
||||||
|
3. Improve confidence thresholds
|
||||||
|
4. Validate complete position
|
||||||
138
cline_docs/error
138
cline_docs/error
|
|
@ -1,138 +0,0 @@
|
||||||
# Error Resolution Log
|
|
||||||
|
|
||||||
## Fixed Issues (2023)
|
|
||||||
|
|
||||||
### ScreenCaptureKit API Updates
|
|
||||||
1. SCContentFilter Initialization
|
|
||||||
- Fixed by using correct initializer and parameters:
|
|
||||||
```swift
|
|
||||||
SCContentFilter(display: display, excludingWindows: [])
|
|
||||||
```
|
|
||||||
- Using SCDisplay object directly (not displayID)
|
|
||||||
- Correct parameter name: excludingWindows
|
|
||||||
- Removed incorrect parameters (includingWindows/exceptingWindows)
|
|
||||||
|
|
||||||
2. Stream Output Type
|
|
||||||
- Fixed type inference issue by explicit declaration:
|
|
||||||
```swift
|
|
||||||
let outputType: SCStreamOutputType = .screen
|
|
||||||
try stream.addStreamOutput(self, type: outputType, ...)
|
|
||||||
```
|
|
||||||
- Ensures proper type resolution for .screen member
|
|
||||||
|
|
||||||
### Required Imports
|
|
||||||
- Added necessary framework imports:
|
|
||||||
* CoreMedia
|
|
||||||
* AVFoundation
|
|
||||||
* ScreenCaptureKit
|
|
||||||
* CoreGraphics
|
|
||||||
* AppKit
|
|
||||||
* Foundation
|
|
||||||
|
|
||||||
### Window Capture Strategy
|
|
||||||
1. Window Detection
|
|
||||||
- Precise window identification:
|
|
||||||
```swift
|
|
||||||
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
|
|
||||||
```
|
|
||||||
- Multiple validation checks:
|
|
||||||
* Exact bundle ID match
|
|
||||||
* Window is currently on screen
|
|
||||||
* Window has valid dimensions
|
|
||||||
- Handles iOS apps running on Mac properly
|
|
||||||
|
|
||||||
2. Capture Method
|
|
||||||
- Implemented continuous capture:
|
|
||||||
```swift
|
|
||||||
// Start once
|
|
||||||
try await screenCapture.startCapture()
|
|
||||||
|
|
||||||
// Process frames continuously
|
|
||||||
while !Task.isCancelled {
|
|
||||||
if let image = screenCapture.getCurrentImage() {
|
|
||||||
try await processImage(image)
|
|
||||||
}
|
|
||||||
try await Task.sleep(nanoseconds: 100_000_000)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- Maintains single active stream
|
|
||||||
- Eliminates capture flickering
|
|
||||||
- Proper cleanup on stop
|
|
||||||
|
|
||||||
### Thread Safety and Async Handling
|
|
||||||
1. Main Actor Isolation
|
|
||||||
- Added @MainActor to ViewModel class:
|
|
||||||
```swift
|
|
||||||
@MainActor
|
|
||||||
class ScreenCaptureViewModel: ObservableObject
|
|
||||||
```
|
|
||||||
- Ensures all @Published property updates happen on main thread
|
|
||||||
- Proper thread safety for SwiftUI bindings
|
|
||||||
|
|
||||||
2. Async Operation Handling
|
|
||||||
- Optimized async/await usage:
|
|
||||||
```swift
|
|
||||||
// Only use await for truly async operations
|
|
||||||
try await screenCapture.startCapture()
|
|
||||||
try await Task.sleep(nanoseconds: 100_000_000)
|
|
||||||
```
|
|
||||||
- Removed unnecessary await keywords:
|
|
||||||
* Non-async error handling
|
|
||||||
* UI state updates
|
|
||||||
* Image processing
|
|
||||||
- Proper @MainActor usage for thread safety
|
|
||||||
|
|
||||||
3. Task and Error Handling
|
|
||||||
- Improved task cancellation:
|
|
||||||
```swift
|
|
||||||
captureLoop: while !Task.isCancelled {
|
|
||||||
do {
|
|
||||||
// Process frame
|
|
||||||
} catch is CancellationError {
|
|
||||||
break captureLoop
|
|
||||||
} catch {
|
|
||||||
// Continue capturing on non-critical errors
|
|
||||||
await handleCaptureError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- Proper error recovery:
|
|
||||||
* Continues on board detection failures
|
|
||||||
* Shows error state without stopping
|
|
||||||
* Clears errors on successful detection
|
|
||||||
- Clean error state on stop:
|
|
||||||
```swift
|
|
||||||
captureError = nil // Clear any error when stopping
|
|
||||||
```
|
|
||||||
- Proper cleanup on task exit
|
|
||||||
|
|
||||||
## Technical Notes
|
|
||||||
- Resource Management:
|
|
||||||
* Shared CIContext to prevent command queue exhaustion:
|
|
||||||
```swift
|
|
||||||
private static let shared = CIContext()
|
|
||||||
private var context: CIContext { Self.shared }
|
|
||||||
```
|
|
||||||
* Prevents "Command queue creation failed" errors
|
|
||||||
* Reduces Metal resource usage
|
|
||||||
* Proper cleanup on task completion
|
|
||||||
|
|
||||||
- Using correct SCContentFilter API with proper parameter names
|
|
||||||
- Proper type safety throughout the implementation:
|
|
||||||
* Proper Vision framework result handling:
|
|
||||||
```swift
|
|
||||||
guard let observations = request.results,
|
|
||||||
!observations.isEmpty else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let bestObservation = observations[0]
|
|
||||||
```
|
|
||||||
* No unnecessary type casting
|
|
||||||
* Safe array access
|
|
||||||
* Proper optional handling
|
|
||||||
- Appropriate framework dependencies
|
|
||||||
- Follows current ScreenCaptureKit best practices
|
|
||||||
|
|
@ -1,199 +1,60 @@
|
||||||
# Product Context
|
# Product Overview
|
||||||
|
ChessPrism is a macOS application that captures and analyzes chess positions from the screen in real-time.
|
||||||
|
|
||||||
## Project Overview
|
## Core Features
|
||||||
ChessPrism is an advanced chess analysis tool that enhances the online chess experience by providing real-time analysis and move suggestions. It works by capturing and analyzing the chess board from popular chess websites and platforms.
|
1. Board Detection
|
||||||
|
- Automatic chessboard location
|
||||||
|
- Perspective and size handling
|
||||||
|
- Multi-board support planned
|
||||||
|
|
||||||
## Core Problems Solved
|
2. Piece Recognition
|
||||||
|
- ML-based piece classification
|
||||||
|
- Empty square detection
|
||||||
|
- Position-aware confidence adjustments
|
||||||
|
|
||||||
### Chess Analysis Accessibility
|
3. Position Analysis
|
||||||
- Makes professional-level chess analysis accessible during online play
|
- FEN string generation
|
||||||
- Provides real-time insights without manual position input
|
- Position validation
|
||||||
- Integrates seamlessly with existing chess platforms
|
- Move tracking (planned)
|
||||||
|
|
||||||
### Visual Recognition
|
|
||||||
- Accurately detects chess board from screen content
|
|
||||||
- Recognizes board coordinates and boundaries
|
|
||||||
- Handles various board themes and orientations
|
|
||||||
- Maintains accuracy during game play
|
|
||||||
- Provides clean board snapshots for analysis
|
|
||||||
|
|
||||||
### Real-time Processing
|
|
||||||
- Captures and processes screen content in real-time
|
|
||||||
- Provides immediate feedback and analysis
|
|
||||||
- Maintains performance during long sessions
|
|
||||||
- Supports manual snapshot capture for detailed analysis
|
|
||||||
|
|
||||||
## User Experience Goals
|
|
||||||
|
|
||||||
### Seamless Integration
|
|
||||||
1. Non-intrusive Operation
|
|
||||||
- Works with Chess.com desktop app
|
|
||||||
- Minimal setup requirements
|
|
||||||
- Automatic board detection and tracking
|
|
||||||
- Clean snapshot capture without cursor interference
|
|
||||||
|
|
||||||
2. Intuitive Interface
|
|
||||||
- Clear visualization of analysis
|
|
||||||
- Easy-to-understand suggestions
|
|
||||||
- Minimal user intervention required
|
|
||||||
- Visual feedback for capture states
|
|
||||||
- Manual snapshot control
|
|
||||||
|
|
||||||
### Reliable Detection
|
|
||||||
1. Board Recognition
|
|
||||||
- Two-phase detection strategy:
|
|
||||||
* Pattern recognition for known interfaces
|
|
||||||
* Coordinate-based fallback for reliability
|
|
||||||
- Proper coordinate system handling
|
|
||||||
- Consistent board capture across sessions
|
|
||||||
- High-quality snapshots for analysis
|
|
||||||
|
|
||||||
2. Position Analysis
|
|
||||||
- Accurate piece recognition (planned)
|
|
||||||
- Current position evaluation (planned)
|
|
||||||
- Move suggestion visualization (planned)
|
|
||||||
|
|
||||||
## Target Users
|
|
||||||
|
|
||||||
### Chess Players
|
|
||||||
- Amateur to intermediate players
|
|
||||||
- Chess.com desktop app users
|
|
||||||
- Players seeking to improve
|
|
||||||
|
|
||||||
### Use Cases
|
|
||||||
1. Learning
|
|
||||||
- Understanding position evaluation
|
|
||||||
- Learning from mistakes
|
|
||||||
- Exploring alternative moves
|
|
||||||
- Analyzing specific positions via snapshots
|
|
||||||
|
|
||||||
2. Analysis
|
|
||||||
- Real-time position assessment
|
|
||||||
- Move validation
|
|
||||||
- Strategic planning
|
|
||||||
- Detailed position study
|
|
||||||
|
|
||||||
## Product Requirements
|
|
||||||
|
|
||||||
### Essential Features
|
|
||||||
1. Board Detection (Current Focus)
|
|
||||||
- Accurate boundary recognition
|
|
||||||
- Full board capture
|
|
||||||
- Support for Chess.com desktop app
|
|
||||||
- Reliable coordinate transformations
|
|
||||||
- Clean snapshot capability
|
|
||||||
|
|
||||||
2. Position Analysis (Planned)
|
|
||||||
- Real-time evaluation
|
|
||||||
- Move suggestions
|
|
||||||
- Tactical opportunities
|
|
||||||
|
|
||||||
3. User Interface
|
|
||||||
- Analysis overlay
|
|
||||||
- Control panel
|
|
||||||
- Settings management
|
|
||||||
- Snapshot controls
|
|
||||||
- Visual status indicators
|
|
||||||
|
|
||||||
### Quality Standards
|
|
||||||
1. Accuracy
|
|
||||||
- Reliable board detection
|
|
||||||
- Complete board capture
|
|
||||||
- Precise coordinate handling
|
|
||||||
- Clean snapshots without artifacts
|
|
||||||
|
|
||||||
2. Performance
|
|
||||||
- Real-time processing
|
|
||||||
- Minimal resource usage
|
|
||||||
- Stable operation
|
|
||||||
- Efficient snapshot handling
|
|
||||||
|
|
||||||
3. Usability
|
|
||||||
- Intuitive controls
|
|
||||||
- Clear feedback
|
|
||||||
- Minimal setup
|
|
||||||
- Simple snapshot workflow
|
|
||||||
|
|
||||||
## Success Metrics
|
|
||||||
|
|
||||||
### Technical Metrics
|
|
||||||
- Board detection accuracy rate
|
|
||||||
- Full board capture success rate
|
|
||||||
- Processing speed per frame
|
|
||||||
- Error recovery rate
|
|
||||||
- Snapshot quality assessment
|
|
||||||
|
|
||||||
### User Metrics
|
|
||||||
- Setup success rate
|
|
||||||
- Analysis accuracy
|
|
||||||
- User engagement time
|
|
||||||
- Feature utilization
|
|
||||||
- Snapshot usage patterns
|
|
||||||
|
|
||||||
## Current Challenges
|
## Current Challenges
|
||||||
|
|
||||||
### Board Detection
|
### Recognition Features
|
||||||
1. Coordinate Systems
|
1. Empty Square Detection
|
||||||
- Vision framework (bottom-left origin)
|
- Explicit empty_dark/empty_light classes
|
||||||
- NSImage/CGImage (bottom-left origin)
|
- Direct square color recognition
|
||||||
- SwiftUI (top-left origin)
|
- High confidence classification
|
||||||
- Proper transformations between systems
|
|
||||||
|
|
||||||
2. Detection Accuracy
|
2. Piece Recognition
|
||||||
- Full board capture
|
- Accurate piece type detection
|
||||||
- Consistent positioning
|
- Color differentiation
|
||||||
- Reliable boundaries
|
- Position-aware confidence
|
||||||
- Clean snapshots
|
|
||||||
|
|
||||||
### Next Steps
|
3. Performance Optimization
|
||||||
1. Refine board detection
|
- Fast-path empty detection
|
||||||
- Improve coordinate handling
|
- Efficient classification flow
|
||||||
- Ensure full board capture
|
- Resource-aware processing
|
||||||
- Validate transformations
|
|
||||||
- Optimize snapshot quality
|
|
||||||
|
|
||||||
2. Move to position analysis
|
## Future Improvements
|
||||||
- Piece recognition
|
|
||||||
- Position evaluation
|
|
||||||
- Move suggestions
|
|
||||||
|
|
||||||
## Future Enhancements
|
### Short Term
|
||||||
|
1. Recognition Enhancement
|
||||||
|
- Fine-tune confidence thresholds
|
||||||
|
- Validate square colors
|
||||||
|
- Improve error messages
|
||||||
|
|
||||||
### Planned Features
|
2. Position Analysis
|
||||||
1. Advanced Analysis
|
- Move validation
|
||||||
- Deep position evaluation
|
- Game state tracking
|
||||||
- Opening recognition
|
- Historical context
|
||||||
- Endgame tablebases
|
|
||||||
- Position comparison from snapshots
|
|
||||||
|
|
||||||
2. Learning Tools
|
### Long Term
|
||||||
- Mistake analysis
|
1. Advanced Features
|
||||||
- Improvement suggestions
|
- Move detection
|
||||||
- Progress tracking
|
- Game recording
|
||||||
- Position database from snapshots
|
- Multiple board styles
|
||||||
|
|
||||||
3. Customization
|
2. User Experience
|
||||||
- Analysis depth control
|
- Confidence visualization
|
||||||
- Visual preference settings
|
- Manual corrections
|
||||||
- Platform-specific optimizations
|
- Custom training
|
||||||
- Snapshot management options
|
|
||||||
|
|
||||||
## Product Roadmap
|
|
||||||
|
|
||||||
### Current Phase
|
|
||||||
- Core board detection system
|
|
||||||
- Coordinate system handling
|
|
||||||
- Basic user interface
|
|
||||||
- Manual snapshot system
|
|
||||||
|
|
||||||
### Next Phase
|
|
||||||
- Position analysis
|
|
||||||
- Move suggestion system
|
|
||||||
- Visual overlay implementation
|
|
||||||
- Enhanced snapshot analysis
|
|
||||||
|
|
||||||
### Future Phase
|
|
||||||
- Advanced analysis features
|
|
||||||
- Learning tools integration
|
|
||||||
- Customization options
|
|
||||||
- Snapshot database and comparison tools
|
|
||||||
|
|
|
||||||
|
|
@ -1,228 +1,81 @@
|
||||||
# System Patterns
|
# System Architecture
|
||||||
|
|
||||||
## Window Capture Architecture
|
## Piece Recognition Pipeline
|
||||||
|
1. Board Detection
|
||||||
|
- VNDetectRectanglesRequest for board location
|
||||||
|
- Aspect ratio and size validation
|
||||||
|
- Square extraction with equal dimensions
|
||||||
|
|
||||||
### Window Detection Pattern
|
2. Image Preprocessing
|
||||||
1. SCShareableContent Access
|
- Contrast and brightness adjustment
|
||||||
- Async/await pattern for content access
|
- Unsharp mask for edge enhancement
|
||||||
- Proper error propagation
|
- Consistent image scaling
|
||||||
- Permission handling
|
|
||||||
|
|
||||||
2. Window Identification
|
3. ML Classification
|
||||||
- Multiple validation criteria:
|
- CoreML model prediction
|
||||||
```swift
|
- Confidence score analysis
|
||||||
let bundleID = window.owningApplication?.bundleIdentifier ?? ""
|
- Position-based adjustments
|
||||||
let isChessApp = bundleID == "com.chess.iphone"
|
|
||||||
let hasValidSize = window.frame.width > 100 && window.frame.height > 100
|
|
||||||
return isChessApp && window.isOnScreen && hasValidSize
|
|
||||||
```
|
|
||||||
- Fail-fast approach with guard statements
|
|
||||||
- Clear error states
|
|
||||||
|
|
||||||
### Capture System Pattern
|
## Recognition Patterns
|
||||||
1. Stream Configuration
|
|
||||||
- Window-specific capture setup
|
|
||||||
- Frame dimension matching
|
|
||||||
- Proper delegate handling
|
|
||||||
- Cursor visibility control:
|
|
||||||
* Configurable cursor display
|
|
||||||
* Clean snapshot support
|
|
||||||
* State preservation
|
|
||||||
|
|
||||||
2. Frame Processing
|
### Square Classification
|
||||||
- Main thread safety for UI updates
|
1. Empty Square Detection
|
||||||
- Efficient image conversion pipeline
|
- Exact empty_dark/empty_light matching
|
||||||
- Resource cleanup
|
- High confidence threshold (>0.9)
|
||||||
|
- Early detection and return
|
||||||
|
|
||||||
### Snapshot System Pattern
|
2. Piece Recognition
|
||||||
1. Cursor-Free Capture
|
- Strict label format validation
|
||||||
- Temporary capture session:
|
- Position-based confidence adjustment
|
||||||
* Disables cursor visibility
|
- Piece count tracking
|
||||||
* Takes clean snapshot
|
|
||||||
* Restores normal capture
|
|
||||||
- Error handling:
|
|
||||||
* Session cleanup
|
|
||||||
* State recovery
|
|
||||||
* Capture restoration
|
|
||||||
|
|
||||||
2. Process Flow
|
3. Error Prevention
|
||||||
- Stop current capture
|
- Empty square validation
|
||||||
- Start cursor-free capture
|
- Label format checking
|
||||||
- Wait for stabilization
|
- Position rule enforcement
|
||||||
- Take snapshot
|
|
||||||
- Process image
|
|
||||||
- Restore normal capture
|
|
||||||
|
|
||||||
### Error Handling Pattern
|
### Classification Flow
|
||||||
1. Task Management
|
1. Input Validation
|
||||||
- Proper cancellation points
|
- Image dimensions check
|
||||||
- Clean state management
|
- Model availability check
|
||||||
- Resource cleanup
|
- Configuration setup
|
||||||
|
|
||||||
2. Error States
|
2. Square Analysis
|
||||||
- Clear error types
|
- Empty square check first
|
||||||
- User-friendly messages
|
- Piece classification second
|
||||||
- State recovery
|
- Position validation last
|
||||||
|
|
||||||
## UI Architecture
|
3. Confidence Checks
|
||||||
|
- Empty squares: >0.9
|
||||||
|
- Pieces: >0.98 with >5.0 ratio
|
||||||
|
- Position adjustments
|
||||||
|
|
||||||
### MVVM Implementation
|
4. Error Handling
|
||||||
1. ViewModel
|
- Clear error messages
|
||||||
- @MainActor for thread safety
|
- Detailed logging
|
||||||
- Published properties for state
|
- Safe fallbacks
|
||||||
- Clear separation of concerns
|
|
||||||
|
|
||||||
2. View Layer
|
## Validation Patterns
|
||||||
- SwiftUI declarative UI
|
1. Piece Count Rules
|
||||||
- State-driven updates
|
- Maximum 1: king, queen
|
||||||
- Error presentation
|
- Maximum 2: rooks, bishops, knights
|
||||||
|
- Maximum 8: pawns
|
||||||
|
- Track by color and type
|
||||||
|
|
||||||
### Async Operations
|
2. Position Rules
|
||||||
1. Task Management
|
- Kings: not on opponent's back rank
|
||||||
- Structured concurrency
|
- Pawns: no backward movement
|
||||||
- Proper cancellation
|
- All pieces: within board bounds
|
||||||
- State synchronization
|
- All pieces: valid movement patterns
|
||||||
|
|
||||||
2. State Updates
|
3. Piece Tracking
|
||||||
- Main thread safety
|
- Maximum piece counts
|
||||||
- Clear state transitions
|
- Color-specific tracking
|
||||||
- Error recovery
|
- Total position validation
|
||||||
|
- Captured piece limits
|
||||||
|
|
||||||
## Core Architecture
|
4. Recognition Flow
|
||||||
|
- Empty square detection first
|
||||||
### Resource Management Patterns
|
- Piece classification second
|
||||||
1. Shared CIContext Pattern
|
- Position validation last
|
||||||
- Static shared instance:
|
- Clear error reporting
|
||||||
```swift
|
|
||||||
private static let shared = CIContext()
|
|
||||||
private var context: CIContext { Self.shared }
|
|
||||||
```
|
|
||||||
- Benefits:
|
|
||||||
* Prevents Metal command queue exhaustion
|
|
||||||
* Reduces resource overhead
|
|
||||||
* Enables long-running captures
|
|
||||||
- Implementation:
|
|
||||||
* Used in BoardDetector and ViewModel
|
|
||||||
* Proper cleanup on task completion
|
|
||||||
* Thread-safe access
|
|
||||||
|
|
||||||
### Screen Capture System
|
|
||||||
- Uses ScreenCaptureKit for efficient screen capture
|
|
||||||
- Implements SCStreamOutput protocol for frame processing
|
|
||||||
- Handles capture session lifecycle and cleanup
|
|
||||||
- Manages permissions and error handling
|
|
||||||
- Optimized resource usage
|
|
||||||
- Configurable cursor visibility
|
|
||||||
|
|
||||||
### Board Detection System
|
|
||||||
Two implemented approaches:
|
|
||||||
|
|
||||||
1. Pattern Recognition Approach (Primary)
|
|
||||||
- Rectangle detection with Vision framework
|
|
||||||
- Aspect ratio-based filtering (0.3-0.5 for taller rectangles)
|
|
||||||
- Size-based filtering (0.4 minimum for larger areas)
|
|
||||||
- Single observation for precision
|
|
||||||
- Board extraction from upper portion
|
|
||||||
- Width-based square calculation
|
|
||||||
|
|
||||||
2. Coordinate Detection (Fallback)
|
|
||||||
- Text recognition for board coordinates
|
|
||||||
- Rectangle detection with Vision framework
|
|
||||||
- Grid-based validation
|
|
||||||
- Coordinate-based refinement
|
|
||||||
|
|
||||||
3. Common Infrastructure
|
|
||||||
- Asynchronous frame processing
|
|
||||||
- Dedicated processing queue
|
|
||||||
- Efficient memory management
|
|
||||||
- Performance monitoring
|
|
||||||
|
|
||||||
### Coordinate Systems
|
|
||||||
- Vision framework: Bottom-left origin (0,0)
|
|
||||||
- NSImage/CGImage: Bottom-left origin (0,0)
|
|
||||||
- SwiftUI: Top-left origin (0,0)
|
|
||||||
- Transformations needed between systems:
|
|
||||||
1. Vision → Screen: Flip Y coordinate
|
|
||||||
2. Screen → Image: Direct mapping
|
|
||||||
3. Image → View: SwiftUI handles automatically
|
|
||||||
|
|
||||||
### Notification System
|
|
||||||
- Uses NotificationCenter for event propagation
|
|
||||||
- Key notifications:
|
|
||||||
- boardDetected: Sends detected board rectangle and confidence score
|
|
||||||
- captureStateChanged: Updates capture status
|
|
||||||
- capturedFrame: Delivers processed frames
|
|
||||||
- boardCoordinatesDetected: Reports coordinate detection
|
|
||||||
- detectionStats: Reports performance metrics
|
|
||||||
|
|
||||||
## Design Patterns
|
|
||||||
|
|
||||||
### MVVM Architecture
|
|
||||||
- ScreenCapture: Model layer handling capture logic
|
|
||||||
- ScreenCaptureViewModel: View model managing UI state
|
|
||||||
- ContentView: SwiftUI view for user interface
|
|
||||||
|
|
||||||
### Observer Pattern
|
|
||||||
- NotificationCenter for loose coupling
|
|
||||||
- Enables modular component communication
|
|
||||||
- Supports async event handling
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- Custom ScreenCaptureError enum
|
|
||||||
- Comprehensive error cases
|
|
||||||
- Proper error propagation
|
|
||||||
|
|
||||||
## Technical Decisions
|
|
||||||
|
|
||||||
### Vision Framework
|
|
||||||
- Primary tool for board detection
|
|
||||||
- Provides rectangle and text detection
|
|
||||||
- Handles various board orientations
|
|
||||||
- Requires coordinate system transformation
|
|
||||||
|
|
||||||
### Pattern Recognition
|
|
||||||
- Focus on larger detection areas
|
|
||||||
- Use width as reference measurement
|
|
||||||
- Extract square board from top portion
|
|
||||||
- Maintain aspect ratio constraints
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
- Dedicated dispatch queue for frame processing
|
|
||||||
- Efficient memory management
|
|
||||||
- Proper resource cleanup
|
|
||||||
- Single observation optimization
|
|
||||||
|
|
||||||
## Future Patterns
|
|
||||||
|
|
||||||
### Planned Implementations
|
|
||||||
1. Board Position Analysis
|
|
||||||
- ML model integration
|
|
||||||
- Piece detection system
|
|
||||||
- Position validation
|
|
||||||
|
|
||||||
2. Move Analysis
|
|
||||||
- Stockfish integration
|
|
||||||
- Real-time evaluation
|
|
||||||
- Visual overlay system
|
|
||||||
|
|
||||||
3. State Management
|
|
||||||
- Game state tracking
|
|
||||||
- Move history
|
|
||||||
- Analysis persistence
|
|
||||||
|
|
||||||
## Testing Patterns
|
|
||||||
|
|
||||||
### Unit Testing
|
|
||||||
- ScreenCapture functionality
|
|
||||||
- Board detection accuracy
|
|
||||||
- Coordinate recognition
|
|
||||||
|
|
||||||
### Integration Testing
|
|
||||||
- End-to-end capture workflow
|
|
||||||
- Vision framework integration
|
|
||||||
- Notification system
|
|
||||||
|
|
||||||
### UI Testing
|
|
||||||
- SwiftUI interface validation
|
|
||||||
- User interaction flows
|
|
||||||
- Error state handling
|
|
||||||
|
|
|
||||||
|
|
@ -1,247 +1,69 @@
|
||||||
# Technical Context
|
# Technologies Used
|
||||||
|
|
||||||
|
## Core ML & Vision
|
||||||
|
- ChessPieceClassifier.mlmodel for piece recognition
|
||||||
|
- VNCoreMLModel for image classification
|
||||||
|
- Vision framework for board detection
|
||||||
|
|
||||||
|
## Image Processing
|
||||||
|
- CoreImage for preprocessing
|
||||||
|
- CIColorControls and CIUnsharpMask filters
|
||||||
|
- CGImage for image manipulation
|
||||||
|
|
||||||
## Development Environment
|
## Development Environment
|
||||||
- macOS development platform
|
- Xcode for Swift development
|
||||||
- Xcode IDE
|
- Create ML for model training
|
||||||
- SwiftUI for user interface
|
- SwiftUI for UI components
|
||||||
- Swift 5.x language features
|
|
||||||
|
|
||||||
## Core Technologies
|
# Technical Constraints
|
||||||
|
|
||||||
### Metal Resource Management
|
## ML Model Capabilities
|
||||||
- Shared CIContext pattern:
|
1. Classification Types:
|
||||||
* Static shared instance to prevent command queue exhaustion
|
- Pieces: pawn, rook, knight, bishop, queen, king
|
||||||
* Used across BoardDetector and ViewModel
|
- Colors: black, white
|
||||||
* Proper cleanup and resource management
|
- Empty squares: dark, light
|
||||||
- Performance considerations:
|
- Label formats: color_piece, empty_color
|
||||||
* Reduced Metal command queue creation
|
|
||||||
* Efficient resource utilization
|
|
||||||
* Support for long-running captures
|
|
||||||
|
|
||||||
### ScreenCaptureKit
|
2. Recognition Features:
|
||||||
- System framework for screen capture
|
- Multi-class classification
|
||||||
- Implemented features:
|
- Per-class confidence scores
|
||||||
* Window detection using SCShareableContent
|
- Position-aware validation
|
||||||
* iOS app window capture support
|
- Piece count tracking
|
||||||
* Real-time frame capture
|
|
||||||
* Proper error handling
|
|
||||||
* Configurable cursor visibility
|
|
||||||
- Key components:
|
|
||||||
* SCShareableContent: Window and display access
|
|
||||||
* SCContentFilter: Window-specific capture
|
|
||||||
* SCStream: Frame capture management
|
|
||||||
* SCStreamOutput: Frame processing
|
|
||||||
* SCStreamConfiguration: Capture settings including cursor control
|
|
||||||
|
|
||||||
### Vision Framework (Planned)
|
## Processing Requirements
|
||||||
- Will be used for board and coordinate detection
|
1. Image Requirements:
|
||||||
- Key components to implement:
|
- Square dimensions (1:1 ±10%)
|
||||||
* VNRecognizeTextRequest: Chess coordinate detection
|
- Non-zero dimensions
|
||||||
* VNDetectRectanglesRequest: Board boundary detection
|
- Center-cropped squares
|
||||||
- Planned configuration:
|
- Clear piece visibility
|
||||||
* Text recognition level: accurate
|
|
||||||
* Language correction: disabled
|
|
||||||
* Rectangle aspect ratio: 0.3-0.5
|
|
||||||
* Minimum size: 0.4
|
|
||||||
* Maximum observations: 1
|
|
||||||
|
|
||||||
### Coordinate Systems
|
2. Recognition Rules:
|
||||||
1. Vision Framework
|
- Empty squares: exact class match with >0.9 confidence
|
||||||
- Origin: Bottom-left (0,0)
|
- Pieces: strict format with >0.98 confidence
|
||||||
- Y-axis: Upward positive
|
- Separation ratio: >5.0 between predictions
|
||||||
- Normalized coordinates (0-1)
|
- Position validation: essential rules only
|
||||||
- Used in: VNRectangleObservation, VNTextObservation
|
|
||||||
|
|
||||||
2. NSImage/CGImage
|
3. Error Prevention:
|
||||||
- Origin: Bottom-left (0,0)
|
- Early empty square detection
|
||||||
- Y-axis: Upward positive
|
- Strict label validation
|
||||||
- Pixel coordinates
|
- Safe optional handling
|
||||||
- Used in: Image cropping, processing
|
- Clear error messages
|
||||||
|
|
||||||
3. SwiftUI
|
## Performance Considerations
|
||||||
- Origin: Top-left (0,0)
|
1. Processing Flow:
|
||||||
- Y-axis: Downward positive
|
- Early validation checks
|
||||||
- Point coordinates
|
- Fast empty square detection
|
||||||
- Used in: View layout, rendering
|
- Efficient error handling
|
||||||
|
- Quick rejection paths
|
||||||
|
|
||||||
4. Transformations
|
2. Resource Optimization:
|
||||||
- Vision → Screen: Flip Y coordinate
|
- GPU acceleration for ML
|
||||||
- Screen → Image: Scale to pixel coordinates
|
- Minimal preprocessing
|
||||||
- Image → View: SwiftUI handles automatically
|
- Optimized validation
|
||||||
|
- Efficient logging
|
||||||
|
|
||||||
### SwiftUI
|
# Development Setup
|
||||||
- Modern declarative UI framework
|
1. Clone repository
|
||||||
- Handles view lifecycle
|
2. Open ChessPrism.xcodeproj
|
||||||
- State management via @Published properties
|
3. Build and run on macOS
|
||||||
- Environmental object propagation
|
4. Model at ChessPrism/ChessPieceClassifier.mlmodel
|
||||||
|
|
||||||
## Technical Constraints
|
|
||||||
|
|
||||||
### Window Capture System
|
|
||||||
1. Window Detection
|
|
||||||
- Using SCShareableContent for window access
|
|
||||||
- Multiple validation criteria:
|
|
||||||
* Bundle ID verification
|
|
||||||
* Window visibility check
|
|
||||||
* Size validation
|
|
||||||
- Error handling for missing windows
|
|
||||||
|
|
||||||
2. Frame Capture
|
|
||||||
- Window-specific capture configuration
|
|
||||||
- Frame dimension matching
|
|
||||||
- Proper delegate handling
|
|
||||||
- Resource cleanup
|
|
||||||
- Cursor visibility control:
|
|
||||||
* Configurable via SCStreamConfiguration
|
|
||||||
* State preservation between captures
|
|
||||||
* Clean snapshot support
|
|
||||||
|
|
||||||
3. Performance
|
|
||||||
- Main thread safety for UI updates
|
|
||||||
- Efficient image conversion
|
|
||||||
- Proper task cancellation
|
|
||||||
- Memory management
|
|
||||||
- Shared CIContext for Metal efficiency
|
|
||||||
|
|
||||||
4. Error Handling
|
|
||||||
- Clear error types
|
|
||||||
- User-friendly messages
|
|
||||||
- State recovery
|
|
||||||
- Resource cleanup
|
|
||||||
|
|
||||||
### System Requirements
|
|
||||||
- macOS 12.0 or later
|
|
||||||
- Screen Capture permissions
|
|
||||||
- Sufficient CPU for real-time processing
|
|
||||||
- Adequate memory for frame buffering
|
|
||||||
- Metal-capable GPU for image processing
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### Internal
|
|
||||||
- ScreenCapture.swift: Core capture logic
|
|
||||||
- ScreenCaptureViewModel.swift: State management
|
|
||||||
- BoardDetector.swift: Pattern recognition
|
|
||||||
- ContentView.swift: User interface
|
|
||||||
|
|
||||||
### External
|
|
||||||
- ScreenCaptureKit.framework
|
|
||||||
- Vision.framework
|
|
||||||
- SwiftUI.framework
|
|
||||||
- CoreImage.framework
|
|
||||||
- Metal.framework (via CIContext)
|
|
||||||
|
|
||||||
## Development Guidelines
|
|
||||||
|
|
||||||
### Code Organization
|
|
||||||
- MVVM architecture
|
|
||||||
- Protocol-oriented design
|
|
||||||
- Clear separation of concerns
|
|
||||||
- Comprehensive error handling
|
|
||||||
- Resource sharing patterns
|
|
||||||
|
|
||||||
### Performance Optimization
|
|
||||||
- Shared CIContext for Metal efficiency
|
|
||||||
- Efficient frame processing
|
|
||||||
- Memory management
|
|
||||||
- Resource cleanup
|
|
||||||
- Background queue usage
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- Custom error types
|
|
||||||
- Comprehensive error cases
|
|
||||||
- User-friendly error messages
|
|
||||||
- Proper error propagation
|
|
||||||
|
|
||||||
## Testing Requirements
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
- Board detection accuracy
|
|
||||||
- Coordinate transformations
|
|
||||||
- Error handling
|
|
||||||
- State management
|
|
||||||
- Resource management
|
|
||||||
- Cursor control functionality
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
- End-to-end workflows
|
|
||||||
- Component interaction
|
|
||||||
- Event propagation
|
|
||||||
- Resource sharing
|
|
||||||
- Snapshot system
|
|
||||||
|
|
||||||
### UI Tests
|
|
||||||
- User interaction flows
|
|
||||||
- Error state handling
|
|
||||||
- Visual feedback
|
|
||||||
- Performance monitoring
|
|
||||||
- Snapshot visualization
|
|
||||||
|
|
||||||
## Documentation Requirements
|
|
||||||
|
|
||||||
### Code Documentation
|
|
||||||
- Function documentation
|
|
||||||
- Parameter descriptions
|
|
||||||
- Return value documentation
|
|
||||||
- Error documentation
|
|
||||||
- Resource usage documentation
|
|
||||||
|
|
||||||
### Architecture Documentation
|
|
||||||
- System overview
|
|
||||||
- Component interaction
|
|
||||||
- Data flow diagrams
|
|
||||||
- State management
|
|
||||||
- Resource management patterns
|
|
||||||
|
|
||||||
## Current Challenges
|
|
||||||
|
|
||||||
### Resource Management
|
|
||||||
1. Metal Efficiency
|
|
||||||
- Command queue management
|
|
||||||
- Shared context patterns
|
|
||||||
- Resource cleanup
|
|
||||||
- Performance monitoring
|
|
||||||
|
|
||||||
2. Memory Usage
|
|
||||||
- Frame buffer management
|
|
||||||
- Image processing optimization
|
|
||||||
- Resource pooling
|
|
||||||
- Cleanup strategies
|
|
||||||
|
|
||||||
### Board Detection
|
|
||||||
1. Full Capture
|
|
||||||
- Complete board visibility
|
|
||||||
- Proper positioning
|
|
||||||
- Consistent results
|
|
||||||
- Coordinate accuracy
|
|
||||||
|
|
||||||
2. Performance
|
|
||||||
- Processing efficiency
|
|
||||||
- Memory usage
|
|
||||||
- Resource management
|
|
||||||
- Error recovery
|
|
||||||
|
|
||||||
## Future Considerations
|
|
||||||
|
|
||||||
### Planned Features
|
|
||||||
1. ML Model Integration
|
|
||||||
- Piece detection
|
|
||||||
- Position analysis
|
|
||||||
- Move validation
|
|
||||||
|
|
||||||
2. Engine Integration
|
|
||||||
- Stockfish analysis
|
|
||||||
- Move evaluation
|
|
||||||
- Position scoring
|
|
||||||
|
|
||||||
3. Visual Overlay
|
|
||||||
- Move suggestions
|
|
||||||
- Analysis visualization
|
|
||||||
- Interactive elements
|
|
||||||
|
|
||||||
### Technical Debt
|
|
||||||
- Refactor coordinate handling
|
|
||||||
- Optimize frame processing
|
|
||||||
- Improve error recovery
|
|
||||||
- Enhanced permission handling
|
|
||||||
- Resource usage monitoring
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue