diff --git a/doc/pieces_states.drawio b/doc/pieces_states.drawio new file mode 100644 index 0000000000000000000000000000000000000000..6ce0fb5e1c55bfe4573b331a1d7be491856a6633 --- /dev/null +++ b/doc/pieces_states.drawio @@ -0,0 +1 @@ +<mxfile host="app.diagrams.net" modified="2021-10-01T17:00:24.120Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.83 Safari/537.36" etag="X7S_uXiCLWks2JBqEszE" version="15.4.1" type="device"><diagram name="Page-1" id="58cdce13-f638-feb5-8d6f-7d28b1aa9fa0">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</diagram></mxfile> \ No newline at end of file diff --git a/doc/pieces_states.drawio.svg b/doc/pieces_states.drawio.svg new file mode 100644 index 0000000000000000000000000000000000000000..120cd2f06f7036f3503e2ef4720c91fcd103bbbc --- /dev/null +++ b/doc/pieces_states.drawio.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Do not edit this file with editors other than diagrams.net --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="501px" height="362px" viewBox="-0.5 -0.5 501 362" content="<mxfile host="app.diagrams.net" modified="2021-10-01T17:01:49.302Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.83 Safari/537.36" etag="2gnUP50G2iihb7g0QLF6" version="15.4.1" type="device"><diagram name="Page-1" id="58cdce13-f638-feb5-8d6f-7d28b1aa9fa0">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</diagram></mxfile>" style="background-color: rgb(255, 255, 255);"><defs/><g><ellipse cx="15" cy="70" rx="11" ry="11" fill="#000000" stroke="#ff0000" transform="rotate(90,15,70)" pointer-events="all"/><path d="M 215 70 L 393.63 70" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 398.88 70 L 391.88 73.5 L 393.63 70 L 391.88 66.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 70px; margin-left: 231px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">U</div></div></div></foreignObject><text x="231" y="74" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">U</text></switch></g><path d="M 30 70 L 158.63 70" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 163.88 70 L 156.88 73.5 L 158.63 70 L 156.88 66.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 70px; margin-left: 98px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">Init</div></div></div></foreignObject><text x="98" y="74" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">Init</text></switch></g><ellipse cx="190" cy="70" rx="25" ry="25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 70px; margin-left: 166px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">S0</div></div></div></foreignObject><text x="190" y="74" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S0</text></switch></g><ellipse cx="425" cy="70" rx="25" ry="25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 70px; margin-left: 401px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">S2</div></div></div></foreignObject><text x="425" y="74" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S2</text></switch></g><path d="M 190 95 L 190 173.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 190 178.88 L 186.5 171.88 L 190 173.63 L 193.5 171.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 110px; margin-left: 190px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">ON</div></div></div></foreignObject><text x="190" y="113" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ON</text></switch></g><ellipse cx="425" cy="205" rx="25" ry="25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 205px; margin-left: 401px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">S4</div></div></div></foreignObject><text x="425" y="209" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S4</text></switch></g><ellipse cx="315" cy="295" rx="25" ry="25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 295px; margin-left: 291px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">S3</div></div></div></foreignObject><text x="315" y="299" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S3</text></switch></g><path d="M 207.68 87.68 L 397.39 190.1" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 402.01 192.6 L 394.19 192.35 L 397.39 190.1 L 397.52 186.19 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 105px; margin-left: 224px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">OFF</div></div></div></foreignObject><text x="224" y="108" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">OFF</text></switch></g><path d="M 425 95 L 425 173.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 425 178.88 L 421.5 171.88 L 425 173.63 L 428.5 171.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 110px; margin-left: 425px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">OFF</div></div></div></foreignObject><text x="425" y="113" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">OFF</text></switch></g><path d="M 403.33 82.46 L 217.2 189.38" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 212.65 192 L 216.98 185.48 L 217.2 189.38 L 220.46 191.55 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 96px; margin-left: 380px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">ON</div></div></div></foreignObject><text x="380" y="99" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ON</text></switch></g><path d="M 334.37 279.2 L 400.72 224.87" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 404.79 221.54 L 401.59 228.68 L 400.72 224.87 L 397.15 223.27 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 270px; margin-left: 350px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">OFF<font color="#ff0000">/1</font></div></div></div></foreignObject><text x="350" y="273" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">OFF/1</text></switch></g><path d="M 440 50 Q 440 0 425 0 Q 410 0 410 43.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 410 48.88 L 406.5 41.88 L 410 43.63 L 413.5 41.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 34px; margin-left: 441px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">U</div></div></div></foreignObject><text x="441" y="37" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">U</text></switch></g><ellipse cx="190" cy="205" rx="25" ry="25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 205px; margin-left: 166px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">S1</div></div></div></foreignObject><text x="190" y="209" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">S1</text></switch></g><path d="M 170 190 Q 120 190 120 205 Q 120 220 163.63 220" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 168.88 220 L 161.88 223.5 L 163.63 220 L 161.88 216.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 189px; margin-left: 154px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">ON</div></div></div></foreignObject><text x="154" y="193" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ON</text></switch></g><path d="M 300 315 Q 300 360 315 360 Q 330 360 330 321.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 330 316.12 L 333.5 323.12 L 330 321.37 L 326.5 323.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 330px; margin-left: 299px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">U</div></div></div></foreignObject><text x="299" y="334" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">U</text></switch></g><path d="M 291.65 286.07 Q 250 270 235 260 Q 220 250 207.44 231.07" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 204.54 226.7 L 211.32 230.6 L 207.44 231.07 L 205.49 234.47 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 282px; margin-left: 276px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(255, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"><font color="#000000">ON</font></div></div></div></foreignObject><text x="276" y="286" fill="#FF0000" font-family="Helvetica" font-size="11px" text-anchor="middle">ON</text></switch></g><path d="M 445 190 Q 500 190 500 205 Q 500 220 451.37 220" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 446.12 220 L 453.12 216.5 L 451.37 220 L 453.12 223.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 191px; margin-left: 470px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">OFF,U</div></div></div></foreignObject><text x="470" y="195" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">OFF,U</text></switch></g><path d="M 400.63 210.56 Q 360 220 325 220 Q 290 220 221.03 209.6" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 215.84 208.82 L 223.28 206.4 L 221.03 209.6 L 222.24 213.32 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 215px; margin-left: 384px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">ON</div></div></div></foreignObject><text x="384" y="218" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">ON</text></switch></g><path d="M 214.67 200.95 Q 280 190 315 190 Q 350 190 394.23 198.9" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 399.38 199.94 L 391.82 201.99 L 394.23 198.9 L 393.21 195.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 198px; margin-left: 234px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(255, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"><font color="#000000">OFF</font>/1</div></div></div></foreignObject><text x="234" y="201" fill="#FF0000" font-family="Helvetica" font-size="11px" text-anchor="middle">OFF/1</text></switch></g><path d="M 212.38 216.13 Q 280 250 295.75 270.23" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 298.98 274.37 L 291.92 271 L 295.75 270.23 L 297.44 266.7 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 231px; margin-left: 231px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(255, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;"><font color="#000000">U</font></div></div></div></foreignObject><text x="231" y="234" fill="#FF0000" font-family="Helvetica" font-size="11px" text-anchor="middle">U</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Viewer does not support full SVG 1.1</text></a></switch></svg> \ No newline at end of file diff --git a/memory_lib/__init__.py b/memory_lib/__init__.py index 106adc018de1241a8e58914a4346b2c338cbdd2b..bf6ae896df9a13a83cf2eda8a2d4b81a5fef2201 100644 --- a/memory_lib/__init__.py +++ b/memory_lib/__init__.py @@ -1 +1 @@ -from .memory import MemoryABM, MemoryAruco +from .memory import MemoryABM, MemoryArucoHalf, MemoryArucoFull diff --git a/memory_lib/board.py b/memory_lib/board.py index b44294b70aff05c9f0e50336e7a1f35de9d49045..70b4e7288b4f4b59a6535430145a419eeb9dba8e 100644 --- a/memory_lib/board.py +++ b/memory_lib/board.py @@ -1,441 +1,75 @@ -import cv2 as cv -import numpy as np - -from abc import ABC, abstractmethod +import sys from typing import Tuple, Optional, List, Callable -from .cv_utils import ( - draw_line, - draw_point, - average_color, - get_roi_around_point, - MotionDetector, -) -from .geometry import ( - get_segment_size, - line_eq_from_points, - get_parallel_eq, - get_line_intersection_point, - get_rect_center, - get_segment_center, - get_segement_point_at_dist, -) -from .model import Board, Point, LineEq, Piece, Rect - - -class BoardFactory(ABC): - @abstractmethod - def get_board(self) -> Board: - ... - - -class ABMExtractor(ABC): - @abstractmethod - def get_a_b_m(self, img: np.ndarray) -> Optional[Tuple[Point, Point, Point]]: - ... - - -class PieceVisibilityDetector(ABC): - @abstractmethod - def is_visible(self, img, piece_pos: Point, piece_index) -> bool: - ... - - @abstractmethod - def train(self, board: Board, img: np.ndarray) -> None: - ... - - -class ABMBoardFactory(BoardFactory): - def __init__(self, a: Point, b: Point, m: Point, img: np.ndarray): - self.a = a - self.b = b - self.m = m - # TODO: no need img here, only for debug - self.img = img +from .model import Board, Point, LineEq, Piece, Rect, PieceState +from .piece_state import PiecesStatesTracker - def get_board(self) -> Board: - """ Create a Board object from from A, B and M borad points """ - a = self.a - b = self.b - m = self.m - # F is [AB] center - f = Point((a.x + b.x) // 2, (a.y + b.y) // 2) - ab_eq = line_eq_from_points(a, b) +class PieceTakenTrigger: + """Call 'trigger' function when after a piece is taken""" - # TODO: check - # if ab_m == math.nan or ab_b == math.nan: - # return - - # TODO: debug only - # draw_line(self.img, ab_eq) - - # FM line - fm_eq = line_eq_from_points(f, m) - - # TODO: check - # if fm_m == math.inf or fm_b == math.inf: - # return - - # TODO: debug only - # draw_line(self.img, fm_eq) - fm_s = get_segment_size(f, m) - - # Approximate E, based on FM size. - # First approximate DC line, based on FM size - dc_eq = get_parallel_eq(ab_eq, Point(0, int(a.y - fm_s * 1.5))) - - # TODO: debug only - # draw_line(self.img, dc_eq) - - # Then e is on DC, FM intersection - e = get_line_intersection_point(fm_eq, dc_eq) + def __init__(self, trigger: Callable[[int], None]): + self.trigger = trigger + self.averager = BoardStateAverager() + self.piece_state_tracker = PiecesStatesTracker(trigger) - # Compute the 4 parallels: A1B1, A2B2, A3B3, A4B4 - a1, b1 = self._compute_parallel(a, b, ab_eq, e, f, 0.75, 1 * (fm_s / 10)) - a2, b2 = self._compute_parallel(a, b, ab_eq, e, f, 0.48, 3 * (fm_s / 10)) - a3, b3 = self._compute_parallel(a, b, ab_eq, e, f, 0.25, 4.5 * (fm_s / 10)) - a4, b4 = self._compute_parallel(a, b, ab_eq, e, f, 0.07, 6 * (fm_s / 10)) + def add_board(self, board: Board) -> Board: + new_board = self.averager.add_board(board) + self.piece_state_tracker.update(new_board) + return new_board - # Compoute the pieces position from the above lines - pieces = ( - *self._compute_piece(a1, b1), - *self._compute_piece(a2, b2), - *self._compute_piece(a3, b3), - *self._compute_piece(a4, b4), - ) - # TODO: debug only - debut_print = ( - (a, "A"), - (b, "B"), - (f, "F"), - (e, "E"), - (a1, "A1"), - (b1, "B1"), - (a2, "A2"), - (b2, "B2"), - (a3, "A3"), - (b3, "B3"), - (a4, "A4"), - (b4, "B4"), - ) - # for point, name in debut_print: - # draw_point(self.img, point, name, (0, 255, 0)) - # for idx, piece in enumerate(pieces): - # draw_point(img, piece, f"P{idx}", (0, 0, 255)) - # cv.imshow("IMG", img) +class BoardStateAverager: + """Moving average for board states""" + def __init__(self, window_size: int = 15) -> None: + self.window_size = window_size + self.last_boards: List[Board] = [] - return Board(pieces) + def add_board(self, board: Board) -> Board: + if len(self.last_boards) > self.window_size: + self.last_boards.pop(0) + self.last_boards.append(board) + states_by_pieces = self._states_by_pieces(self.last_boards) + average_state_by_pieces = [self._average_state(states) for states in states_by_pieces] + out_board = Board.from_board(board) + for idx, _ in enumerate(out_board.pieces): + out_board.pieces[idx].state = average_state_by_pieces[idx] + return out_board @staticmethod - def _compute_parallel( - a: Point, - b: Point, - ab_eq: LineEq, - e: Point, - f: Point, - height_reduce_ratio: float, - width_reduce_ratio: float, - ) -> Tuple[Point, Point]: - """ used to find A1B1, A2B2, A3B3, A4B4 from AB and EF""" - m1 = Point( - int(e.x + height_reduce_ratio * (f.x - e.x)), - int(e.y + height_reduce_ratio * (f.y - e.y)), - ) - a1b1_eq = get_parallel_eq(ab_eq, m1) - left_point = Point( - int(a.x + width_reduce_ratio), - int(a1b1_eq.m * (a.x + width_reduce_ratio) + a1b1_eq.b), - ) - right_point = Point( - int(b.x - width_reduce_ratio), - int(a1b1_eq.m * (b.x - width_reduce_ratio) + a1b1_eq.b), - ) - return left_point, right_point + def _average_state(states: List[PieceState]) -> PieceState: + """Get the most present state in a list""" + u = states.count(PieceState.UNKNOWN), PieceState.UNKNOWN + on = states.count(PieceState.ON_BOARD), PieceState.ON_BOARD + off = states.count(PieceState.OFF_BOARD), PieceState.OFF_BOARD + return max(u, on, off, key=lambda t: t[0])[1] @staticmethod - def _compute_piece(left_point: Point, right_point: Point) -> Tuple[Piece, ...]: - """ find the 4 pieces from a horizontal line """ - return ( - Piece( - Point( - int(left_point.x + 0.1 * (right_point.x - left_point.x)), - int(left_point.y + 0.1 * (right_point.y - left_point.y)), - ) - ), - Piece( - Point( - int(left_point.x + 0.35 * (right_point.x - left_point.x)), - int(left_point.y + 0.35 * (right_point.y - left_point.y)), - ) - ), - Piece( - Point( - int(left_point.x + 0.65 * (right_point.x - left_point.x)), - int(left_point.y + 0.65 * (right_point.y - left_point.y)), - ) - ), - Piece( - Point( - int(left_point.x + 0.9 * (right_point.x - left_point.x)), - int(left_point.y + 0.9 * (right_point.y - left_point.y)), - ) - ), - ) - - -class ArucoABMExtractor(ABMExtractor): - """Uses Aruco marker to extract the board A, B and M points - - Aruco marker ids: - 0 for point A - 1 for point B - 2 for point M - """ - - def __init__(self): - self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_50) - self.aruco_params = cv.aruco.DetectorParameters_create() - - def get_a_b_m(self, img: np.ndarray) -> Optional[Tuple[Point, Point, Point]]: - """ If all 3 markers are found, returns the 3 Points A, B and M """ - corners, ids, rejected = cv.aruco.detectMarkers( - img, self.aruco_dict, parameters=self.aruco_params - ) - - # TODO: debug - # cv.aruco.drawDetectedMarkers(img, corners) - - markers = {} - if ids is None: - return None - - for i in range(len(ids)): - rect_val = ((int(x), int(y)) for x, y in corners[i][0].tolist()) - rect = Rect.from_corners(*(Point.from_tuple(t) for t in rect_val)) - markers[ids[i][0]] = get_rect_center(rect) - - a = markers.get(0) - b = markers.get(1) - m = markers.get(2) - - if a and b and m and (a != b): - return a, b, m - return None - - -class LightnessPieceVisibilityDetector(PieceVisibilityDetector): - """ Primitive detector based on lightness of average pixels around the piece""" - - def __init__(self, threshold=80): - self.threshold = threshold - - def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index) -> bool: - roi = get_roi_around_point(img, piece_pos, 3) - if roi.shape != (6, 6, 3): - return False - avg_color = average_color(roi) - return avg_color[2] > self.threshold - - def train(self, board: Board, img: np.ndarray) -> None: - ... - - -class AverageColorPieceVisibilityDetector(PieceVisibilityDetector): - """ Piece detector based on average pixels color around the piece""" - - def __init__(self): - self.pieces_colors: List[Tuple[int, int, int]] = [] - self.board_color: Tuple[int, int, int] = (0, 0, 0) - - def train(self, board: Board, img: np.ndarray) -> None: - """ Store an avraged color value for every pieces in the board """ - if not self.pieces_colors: - self.pieces_colors = [ - average_color(get_roi_around_point(img, piece.postion)) - for piece in board.pieces - ] - # get the socle color from a point below M - middle = get_segment_center( - board.pieces[5].postion, board.pieces[6].postion - ) - self.board_color = average_color(get_roi_around_point(img, middle)) - else: - for idx, piece in enumerate(board.pieces): - c1 = self.pieces_colors[idx] - c2 = average_color(get_roi_around_point(img, piece.postion)) - self.pieces_colors[idx] = self._avg_colors(c1, c2) - middle = get_segment_center( - board.pieces[5].postion, board.pieces[6].postion - ) - self.board_color = self._avg_colors( - self.board_color, average_color(get_roi_around_point(img, middle)) - ) - - def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index: int) -> bool: - # naive color distance detection, TODO: uses device independent color spaces? in_range? - - # Test if piece color is far from piece and close to board_color - c1 = self.pieces_colors[piece_index] - c2 = average_color(get_roi_around_point(img, piece_pos)) - m1 = self.board_color - m2 = average_color(get_roi_around_point(img, piece_pos)) - return ( - abs(m1[0] - m2[0]) < 100 - and abs(m1[1] - m2[1]) < 100 - and abs(m1[2] - m2[2]) < 100 - ) and ( - abs(c1[0] - c2[0]) > 60 - or abs(c1[1] - c2[1]) > 60 - or abs(c1[2] - c2[2]) > 60 - ) - - @staticmethod - def _avg_colors( - c1: Tuple[int, int, int], c2: Tuple[int, int, int] - ) -> Tuple[int, int, int]: - return (c1[0] + c2[0]) // 2, (c1[1] + c2[1]) // 2, (c1[2] + c2[2]) // 2 - - -class AverageColorAndMovementPieceVisibilityDetector(PieceVisibilityDetector): - """ Piece detector based on average pixels color around the piece, compute only no moving objects""" - - def __init__(self, same_frame_counter_max: int = 5): - """ - :param same_frame_counter_max: how many different frames before considering a movement + def _states_by_pieces(boards: List[Board]) -> list[list[PieceState]]: + # TODO: Improve perf by storing directly the states, not the boards in the fifo + """For every pieces, get the last X states + + res = [ + [...state_at_t-3, state_at_t-2, state_at_t-1], # piece 0 + [...state_at_t-3, state_at_t-2, state_at_t-1], # piece 1 + ... + ] """ - self.average_color_detector = AverageColorPieceVisibilityDetector() - self.same_frame_counter_max = same_frame_counter_max - self.motion_detectors = [MotionDetector() for _ in range(16)] - - def train(self, board: Board, img: np.ndarray) -> None: - self.average_color_detector.train(board, img) - - def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index: int) -> bool: - roi = cv.cvtColor( - get_roi_around_point(img, piece_pos, radius=10), cv.COLOR_BGR2GRAY - ) - - # TODO: debug only - # if piece_index == 5: - # print(self.motion_detectors[piece_index].same_frame_counter) - # cv.imshow("roi", roi) - # if (self.motion_detectors[piece_index]._last_frame is not None): - # cv.imshow("last", self.motion_detectors[piece_index]._last_frame) - - self.motion_detectors[piece_index].add(roi) - if ( - self.motion_detectors[piece_index].same_frame_counter - > self.same_frame_counter_max - ): - return self.average_color_detector.is_visible(img, piece_pos, piece_index) - return False - - -class ArucoPieceVisibilityDetector: - """ Piece detector based Aruco marker - - TODO: - for now this detector does not work as a PieceVisibilityDetector - This detector directly send a board with updated visibility - """ - - def __init__(self): - self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250) - self.aruco_params = cv.aruco.DetectorParameters_create() - self.top_left: Point = Point(-1, -1) - self.top_right: Point = Point(-1, -1) - self.bottom_left: Point = Point(-1, -1) - self.bottom_right: Point = Point(-1, -1) - - def train(self, img: np.ndarray) -> None: - """ Store the position of the 4 markers below the the 4 pieces in the corners""" - corners, ids, rejected = cv.aruco.detectMarkers( - img, self.aruco_dict, parameters=self.aruco_params - ) - - if ids is not None: - all_markers = [] - for i in range(len(ids)): - r = ((int(x), int(y)) for x, y in corners[i][0].tolist()) - rect = Rect.from_corners(*(Point.from_tuple(t) for t in r)) - center = get_rect_center(rect) - all_markers.append(center) - - # we have the 4 corners - if len(all_markers) == 4: - vertical_sort = sorted(all_markers, key=lambda point: point.y) - top_markers = vertical_sort[:2] - bottom_markers = vertical_sort[2:] - self.top_left = min(top_markers, key=lambda point: point.x) - self.top_right = max(top_markers, key=lambda point: point.x) - self.bottom_left = min(bottom_markers, key=lambda point: point.x) - self.bottom_right = max(bottom_markers, key=lambda point: point.x) - - def get_board_with_visibility(self, img: np.ndarray) -> Board: - """Compute a new board from the 4 corners stored during training, with all pieces visibility""" - board = self._board_from_corners() - - corners, ids, rejected = cv.aruco.detectMarkers( - img, self.aruco_dict, parameters=self.aruco_params - ) - - if ids is not None: - all_markers = [] - for i in range(len(ids)): - r = ((int(x), int(y)) for x, y in corners[i][0].tolist()) - rect = Rect.from_corners(*(Point.from_tuple(t) for t in r)) - center = get_rect_center(rect) - all_markers.append(center) - - for marker in all_markers: - closest_piece_idx = np.argmin( - [get_segment_size(marker, piece.postion) for piece in board.pieces] - ) - board.pieces[closest_piece_idx].is_visible = True - - return board - - def _board_from_corners(self) -> Board: - left_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_left, self.top_left) - right_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_right, self.top_right) - row_0 = self._four_pieces_from_two_ends_horizontal(left_pieces[0].postion, right_pieces[0].postion) - row_1 = self._four_pieces_from_two_ends_horizontal(left_pieces[1].postion, right_pieces[1].postion) - row_2 = self._four_pieces_from_two_ends_horizontal(left_pieces[2].postion, right_pieces[2].postion) - row_3 = self._four_pieces_from_two_ends_horizontal(left_pieces[3].postion, right_pieces[3].postion) - return Board(row_0 + row_1 + row_2 + row_3) - - @staticmethod - def _four_pieces_from_two_ends_vertical(p1: Point, p2: Point) -> Tuple[Piece, ...]: - """Get 4 pieces from 2 ends pieces""" - return ( - Piece(p1), - Piece(get_segement_point_at_dist(p1, p2, 0.45)), - Piece(get_segement_point_at_dist(p2, p1, 0.20)), - Piece(p2), - ) - - @staticmethod - def _four_pieces_from_two_ends_horizontal(p1: Point, p2: Point) -> Tuple[Piece, ...]: - """Get 4 pieces from 2 ends pieces""" - return ( - Piece(p1), - Piece(get_segement_point_at_dist(p1, p2, 0.33)), - Piece(get_segement_point_at_dist(p2, p1, 0.33)), - Piece(p2), - ) + boards_pieces = [board.pieces for board in boards] + transposed = [elem for elem in zip(*boards_pieces)] + res = [] + for last_pieces_for_one_piece in transposed: + construct = [] + for piece in last_pieces_for_one_piece: + construct.append(piece.state) + res.append(construct) + return res class PieceTakenDetectionEdgesTrigger: - """Call 'trigger' function when after a piece is taken - - It waits for 'rising_count' times the same piece detection state before triggering when we take a piece - It waits for 'falling_count' times the same piece detection state before triggering when we put back a piece - - TODO: replace this average impl with a counter to increase perf - """ + """ WARNING: deprectated: replaced with PieceTakenTrigger""" def __init__(self, trigger: Callable[[int], None], rising_count: int = 5, falling_count: int = 5): + print(self.__doc__, file=sys.stderr) self.trigger = trigger self.rising_count = rising_count self.falling_count = falling_count @@ -477,3 +111,5 @@ class PieceTakenDetectionEdgesTrigger: # print([int(b.pieces[idx].is_visible) for b in self.last_boards]) return next_out_board + + diff --git a/memory_lib/cv_utils.py b/memory_lib/cv_utils.py index afcf5fd418d10feaf91af46a02222f41a99f6012..b6ec89386b457acd5b3c10610d549d12b655642f 100644 --- a/memory_lib/cv_utils.py +++ b/memory_lib/cv_utils.py @@ -93,32 +93,3 @@ def draw_rect(img: np.ndarray, rect: Rect, color=(255, 0, 0)): """ Draw rectangle rect on img """ x, y, w, h = rect.to_tuple() cv.rectangle(img, (x, y), (x + w, y + h), color) - - -class MotionDetector: - """ Compute how many frames are the same """ - - def __init__(self, pixels_count_to_diff: int = 3): - """ - :param pixels_count_to_diff: When you diff to frame, how many pixels can change to consider same/different frame - """ - self.pixels_count_to_diff = pixels_count_to_diff - self._same_frame_counter = 0 - self._last_frame = None - self._dbg_last_frame_img = None - - def add(self, img: np.ndarray) -> None: - self._dbg_last_frame_img = img.copy() - - canny = cv.Canny(img, 0, 150) - if self._last_frame is not None: - diff = cv.absdiff(canny, self._last_frame) - if cv.countNonZero(diff) < self.pixels_count_to_diff: - self._same_frame_counter += 1 - else: - self._same_frame_counter = 0 - self._last_frame = canny.copy() - - @property - def same_frame_counter(self): - return self._same_frame_counter diff --git a/memory_lib/detectors/board_detectors.py b/memory_lib/detectors/board_detectors.py new file mode 100644 index 0000000000000000000000000000000000000000..f09ab23e7cfad75e093ea1bfd08d23129624ff36 --- /dev/null +++ b/memory_lib/detectors/board_detectors.py @@ -0,0 +1,54 @@ +import cv2 as cv +import numpy as np + +from abc import ABC, abstractmethod +from typing import Tuple, Optional + +from memory_lib.geometry import get_rect_center +from memory_lib.model import Point, Rect + + +class ABMExtractor(ABC): + @abstractmethod + def get_a_b_m(self, img: np.ndarray) -> Optional[Tuple[Point, Point, Point]]: + ... + + +class ArucoABMExtractor(ABMExtractor): + """Uses Aruco marker to extract the board A, B and M points + + Aruco marker ids: + 0 for point A + 1 for point B + 2 for point M + """ + + def __init__(self): + self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_50) + self.aruco_params = cv.aruco.DetectorParameters_create() + + def get_a_b_m(self, img: np.ndarray) -> Optional[Tuple[Point, Point, Point]]: + """ If all 3 markers are found, returns the 3 Points A, B and M """ + corners, ids, rejected = cv.aruco.detectMarkers( + img, self.aruco_dict, parameters=self.aruco_params + ) + + # TODO: debug + # cv.aruco.drawDetectedMarkers(img, corners) + + markers = {} + if ids is None: + return None + + for i in range(len(ids)): + rect_val = ((int(x), int(y)) for x, y in corners[i][0].tolist()) + rect = Rect.from_corners(*(Point.from_tuple(t) for t in rect_val)) + markers[ids[i][0]] = get_rect_center(rect) + + a = markers.get(0) + b = markers.get(1) + m = markers.get(2) + + if a and b and m and (a != b): + return a, b, m + return None diff --git a/memory_lib/detectors/board_factory.py b/memory_lib/detectors/board_factory.py new file mode 100644 index 0000000000000000000000000000000000000000..fc4ca5e50529a955afeeb8b170827d4b8a1656f3 --- /dev/null +++ b/memory_lib/detectors/board_factory.py @@ -0,0 +1,153 @@ +from abc import ABC, abstractmethod +from typing import Tuple + +import numpy as np + +from memory_lib.geometry import line_eq_from_points, get_segment_size, get_parallel_eq, get_line_intersection_point +from memory_lib.model import Board, Point, LineEq, Piece + + +class BoardFactory(ABC): + @abstractmethod + def get_board(self) -> Board: + ... + + +class ABMBoardFactory(BoardFactory): + def __init__(self, a: Point, b: Point, m: Point, img: np.ndarray): + self.a = a + self.b = b + self.m = m + # TODO: no need img here, only for debug + self.img = img + + def get_board(self) -> Board: + """ Create a Board object from from A, B and M borad points """ + a = self.a + b = self.b + m = self.m + + # F is [AB] center + f = Point((a.x + b.x) // 2, (a.y + b.y) // 2) + ab_eq = line_eq_from_points(a, b) + + # TODO: check + # if ab_m == math.nan or ab_b == math.nan: + # return + + # TODO: debug only + # draw_line(self.img, ab_eq) + + # FM line + fm_eq = line_eq_from_points(f, m) + + # TODO: check + # if fm_m == math.inf or fm_b == math.inf: + # return + + # TODO: debug only + # draw_line(self.img, fm_eq) + fm_s = get_segment_size(f, m) + + # Approximate E, based on FM size. + # First approximate DC line, based on FM size + dc_eq = get_parallel_eq(ab_eq, Point(0, int(a.y - fm_s * 1.5))) + + # TODO: debug only + # draw_line(self.img, dc_eq) + + # Then e is on DC, FM intersection + e = get_line_intersection_point(fm_eq, dc_eq) + + # Compute the 4 parallels: A1B1, A2B2, A3B3, A4B4 + a1, b1 = self._compute_parallel(a, b, ab_eq, e, f, 0.75, 1 * (fm_s / 10)) + a2, b2 = self._compute_parallel(a, b, ab_eq, e, f, 0.48, 3 * (fm_s / 10)) + a3, b3 = self._compute_parallel(a, b, ab_eq, e, f, 0.25, 4.5 * (fm_s / 10)) + a4, b4 = self._compute_parallel(a, b, ab_eq, e, f, 0.07, 6 * (fm_s / 10)) + + # Compoute the pieces position from the above lines + pieces = ( + *self._compute_piece(a1, b1), + *self._compute_piece(a2, b2), + *self._compute_piece(a3, b3), + *self._compute_piece(a4, b4), + ) + + # TODO: debug only + debut_print = ( + (a, "A"), + (b, "B"), + (f, "F"), + (e, "E"), + (a1, "A1"), + (b1, "B1"), + (a2, "A2"), + (b2, "B2"), + (a3, "A3"), + (b3, "B3"), + (a4, "A4"), + (b4, "B4"), + ) + # for point, name in debut_print: + # draw_point(self.img, point, name, (0, 255, 0)) + # for idx, piece in enumerate(pieces): + # draw_point(img, piece, f"P{idx}", (0, 0, 255)) + # cv.imshow("IMG", img) + + return Board(pieces) + + @staticmethod + def _compute_parallel( + a: Point, + b: Point, + ab_eq: LineEq, + e: Point, + f: Point, + height_reduce_ratio: float, + width_reduce_ratio: float, + ) -> Tuple[Point, Point]: + """ used to find A1B1, A2B2, A3B3, A4B4 from AB and EF""" + m1 = Point( + int(e.x + height_reduce_ratio * (f.x - e.x)), + int(e.y + height_reduce_ratio * (f.y - e.y)), + ) + a1b1_eq = get_parallel_eq(ab_eq, m1) + left_point = Point( + int(a.x + width_reduce_ratio), + int(a1b1_eq.m * (a.x + width_reduce_ratio) + a1b1_eq.b), + ) + right_point = Point( + int(b.x - width_reduce_ratio), + int(a1b1_eq.m * (b.x - width_reduce_ratio) + a1b1_eq.b), + ) + return left_point, right_point + + @staticmethod + def _compute_piece(left_point: Point, right_point: Point) -> Tuple[Piece, ...]: + """ find the 4 pieces from a horizontal line """ + return ( + Piece( + Point( + int(left_point.x + 0.1 * (right_point.x - left_point.x)), + int(left_point.y + 0.1 * (right_point.y - left_point.y)), + ) + ), + Piece( + Point( + int(left_point.x + 0.35 * (right_point.x - left_point.x)), + int(left_point.y + 0.35 * (right_point.y - left_point.y)), + ) + ), + Piece( + Point( + int(left_point.x + 0.65 * (right_point.x - left_point.x)), + int(left_point.y + 0.65 * (right_point.y - left_point.y)), + ) + ), + Piece( + Point( + int(left_point.x + 0.9 * (right_point.x - left_point.x)), + int(left_point.y + 0.9 * (right_point.y - left_point.y)), + ) + ), + ) diff --git a/memory_lib/detectors/misc_detectors.py b/memory_lib/detectors/misc_detectors.py new file mode 100644 index 0000000000000000000000000000000000000000..b017bed0115c8a3d4a0b4bb14ef464d26cc4faf8 --- /dev/null +++ b/memory_lib/detectors/misc_detectors.py @@ -0,0 +1,31 @@ +import cv2 as cv +import numpy as np + + +class MotionDetector: + """ Compute how many frames are the same """ + + def __init__(self, pixels_count_to_diff: int = 3): + """ + :param pixels_count_to_diff: When you diff to frame, how many pixels can change to consider same/different frame + """ + self.pixels_count_to_diff = pixels_count_to_diff + self._same_frame_counter = 0 + self._last_frame = None + self._dbg_last_frame_img = None + + def add(self, img: np.ndarray) -> None: + self._dbg_last_frame_img = img.copy() + + canny = cv.Canny(img, 0, 150) + if self._last_frame is not None: + diff = cv.absdiff(canny, self._last_frame) + if cv.countNonZero(diff) < self.pixels_count_to_diff: + self._same_frame_counter += 1 + else: + self._same_frame_counter = 0 + self._last_frame = canny.copy() + + @property + def same_frame_counter(self): + return self._same_frame_counter diff --git a/memory_lib/detectors/piece_state_detectors.py b/memory_lib/detectors/piece_state_detectors.py new file mode 100644 index 0000000000000000000000000000000000000000..5479bef6d9b8ecc7c469e53ce01864d8bc24154c --- /dev/null +++ b/memory_lib/detectors/piece_state_detectors.py @@ -0,0 +1,352 @@ +from abc import ABC, abstractmethod +from typing import List, Tuple +import cv2 as cv + +import numpy as np + +from memory_lib.cv_utils import get_roi_around_point, average_color +from memory_lib.detectors.misc_detectors import MotionDetector +from memory_lib.geometry import get_segment_center, get_rect_center, get_segement_point_at_dist, get_segment_size +from memory_lib.model import Point, Board, Rect, Piece, PieceState + + +class PieceStateDetector(ABC): + """ Use to extract if a piece is visible or not""" + @abstractmethod + def is_visible(self, img, piece_pos: Point, piece_index) -> bool: + ... + + @abstractmethod + def train(self, board: Board, img: np.ndarray) -> None: + ... + + +class PieceStateExtractor(ABC): + """ Use to get a board with updated pieces states""" + @abstractmethod + def get_board_with_visibility(self, img: np.ndarray) -> Board: + ... + + @abstractmethod + def train(self, img: np.ndarray) -> None: + ... + + @abstractmethod + def is_ready(self) -> bool: + ... + + +class LightnessPieceStateDetector(PieceStateDetector): + """ Primitive detector based on lightness of average pixels around the piece""" + + def __init__(self, threshold=80): + self.threshold = threshold + + def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index) -> bool: + roi = get_roi_around_point(img, piece_pos, 3) + if roi.shape != (6, 6, 3): + return False + avg_color = average_color(roi) + return avg_color[2] > self.threshold + + def train(self, board: Board, img: np.ndarray) -> None: + ... + + +class AverageColorPieceStateDetector(PieceStateDetector): + """ Piece detector based on average pixels color around the piece""" + + def __init__(self): + self.pieces_colors: List[Tuple[int, int, int]] = [] + self.board_color: Tuple[int, int, int] = (0, 0, 0) + + def train(self, board: Board, img: np.ndarray) -> None: + """ Store an avraged color value for every pieces in the board """ + if not self.pieces_colors: + self.pieces_colors = [ + average_color(get_roi_around_point(img, piece.postion)) + for piece in board.pieces + ] + # get the socle color from a point below M + middle = get_segment_center( + board.pieces[5].postion, board.pieces[6].postion + ) + self.board_color = average_color(get_roi_around_point(img, middle)) + else: + for idx, piece in enumerate(board.pieces): + c1 = self.pieces_colors[idx] + c2 = average_color(get_roi_around_point(img, piece.postion)) + self.pieces_colors[idx] = self._avg_colors(c1, c2) + middle = get_segment_center( + board.pieces[5].postion, board.pieces[6].postion + ) + self.board_color = self._avg_colors( + self.board_color, average_color(get_roi_around_point(img, middle)) + ) + + def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index: int) -> bool: + # naive color distance detection, TODO: uses device independent color spaces? in_range? + + # Test if piece color is far from piece and close to board_color + c1 = self.pieces_colors[piece_index] + c2 = average_color(get_roi_around_point(img, piece_pos)) + m1 = self.board_color + m2 = average_color(get_roi_around_point(img, piece_pos)) + return ( + abs(m1[0] - m2[0]) < 100 + and abs(m1[1] - m2[1]) < 100 + and abs(m1[2] - m2[2]) < 100 + ) and ( + abs(c1[0] - c2[0]) > 60 + or abs(c1[1] - c2[1]) > 60 + or abs(c1[2] - c2[2]) > 60 + ) + + @staticmethod + def _avg_colors( + c1: Tuple[int, int, int], c2: Tuple[int, int, int] + ) -> Tuple[int, int, int]: + return (c1[0] + c2[0]) // 2, (c1[1] + c2[1]) // 2, (c1[2] + c2[2]) // 2 + + +class AverageColorAndMovementPieceVisibilityDetector(PieceStateDetector): + """ Piece detector based on average pixels color around the piece, compute only no moving objects""" + + def __init__(self, same_frame_counter_max: int = 5): + """ + :param same_frame_counter_max: how many different frames before considering a movement + """ + self.average_color_detector = AverageColorPieceStateDetector() + self.same_frame_counter_max = same_frame_counter_max + self.motion_detectors = [MotionDetector() for _ in range(16)] + + def train(self, board: Board, img: np.ndarray) -> None: + self.average_color_detector.train(board, img) + + def is_visible(self, img: np.ndarray, piece_pos: Point, piece_index: int) -> bool: + roi = cv.cvtColor( + get_roi_around_point(img, piece_pos, radius=10), cv.COLOR_BGR2GRAY + ) + + # TODO: debug only + # if piece_index == 5: + # print(self.motion_detectors[piece_index].same_frame_counter) + # cv.imshow("roi", roi) + # if (self.motion_detectors[piece_index]._last_frame is not None): + # cv.imshow("last", self.motion_detectors[piece_index]._last_frame) + + self.motion_detectors[piece_index].add(roi) + if ( + self.motion_detectors[piece_index].same_frame_counter + > self.same_frame_counter_max + ): + return self.average_color_detector.is_visible(img, piece_pos, piece_index) + return False + + +class ArucoHalfPieceStateExtractor(PieceStateExtractor): + """ Piece detector based on Aruco marker below every piece + + TODO: + for now this detector does not work as a PieceVisibilityDetector + This detector directly send a board with updated visibility + """ + + def __init__(self): + self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250) + self.aruco_params = cv.aruco.DetectorParameters_create() + self.top_left: Point = Point(-1, -1) + self.top_right: Point = Point(-1, -1) + self.bottom_left: Point = Point(-1, -1) + self.bottom_right: Point = Point(-1, -1) + + def train(self, img: np.ndarray) -> None: + """ Store the position of the 4 markers below the the 4 pieces in the corners""" + corners, ids, rejected = cv.aruco.detectMarkers( + img, self.aruco_dict, parameters=self.aruco_params + ) + + if ids is not None: + all_markers = [] + for i in range(len(ids)): + r = ((int(x), int(y)) for x, y in corners[i][0].tolist()) + rect = Rect.from_corners(*(Point.from_tuple(t) for t in r)) + center = get_rect_center(rect) + all_markers.append(center) + + # we have the 4 corners + if len(all_markers) == 4: + vertical_sort = sorted(all_markers, key=lambda point: point.y) + top_markers = vertical_sort[:2] + bottom_markers = vertical_sort[2:] + self.top_left = min(top_markers, key=lambda point: point.x) + self.top_right = max(top_markers, key=lambda point: point.x) + self.bottom_left = min(bottom_markers, key=lambda point: point.x) + self.bottom_right = max(bottom_markers, key=lambda point: point.x) + + def get_board_with_visibility(self, img: np.ndarray) -> Board: + """Compute a new board from the 4 corners stored during training, with all pieces visibility""" + board = self._board_from_corners() + + corners, ids, rejected = cv.aruco.detectMarkers( + img, self.aruco_dict, parameters=self.aruco_params + ) + + if ids is not None: + all_markers = [] + for i in range(len(ids)): + r = ((int(x), int(y)) for x, y in corners[i][0].tolist()) + rect = Rect.from_corners(*(Point.from_tuple(t) for t in r)) + center = get_rect_center(rect) + all_markers.append(center) + + for marker in all_markers: + closest_piece_idx = np.argmin( + [get_segment_size(marker, piece.postion) for piece in board.pieces] + ) + board.pieces[closest_piece_idx].is_visible = True + + return board + + def _board_from_corners(self) -> Board: + left_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_left, self.top_left) + right_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_right, self.top_right) + row_0 = self._four_pieces_from_two_ends_horizontal(left_pieces[0].postion, right_pieces[0].postion) + row_1 = self._four_pieces_from_two_ends_horizontal(left_pieces[1].postion, right_pieces[1].postion) + row_2 = self._four_pieces_from_two_ends_horizontal(left_pieces[2].postion, right_pieces[2].postion) + row_3 = self._four_pieces_from_two_ends_horizontal(left_pieces[3].postion, right_pieces[3].postion) + return Board(row_0 + row_1 + row_2 + row_3) + + @staticmethod + def _four_pieces_from_two_ends_vertical(p1: Point, p2: Point) -> Tuple[Piece, ...]: + """Get 4 pieces from 2 ends pieces""" + return ( + Piece(p1), + Piece(get_segement_point_at_dist(p1, p2, 0.45)), + Piece(get_segement_point_at_dist(p2, p1, 0.20)), + Piece(p2), + ) + + @staticmethod + def _four_pieces_from_two_ends_horizontal(p1: Point, p2: Point) -> Tuple[Piece, ...]: + """Get 4 pieces from 2 ends pieces""" + return ( + Piece(p1), + Piece(get_segement_point_at_dist(p1, p2, 0.33)), + Piece(get_segement_point_at_dist(p2, p1, 0.33)), + Piece(p2), + ) + + +class ArucoFullPieceStateExtractor(PieceStateExtractor): + """ Piece detector based on Aruco markers above and below every piece""" + + MARKER_ON_BOARD_ID = 930 + MARKER_OFF_BOARD_ID = 190 + + def __init__(self): + self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_1000) + self.aruco_params = cv.aruco.DetectorParameters_create() + + # TODO: to PieceStateExtractor property + self.ready = False + + self.top_left: Point = Point(-1, -1) + self.top_right: Point = Point(-1, -1) + self.bottom_left: Point = Point(-1, -1) + self.bottom_right: Point = Point(-1, -1) + self.board = None + + def train(self, img: np.ndarray) -> None: + """ Store the position of the 4 markers below the the 4 pieces in the corners""" + corners, ids, rejected = cv.aruco.detectMarkers( + img, self.aruco_dict, parameters=self.aruco_params + ) + + if ids is not None: + all_markers = [] + for idx, marker_id in enumerate(ids): + if marker_id in (self.MARKER_ON_BOARD_ID, self.MARKER_OFF_BOARD_ID): + r = ((int(x), int(y)) for x, y in corners[idx][0].tolist()) + rect = Rect.from_corners(*(Point.from_tuple(t) for t in r)) + # TODO + # BUG: the center is only correct if the marker is in the right orientation + # otherwise, we get complute the center on a corner + center = get_rect_center(rect) + cv.circle(img, (center.x, center.y), 10, (123,0,0)) + cv.rectangle(img, (rect.top_left.x, rect.top_left.y), (rect.top_left.x+rect.width, rect.top_left.y+rect.height), (255, 0 ,0)) + all_markers.append(center) + + if len(all_markers) == 16: + vertical_sort = sorted(all_markers, key=lambda point: point.y) + top_markers = vertical_sort[:4] + bottom_markers = vertical_sort[-4:] + self.top_left = min(top_markers, key=lambda point: point.x) + self.top_right = max(top_markers, key=lambda point: point.x) + self.bottom_left = min(bottom_markers, key=lambda point: point.x) + self.bottom_right = max(bottom_markers, key=lambda point: point.x) + self.ready = True + + def get_board_with_visibility(self, img: np.ndarray) -> Board: + """Compute a new board from the 4 corners stored during training, with all pieces visibility""" + board = self._board_from_corners() + + corners, ids, rejected = cv.aruco.detectMarkers( + img, self.aruco_dict, parameters=self.aruco_params + ) + + # DEBUG + # cv.aruco.drawDetectedMarkers(img, corners) + + if ids is not None: + all_markers = [] + for idx, marker_ids in enumerate(ids): + r = ((int(x), int(y)) for x, y in corners[idx][0].tolist()) + rect = Rect.from_corners(*(Point.from_tuple(t) for t in r)) + center = get_rect_center(rect) + all_markers.append((marker_ids[0], center)) + + for marker_id, center in all_markers: + closest_piece_idx = np.argmin( # argmin to get the index or the min + [get_segment_size(center, piece.postion) for piece in board.pieces] + ) + if marker_id == self.MARKER_ON_BOARD_ID: + board.pieces[closest_piece_idx].state = PieceState.ON_BOARD + elif marker_id == self.MARKER_OFF_BOARD_ID: + board.pieces[closest_piece_idx].state = PieceState.OFF_BOARD + else: + board.pieces[closest_piece_idx].state = PieceState.UNKNOWN + + return board + + def is_ready(self) -> bool: + return self.ready + + def _board_from_corners(self) -> Board: + left_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_left, self.top_left) + right_pieces = self._four_pieces_from_two_ends_vertical(self.bottom_right, self.top_right) + row_0 = self._four_pieces_from_two_ends_horizontal(left_pieces[0].postion, right_pieces[0].postion) + row_1 = self._four_pieces_from_two_ends_horizontal(left_pieces[1].postion, right_pieces[1].postion) + row_2 = self._four_pieces_from_two_ends_horizontal(left_pieces[2].postion, right_pieces[2].postion) + row_3 = self._four_pieces_from_two_ends_horizontal(left_pieces[3].postion, right_pieces[3].postion) + return Board(row_0 + row_1 + row_2 + row_3) + + @staticmethod + def _four_pieces_from_two_ends_vertical(p1: Point, p2: Point) -> Tuple[Piece, ...]: + """Get 4 pieces from 2 ends pieces""" + return ( + Piece(p1), + Piece(get_segement_point_at_dist(p1, p2, 0.45)), + Piece(get_segement_point_at_dist(p2, p1, 0.20)), + Piece(p2), + ) + + @staticmethod + def _four_pieces_from_two_ends_horizontal(p1: Point, p2: Point) -> Tuple[Piece, ...]: + """Get 4 pieces from 2 ends pieces""" + return ( + Piece(p1), + Piece(get_segement_point_at_dist(p1, p2, 0.33)), + Piece(get_segement_point_at_dist(p2, p1, 0.33)), + Piece(p2), + ) diff --git a/memory_lib/memory.py b/memory_lib/memory.py index 1324d84cd3215a9672ac1612e77ceb51fcb95597..3375a968e3b1b2b9a53d2fd89d6b135fef906fc9 100644 --- a/memory_lib/memory.py +++ b/memory_lib/memory.py @@ -5,20 +5,13 @@ import numpy as np from abc import ABC, abstractmethod -from memory_lib.geometry import get_rect_center - -from memory_lib.cv_utils import draw_rect, draw_point +from memory_lib.detectors.board_detectors import ABMExtractor, ArucoABMExtractor +from memory_lib.detectors.board_factory import ABMBoardFactory +from memory_lib.detectors.piece_state_detectors import ArucoFullPieceStateExtractor, ArucoHalfPieceStateExtractor, \ + AverageColorAndMovementPieceVisibilityDetector, PieceStateDetector, PieceStateExtractor from .board import ( - ArucoABMExtractor, - ABMBoardFactory, - PieceVisibilityDetector, - LightnessPieceVisibilityDetector, - AverageColorPieceVisibilityDetector, - ArucoPieceVisibilityDetector, - ABMExtractor, - AverageColorAndMovementPieceVisibilityDetector, - PieceTakenDetectionEdgesTrigger, + PieceTakenDetectionEdgesTrigger, PieceTakenTrigger, ) from .model import Board, PiecesObserver, Rect, Point @@ -41,20 +34,27 @@ class Memory(ABC, threading.Thread, PiecesObserver): threading.Thread.__init__(self) PiecesObserver.__init__(self) self.video_caputre = video_capture + self.running = False + self.frame = None def run(self): - while True: + self.running = True + while self.running: ret, frame = self.video_caputre.read() if ret: time.sleep(0.04) - cv.imshow("vid", frame) + # cv.imshow("vid", frame) self.process(frame) - key = cv.waitKey(1) & 0xFF - if key == ord("r"): - self.reset() - if key == ord("q"): - self.stopped_trigger() - break + self.frame = frame + # key = cv.waitKey(1) & 0xFF + # if key == ord("r"): + # self.reset() + # if key == ord("q"): + # self.stopped_trigger() + # break + + def stop_game(self): + self.running = False @abstractmethod def process(self, img: np.ndarray) -> None: @@ -65,39 +65,78 @@ class Memory(ABC, threading.Thread, PiecesObserver): ... -class MemoryAruco(Memory): +class MemoryArucoFull(Memory): + """Memory pieces detection based on Aruco above and below every piece""" + + def __init__( + self, + video_capture: cv.VideoCapture, + ): + Memory.__init__(self, video_capture) + self.piece_state_extractor = ArucoFullPieceStateExtractor() + self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_1000) + self.aruco_params = cv.aruco.DetectorParameters_create() + self.average_trigger = PieceTakenTrigger(self.piece_trigger) + + def process(self, img: np.ndarray) -> None: + # For the 5 first frames, store the corners + if not self.piece_state_extractor.ready: + self.piece_state_extractor.train(img) + return + + board = self.piece_state_extractor.get_board_with_visibility(img) + # self.board_trigger(board, img) + out_board = self.average_trigger.add_board(board) + + # TODO: debug + # cv.aruco.drawDetectedMarkers(img, corners) + out_board.draw(img) + self.board_trigger(board, img) + # cv.imshow("marker", img) + + def reset(self) -> None: + """Reset board position""" + print("postion reset") + self.piece_state_extractor = ArucoFullPieceStateExtractor() + + +class MemoryArucoHalf(Memory): """Memory pieces detection based on Aruco below every piece""" def __init__( self, video_capture: cv.VideoCapture, - visibility_detector: ArucoPieceVisibilityDetector = ArucoPieceVisibilityDetector() + piece_state_extractor: PieceStateExtractor = ArucoFullPieceStateExtractor(), ): Memory.__init__(self, video_capture) - self.visibility_detector = visibility_detector + self.piece_extractor = piece_state_extractor self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250) self.aruco_params = cv.aruco.DetectorParameters_create() - self.average_trigger = PieceTakenDetectionEdgesTrigger(self.piece_trigger, 10, 40) + self.average_trigger = PieceTakenDetectionEdgesTrigger( # Deprecated + self.piece_trigger, 10, 40 + ) self.frame_counter = 0 def process(self, img: np.ndarray) -> None: # For the 5 first frames, store the corners if self.frame_counter < 10: - self.visibility_detector.train(img) + self.piece_extractor.train(img) self.frame_counter += 1 return - board = self.visibility_detector.get_board_with_visibility(img) - self.board_trigger(board) + board = self.piece_extractor.get_board_with_visibility(img) + # self.board_trigger(board, img) self.average_trigger.add_board(board) # TODO: debug # cv.aruco.drawDetectedMarkers(img, corners) self.average_trigger.out_board.draw(img) - cv.imshow("marker", img) + self.board_trigger(board, img) + # cv.imshow("marker", img) def reset(self) -> None: """Reset board position""" + print("postion reset") self.frame_counter = 0 @@ -113,13 +152,13 @@ class MemoryABM(Memory): self, video_capture: cv.VideoCapture, abm_extractor: ABMExtractor = ArucoABMExtractor(), - visibility_detector: PieceVisibilityDetector = AverageColorAndMovementPieceVisibilityDetector(), + visibility_detector: PieceStateDetector = AverageColorAndMovementPieceVisibilityDetector(), ): Memory.__init__(self, video_capture) self.abm_extractor = abm_extractor self.visibility_detector = visibility_detector self.frame_counter = 0 - self.average_trigger = PieceTakenDetectionEdgesTrigger(self.piece_trigger, 5) + self.average_trigger = PieceTakenDetectionEdgesTrigger(self.piece_trigger, 5) # Deprecated self.last_board = None def process(self, img: np.ndarray) -> None: diff --git a/memory_lib/model.py b/memory_lib/model.py index 506981909d57a3fc132d0407643924a83ece4016..58f31dc2fefa2d4c4f1e3f6fe799905ba1eeb283 100644 --- a/memory_lib/model.py +++ b/memory_lib/model.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from enum import Enum from typing import Tuple, Callable import numpy as np import cv2 as cv @@ -39,20 +40,32 @@ class LineEq: b: float +class PieceState(Enum): + ON_BOARD = 1 + OFF_BOARD = 2 + UNKNOWN = 3 + + @dataclass class Piece: postion: Point = Point(0, 0) - is_visible: bool = False + state: PieceState = PieceState.UNKNOWN @dataclass class Board: pieces: Tuple[Piece, ...] # All 16 pieces + _draw_colors = { + PieceState.ON_BOARD: (255, 0, 0), + PieceState.OFF_BOARD: (0, 255, 0), + PieceState.UNKNOWN: (0, 0, 255), + } + def draw(self, img: np.ndarray): """ Draw circle for each piece position. Green if the piece is visible, red otherwise""" for piece in self.pieces: - color = (0, 255, 0) if piece.is_visible else (0, 0, 255) + color = self._draw_colors[piece.state] cv.circle(img, piece.postion.to_tuple(), 10, color) def update_pieces(self, img: np.ndarray, piece_detector) -> None: @@ -61,7 +74,7 @@ class Board: @classmethod def from_board(cls, board: "Board") -> "Board": - pieces = tuple(Piece(p.postion, p.is_visible) for p in board.pieces) + pieces = tuple(Piece(p.postion, p.state) for p in board.pieces) return cls(pieces) @@ -85,12 +98,7 @@ class PiecesObserver: for callback in self._pieces_observers: callback(piece_id) - def board_trigger(self, board: Board): + def board_trigger(self, board: Board, img): """On every frame, send the full board""" for callback in self._board_observers: - callback(board) - - def stopped_trigger(self): - """On every frame, send the full board""" - for callback in self._stopped_observers: - callback() + callback(board, img) diff --git a/memory_lib/piece_state.py b/memory_lib/piece_state.py new file mode 100644 index 0000000000000000000000000000000000000000..bcb00cf81696f03d9c581f4b99bb47619a9c7855 --- /dev/null +++ b/memory_lib/piece_state.py @@ -0,0 +1,115 @@ +from abc import ABC, abstractmethod +from enum import Enum +from typing import Callable + +from memory_lib.model import PieceState, Board + + +class StateMachineInput(ABC): + def __init__(self, enum: Enum) -> None: + self.enum = enum + + +class StateMachineInputPiece(StateMachineInput): + def __init__(self, piece_state: PieceState): + super().__init__(piece_state) + + +class State(ABC): + + @staticmethod + @abstractmethod + def next(sm_input: StateMachineInput) -> tuple["State", bool]: + ... + + +class StateMachine: + def __init__(self, inital_state: State) -> None: + self.current_state = inital_state + + def next(self, sm_input: StateMachineInput) -> bool: + """Go to the next state according to current state and input. Return if the output needs to be triggered""" + self.current_state, trigger_output = self.current_state.next(sm_input) + return trigger_output + + +class PieceStateStateMachine(StateMachine): + def __init__(self) -> None: + super().__init__(state_0) + + +class State0(State): + @staticmethod + def next(sm_input: StateMachineInputPiece) -> tuple[State, bool]: + if sm_input.enum == PieceState.UNKNOWN: + return state_2, False + if sm_input.enum == PieceState.ON_BOARD: + return state_1, False + if sm_input.enum == PieceState.OFF_BOARD: + return state_4, False + + +class State1(State): + @staticmethod + def next(sm_input: StateMachineInputPiece) -> tuple[State, bool]: + if sm_input.enum == PieceState.UNKNOWN: + return state_3, False + if sm_input.enum == PieceState.ON_BOARD: + return state_1, False + if sm_input.enum == PieceState.OFF_BOARD: + return state_4, True + + +class State2(State): + @staticmethod + def next(sm_input: StateMachineInputPiece) -> tuple[State, bool]: + if sm_input.enum == PieceState.UNKNOWN: + return state_2, False + if sm_input.enum == PieceState.ON_BOARD: + return state_1, False + if sm_input.enum == PieceState.OFF_BOARD: + return state_4, False + + +class State3(State): + @staticmethod + def next(sm_input: StateMachineInputPiece) -> tuple[State, bool]: + if sm_input.enum == PieceState.UNKNOWN: + return state_3, False + if sm_input.enum == PieceState.ON_BOARD: + return state_1, False + if sm_input.enum == PieceState.OFF_BOARD: + return state_4, True + + +class State4(State): + @staticmethod + def next(sm_input: StateMachineInputPiece) -> tuple[State, bool]: + if sm_input.enum == PieceState.UNKNOWN: + return state_4, False + if sm_input.enum == PieceState.ON_BOARD: + return state_1, False + if sm_input.enum == PieceState.OFF_BOARD: + return state_4, False + + +state_0 = State0() +state_1 = State1() +state_2 = State2() +state_3 = State3() +state_4 = State4() + + +class PiecesStatesTracker: + def __init__(self, output_trigger: Callable): + """Create a PieceStateMachine for every piece on the board + :param output_trigger: function to be called when state machine output is triggered + """ + self.output_f = output_trigger + self.state_machines = [PieceStateStateMachine() for _ in range(16)] + + def update(self, new_board: Board): + for idx, piece in enumerate(new_board.pieces): + if self.state_machines[idx].next(StateMachineInputPiece(piece.state)): + self.output_f(idx) + diff --git a/memory_lib/tests/aruco.py b/memory_lib/tests/aruco.py index 1780e47a2f3391df35c023247f1a0d9b7ee6e337..f117c73113490d7f7be571e8ffee84592feed762 100644 --- a/memory_lib/tests/aruco.py +++ b/memory_lib/tests/aruco.py @@ -1,9 +1,9 @@ import cv2 as cv import numpy as np -# cap = cv.VideoCapture(0) -cap = cv.VideoCapture("res/webcam_05_aruco.avi") -aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250) +cap = cv.VideoCapture(0) +# cap = cv.VideoCapture("res/webcam_05_aruco.avi") +aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_1000) aruco_params = cv.aruco.DetectorParameters_create() while True: diff --git a/memory_lib/tests/res/marker_930.png b/memory_lib/tests/res/marker_930.png new file mode 100644 index 0000000000000000000000000000000000000000..f07c8b48de338c5af5b1cea41d2b5707eac4f77a Binary files /dev/null and b/memory_lib/tests/res/marker_930.png differ diff --git a/memory_lib/tests/res/markers_930.png b/memory_lib/tests/res/markers_930.png new file mode 100644 index 0000000000000000000000000000000000000000..af58fe07e79560c9b8d5b3d9cb8886f753b1ff65 Binary files /dev/null and b/memory_lib/tests/res/markers_930.png differ diff --git a/memory_lib/tests/run_memory.py b/memory_lib/tests/run_memory.py index 8a222c8b36d04653514db2f400b50acd79998116..6e149ee7cef481bb065bf1a890e4300eab550f1a 100644 --- a/memory_lib/tests/run_memory.py +++ b/memory_lib/tests/run_memory.py @@ -36,12 +36,19 @@ def main(): # cap = cv.VideoCapture("res/webcam_05_aruco.avi") # cap = cv.VideoCapture(0) - cap = cv.VideoCapture("res/webcam_11_aruco.avi") - memory = memory_lib.MemoryAruco(cap) + cap = cv.VideoCapture("res/webcam_12_aruco_full.avi") + memory = memory_lib.MemoryArucoFull(cap) - memory.bind(hello_piece) + memory.bind_pieces(hello_piece) memory.start() + while True: + frame = memory.frame + if frame is not None: + cv.imshow('webcam', frame) + if cv.waitKey(1) == 27: + break + # process_vid("res/webcam_03_shadow.avi", memory) # process_cam() diff --git a/memory_lib/tests/test_pieces_states.py b/memory_lib/tests/test_pieces_states.py new file mode 100644 index 0000000000000000000000000000000000000000..9573d53302074810c4cdd36aea69c23f1fe2a464 --- /dev/null +++ b/memory_lib/tests/test_pieces_states.py @@ -0,0 +1,58 @@ + +from unittest import TestCase + +from memory_lib.model import PieceState +from memory_lib.piece_state import PieceStateStateMachine, state_0, StateMachineInputPiece, state_1, state_2, state_3, state_4 + + +class TestPiecesStates(TestCase): + + def test_state_1(self): + pieces_states = [PieceState.ON_BOARD, PieceState.ON_BOARD, PieceState.OFF_BOARD] + expected_states = [state_1, state_1, state_4] + expected_outputs = [False, False, True] + self._test_state(pieces_states, expected_states, expected_outputs) + + pieces_states = [PieceState.ON_BOARD, PieceState.UNKNOWN] + expected_states = [state_1, state_3] + expected_outputs = [False, False] + self._test_state(pieces_states, expected_states, expected_outputs) + + def test_state_2(self): + pieces_states = [PieceState.UNKNOWN, PieceState.UNKNOWN, PieceState.OFF_BOARD] + expected_states = [state_2, state_2, state_4] + expected_outputs = [False, False, False] + self._test_state(pieces_states, expected_states, expected_outputs) + + pieces_states = [PieceState.UNKNOWN, PieceState.ON_BOARD] + expected_states = [state_2, state_1] + expected_outputs = [False, False] + self._test_state(pieces_states, expected_states, expected_outputs) + + def test_state_3(self): + pieces_states = [PieceState.ON_BOARD, PieceState.UNKNOWN, PieceState.UNKNOWN, PieceState.OFF_BOARD] + expected_states = [state_1, state_3, state_3, state_4] + expected_outputs = [False, False, False, True] + self._test_state(pieces_states, expected_states, expected_outputs) + + pieces_states = [PieceState.ON_BOARD, PieceState.UNKNOWN, PieceState.ON_BOARD] + expected_states = [state_1, state_3, state_1] + expected_outputs = [False, False, False] + self._test_state(pieces_states, expected_states, expected_outputs) + + def test_state_4(self): + pieces_states = [PieceState.OFF_BOARD, PieceState.UNKNOWN, PieceState.OFF_BOARD, PieceState.ON_BOARD] + expected_states = [state_4, state_4, state_4, state_1] + expected_outputs = [False, False, False, False] + self._test_state(pieces_states, expected_states, expected_outputs) + + def _test_state(self, pieces_states: list[PieceState], expected_states: list[StateMachineInputPiece], expected_outputs: list[bool]) -> None: + expected_index = 0 + state_machine = PieceStateStateMachine() + for piece_state in pieces_states: + state_machine_input = StateMachineInputPiece(piece_state) + need_to_trigger = state_machine.next(state_machine_input) + self.assertEqual(expected_states[expected_index], state_machine.current_state) + self.assertEqual(expected_outputs[expected_index], need_to_trigger) + expected_index += 1 + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..b055a1807504afa845874e1f3ec46daaeec0028a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.pyright] +venvPath = "." +venv = "venv"