Skip to content
Snippets Groups Projects
Commit a4bd7e81 authored by Adrien Lescourt's avatar Adrien Lescourt
Browse files

Merge remote-tracking branch 'memo/structure_refactor'

parents fc59b5ee 955fa24b
Branches
No related tags found
No related merge requests found
Showing
with 928 additions and 496 deletions
<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
<?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="&lt;mxfile host=&quot;app.diagrams.net&quot; modified=&quot;2021-10-01T17:01:49.302Z&quot; agent=&quot;5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.83 Safari/537.36&quot; etag=&quot;2gnUP50G2iihb7g0QLF6&quot; version=&quot;15.4.1&quot; type=&quot;device&quot;&gt;&lt;diagram name=&quot;Page-1&quot; id=&quot;58cdce13-f638-feb5-8d6f-7d28b1aa9fa0&quot;&gt;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&lt;/diagram&gt;&lt;/mxfile&gt;" 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
from .memory import MemoryABM, MemoryAruco from .memory import MemoryABM, MemoryArucoHalf, MemoryArucoFull
import cv2 as cv import sys
import numpy as np
from abc import ABC, abstractmethod
from typing import Tuple, Optional, List, Callable from typing import Tuple, Optional, List, Callable
from .cv_utils import ( from .model import Board, Point, LineEq, Piece, Rect, PieceState
draw_line, from .piece_state import PiecesStatesTracker
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
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. class PieceTakenTrigger:
# First approximate DC line, based on FM size """Call 'trigger' function when after a piece is taken"""
dc_eq = get_parallel_eq(ab_eq, Point(0, int(a.y - fm_s * 1.5)))
# TODO: debug only def __init__(self, trigger: Callable[[int], None]):
# draw_line(self.img, dc_eq) self.trigger = trigger
self.averager = BoardStateAverager()
# Then e is on DC, FM intersection self.piece_state_tracker = PiecesStatesTracker(trigger)
e = get_line_intersection_point(fm_eq, dc_eq)
# Compute the 4 parallels: A1B1, A2B2, A3B3, A4B4 def add_board(self, board: Board) -> Board:
a1, b1 = self._compute_parallel(a, b, ab_eq, e, f, 0.75, 1 * (fm_s / 10)) new_board = self.averager.add_board(board)
a2, b2 = self._compute_parallel(a, b, ab_eq, e, f, 0.48, 3 * (fm_s / 10)) self.piece_state_tracker.update(new_board)
a3, b3 = self._compute_parallel(a, b, ab_eq, e, f, 0.25, 4.5 * (fm_s / 10)) return new_board
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 class BoardStateAverager:
debut_print = ( """Moving average for board states"""
(a, "A"), def __init__(self, window_size: int = 15) -> None:
(b, "B"), self.window_size = window_size
(f, "F"), self.last_boards: List[Board] = []
(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) 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 @staticmethod
def _compute_parallel( def _average_state(states: List[PieceState]) -> PieceState:
a: Point, """Get the most present state in a list"""
b: Point, u = states.count(PieceState.UNKNOWN), PieceState.UNKNOWN
ab_eq: LineEq, on = states.count(PieceState.ON_BOARD), PieceState.ON_BOARD
e: Point, off = states.count(PieceState.OFF_BOARD), PieceState.OFF_BOARD
f: Point, return max(u, on, off, key=lambda t: t[0])[1]
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 @staticmethod
def _compute_piece(left_point: Point, right_point: Point) -> Tuple[Piece, ...]: def _states_by_pieces(boards: List[Board]) -> list[list[PieceState]]:
""" find the 4 pieces from a horizontal line """ # TODO: Improve perf by storing directly the states, not the boards in the fifo
return ( """For every pieces, get the last X states
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: 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
... ...
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
"""
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
""" """
boards_pieces = [board.pieces for board in boards]
def __init__(self): transposed = [elem for elem in zip(*boards_pieces)]
self.aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250) res = []
self.aruco_params = cv.aruco.DetectorParameters_create() for last_pieces_for_one_piece in transposed:
self.top_left: Point = Point(-1, -1) construct = []
self.top_right: Point = Point(-1, -1) for piece in last_pieces_for_one_piece:
self.bottom_left: Point = Point(-1, -1) construct.append(piece.state)
self.bottom_right: Point = Point(-1, -1) res.append(construct)
return res
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 PieceTakenDetectionEdgesTrigger: class PieceTakenDetectionEdgesTrigger:
"""Call 'trigger' function when after a piece is taken """ WARNING: deprectated: replaced with PieceTakenTrigger"""
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
"""
def __init__(self, trigger: Callable[[int], None], rising_count: int = 5, falling_count: int = 5): 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.trigger = trigger
self.rising_count = rising_count self.rising_count = rising_count
self.falling_count = falling_count self.falling_count = falling_count
...@@ -477,3 +111,5 @@ class PieceTakenDetectionEdgesTrigger: ...@@ -477,3 +111,5 @@ class PieceTakenDetectionEdgesTrigger:
# print([int(b.pieces[idx].is_visible) for b in self.last_boards]) # print([int(b.pieces[idx].is_visible) for b in self.last_boards])
return next_out_board return next_out_board
...@@ -93,32 +93,3 @@ def draw_rect(img: np.ndarray, rect: Rect, color=(255, 0, 0)): ...@@ -93,32 +93,3 @@ def draw_rect(img: np.ndarray, rect: Rect, color=(255, 0, 0)):
""" Draw rectangle rect on img """ """ Draw rectangle rect on img """
x, y, w, h = rect.to_tuple() x, y, w, h = rect.to_tuple()
cv.rectangle(img, (x, y), (x + w, y + h), color) 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
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
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)),
)
),
)
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
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),
)
...@@ -5,20 +5,13 @@ import numpy as np ...@@ -5,20 +5,13 @@ import numpy as np
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from memory_lib.geometry import get_rect_center from memory_lib.detectors.board_detectors import ABMExtractor, ArucoABMExtractor
from memory_lib.detectors.board_factory import ABMBoardFactory
from memory_lib.cv_utils import draw_rect, draw_point from memory_lib.detectors.piece_state_detectors import ArucoFullPieceStateExtractor, ArucoHalfPieceStateExtractor, \
AverageColorAndMovementPieceVisibilityDetector, PieceStateDetector, PieceStateExtractor
from .board import ( from .board import (
ArucoABMExtractor, PieceTakenDetectionEdgesTrigger, PieceTakenTrigger,
ABMBoardFactory,
PieceVisibilityDetector,
LightnessPieceVisibilityDetector,
AverageColorPieceVisibilityDetector,
ArucoPieceVisibilityDetector,
ABMExtractor,
AverageColorAndMovementPieceVisibilityDetector,
PieceTakenDetectionEdgesTrigger,
) )
from .model import Board, PiecesObserver, Rect, Point from .model import Board, PiecesObserver, Rect, Point
...@@ -41,20 +34,27 @@ class Memory(ABC, threading.Thread, PiecesObserver): ...@@ -41,20 +34,27 @@ class Memory(ABC, threading.Thread, PiecesObserver):
threading.Thread.__init__(self) threading.Thread.__init__(self)
PiecesObserver.__init__(self) PiecesObserver.__init__(self)
self.video_caputre = video_capture self.video_caputre = video_capture
self.running = False
self.frame = None
def run(self): def run(self):
while True: self.running = True
while self.running:
ret, frame = self.video_caputre.read() ret, frame = self.video_caputre.read()
if ret: if ret:
time.sleep(0.04) time.sleep(0.04)
cv.imshow("vid", frame) # cv.imshow("vid", frame)
self.process(frame) self.process(frame)
key = cv.waitKey(1) & 0xFF self.frame = frame
if key == ord("r"): # key = cv.waitKey(1) & 0xFF
self.reset() # if key == ord("r"):
if key == ord("q"): # self.reset()
self.stopped_trigger() # if key == ord("q"):
break # self.stopped_trigger()
# break
def stop_game(self):
self.running = False
@abstractmethod @abstractmethod
def process(self, img: np.ndarray) -> None: def process(self, img: np.ndarray) -> None:
...@@ -65,39 +65,78 @@ class Memory(ABC, threading.Thread, PiecesObserver): ...@@ -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""" """Memory pieces detection based on Aruco below every piece"""
def __init__( def __init__(
self, self,
video_capture: cv.VideoCapture, video_capture: cv.VideoCapture,
visibility_detector: ArucoPieceVisibilityDetector = ArucoPieceVisibilityDetector() piece_state_extractor: PieceStateExtractor = ArucoFullPieceStateExtractor(),
): ):
Memory.__init__(self, video_capture) 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_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250)
self.aruco_params = cv.aruco.DetectorParameters_create() 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 self.frame_counter = 0
def process(self, img: np.ndarray) -> None: def process(self, img: np.ndarray) -> None:
# For the 5 first frames, store the corners # For the 5 first frames, store the corners
if self.frame_counter < 10: if self.frame_counter < 10:
self.visibility_detector.train(img) self.piece_extractor.train(img)
self.frame_counter += 1 self.frame_counter += 1
return return
board = self.visibility_detector.get_board_with_visibility(img) board = self.piece_extractor.get_board_with_visibility(img)
self.board_trigger(board) # self.board_trigger(board, img)
self.average_trigger.add_board(board) self.average_trigger.add_board(board)
# TODO: debug # TODO: debug
# cv.aruco.drawDetectedMarkers(img, corners) # cv.aruco.drawDetectedMarkers(img, corners)
self.average_trigger.out_board.draw(img) self.average_trigger.out_board.draw(img)
cv.imshow("marker", img) self.board_trigger(board, img)
# cv.imshow("marker", img)
def reset(self) -> None: def reset(self) -> None:
"""Reset board position""" """Reset board position"""
print("postion reset")
self.frame_counter = 0 self.frame_counter = 0
...@@ -113,13 +152,13 @@ class MemoryABM(Memory): ...@@ -113,13 +152,13 @@ class MemoryABM(Memory):
self, self,
video_capture: cv.VideoCapture, video_capture: cv.VideoCapture,
abm_extractor: ABMExtractor = ArucoABMExtractor(), abm_extractor: ABMExtractor = ArucoABMExtractor(),
visibility_detector: PieceVisibilityDetector = AverageColorAndMovementPieceVisibilityDetector(), visibility_detector: PieceStateDetector = AverageColorAndMovementPieceVisibilityDetector(),
): ):
Memory.__init__(self, video_capture) Memory.__init__(self, video_capture)
self.abm_extractor = abm_extractor self.abm_extractor = abm_extractor
self.visibility_detector = visibility_detector self.visibility_detector = visibility_detector
self.frame_counter = 0 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 self.last_board = None
def process(self, img: np.ndarray) -> None: def process(self, img: np.ndarray) -> None:
......
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from typing import Tuple, Callable from typing import Tuple, Callable
import numpy as np import numpy as np
import cv2 as cv import cv2 as cv
...@@ -39,20 +40,32 @@ class LineEq: ...@@ -39,20 +40,32 @@ class LineEq:
b: float b: float
class PieceState(Enum):
ON_BOARD = 1
OFF_BOARD = 2
UNKNOWN = 3
@dataclass @dataclass
class Piece: class Piece:
postion: Point = Point(0, 0) postion: Point = Point(0, 0)
is_visible: bool = False state: PieceState = PieceState.UNKNOWN
@dataclass @dataclass
class Board: class Board:
pieces: Tuple[Piece, ...] # All 16 pieces 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): def draw(self, img: np.ndarray):
""" Draw circle for each piece position. Green if the piece is visible, red otherwise""" """ Draw circle for each piece position. Green if the piece is visible, red otherwise"""
for piece in self.pieces: 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) cv.circle(img, piece.postion.to_tuple(), 10, color)
def update_pieces(self, img: np.ndarray, piece_detector) -> None: def update_pieces(self, img: np.ndarray, piece_detector) -> None:
...@@ -61,7 +74,7 @@ class Board: ...@@ -61,7 +74,7 @@ class Board:
@classmethod @classmethod
def from_board(cls, board: "Board") -> "Board": 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) return cls(pieces)
...@@ -85,12 +98,7 @@ class PiecesObserver: ...@@ -85,12 +98,7 @@ class PiecesObserver:
for callback in self._pieces_observers: for callback in self._pieces_observers:
callback(piece_id) callback(piece_id)
def board_trigger(self, board: Board): def board_trigger(self, board: Board, img):
"""On every frame, send the full board""" """On every frame, send the full board"""
for callback in self._board_observers: for callback in self._board_observers:
callback(board) callback(board, img)
def stopped_trigger(self):
"""On every frame, send the full board"""
for callback in self._stopped_observers:
callback()
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)
import cv2 as cv import cv2 as cv
import numpy as np import numpy as np
# cap = cv.VideoCapture(0) cap = cv.VideoCapture(0)
cap = cv.VideoCapture("res/webcam_05_aruco.avi") # cap = cv.VideoCapture("res/webcam_05_aruco.avi")
aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250) aruco_dict = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_1000)
aruco_params = cv.aruco.DetectorParameters_create() aruco_params = cv.aruco.DetectorParameters_create()
while True: while True:
......
memory_lib/tests/res/marker_930.png

898 B

memory_lib/tests/res/markers_930.png

63.7 KiB

...@@ -36,12 +36,19 @@ def main(): ...@@ -36,12 +36,19 @@ def main():
# cap = cv.VideoCapture("res/webcam_05_aruco.avi") # cap = cv.VideoCapture("res/webcam_05_aruco.avi")
# cap = cv.VideoCapture(0) # cap = cv.VideoCapture(0)
cap = cv.VideoCapture("res/webcam_11_aruco.avi") cap = cv.VideoCapture("res/webcam_12_aruco_full.avi")
memory = memory_lib.MemoryAruco(cap) memory = memory_lib.MemoryArucoFull(cap)
memory.bind(hello_piece) memory.bind_pieces(hello_piece)
memory.start() 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_vid("res/webcam_03_shadow.avi", memory)
# process_cam() # process_cam()
......
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
[tool.pyright]
venvPath = "."
venv = "venv"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment